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"
|
"encoding/json"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@@ -21,15 +22,30 @@ const (
|
|||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
ReadBufferSize: 1024,
|
ReadBufferSize: 1024,
|
||||||
WriteBufferSize: 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
|
// SetAllowedOrigins replaces the default upgrader with one that validates
|
||||||
// the Origin header against a trusted set. Call once at startup.
|
// 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) {
|
func SetAllowedOrigins(origins []string) {
|
||||||
originSet := make(map[string]struct{}, len(origins))
|
originSet := make(map[string]struct{}, len(origins))
|
||||||
|
hostSet := make(map[string]struct{}, len(origins))
|
||||||
for _, o := range origins {
|
for _, o := range origins {
|
||||||
originSet[o] = struct{}{}
|
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{
|
upgrader = websocket.Upgrader{
|
||||||
ReadBufferSize: 1024,
|
ReadBufferSize: 1024,
|
||||||
@@ -37,10 +53,30 @@ func SetAllowedOrigins(origins []string) {
|
|||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
origin := r.Header.Get("Origin")
|
origin := r.Header.Get("Origin")
|
||||||
if origin == "" {
|
if origin == "" {
|
||||||
return false
|
return true // non-browser clients
|
||||||
}
|
}
|
||||||
_, ok := originSet[origin]
|
if _, ok := originSet[origin]; ok {
|
||||||
return 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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record activity for idle detection.
|
|
||||||
c.Hub.RecordActivity(c.UserID)
|
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 {
|
switch event.Type {
|
||||||
case EventTypingStart, EventPresenceUpdate:
|
case EventTypingStart, EventPresenceUpdate:
|
||||||
c.Hub.BroadcastEvent(event)
|
c.Hub.BroadcastEvent(event)
|
||||||
@@ -119,7 +152,6 @@ func (c *Client) writePump() {
|
|||||||
}
|
}
|
||||||
w.Write(message)
|
w.Write(message)
|
||||||
|
|
||||||
// Drain queued messages into the current write
|
|
||||||
n := len(c.send)
|
n := len(c.send)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
w.Write([]byte{'\n'})
|
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 {
|
type authMessage struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeWS handles websocket requests from the peer. It upgrades the
|
// ServeWS handles websocket requests from the peer.
|
||||||
// connection first, then waits for an auth message frame containing the
|
// It authenticates via cookie (browser) or message-frame (TUI/bots).
|
||||||
// session token. This avoids leaking the token in the URL (which would
|
|
||||||
// appear in server logs, browser history, and Referer headers).
|
|
||||||
func ServeWS(db *sql.DB, hub *Hub, logger *slog.Logger, w http.ResponseWriter, r *http.Request, cookieName string) {
|
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)
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -155,9 +185,7 @@ func ServeWS(db *sql.DB, hub *Hub, logger *slog.Logger, w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short deadline for the auth frame; we don't want unauthenticated
|
// Try cookie-based auth first (browser clients).
|
||||||
// connections hanging around consuming resources.
|
|
||||||
// Try cookie-based auth first (browser clients send cookies automatically).
|
|
||||||
var token string
|
var token string
|
||||||
if cookie, cookieErr := r.Cookie(cookieName); cookieErr == nil && cookie.Value != "" {
|
if cookie, cookieErr := r.Cookie(cookieName); cookieErr == nil && cookie.Value != "" {
|
||||||
token = cookie.Value
|
token = cookie.Value
|
||||||
@@ -195,10 +223,7 @@ func ServeWS(db *sql.DB, hub *Hub, logger *slog.Logger, w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth succeeded. Clear the auth deadline (readPump sets its own).
|
|
||||||
conn.SetReadDeadline(time.Time{})
|
conn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
// Send ready signal so the client knows auth passed.
|
|
||||||
conn.WriteMessage(websocket.TextMessage, []byte(`{"type":"ready"}`))
|
conn.WriteMessage(websocket.TextMessage, []byte(`{"type":"ready"}`))
|
||||||
|
|
||||||
client := &Client{
|
client := &Client{
|
||||||
|
|||||||
Reference in New Issue
Block a user