Files
job-search/internal/websocket/http.go
T
bwilliams c9f42b62ad Added websocket hub for handling websocket clients, with a wsserve command
which runs a plain TCP server for receiving websocket hub clients, with
server config, CSRF and rate limit middleware.

- internal/websocket: Hub pushing messages to WebSocket clients (ServeHTTP,
  example.com/ws upgrade with optional CSRF check) and plain TCP clients (ServeTCP,
  newline-delimited JSON envelopes); ping/pong keepalive, per-client message
  rate limit, slow consumer handling, graceful Shutdown, debug logging of
  messages sent and received
- internal/websocket/tcpserver: standalone TCP server for a hub, with accept
  back-off, a per-IP connection limit (server.websocket.connection_limit) and
  graceful shutdown
- internal/cli/wsserve: `wsserve` command running the TCP server, with
  --listen/--host/--port overrides
- internal/middleware: keyed token bucket rate limiter, real client IP from
  trusted proxies
- internal/httpx: RFC 9457 problem responses, request IDs in context
- internal/config: server section (host, port, shutdown timeout, rate limiter
  idle TTL, websocket); util.ByteSize settings (e.g. "32KiB"); lists of
  sections as single keys; empty lists stored as nil; isExplicitlySet for
  printLoadedConfig; JobSearchConfigSource renamed to JSCSourcesSource;
  TestSections checks every section's methods
- internal/util: ByteSize, parsed and formatted with go-humanize
2026-10-14 15:55:25 +01:00

45 lines
1.2 KiB
Go

package websocket
import (
"context"
"net/http"
"git.avsdev.uk/Go/job-search/internal/middleware"
ws "github.com/coder/websocket"
)
// ServeHTTP upgrades an http connection and runs the client until it closes.
func (h *Hub) ServeHTTP(w http.ResponseWriter, r *http.Request) {
mode := ws.CompressionDisabled
if h.conf.EnableCompression {
mode = ws.CompressionContextTakeover
}
// Accept enforces the Origin check: same host, or one of OriginPatterns.
// That is the defence against cross-site WebSocket hijacking.
conn, err := ws.Accept(w, r, &ws.AcceptOptions{
CompressionMode: mode,
})
if err != nil {
h.log.Debug("upgrade failed", "error", err)
return // Accept has already written the HTTP error.
}
defer conn.CloseNow() //nolint:errcheck // finished with; it may already be closed
conn.SetReadLimit(int64(h.maxMessageSize()))
ip := middleware.ClientIP(r)
ctx, cancel := context.WithCancel(r.Context())
c := newClient(h, conn, ip, cancel)
if !h.register(c) {
cancel()
conn.Close(ws.StatusGoingAway, "server shutting down") //nolint:errcheck,gosec // turned away
return
}
defer h.unregister(c)
h.log.Info("client connected", "client", c.id, "ip", ip)
c.run(ctx)
h.log.Info("client disconnected", "client", c.id, "ip", ip)
}