mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-10 14:15:42 +00:00
f7cbfb525c
- Gate widget inbox_id on UUID to prevent enumeration. - Throttle inbound WS frames per connection (typing/page_visit/ping) - Restrict admin-set URLs to http/https - Apply closed-conversation reply guard to media upload path (was text-only) - Reject empty uploads and use io.ReadAll - Validate inbox config before DB create; drop unused func VerifySignature
193 lines
7.5 KiB
Go
193 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/abhinavxd/libredesk/internal/envelope"
|
|
"github.com/abhinavxd/libredesk/internal/httputil"
|
|
"github.com/abhinavxd/libredesk/internal/inbox/channel/livechat"
|
|
imodels "github.com/abhinavxd/libredesk/internal/inbox/models"
|
|
umodels "github.com/abhinavxd/libredesk/internal/user/models"
|
|
realip "github.com/ferluci/fast-realip"
|
|
"github.com/google/uuid"
|
|
"github.com/valyala/fasthttp"
|
|
"github.com/zerodha/fastglue"
|
|
)
|
|
|
|
const (
|
|
ctxWidgetContactID = "widget_contact_id"
|
|
ctxWidgetIsVisitor = "widget_is_visitor"
|
|
ctxWidgetInbox = "widget_inbox"
|
|
ctxWidgetConfig = "widget_config"
|
|
|
|
hdrWidgetInboxID = "X-Libredesk-Inbox-ID"
|
|
hdrWidgetVisitorToken = "X-Libredesk-Visitor-Token"
|
|
hdrClearVisitorToken = "X-Libredesk-Clear-Visitor"
|
|
)
|
|
|
|
// validateWidgetInbox middleware validates the inbox from the request header or query param,
|
|
// checks IP/domain restrictions, and sets inbox + config in context.
|
|
func validateWidgetInbox(next func(*fastglue.Request) error) func(*fastglue.Request) error {
|
|
return func(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
|
|
inboxUUID := string(r.RequestCtx.Request.Header.Peek(hdrWidgetInboxID))
|
|
if inboxUUID == "" {
|
|
inboxUUID = string(r.RequestCtx.QueryArgs().Peek("inbox_id"))
|
|
}
|
|
if inboxUUID == "" {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.required", "name", "{globals.terms.inbox}"), nil, envelope.InputError)
|
|
}
|
|
|
|
// Require a UUID here so widget callers cannot enumerate inboxes by numeric ID.
|
|
if _, err := uuid.Parse(inboxUUID); err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("validation.notFoundInbox"), nil, envelope.InputError)
|
|
}
|
|
|
|
inbox, err := app.inbox.GetDBRecord(inboxUUID)
|
|
if err != nil {
|
|
app.lo.Error("error fetching inbox", "inbox_uuid", inboxUUID, "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("validation.notFoundInbox"), nil, envelope.InputError)
|
|
}
|
|
if !inbox.Enabled || inbox.Channel != livechat.ChannelLiveChat {
|
|
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("validation.notFoundInbox"), nil, envelope.InputError)
|
|
}
|
|
|
|
var config livechat.Config
|
|
if err := json.Unmarshal(inbox.Config, &config); err != nil {
|
|
app.lo.Error("error parsing live chat config", "error", err)
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
|
|
}
|
|
|
|
if len(config.BlockedIPs) > 0 {
|
|
clientIP := realip.FromRequest(r.RequestCtx)
|
|
if httputil.IsIPBlocked(clientIP, config.BlockedIPs) {
|
|
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.T("widget.ipBlocked"), nil, envelope.PermissionError)
|
|
}
|
|
}
|
|
|
|
r.RequestCtx.SetUserValue(ctxWidgetInbox, inbox)
|
|
r.RequestCtx.SetUserValue(ctxWidgetConfig, config)
|
|
return next(r)
|
|
}
|
|
}
|
|
|
|
// widgetAuth middleware validates the session token from the Authorization header
|
|
// using Redis lookup. Wraps validateWidgetInbox for inbox validation.
|
|
// For /conversations/init without a token, allows visitor creation.
|
|
func widgetAuth(next func(*fastglue.Request) error) func(*fastglue.Request) error {
|
|
return validateWidgetInbox(func(r *fastglue.Request) error {
|
|
app := r.Context.(*App)
|
|
inbox, err := getWidgetInbox(r)
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
|
|
}
|
|
config, err := getWidgetConfig(r)
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
|
|
}
|
|
|
|
authHeader := string(r.RequestCtx.Request.Header.Peek("Authorization"))
|
|
|
|
// For init endpoint, allow requests without token (visitor creation).
|
|
if authHeader == "" && strings.HasSuffix(string(r.RequestCtx.Path()), "/conversations/init") {
|
|
return next(r)
|
|
}
|
|
|
|
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
|
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
|
}
|
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
|
|
|
session, err := loadSession(app, token, config)
|
|
if err != nil {
|
|
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
|
}
|
|
|
|
// Verify session belongs to this inbox.
|
|
if session.InboxID != inbox.ID {
|
|
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
|
}
|
|
|
|
// Verify user exists, is enabled, and is a contact or visitor.
|
|
u, err := app.user.Get(session.UserID, "", []string{umodels.UserTypeContact, umodels.UserTypeVisitor})
|
|
if err != nil || !u.Enabled {
|
|
return r.SendErrorEnvelope(fasthttp.StatusUnauthorized, app.i18n.T("globals.terms.unAuthorized"), nil, envelope.UnauthorizedError)
|
|
}
|
|
|
|
r.RequestCtx.SetUserValue(ctxWidgetContactID, session.UserID)
|
|
r.RequestCtx.SetUserValue(ctxWidgetIsVisitor, session.IsVisitor)
|
|
|
|
// Merge visitor to contact if visitor token is provided.
|
|
visitorToken := string(r.RequestCtx.Request.Header.Peek(hdrWidgetVisitorToken))
|
|
if visitorToken != "" && session.ExternalUserID != "" && session.UserID > 0 {
|
|
visitorSession, vErr := loadSession(app, visitorToken, config)
|
|
if vErr == nil && visitorSession.IsVisitor && visitorSession.UserID > 0 && visitorSession.UserID != session.UserID && visitorSession.InboxID == inbox.ID {
|
|
if err := app.user.MergeVisitorToContact(visitorSession.UserID, session.UserID); err != nil {
|
|
app.lo.Error("error merging visitor to contact", "visitor_id", visitorSession.UserID, "contact_id", session.UserID, "error", err)
|
|
} else {
|
|
app.lo.Info("merged visitor to contact", "visitor_id", visitorSession.UserID, "contact_id", session.UserID)
|
|
deleteSessionToken(app, visitorToken)
|
|
r.RequestCtx.Response.Header.Set(hdrClearVisitorToken, "true")
|
|
}
|
|
}
|
|
}
|
|
|
|
return next(r)
|
|
})
|
|
}
|
|
|
|
// getWidgetContactID extracts contact ID from request context.
|
|
func getWidgetContactID(r *fastglue.Request) (int, error) {
|
|
val := r.RequestCtx.UserValue(ctxWidgetContactID)
|
|
if val == nil {
|
|
return 0, fmt.Errorf("widget middleware not applied: missing contact ID in context")
|
|
}
|
|
contactID, ok := val.(int)
|
|
if !ok {
|
|
return 0, fmt.Errorf("invalid contact ID type in context")
|
|
}
|
|
return contactID, nil
|
|
}
|
|
|
|
// getWidgetIsVisitor extracts the visitor flag from request context.
|
|
func getWidgetIsVisitor(r *fastglue.Request) bool {
|
|
val := r.RequestCtx.UserValue(ctxWidgetIsVisitor)
|
|
if val == nil {
|
|
return true
|
|
}
|
|
v, ok := val.(bool)
|
|
if !ok {
|
|
return true
|
|
}
|
|
return v
|
|
}
|
|
|
|
// getWidgetInbox extracts inbox model from request context.
|
|
func getWidgetInbox(r *fastglue.Request) (imodels.Inbox, error) {
|
|
val := r.RequestCtx.UserValue(ctxWidgetInbox)
|
|
if val == nil {
|
|
return imodels.Inbox{}, fmt.Errorf("widget middleware not applied: missing inbox in context")
|
|
}
|
|
inbox, ok := val.(imodels.Inbox)
|
|
if !ok {
|
|
return imodels.Inbox{}, fmt.Errorf("invalid inbox type in context")
|
|
}
|
|
return inbox, nil
|
|
}
|
|
|
|
// getWidgetConfig extracts parsed livechat config from request context.
|
|
func getWidgetConfig(r *fastglue.Request) (livechat.Config, error) {
|
|
val := r.RequestCtx.UserValue(ctxWidgetConfig)
|
|
if val == nil {
|
|
return livechat.Config{}, fmt.Errorf("widget middleware not applied: missing config in context")
|
|
}
|
|
config, ok := val.(livechat.Config)
|
|
if !ok {
|
|
return livechat.Config{}, fmt.Errorf("invalid config type in context")
|
|
}
|
|
return config, nil
|
|
}
|