118 lines
3.8 KiB
Go
118 lines
3.8 KiB
Go
// Package httpserver assembles the HTTP surface of the engine: the WebSocket
|
|
// upgrade endpoint, the ES-module SDK routes, the public asset directory, and
|
|
// the /api control plane.
|
|
package httpserver
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"git.saqut.com/saqut/mwse/internal/bridge"
|
|
"git.saqut.com/saqut/mwse/internal/config"
|
|
"git.saqut.com/saqut/mwse/internal/ws"
|
|
)
|
|
|
|
// ServerOptions holds optional wiring for httpserver.New.
|
|
type ServerOptions struct {
|
|
// BridgeInbox, when non-nil, enables POST /api/bridge/inbox so an application
|
|
// server can drain client messages routed via bridge/send (#46).
|
|
BridgeInbox *bridge.Inbox
|
|
// Approver, when non-nil, gates each incoming WebSocket connection by asking
|
|
// the approver before the HTTP upgrade (#46).
|
|
Approver ws.Approver
|
|
}
|
|
|
|
// New builds the *http.Server. WebSocket upgrades are detected on any path and
|
|
// routed to the engine; all other requests go through the static/API mux.
|
|
func New(hub *ws.Hub, cfg config.Config, srvOpts ...ServerOptions) *http.Server {
|
|
var so ServerOptions
|
|
if len(srvOpts) > 0 {
|
|
so = srvOpts[0]
|
|
}
|
|
wsServer := ws.NewServer(hub, ws.Options{
|
|
OutboundBuffer: cfg.Conn.OutboundBuffer,
|
|
MaxMessageSize: cfg.Conn.MaxMessageSize,
|
|
ReadBufferSize: cfg.Conn.ReadBufferSize,
|
|
WriteBufferSize: cfg.Conn.WriteBufferSize,
|
|
PingInterval: cfg.Conn.PingInterval,
|
|
PongWait: cfg.Conn.PongWait,
|
|
WriteWait: cfg.Conn.WriteWait,
|
|
Approver: so.Approver,
|
|
})
|
|
mux := http.NewServeMux()
|
|
|
|
registerAPI(mux, hub, so.BridgeInbox)
|
|
registerStatic(mux, cfg)
|
|
|
|
root := func(w http.ResponseWriter, r *http.Request) {
|
|
if websocket.IsWebSocketUpgrade(r) {
|
|
wsServer.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
mux.ServeHTTP(w, r)
|
|
}
|
|
|
|
return &http.Server{
|
|
Addr: cfg.Addr(),
|
|
Handler: http.HandlerFunc(root),
|
|
ReadHeaderTimeout: cfg.ReadHeaderTimeout,
|
|
}
|
|
}
|
|
|
|
// registerStatic wires the asset routes:
|
|
//
|
|
// - /sdk.js -> 301 /sdk/index.js (import.meta.url resolves correctly for relative imports)
|
|
// - /sdk/ -> ES-module SDK files served from cfg.SDKDir
|
|
// - / -> /sdk/index.js redirect (bare URL returns the SDK entry)
|
|
// - /<file> -> matching file under cfg.PublicDir
|
|
// - anything -> public/status.xml fallback
|
|
func registerStatic(mux *http.ServeMux, cfg config.Config) {
|
|
statusDoc := filepath.Join(cfg.PublicDir, "status.xml")
|
|
|
|
// /sdk.js → /sdk/index.js: keeps import.meta.url = /sdk/index.js so that
|
|
// ./EventTarget.js etc. resolve to /sdk/EventTarget.js (same origin).
|
|
mux.HandleFunc("/sdk.js", func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/sdk/index.js", http.StatusMovedPermanently)
|
|
})
|
|
mux.Handle("/sdk/", http.StripPrefix("/sdk/", http.FileServer(http.Dir(cfg.SDKDir))))
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/" {
|
|
http.Redirect(w, r, "/sdk/index.js", http.StatusFound)
|
|
return
|
|
}
|
|
if f, ok := safePublicFile(cfg.PublicDir, r.URL.Path); ok {
|
|
http.ServeFile(w, r, f)
|
|
return
|
|
}
|
|
http.ServeFile(w, r, statusDoc)
|
|
})
|
|
}
|
|
|
|
// safePublicFile resolves urlPath to a regular file under publicDir, guarding
|
|
// against path traversal. It returns ok=false when no such file exists.
|
|
func safePublicFile(publicDir, urlPath string) (string, bool) {
|
|
clean := filepath.Clean("/" + strings.TrimPrefix(urlPath, "/"))
|
|
full := filepath.Join(publicDir, clean)
|
|
|
|
absPublic, err := filepath.Abs(publicDir)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
absFull, err := filepath.Abs(full)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
if absFull != absPublic && !strings.HasPrefix(absFull, absPublic+string(os.PathSeparator)) {
|
|
return "", false // escaped the public directory
|
|
}
|
|
if fi, err := os.Stat(absFull); err == nil && !fi.IsDir() {
|
|
return absFull, true
|
|
}
|
|
return "", false
|
|
}
|