238 lines
6.9 KiB
Go
238 lines
6.9 KiB
Go
package httpserver
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"git.saqut.com/saqut/mwse/internal/bridge"
|
|
"git.saqut.com/saqut/mwse/internal/ws"
|
|
)
|
|
|
|
// apiKeyStore holds the issued server-to-server API keys. The original kept these
|
|
// in a process-local Map; this matches that (keys do not survive a restart).
|
|
type apiKeyStore struct {
|
|
mu sync.RWMutex
|
|
keys map[string]string // key -> domain
|
|
}
|
|
|
|
func newAPIKeyStore() *apiKeyStore {
|
|
return &apiKeyStore{keys: make(map[string]string)}
|
|
}
|
|
|
|
func (s *apiKeyStore) issue(domain string) string {
|
|
key := newToken()
|
|
s.mu.Lock()
|
|
s.keys[key] = domain
|
|
s.mu.Unlock()
|
|
return key
|
|
}
|
|
|
|
func (s *apiKeyStore) domain(key string) (string, bool) {
|
|
s.mu.RLock()
|
|
d, ok := s.keys[key]
|
|
s.mu.RUnlock()
|
|
return d, ok
|
|
}
|
|
|
|
// auth wraps a handler with x-api-key validation, passing the caller's domain on
|
|
// the request context-free closure argument.
|
|
func (s *apiKeyStore) auth(next func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
key := r.Header.Get("x-api-key")
|
|
if key == "" {
|
|
writeJSON(w, http.StatusUnauthorized, fail("API_KEY_REQUIRED"))
|
|
return
|
|
}
|
|
domain, ok := s.domain(key)
|
|
if !ok {
|
|
writeJSON(w, http.StatusUnauthorized, fail("INVALID_API_KEY"))
|
|
return
|
|
}
|
|
next(w, r, domain)
|
|
}
|
|
}
|
|
|
|
// registerAPI mounts the /api control-plane routes onto mux. It ports the read
|
|
// endpoints and the core server-initiated messaging endpoints from api.js. The
|
|
// server-as-room-participant (join/leave) and webhook endpoints are intentionally
|
|
// deferred to feature-parity work (see REVIEW.md).
|
|
//
|
|
// When bridgeInbox is non-nil, the bridge drain endpoint is also registered:
|
|
// - POST /api/bridge/inbox — drain all queued client→app messages (#46)
|
|
func registerAPI(mux *http.ServeMux, hub *ws.Hub, bridgeInbox *bridge.Inbox) {
|
|
keys := newAPIKeyStore()
|
|
|
|
mux.HandleFunc("POST /api/auth/key", func(w http.ResponseWriter, r *http.Request) {
|
|
var body struct {
|
|
Domain string `json:"domain"`
|
|
}
|
|
if !decode(w, r, &body) {
|
|
return
|
|
}
|
|
if body.Domain == "" {
|
|
writeJSON(w, http.StatusOK, fail("DOMAIN_REQUIRED"))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "success", "key": keys.issue(body.Domain)})
|
|
})
|
|
|
|
mux.HandleFunc("GET /api/rooms", func(w http.ResponseWriter, r *http.Request) {
|
|
rooms := make([]map[string]any, 0)
|
|
for _, room := range hub.Rooms() {
|
|
rooms = append(rooms, map[string]any{
|
|
"id": room.ID,
|
|
"name": room.Name,
|
|
"accessType": room.AccessType,
|
|
"joinType": room.JoinType,
|
|
"description": room.Description,
|
|
"clientCount": room.Size(),
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "success", "rooms": rooms})
|
|
})
|
|
|
|
mux.HandleFunc("GET /api/clients", func(w http.ResponseWriter, r *http.Request) {
|
|
clients := make([]map[string]any, 0)
|
|
for _, c := range hub.Clients() {
|
|
clients = append(clients, map[string]any{
|
|
"id": c.ID,
|
|
"rooms": c.Rooms(),
|
|
"pairs": c.Pairs(),
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "success", "clients": clients})
|
|
})
|
|
|
|
mux.HandleFunc("GET /api/room/{id}", func(w http.ResponseWriter, r *http.Request) {
|
|
room, ok := hub.Room(r.PathValue("id"))
|
|
if !ok {
|
|
writeJSON(w, http.StatusOK, fail("ROOM_NOT_FOUND"))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "success", "room": room.ToJSON(false)})
|
|
})
|
|
|
|
mux.HandleFunc("POST /api/client/{id}/send", keys.auth(func(w http.ResponseWriter, r *http.Request, domain string) {
|
|
var body struct {
|
|
Pack any `json:"pack"`
|
|
}
|
|
if !decode(w, r, &body) {
|
|
return
|
|
}
|
|
client, ok := hub.Client(r.PathValue("id"))
|
|
if !ok {
|
|
writeJSON(w, http.StatusOK, fail("CLIENT_NOT_FOUND"))
|
|
return
|
|
}
|
|
if body.Pack == nil {
|
|
writeJSON(w, http.StatusOK, fail("PACK_REQUIRED"))
|
|
return
|
|
}
|
|
client.Signal("server/pack", map[string]any{"from": "server", "fromServer": domain, "pack": body.Pack})
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "success"})
|
|
}))
|
|
|
|
mux.HandleFunc("POST /api/room/{id}/send", keys.auth(func(w http.ResponseWriter, r *http.Request, domain string) {
|
|
var body struct {
|
|
Pack any `json:"pack"`
|
|
Wom bool `json:"wom"`
|
|
}
|
|
if !decode(w, r, &body) {
|
|
return
|
|
}
|
|
id := r.PathValue("id")
|
|
room, ok := hub.Room(id)
|
|
if !ok {
|
|
writeJSON(w, http.StatusOK, fail("ROOM_NOT_FOUND"))
|
|
return
|
|
}
|
|
if body.Pack == nil {
|
|
writeJSON(w, http.StatusOK, fail("PACK_REQUIRED"))
|
|
return
|
|
}
|
|
room.Broadcast(
|
|
"server/pack/room",
|
|
map[string]any{"from": "server", "fromServer": domain, "pack": body.Pack, "roomId": id},
|
|
"",
|
|
nil,
|
|
)
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "success"})
|
|
}))
|
|
|
|
mux.HandleFunc("POST /api/room/create", keys.auth(func(w http.ResponseWriter, r *http.Request, domain string) {
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
AccessType string `json:"accessType"`
|
|
JoinType string `json:"joinType"`
|
|
Description string `json:"description"`
|
|
Credential string `json:"credential"`
|
|
}
|
|
if !decode(w, r, &body) {
|
|
return
|
|
}
|
|
if body.Name == "" {
|
|
writeJSON(w, http.StatusOK, fail("NAME_REQUIRED"))
|
|
return
|
|
}
|
|
if _, exists := hub.RoomByName(body.Name); exists {
|
|
writeJSON(w, http.StatusOK, fail("ROOM_ALREADY_EXISTS"))
|
|
return
|
|
}
|
|
room := ws.NewRoom(hub)
|
|
room.Name = body.Name
|
|
room.AccessType = orDefault(body.AccessType, "public")
|
|
room.JoinType = orDefault(body.JoinType, "free")
|
|
room.Description = body.Description
|
|
room.OwnerID = "server"
|
|
if body.Credential != "" {
|
|
room.Credential = sha256hex(body.Credential)
|
|
}
|
|
room.Publish()
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "success", "room": room.ToJSON(false)})
|
|
}))
|
|
|
|
// Bridge endpoints (#46) — only registered when the inbox is configured.
|
|
if bridgeInbox != nil {
|
|
// POST /api/bridge/inbox drains all queued client→app messages atomically.
|
|
// The application server polls this to receive messages sent via bridge/send.
|
|
mux.HandleFunc("POST /api/bridge/inbox", keys.auth(func(w http.ResponseWriter, r *http.Request, domain string) {
|
|
msgs := bridgeInbox.Drain()
|
|
if msgs == nil {
|
|
msgs = []bridge.Message{}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"status": "success", "messages": msgs})
|
|
}))
|
|
}
|
|
}
|
|
|
|
func orDefault(v, def string) string {
|
|
if v == "" {
|
|
return def
|
|
}
|
|
return v
|
|
}
|
|
|
|
func fail(message string) map[string]any {
|
|
return map[string]any{"status": "fail", "message": message}
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, body any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(body)
|
|
}
|
|
|
|
// decode reads a JSON request body, writing a fail response and returning false
|
|
// when the body is malformed.
|
|
func decode(w http.ResponseWriter, r *http.Request, dst any) bool {
|
|
if r.Body == nil {
|
|
return true
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(dst); err != nil && err.Error() != "EOF" {
|
|
writeJSON(w, http.StatusBadRequest, fail("INVALID_JSON"))
|
|
return false
|
|
}
|
|
return true
|
|
}
|