package httpapi import ( "encoding/json" "errors" "net/http" "time" "commilitia.net/cdrop/internal/hub" "commilitia.net/cdrop/internal/jwtauth" ) // 4 KB caps DoS-via-paste; longer payloads should use file transfer instead. const maxMessageBytes = 4 * 1024 // maxMessageBodyBytes bounds the raw request body BEFORE decode (R5): the 4 KB // limit above checks the decoded Go string, so on its own the whole body is // still read into memory first. The wire form can be larger than the decoded // text — JSON escaping inflates quotes/control chars (worst case ~6×) — so the // body cap is the content cap with headroom, not equal to it, to avoid falsely // rejecting a legitimate 4 KB message while still bounding memory per request. const maxMessageBodyBytes = maxMessageBytes * 8 type messageReq struct { To string `json:"to"` Text string `json:"text"` } // handleMessage forwards a one-shot text payload to a peer device. Reuses the // SSE Hub the same way /api/hub/signal does — no DB row, no transfer state // machine. Receiver sees a `message` event; the frontend keeps it in memory // only until the tab closes (per user spec). // // 204 if delivered, 410 if peer offline, 413 if oversized. func (s *Server) handleMessage(w http.ResponseWriter, r *http.Request) { claims, _ := jwtauth.ClaimsFromContext(r.Context()) from, _ := jwtauth.DeviceNameFromContext(r.Context()) r.Body = http.MaxBytesReader(w, r.Body, maxMessageBodyBytes) var req messageReq 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": "message exceeds 4 KB"}) 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.Text == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "empty text"}) return } if len(req.Text) > maxMessageBytes { writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "message exceeds 4 KB"}) return } if req.To == from { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "cannot send to self"}) return } delivered := s.hub.SendTo(claims.UserID, req.To, hub.Event{ Type: "message", Data: map[string]any{ "from": from, "text": req.Text, "sent_at": time.Now().Unix(), }, }) if !delivered { writeJSON(w, http.StatusGone, map[string]string{"error": "peer offline"}) return } w.WriteHeader(http.StatusNoContent) }