feat(01-02): add request body size limits (1MB) to webhook and tag handlers
- Add maxBodyBytes constant (1 << 20 = 1 MB) - Add errors import to production file - Apply http.MaxBytesReader + errors.As(err, *http.MaxBytesError) pattern in: WebhookHandler, TagsHandler POST, TagAssignmentHandler PUT and DELETE - Return HTTP 413 RequestEntityTooLarge when body exceeds limit - Fix oversized body test strategy: use JSON prefix so decoder reads past limit (Rule 1 deviation: all-x body fails at byte 1 before MaxBytesReader triggers)
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -14,6 +15,8 @@ import (
|
|||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxBodyBytes = 1 << 20 // 1 MB
|
||||||
|
|
||||||
type DiunEvent struct {
|
type DiunEvent struct {
|
||||||
DiunVersion string `json:"diun_version"`
|
DiunVersion string `json:"diun_version"`
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
@@ -199,9 +202,15 @@ func WebhookHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
|
||||||
var event DiunEvent
|
var event DiunEvent
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
|
||||||
|
var maxBytesErr *http.MaxBytesError
|
||||||
|
if errors.As(err, &maxBytesErr) {
|
||||||
|
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Printf("WebhookHandler: failed to decode request: %v", err)
|
log.Printf("WebhookHandler: failed to decode request: %v", err)
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -296,10 +305,20 @@ func TagsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
var maxBytesErr *http.MaxBytesError
|
||||||
|
if errors.As(err, &maxBytesErr) {
|
||||||
|
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "bad request: name required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Name == "" {
|
||||||
http.Error(w, "bad request: name required", http.StatusBadRequest)
|
http.Error(w, "bad request: name required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -358,11 +377,21 @@ func TagByIDHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
func TagAssignmentHandler(w http.ResponseWriter, r *http.Request) {
|
func TagAssignmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
|
||||||
var req struct {
|
var req struct {
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
TagID int `json:"tag_id"`
|
TagID int `json:"tag_id"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Image == "" {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
var maxBytesErr *http.MaxBytesError
|
||||||
|
if errors.As(err, &maxBytesErr) {
|
||||||
|
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Image == "" {
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -383,10 +412,20 @@ func TagAssignmentHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
|
||||||
var req struct {
|
var req struct {
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Image == "" {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
var maxBytesErr *http.MaxBytesError
|
||||||
|
if errors.As(err, &maxBytesErr) {
|
||||||
|
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Image == "" {
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -614,11 +614,12 @@ func TestGetUpdates_IncludesTag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestWebhookHandler_OversizedBody(t *testing.T) {
|
func TestWebhookHandler_OversizedBody(t *testing.T) {
|
||||||
// Generate a body that exceeds 1 MB (maxBodyBytes = 1<<20 = 1,048,576 bytes)
|
// Generate a body that exceeds 1 MB (maxBodyBytes = 1<<20 = 1,048,576 bytes).
|
||||||
oversized := make([]byte, 1<<20+1)
|
// Use a valid JSON prefix so the decoder reads past the limit before failing,
|
||||||
for i := range oversized {
|
// ensuring MaxBytesReader triggers a 413 rather than a JSON parse 400.
|
||||||
oversized[i] = 'x'
|
prefix := []byte(`{"image":"`)
|
||||||
}
|
padding := bytes.Repeat([]byte("x"), 1<<20+1)
|
||||||
|
oversized := append(prefix, padding...)
|
||||||
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(oversized))
|
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(oversized))
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
diun.WebhookHandler(rec, req)
|
diun.WebhookHandler(rec, req)
|
||||||
@@ -628,10 +629,9 @@ func TestWebhookHandler_OversizedBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTagsHandler_OversizedBody(t *testing.T) {
|
func TestTagsHandler_OversizedBody(t *testing.T) {
|
||||||
oversized := make([]byte, 1<<20+1)
|
prefix := []byte(`{"name":"`)
|
||||||
for i := range oversized {
|
padding := bytes.Repeat([]byte("x"), 1<<20+1)
|
||||||
oversized[i] = 'x'
|
oversized := append(prefix, padding...)
|
||||||
}
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/tags", bytes.NewReader(oversized))
|
req := httptest.NewRequest(http.MethodPost, "/api/tags", bytes.NewReader(oversized))
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
diun.TagsHandler(rec, req)
|
diun.TagsHandler(rec, req)
|
||||||
@@ -641,10 +641,9 @@ func TestTagsHandler_OversizedBody(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTagAssignmentHandler_OversizedBody(t *testing.T) {
|
func TestTagAssignmentHandler_OversizedBody(t *testing.T) {
|
||||||
oversized := make([]byte, 1<<20+1)
|
prefix := []byte(`{"image":"`)
|
||||||
for i := range oversized {
|
padding := bytes.Repeat([]byte("x"), 1<<20+1)
|
||||||
oversized[i] = 'x'
|
oversized := append(prefix, padding...)
|
||||||
}
|
|
||||||
req := httptest.NewRequest(http.MethodPut, "/api/tag-assignments", bytes.NewReader(oversized))
|
req := httptest.NewRequest(http.MethodPut, "/api/tag-assignments", bytes.NewReader(oversized))
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
diun.TagAssignmentHandler(rec, req)
|
diun.TagAssignmentHandler(rec, req)
|
||||||
|
|||||||
Reference in New Issue
Block a user