Module: ZeroClick::Sellers::Middleware
- Defined in:
- lib/zeroclick/sellers/middleware.rb
Overview
Rack middleware. Rails is a Rack app, so this covers Rails, Sinatra, Hanami, Roda and anything else Rack — one implementation, every framework.
Deliberately named Middleware rather than Rack: a nested ::ZeroClick::Sellers::Rack would shadow the real ::Rack for every constant lookup inside this file.
Nothing here requires the rack gem. A Rack middleware is duck-typed — an object with #call(env) returning [status, headers, body] — so the SDK adds no runtime dependency, and these are testable with a plain Hash.
Defined Under Namespace
Classes: Base, Identify, Meter
Constant Summary collapse
- CONTEXT_ENV_KEY =
What a guarded request proved about its caller, in the Rack env:
zc = env["zeroclick.context"] zc.zc_agent_id # the buyer zc.zc_request_id # correlates with ZeroClick's logs; use for idempotency "zeroclick.context"- DEFAULT_MAX_BODY_BYTES =
Bounds the request body a guarded endpoint will read.
Verification covers the whole body, so the whole body must be held in memory to check the signature. Without a ceiling that is an unauthenticated memory DoS: the attacker does not need a valid signature to make us buffer.
10 * 1024 * 1024
- PATH_SAFE =
Characters legal unencoded in a path, which must survive a re-encode.
%r{[^a-zA-Z0-9/~\-._!$&'()*+,;=:@%]}- RAW_TARGET_KEYS =
Servers that expose the raw request target set one of these. First match wins; each carries the full target, query string included.
Puma sets REQUEST_URI. Rails copies it to ORIGINAL_FULLPATH.
%w[REQUEST_URI ORIGINAL_FULLPATH].freeze
Class Method Summary collapse
-
.path_and_query_from_env(env) ⇒ Object
Recover the raw, percent-encoded request target from a Rack env.
-
.request_from_env(env, max_body_bytes: DEFAULT_MAX_BODY_BYTES) ⇒ Object
Build a Request from a Rack env, and leave rack.input readable by the app behind us.
Class Method Details
.path_and_query_from_env(env) ⇒ Object
Recover the raw, percent-encoded request target from a Rack env.
Prefers the server's raw target. Falls back to re-encoding PATH_INFO, which is exact for every character except an encoded separator: a server that decodes %2F to / before we are called leaves nothing downstream able to tell that apart from a literal /. Deploy behind Puma (which sets REQUEST_URI) if your routes can contain one.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/zeroclick/sellers/middleware.rb', line 54 def path_and_query_from_env(env) RAW_TARGET_KEYS.each do |key| raw = env[key] next if raw.nil? || raw.empty? # Some servers give absolute-form ("http://host/path?q"). The # signature covers the path and query only. return raw.sub(%r{\Ahttps?://[^/]+}, "") end path = "#{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}" encoded = path.gsub(PATH_SAFE) { |char| format("%%%02X", char.ord) } query = env["QUERY_STRING"] query.nil? || query.empty? ? encoded : "#{encoded}?#{query}" end |
.request_from_env(env, max_body_bytes: DEFAULT_MAX_BODY_BYTES) ⇒ Object
Build a Request from a Rack env, and leave rack.input readable by the app behind us.
It REPLACES rack.input rather than rewinding it. Rack 3 dropped the requirement that input be rewindable — a streaming server's input is consumed once and #rewind either raises or silently does nothing, which hands the application an empty body. Since verification must read the whole body anyway, handing back a fresh stream over those same bytes is both correct and free.
Returns [request, too_large].
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 |
# File 'lib/zeroclick/sellers/middleware.rb', line 81 def request_from_env(env, max_body_bytes: DEFAULT_MAX_BODY_BYTES) headers = {} env.each do |key, value| next unless key.is_a?(String) && key.start_with?("HTTP_") headers[key[5..].tr("_", "-").downcase] = value end headers["content-type"] = env["CONTENT_TYPE"] if env["CONTENT_TYPE"] headers["content-length"] = env["CONTENT_LENGTH"] if env["CONTENT_LENGTH"] input = env["rack.input"] body = +"" if input # Read one byte past the ceiling so an over-size body is detected # rather than silently truncated into an invalid signature. body = input.read(max_body_bytes + 1) || +"" body = body.dup.force_encoding(Encoding::BINARY) env["rack.input"] = StringIO.new(body) end [ Request.new( method: env["REQUEST_METHOD"] || "GET", path_and_query: path_and_query_from_env(env), headers: headers, body: body ), body.bytesize > max_body_bytes ] end |