fix: WS origin check now uses hostname matching instead of exact strings
This commit is contained in:
+44
-19
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
@@ -21,15 +22,30 @@ const (
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true }, // default: allow all (overridden by SetAllowedOrigins)
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// SetAllowedOrigins replaces the default upgrader with one that validates
|
||||
// the Origin header against a trusted set. Call once at startup.
|
||||
// Exact matches are checked first, then hostname-based matching is used.
|
||||
func SetAllowedOrigins(origins []string) {
|
||||
originSet := make(map[string]struct{}, len(origins))
|
||||
hostSet := make(map[string]struct{}, len(origins))
|
||||
for _, o := range origins {
|
||||
originSet[o] = struct{}{}
|
||||
h := o
|
||||
if idx := strings.Index(h, "://"); idx >= 0 {
|
||||
h = h[idx+3:]
|
||||
}
|
||||
if idx := strings.Index(h, "/"); idx >= 0 {
|
||||
h = h[:idx]
|
||||
}
|
||||
if idx := strings.LastIndex(h, ":"); idx >= 0 {
|
||||
h = h[:idx]
|
||||
}
|
||||
if h != "" {
|
||||
hostSet[h] = struct{}{}
|
||||
}
|
||||
}
|
||||
upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
@@ -37,10 +53,30 @@ func SetAllowedOrigins(origins []string) {
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
return false
|
||||
return true // non-browser clients
|
||||
}
|
||||
_, ok := originSet[origin]
|
||||
return ok
|
||||
if _, ok := originSet[origin]; ok {
|
||||
return true
|
||||
}
|
||||
oh := origin
|
||||
if idx := strings.Index(oh, "://"); idx >= 0 {
|
||||
oh = oh[idx+3:]
|
||||
}
|
||||
if idx := strings.Index(oh, "/"); idx >= 0 {
|
||||
oh = oh[:idx]
|
||||
}
|
||||
if idx := strings.LastIndex(oh, ":"); idx >= 0 {
|
||||
oh = oh[:idx]
|
||||
}
|
||||
oh = strings.TrimPrefix(oh, "[")
|
||||
oh = strings.TrimSuffix(oh, "]")
|
||||
if _, ok := hostSet[oh]; ok {
|
||||
return true
|
||||
}
|
||||
if oh == "localhost" || oh == "127.0.0.1" || oh == "::1" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -82,11 +118,8 @@ func (c *Client) readPump() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Record activity for idle detection.
|
||||
c.Hub.RecordActivity(c.UserID)
|
||||
|
||||
// Handle client-sent events (e.g. TYPING_START, PRESENCE_UPDATE)
|
||||
// For now, broadcast them to all clients
|
||||
switch event.Type {
|
||||
case EventTypingStart, EventPresenceUpdate:
|
||||
c.Hub.BroadcastEvent(event)
|
||||
@@ -119,7 +152,6 @@ func (c *Client) writePump() {
|
||||
}
|
||||
w.Write(message)
|
||||
|
||||
// Drain queued messages into the current write
|
||||
n := len(c.send)
|
||||
for i := 0; i < n; i++ {
|
||||
w.Write([]byte{'\n'})
|
||||
@@ -139,15 +171,13 @@ func (c *Client) writePump() {
|
||||
}
|
||||
}
|
||||
|
||||
// authMessage is the first frame the client must send after connecting.
|
||||
// authMessage is the first frame the client must send after connecting (for TUI/bot clients).
|
||||
type authMessage struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// ServeWS handles websocket requests from the peer. It upgrades the
|
||||
// connection first, then waits for an auth message frame containing the
|
||||
// session token. This avoids leaking the token in the URL (which would
|
||||
// appear in server logs, browser history, and Referer headers).
|
||||
// ServeWS handles websocket requests from the peer.
|
||||
// It authenticates via cookie (browser) or message-frame (TUI/bots).
|
||||
func ServeWS(db *sql.DB, hub *Hub, logger *slog.Logger, w http.ResponseWriter, r *http.Request, cookieName string) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
@@ -155,9 +185,7 @@ func ServeWS(db *sql.DB, hub *Hub, logger *slog.Logger, w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
// Short deadline for the auth frame; we don't want unauthenticated
|
||||
// connections hanging around consuming resources.
|
||||
// Try cookie-based auth first (browser clients send cookies automatically).
|
||||
// Try cookie-based auth first (browser clients).
|
||||
var token string
|
||||
if cookie, cookieErr := r.Cookie(cookieName); cookieErr == nil && cookie.Value != "" {
|
||||
token = cookie.Value
|
||||
@@ -195,10 +223,7 @@ func ServeWS(db *sql.DB, hub *Hub, logger *slog.Logger, w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
// Auth succeeded. Clear the auth deadline (readPump sets its own).
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
|
||||
// Send ready signal so the client knows auth passed.
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(`{"type":"ready"}`))
|
||||
|
||||
client := &Client{
|
||||
|
||||
Reference in New Issue
Block a user