package httpapi import ( "encoding/json" "errors" "net/http" "commilitia.net/cdrop/internal/hub" "commilitia.net/cdrop/internal/jwtauth" ) // maxSignalBytes caps the signaling body. WebRTC SDP offers/answers and bundled // ICE candidates are at most a few KB; the payload is an opaque json.RawMessage // with no other length check, so without this a single request could stream an // unbounded body into memory (R4). 64 KiB leaves generous headroom for fat SDP. const maxSignalBytes = 64 * 1024 type signalReq struct { To string `json:"to"` Payload json.RawMessage `json:"payload"` } // handleSignal forwards WebRTC offer/answer/ICE candidates between same-user devices. // Body: { to: deviceName, payload: any-json } // 204 if delivered to live SSE; 410 if peer offline. func (s *Server) handleSignal(w http.ResponseWriter, r *http.Request) { claims, _ := jwtauth.ClaimsFromContext(r.Context()) from, _ := jwtauth.DeviceNameFromContext(r.Context()) r.Body = http.MaxBytesReader(w, r.Body, maxSignalBytes) var req signalReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { var mbe *http.MaxBytesError if errors.As(err, &mbe) { writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "signal payload too large"}) return } writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"}) return } if req.To == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing 'to'"}) return } if req.To == from { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "cannot signal self"}) return } delivered := s.hub.SendTo(claims.UserID, req.To, hub.Event{ Type: "signal", Data: map[string]any{ "from": from, "payload": req.Payload, }, }) if !delivered { writeJSON(w, http.StatusGone, map[string]string{"error": "peer offline"}) return } w.WriteHeader(http.StatusNoContent) }