Module: Sentiero::Web::BodyReader

Defined in:
lib/sentiero/web/body_reader.rb

Overview

Reads a request body (optionally gzip-encoded) with a hard cap (default 512KB, config.max_body_size) on BOTH the compressed and decompressed size, so a gzip bomb can't blow past it. Shared by the two untrusted-input lanes (EventsApp, IngestApp), which differ only in the response they build from the error symbol.

Constant Summary collapse

MAX_BODY_SIZE =

512 KB

524_288
ERRORS =

error symbol => [http_status, message]

{
  too_large: [413, "request body too large"],
  bad_gzip: [400, "invalid gzip encoding"]
}.freeze

Class Method Summary collapse

Class Method Details

.gzip?(env, raw) ⇒ Boolean

Decompress when the client declared gzip or the body starts with the gzip magic number. The magic-byte path lets unload beacons ship compressed as text/plain without a Content-Encoding header, which would otherwise force a CORS preflight sendBeacon can't perform. JSON never begins with these bytes, so detection is unambiguous.

Returns:

  • (Boolean)


49
50
51
52
# File 'lib/sentiero/web/body_reader.rb', line 49

def gzip?(env, raw)
  env["HTTP_CONTENT_ENCODING"]&.downcase == "gzip" ||
    (raw.getbyte(0) == 0x1f && raw.getbyte(1) == 0x8b)
end

.read(env, max_bytes: MAX_BODY_SIZE) ⇒ Object

[body, nil] on success, or [nil, :too_large | :bad_gzip] on failure.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/sentiero/web/body_reader.rb', line 25

def read(env, max_bytes: MAX_BODY_SIZE)
  raw = env["rack.input"].read(max_bytes + 1) || ""
  env["rack.input"].rewind if env["rack.input"].respond_to?(:rewind)
  return [nil, :too_large] if raw.bytesize > max_bytes

  if gzip?(env, raw)
    begin
      gz = Zlib::GzipReader.new(StringIO.new(raw))
      raw = gz.read(max_bytes + 1) || ""
      gz.close
    rescue Zlib::GzipFile::Error
      return [nil, :bad_gzip]
    end
    return [nil, :too_large] if raw.bytesize > max_bytes
  end

  [raw, nil]
end