96 lines
3.0 KiB
Go
96 lines
3.0 KiB
Go
// Package httpserver assembles the HTTP surface of the engine: the WebSocket
|
|
// upgrade endpoint, the static asset routes (the built SDK and the public files),
|
|
// and the /api control plane. It mirrors the routing of the original
|
|
// HTTPServer.js while adding timeouts and graceful shutdown (#25).
|
|
package httpserver
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"git.saqut.com/saqut/mwse/internal/config"
|
|
"git.saqut.com/saqut/mwse/internal/ws"
|
|
)
|
|
|
|
// New builds the *http.Server. WebSocket upgrades are detected on ANY path and
|
|
// routed to the engine (the SDK derives its endpoint from wherever the script was
|
|
// served, so the upgrade may arrive at "/" or "/script/"). All other requests go
|
|
// through the static/API mux.
|
|
func New(hub *ws.Hub, cfg config.Config) *http.Server {
|
|
wsServer := ws.NewServer(hub)
|
|
mux := http.NewServeMux()
|
|
|
|
registerAPI(mux, hub)
|
|
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:
|
|
//
|
|
// - /script -> the built SDK entry (script/index.js)
|
|
// - /script/<file> -> files under the script directory
|
|
// - / -> the SDK entry (so a bare visit returns the script)
|
|
// - /<file> -> a matching file under the public directory
|
|
// - anything else -> the status document (status.xml)
|
|
func registerStatic(mux *http.ServeMux, cfg config.Config) {
|
|
scriptIndex := filepath.Join(cfg.ScriptDir, "index.js")
|
|
statusDoc := filepath.Join(cfg.ScriptDir, "status.xml")
|
|
|
|
mux.HandleFunc("/script", func(w http.ResponseWriter, r *http.Request) {
|
|
http.ServeFile(w, r, scriptIndex)
|
|
})
|
|
mux.Handle("/script/", http.StripPrefix("/script/", http.FileServer(http.Dir(cfg.ScriptDir))))
|
|
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/" {
|
|
http.ServeFile(w, r, scriptIndex)
|
|
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
|
|
}
|