Class: TIMEx::Propagation::RackMiddleware
- Inherits:
-
Object
- Object
- TIMEx::Propagation::RackMiddleware
- Defined in:
- lib/timex/propagation/rack_middleware.rb
Overview
The header is untrusted on public networks. Combine max_seconds:,
max_depth:, and network controls; see class body for threat summary.
Rack middleware: parses inbound HttpHeader::HEADER_NAME, stores
env["timex.deadline"], optionally clamps and rejects abusive values, and
can echo remaining budget on the response.
Constant Summary collapse
- ENV_KEY =
"timex.deadline"- RAW_HEADER_KEY =
HttpHeader::RACK_HEADER_KEY
- HEADER_NAMES =
Rack 3 mandates lower-case response header names; Rack 2 (and many 3rd-party middlewares that haven't migrated) still emit canonical case. Pass
header_case: :canonicalto switch the response header names toContent-Type/X-TIMEx-*for Rack-2-era stacks. { rack3: { remaining: "x-timex-remaining-ms", outcome: "x-timex-outcome", content_type: "content-type" }.freeze, canonical: { remaining: "X-TIMEx-Remaining-Ms", outcome: "X-TIMEx-Outcome", content_type: "Content-Type" }.freeze }.freeze
Instance Method Summary collapse
-
#call(env) ⇒ Array(Integer, Hash, #each)
Security: this header is taken from the inbound HTTP request without authentication.
-
#initialize(app, default_seconds: nil, max_seconds: nil, max_depth: nil, expose_remaining: false, clamp_infinite_to_default: false, header_case: :rack3) ⇒ RackMiddleware
constructor
A new instance of RackMiddleware.
Constructor Details
#initialize(app, default_seconds: nil, max_seconds: nil, max_depth: nil, expose_remaining: false, clamp_infinite_to_default: false, header_case: :rack3) ⇒ RackMiddleware
Returns a new instance of RackMiddleware.
44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/timex/propagation/rack_middleware.rb', line 44 def initialize(app, default_seconds: nil, max_seconds: nil, max_depth: nil, # rubocop:disable Metrics/ParameterLists expose_remaining: false, clamp_infinite_to_default: false, header_case: :rack3) raise ArgumentError, "header_case must be :rack3 or :canonical" unless HEADER_NAMES.key?(header_case) @app = app @default_seconds = default_seconds @max_seconds = max_seconds @max_depth = max_depth @expose_remaining = expose_remaining @clamp_infinite_to_default = clamp_infinite_to_default @headers = HEADER_NAMES.fetch(header_case) end |
Instance Method Details
#call(env) ⇒ Array(Integer, Hash, #each)
Security: this header is taken from the inbound HTTP request without
authentication. An attacker who can reach this endpoint can send
ms=0 to force an immediate 503, or a large ms= value to extend a
request's allowed processing window beyond what your server intended.
Only mount this middleware on networks where the upstream is trusted
(e.g. internal service mesh, signed/authenticated requests).
For internet-facing deployments, always pass max_seconds: so any
incoming deadline is clamped to that ceiling, and max_depth: to bound
propagation hops (example: use TIMEx::Propagation::RackMiddleware, max_seconds: 30, max_depth: 8).
Deadline.from_header also caps untrusted input length at
Deadline::MAX_HEADER_BYTESIZE, rejects non-finite/negative/very large
ms= values, and clamps depth= at Deadline::MAX_DEPTH.
max_depth is enforced on the parsed inbound deadline before
max_seconds clamping. Clamping via Deadline#min can yield a fresh
deadline without propagation metadata; checking depth only after clamp
would let a client bypass the hop limit with an oversized ms=.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/timex/propagation/rack_middleware.rb', line 80 def call(env) # Distinguish "no header sent" from "header present but unparseable": # the latter is suspicious (truncation, smuggling attempt) and # deserves a telemetry signal even though we still fall through to # `default_seconds` / unbounded handling. raw = env[RAW_HEADER_KEY] deadline = HttpHeader.from_rack_env(env) if raw && !raw.empty? && deadline.nil? TIMEx::Telemetry.emit( event: "rack.deadline.unparseable", bytesize: raw.bytesize ) end if depth_exceeded?(deadline) TIMEx::Telemetry.emit( event: "rack.deadline.rejected", reason: :max_depth_exceeded, depth: deadline.depth, origin: deadline.origin ) return reject_response("max-depth-exceeded", "Deadline propagation depth exceeded") end deadline = nil if deadline&.infinite? && @clamp_infinite_to_default && @default_seconds deadline = clamp(deadline) deadline ||= Deadline.in(@default_seconds) if @default_seconds if deadline env[ENV_KEY] = deadline env[RAW_HEADER_KEY] = deadline.to_header else env.delete(RAW_HEADER_KEY) end if deadline&.expired? TIMEx::Telemetry.emit( event: "rack.deadline.rejected", reason: :expired_on_arrival, origin: deadline.origin ) return reject_response("expired-on-arrival", "Deadline expired before request handling") end status, headers, body = @app.call(env) headers = inject_remaining(headers, deadline) if @expose_remaining [status, headers, body] end |