body

package
v0.0.0-...-a19e008 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package body provides body-handling primitives for the openappsec Caddy attachment: request-body decompression for inspection, chunking of the decompressed stream into protocol BODY_CHUNK payloads, response-side buffering with a passthrough policy, and response re-compression.

The HTTP handler wave (not this package) orchestrates the primitives:

decompress -> chunk -> submit via app -> buffer response -> recompress -> write

Every type here is a pure, goroutine-free state machine: a single caller owns each instance. No caddy.Context and no http.ResponseWriter appears in this package by design.

Index

Constants

View Source
const ChunkSize = 60000

ChunkSize is the payload size of one BODY_CHUNK frame, in bytes.

Justification (docs/attachment-protocol.md §D.3, §E.8): the ring queue caps a single message at max_write_size = 0xfffc = 65532 bytes, and the BODY frame adds 8 bytes of framing on top of the payload (uint16 data_type, uint32 session_id, uint8 is_last_chunk, uint8 part_count), so the largest legal payload is 65524 bytes. 60000 is a conservative choice: it fits any frame well inside the ring cap, leaves headroom for future framing growth, and keeps per-chunk allocations modest.

View Source
const DefaultResponseBufferCap = 4 * 1024 * 1024

DefaultResponseBufferCap is the buffer cap used when the caller has no configured value. It mirrors config.DefaultResponseBufferLimit (4 MiB, internal/config/config.go), the response-side inspection limit of the Caddy handler config.

There is no response-cap field in EngineConfig (internal/config/engine.go); the config task owner should wire HandlerConfig.ResponseBufferLimit (json: response_buffer_limit) through to NewResponseBuffer when the handler wave lands.

Variables

View Source
var ErrUnsupportedEncoding = errors.New("body: unsupported Content-Encoding")

ErrUnsupportedEncoding is returned by OpenRequestBody when the declared Content-Encoding cannot be decoded. The handler decides how to fall back; OpenRequestBody never silently produces corrupt data.

Brotli ("br"/"brotli") is [UNVERIFIED]: Go's standard library has no brotli decoder, and the brotli package of the only in-graph brotli-capable module (github.com/klauspost/compress) is not obtainable in this environment, so this build reports brotli as unsupported rather than misdecoding it. The nginx reference gates brotli inspection behind is_brotli_inspection_enabled (default off) for the same practical reason.

Functions

func OpenRequestBody

func OpenRequestBody(r io.Reader, encoding string) (io.ReadCloser, error)

OpenRequestBody wraps r with the decoder matching the declared Content-Encoding. It returns an io.ReadCloser whose Close releases decoder resources. Supported encodings:

  • "" or "identity": passthrough (no decoder)
  • "gzip": compress/gzip
  • "deflate": compress/zlib (the zlib wrapper is what HTTP servers commonly emit for Content-Encoding: deflate)

The encoding is matched case-insensitively with surrounding whitespace trimmed. Any other value, including "br"/"brotli" in this build, returns an error wrapping ErrUnsupportedEncoding; the caller must not fall back to reading the stream undecoded, because that would submit compressed bytes to the inspection engine as if they were plain text.

func Recompress

func Recompress(body []byte, acceptEncoding string) ([]byte, string, error)

Recompress re-encodes body according to the client's Accept-Encoding header and returns the encoded bytes together with the Content-Encoding value the caller must set on the response.

Negotiation is gzip-preferred with identity fallback: gzip is used when the header accepts it (explicitly with q > 0, or via a wildcard "*" when gzip is not explicitly listed); otherwise the body is returned unchanged with encoding "identity". Brotli recompression is [UNVERIFIED] and never selected; a client that accepts only "br" receives identity, which every HTTP client must accept.

Types

type Chunker

type Chunker struct {
	// contains filtered or unexported fields
}

Chunker splits an io.Reader into payloads of at most ChunkSize bytes, sized to wrap directly in a protocol.BodyChunk frame. The caller owns framing: it sets IsLastChunk on the chunk returned by the final successful Next call and PartCount from its own counter.

A Chunker is single-goroutine: one caller drives it to completion.

func NewChunker

func NewChunker(r io.Reader) *Chunker

NewChunker returns a Chunker reading from r. A nil reader is not allowed; pass an empty reader for an empty body.

func (*Chunker) Next

func (c *Chunker) Next() ([]byte, error)

Next returns the next chunk of at most ChunkSize bytes, or io.EOF when the stream is exhausted. An empty body yields io.EOF on the first call (zero chunks). A stream whose length is an exact multiple of ChunkSize yields all full chunks and then io.EOF — Next never reports EOF on the call that returned the final full chunk.

If the underlying reader fails mid-stream, Next returns that error and the Chunker is finished: any bytes read before the error are discarded, the caller should abort the session (the body is incomplete), and subsequent calls keep returning the same error.

The returned slice is owned by the Chunker and is only valid until the next call to Next; the caller must not retain or modify it.

type ResponseBuffer

type ResponseBuffer struct {
	// contains filtered or unexported fields
}

ResponseBuffer is the response-side buffering policy state machine. It buffers the response body up to a cap so it can be inspected and re-emitted after the verdict, and it switches to passthrough — never buffering again — once any of these conditions holds:

  • the declared Content-Length exceeds the cap (SetContentLength pre-check)
  • buffered bytes exceed the cap mid-stream (Write transition)
  • the content type is text/event-stream (MarkSSE)
  • the cap is zero or negative (constructed already in passthrough)

When the mid-stream transition fires, Write returns fewer bytes than it received; the caller must forward Buffered() followed by the remainder of that write, then stream everything after. A ResponseBuffer is single-goroutine: one caller drives it.

func NewResponseBuffer

func NewResponseBuffer(cap int) *ResponseBuffer

NewResponseBuffer returns a ResponseBuffer that stops buffering once len(buf) would exceed cap. A cap <= 0 starts the buffer in passthrough state (never buffers).

func (*ResponseBuffer) Buffered

func (rb *ResponseBuffer) Buffered() []byte

Buffered returns the bytes accumulated so far, in order. The returned slice is owned by the buffer and must not be modified; it is valid until the next Write.

func (*ResponseBuffer) Len

func (rb *ResponseBuffer) Len() int

Len returns the number of buffered bytes.

func (*ResponseBuffer) MarkSSE

func (rb *ResponseBuffer) MarkSSE()

MarkSSE forces passthrough for text/event-stream responses, which must stream to the client unmodified and unbuffered.

func (*ResponseBuffer) PassThrough

func (rb *ResponseBuffer) PassThrough() bool

PassThrough reports whether the buffer is in passthrough state. Once true it stays true.

func (*ResponseBuffer) SetContentLength

func (rb *ResponseBuffer) SetContentLength(n int64)

SetContentLength performs the Content-Length pre-check: if the response's declared length exceeds the cap, the buffer transitions to passthrough immediately without buffering. A length within the cap keeps buffering; the byte-level check in Write still guards against a lying Content-Length. Call it once with the parsed header value before the first Write.

func (*ResponseBuffer) Write

func (rb *ResponseBuffer) Write(p []byte) (int, error)

Write appends p to the buffer while in buffering state and returns the number of bytes accepted. While in passthrough state it accepts nothing and returns len(p) — the caller streams p directly.

If appending p would push the buffered total past the cap, Write buffers only the prefix that fits, transitions to passthrough, and returns that prefix length; the caller must then forward Buffered() and the rest of p. The transition fires at most once.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL