check / check (push) Failing after 1s
A buffer failure on POST /api/v1/reports now returns 500 instead of a
false {"status":"ok"}, so clients can retry. Decode errors are split:
an over-limit body returns 413 (via errors.As on *http.MaxBytesError),
malformed JSON stays 400. A new MaxBodyBytes middleware caps every
route (1 MiB default; rejects an oversized Content-Length up-front and
caps the read otherwise), replacing the per-route reader so the health
check and future routes are bounded too. The raw attacker-controlled
geo blob is no longer logged — only its byte length — and client_id and
timestamp are length-bounded before logging. A decodeJSON handler helper
is added per the HTTP server conventions. Panic recovery is now a local
middleware that routes the stack through slog as structured JSON rather
than chi's plain-text stderr. Error bodies still leak nothing internal.
Chosen failure code for a storage failure: 500, since a full buffer or
write error is server-side and retryable, not the client's fault.
Model: opus-4-8
115 lines
2.7 KiB
Go
115 lines
2.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
)
|
|
|
|
// maxLoggedFieldBytes bounds untrusted string fields before they
|
|
// are logged, so a caller cannot inflate log volume with an
|
|
// oversized value.
|
|
const maxLoggedFieldBytes = 128
|
|
|
|
type reportSample struct {
|
|
T int64 `json:"t"`
|
|
Latency *int `json:"latency"`
|
|
Error *string `json:"error"`
|
|
}
|
|
|
|
type reportHost struct {
|
|
History []reportSample `json:"history"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
type report struct {
|
|
ClientID string `json:"clientId"`
|
|
Geo json.RawMessage `json:"geo"`
|
|
Hosts []reportHost `json:"hosts"`
|
|
Timestamp string `json:"timestamp"`
|
|
}
|
|
|
|
// HandleReport returns a handler that accepts telemetry
|
|
// reports from NetWatch clients.
|
|
func (s *Handlers) HandleReport() http.HandlerFunc {
|
|
type response struct {
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var rpt report
|
|
|
|
err := s.decodeJSON(w, r, &rpt)
|
|
if err != nil {
|
|
s.respondJSON(w, r,
|
|
&response{Status: "error"},
|
|
s.decodeErrorStatus(err),
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
s.logReportReceived(rpt)
|
|
|
|
err = s.buf.Append(rpt)
|
|
if err != nil {
|
|
s.log.Error("failed to buffer report", "error", err)
|
|
s.respondJSON(w, r,
|
|
&response{Status: "error"},
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
s.respondJSON(w, r, &response{Status: "ok"}, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// decodeErrorStatus logs a report decode failure and returns the
|
|
// status to send: 413 when the body exceeded the size limit,
|
|
// otherwise 400 for malformed JSON.
|
|
func (s *Handlers) decodeErrorStatus(err error) int {
|
|
var tooLarge *http.MaxBytesError
|
|
if errors.As(err, &tooLarge) {
|
|
s.log.Warn("report body too large", "limit_bytes", tooLarge.Limit)
|
|
|
|
return http.StatusRequestEntityTooLarge
|
|
}
|
|
|
|
s.log.Error("failed to decode report", "error", err)
|
|
|
|
return http.StatusBadRequest
|
|
}
|
|
|
|
// logReportReceived logs an accepted report. Untrusted fields are
|
|
// bounded (client_id, timestamp) or reduced to a length
|
|
// (geo_bytes) so the raw attacker-controlled body never reaches
|
|
// the log.
|
|
func (s *Handlers) logReportReceived(rpt report) {
|
|
totalSamples := 0
|
|
for _, h := range rpt.Hosts {
|
|
totalSamples += len(h.History)
|
|
}
|
|
|
|
s.log.Info("report received",
|
|
"client_id", boundedForLog(rpt.ClientID),
|
|
"timestamp", boundedForLog(rpt.Timestamp),
|
|
"host_count", len(rpt.Hosts),
|
|
"total_samples", totalSamples,
|
|
"geo_bytes", len(rpt.Geo),
|
|
)
|
|
}
|
|
|
|
// boundedForLog truncates an untrusted string to a fixed byte
|
|
// bound so an attacker-controlled field cannot dominate the log.
|
|
func boundedForLog(s string) string {
|
|
if len(s) > maxLoggedFieldBytes {
|
|
return s[:maxLoggedFieldBytes]
|
|
}
|
|
|
|
return s
|
|
}
|