mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 05:11:57 +00:00
Compare commits
3 Commits
16ca6b6df7
...
9c43b8858c
Author | SHA1 | Date | |
---|---|---|---|
|
9c43b8858c | ||
|
a4b5340a61 | ||
|
f7e243f3fc |
11
cmd/init.go
11
cmd/init.go
@@ -473,6 +473,8 @@ func initMedia(db *sqlx.DB, i18n *i18n.I18n) *media.Manager {
|
||||
UploadURI: "/uploads",
|
||||
UploadPath: filepath.Clean(ko.String("upload.fs.upload_path")),
|
||||
RootURL: appRootURL,
|
||||
Expiry: ko.Duration("upload.fs.expiry"),
|
||||
Secret: ko.String("upload.fs.secret"),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("error initializing fs media store: %v", err)
|
||||
@@ -482,11 +484,10 @@ func initMedia(db *sqlx.DB, i18n *i18n.I18n) *media.Manager {
|
||||
}
|
||||
|
||||
media, err := media.New(media.Opts{
|
||||
Store: store,
|
||||
Lo: lo,
|
||||
DB: db,
|
||||
I18n: i18n,
|
||||
Secret: ko.String("upload.secret"),
|
||||
Store: store,
|
||||
Lo: lo,
|
||||
DB: db,
|
||||
I18n: i18n,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("error initializing media: %v", err)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-end text-left">
|
||||
<!-- Do not show live chat continuity emails -->
|
||||
<div class="flex flex-col items-end text-left" v-if="!message.meta.continuity_email">
|
||||
<!-- Sender Name -->
|
||||
<div class="pr-[47px] mb-1">
|
||||
<p class="text-muted-foreground text-sm font-medium">
|
||||
|
@@ -133,7 +133,7 @@ type userStore interface {
|
||||
type mediaStore interface {
|
||||
GetBlob(name string) ([]byte, error)
|
||||
GetURL(name string) string
|
||||
GetSignedURL(name string, expiresAt time.Time) string
|
||||
GetSignedURL(name string) string
|
||||
Attach(id int, model string, modelID int) error
|
||||
GetByModel(id int, model string) ([]mmodels.Media, error)
|
||||
ContentIDExists(contentID string) (bool, string, error)
|
||||
@@ -1308,8 +1308,7 @@ func (m *Manager) BuildWidgetConversationView(conversation models.Conversation)
|
||||
avatarPath := assignee.AvatarURL.String
|
||||
if strings.HasPrefix(avatarPath, "/uploads/") {
|
||||
avatarUUID := strings.TrimPrefix(avatarPath, "/uploads/")
|
||||
expiresAt := time.Now().Add(1 * time.Hour)
|
||||
assignee.AvatarURL = null.StringFrom(m.mediaStore.GetSignedURL(avatarUUID, expiresAt))
|
||||
assignee.AvatarURL = null.StringFrom(m.mediaStore.GetSignedURL(avatarUUID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1366,8 +1365,7 @@ func (m *Manager) BuildWidgetConversationResponse(conversation models.Conversati
|
||||
// Generate signed URLs for attachments
|
||||
attachments := msg.Attachments
|
||||
for j := range attachments {
|
||||
expiresAt := time.Now().Add(8 * time.Hour)
|
||||
attachments[j].URL = m.mediaStore.GetSignedURL(attachments[j].UUID, expiresAt)
|
||||
attachments[j].URL = m.mediaStore.GetSignedURL(attachments[j].UUID)
|
||||
}
|
||||
|
||||
// Fetch sender from cache or store
|
||||
|
@@ -998,7 +998,7 @@ func (m *Manager) fetchMessageAttachments(messageID int) (attachment.Attachments
|
||||
Content: blob,
|
||||
Size: media.Size,
|
||||
Header: attachment.MakeHeader(media.ContentType, media.UUID, media.Filename, "base64", media.Disposition.String),
|
||||
URL: m.mediaStore.GetURL(media.UUID),
|
||||
URL: m.mediaStore.GetSignedURL(media.UUID),
|
||||
}
|
||||
attachments = append(attachments, attachment)
|
||||
}
|
||||
|
@@ -41,8 +41,8 @@ type Store interface {
|
||||
// This is optional and only implemented by stores that need signed URL functionality (like fs).
|
||||
type SignedURLStore interface {
|
||||
Store
|
||||
GetSignedURL(name string, expiresAt time.Time, secret []byte) string
|
||||
VerifySignature(name, signature string, expiresAt time.Time, secret []byte) bool
|
||||
GetSignedURL(name string) string
|
||||
VerifySignature(name, signature string, expiresAt time.Time) bool
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
@@ -50,16 +50,14 @@ type Manager struct {
|
||||
lo *logf.Logger
|
||||
i18n *i18n.I18n
|
||||
queries queries
|
||||
secret string
|
||||
}
|
||||
|
||||
// Opts provides options for configuring the Manager.
|
||||
type Opts struct {
|
||||
Store Store
|
||||
Lo *logf.Logger
|
||||
DB *sqlx.DB
|
||||
I18n *i18n.I18n
|
||||
Secret string
|
||||
Store Store
|
||||
Lo *logf.Logger
|
||||
DB *sqlx.DB
|
||||
I18n *i18n.I18n
|
||||
}
|
||||
|
||||
// New initializes and returns a new Manager instance for handling media operations.
|
||||
@@ -73,7 +71,6 @@ func New(opt Opts) (*Manager, error) {
|
||||
lo: opt.Lo,
|
||||
i18n: opt.I18n,
|
||||
queries: q,
|
||||
secret: opt.Secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -233,10 +230,10 @@ func (m *Manager) deleteUnlinkedMessageMedia() error {
|
||||
|
||||
// GetSignedURL returns a signed URL for accessing a media file with expiration.
|
||||
// This delegates to the store if it supports signed URLs (like fs), otherwise returns the normal URL.
|
||||
func (m *Manager) GetSignedURL(name string, expiresAt time.Time) string {
|
||||
func (m *Manager) GetSignedURL(name string) string {
|
||||
// Check if the store supports signed URLs
|
||||
if signedStore, ok := m.store.(SignedURLStore); ok {
|
||||
return signedStore.GetSignedURL(name, expiresAt, []byte(m.secret))
|
||||
return signedStore.GetSignedURL(name)
|
||||
}
|
||||
// Fallback to regular URL for stores that handle signing internally (like S3)
|
||||
return m.store.GetURL(name)
|
||||
@@ -249,32 +246,32 @@ func (m *Manager) VerifySignature(r *fastglue.Request) error {
|
||||
if uuid == nil {
|
||||
return fmt.Errorf("missing uuid parameter")
|
||||
}
|
||||
|
||||
|
||||
signature := string(r.RequestCtx.QueryArgs().Peek("signature"))
|
||||
expiresStr := string(r.RequestCtx.QueryArgs().Peek("expires"))
|
||||
|
||||
|
||||
if signature == "" || expiresStr == "" {
|
||||
return fmt.Errorf("missing signature or expires parameter")
|
||||
}
|
||||
|
||||
|
||||
// Parse expiration time
|
||||
var expires int64
|
||||
if _, err := fmt.Sscanf(expiresStr, "%d", &expires); err != nil {
|
||||
return fmt.Errorf("invalid expires parameter: %v", err)
|
||||
}
|
||||
|
||||
|
||||
expiresAt := time.Unix(expires, 0)
|
||||
|
||||
|
||||
// Check if store supports signature verification
|
||||
if signedStore, ok := m.store.(SignedURLStore); ok {
|
||||
// Strip thumb_ prefix for signature verification to match the base UUID
|
||||
verificationName := strings.TrimPrefix(uuid.(string), "thumb_")
|
||||
if !signedStore.VerifySignature(verificationName, signature, expiresAt, []byte(m.secret)) {
|
||||
if !signedStore.VerifySignature(verificationName, signature, expiresAt) {
|
||||
return fmt.Errorf("signature verification failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// For stores that don't support signing (like S3), always allow
|
||||
return nil
|
||||
}
|
||||
|
@@ -21,6 +21,8 @@ type Opts struct {
|
||||
UploadPath string
|
||||
UploadURI string
|
||||
RootURL string
|
||||
Expiry time.Duration
|
||||
Secret string
|
||||
}
|
||||
|
||||
// Client implements `media.Store`
|
||||
@@ -82,53 +84,52 @@ func (c *Client) Name() string {
|
||||
|
||||
// GetSignedURL generates a signed URL for the file with expiration.
|
||||
// This implements the SignedURLStore interface for secure public access.
|
||||
func (c *Client) GetSignedURL(name string, expiresAt time.Time, secret []byte) string {
|
||||
func (c *Client) GetSignedURL(name string) string {
|
||||
// Generate base URL
|
||||
baseURL := c.GetURL(name)
|
||||
|
||||
|
||||
// Create the signature payload: name + expires timestamp
|
||||
expires := expiresAt.Unix()
|
||||
expires := time.Now().Add(c.opts.Expiry).Unix()
|
||||
payload := name + strconv.FormatInt(expires, 10)
|
||||
|
||||
|
||||
// Generate HMAC-SHA256 signature
|
||||
h := hmac.New(sha256.New, secret)
|
||||
h := hmac.New(sha256.New, []byte(c.opts.Secret))
|
||||
h.Write([]byte(payload))
|
||||
signature := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
|
||||
// Parse base URL and add query parameters
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
// Fallback to base URL if parsing fails
|
||||
return baseURL
|
||||
}
|
||||
|
||||
|
||||
// Add signature and expires parameters
|
||||
query := u.Query()
|
||||
query.Set("signature", signature)
|
||||
query.Set("expires", strconv.FormatInt(expires, 10))
|
||||
u.RawQuery = query.Encode()
|
||||
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// VerifySignature verifies that a signature is valid for the given parameters.
|
||||
// This implements the SignedURLStore interface for secure public access.
|
||||
func (c *Client) VerifySignature(name, signature string, expiresAt time.Time, secret []byte) bool {
|
||||
func (c *Client) VerifySignature(name, signature string, expiresAt time.Time) bool {
|
||||
// Check if URL has expired
|
||||
if time.Now().After(expiresAt) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Recreate the signature payload: name + expires timestamp
|
||||
expires := expiresAt.Unix()
|
||||
payload := name + strconv.FormatInt(expires, 10)
|
||||
|
||||
|
||||
// Generate expected HMAC-SHA256 signature
|
||||
h := hmac.New(sha256.New, secret)
|
||||
h := hmac.New(sha256.New, []byte(c.opts.Secret))
|
||||
h.Write([]byte(payload))
|
||||
expectedSignature := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
|
||||
// Use constant-time comparison to prevent timing attacks
|
||||
|
||||
return subtle.ConstantTimeCompare([]byte(signature), []byte(expectedSignature)) == 1
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user