package httpapi import ( "bytes" "context" "errors" "io" "net" "net/http" "os" "strconv" "strings" ) const requestBodyLimit int64 = 8 << 20 // withRequestBodyLimit must run inside authMiddleware. Reading the complete // bounded body before dispatch prevents a valid JSON prefix from committing a // mutation before a later size/read error is discovered. Content-Type is not a // security boundary; CSV retains its additional row/field checks in its handler. func withRequestBodyLimit(next http.Handler) http.Handler { return withBodyLimitAndConcurrency(requestBodyLimit, requestBodyConcurrency(), next) } func withBodyLimit(limit int64, next http.Handler) http.Handler { return withBodyLimitAndConcurrency(limit, 4, next) } func requestBodyConcurrency() int { n, err := strconv.Atoi(strings.TrimSpace(os.Getenv("EVOBGP_API_BODY_MAX_CONCURRENT"))) if err == nil && n >= 1 && n <= 64 { return n } return 4 } func withBodyLimitAndConcurrency(limit int64, concurrency int, next http.Handler) http.Handler { slots := make(chan struct{}, concurrency) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { original := r.Body if original != nil { defer func() { _ = original.Close() }() } if r.ContentLength > limit { writeRequestBodyTooLarge(w) return } if r.Context().Err() != nil { writeProblem(w, http.StatusRequestTimeout, "Request Timeout", "request body read cancelled") return } if r.Body == nil || r.Body == http.NoBody { next.ServeHTTP(w, r) return } select { case slots <- struct{}{}: defer func() { <-slots }() default: w.Header().Set("Retry-After", "1") writeProblem(w, http.StatusTooManyRequests, "Too Many Requests", "request body processing capacity is full") return } bounded := http.MaxBytesReader(w, r.Body, limit) data, err := io.ReadAll(requestContextReader{ctx: r.Context(), reader: bounded}) var tooLarge *http.MaxBytesError if errors.As(err, &tooLarge) { writeRequestBodyTooLarge(w) return } var timeout net.Error if r.Context().Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || (errors.As(err, &timeout) && timeout.Timeout()) { writeProblem(w, http.StatusRequestTimeout, "Request Timeout", "request body read cancelled") return } if err != nil { writeProblem(w, http.StatusBadRequest, "Bad Request", "could not read request body") return } r.Body = io.NopCloser(bytes.NewReader(data)) r.ContentLength = int64(len(data)) next.ServeHTTP(w, r) }) } func writeRequestBodyTooLarge(w http.ResponseWriter) { writeProblem(w, http.StatusRequestEntityTooLarge, "Content Too Large", "request body exceeds the 8 MiB limit") } type requestContextReader struct { ctx context.Context reader io.Reader } func (r requestContextReader) Read(p []byte) (int, error) { if err := r.ctx.Err(); err != nil { return 0, err } return r.reader.Read(p) }