Files
pulse/internal/api/agent_report_decode.go
pulse-triage[bot] d083f50fbc Classify oversized agent reports correctly
Change-source: pulse-maintainer
2026-09-01 13:59:36 +01:00

86 lines
2.6 KiB
Go

package api
import (
"encoding/json"
"errors"
"io"
"net/http"
"strconv"
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
)
const (
agentReportSizeEncodedBody = "encoded_http_body"
agentReportSizeDecodedJSON = "decoded_json"
)
// decodeCompressedAgentReport applies independent encoded and decoded body
// limits and preserves the reason a report could not be decoded. Appliance
// inventories can be large and compress well, so the two limits are distinct.
func decodeCompressedAgentReport(
w http.ResponseWriter,
r *http.Request,
encodedLimit int64,
decodedLimit int64,
destination any,
) bool {
r.Body = http.MaxBytesReader(w, r.Body, encodedLimit)
defer r.Body.Close()
decompressed, err := utils.DecompressBodyIfGzipped(r, decodedLimit)
if err != nil {
var maxBytesErr *http.MaxBytesError
var encodingErr *utils.UnsupportedContentEncodingError
switch {
case errors.As(err, &maxBytesErr):
writeAgentReportSizeError(w, agentReportSizeEncodedBody, maxBytesErr.Limit)
case errors.As(err, &encodingErr):
writeErrorResponse(w, http.StatusUnsupportedMediaType, "unsupported_encoding", encodingErr.Error(), nil)
default:
writeErrorResponse(w, http.StatusBadRequest, "invalid_compression", "Failed to decompress request body", nil)
}
return false
}
defer decompressed.Close()
if err := json.NewDecoder(decompressed).Decode(destination); err != nil {
writeAgentReportDecodeError(w, err)
return false
}
// json.Decoder may finish a complete top-level object before it asks the
// reader for the proof byte above a body limit. Drain the bounded reader so
// compressed bombs and oversized bodies cannot bypass the cap that way.
if _, err := io.Copy(io.Discard, decompressed); err != nil {
writeAgentReportDecodeError(w, err)
return false
}
return true
}
func writeAgentReportDecodeError(w http.ResponseWriter, err error) {
var maxBytesErr *http.MaxBytesError
var decodedSizeErr *utils.DecompressedBodyTooLargeError
switch {
case errors.As(err, &maxBytesErr):
writeAgentReportSizeError(w, agentReportSizeEncodedBody, maxBytesErr.Limit)
case errors.As(err, &decodedSizeErr):
writeAgentReportSizeError(w, agentReportSizeDecodedJSON, decodedSizeErr.Limit)
default:
writeErrorResponse(w, http.StatusBadRequest, "invalid_json", "Failed to decode request body", map[string]string{"error": err.Error()})
}
}
func writeAgentReportSizeError(w http.ResponseWriter, dimension string, limit int64) {
writeErrorResponse(
w,
http.StatusRequestEntityTooLarge,
"report_too_large",
"Agent report exceeds the server size limit",
map[string]string{
"dimension": dimension,
"limitBytes": strconv.FormatInt(limit, 10),
},
)
}