Class: Quicsilver::Protocol::RequestParser

Inherits:
FrameParser
  • Object
show all
Defined in:
lib/quicsilver/protocol/request_parser.rb

Constant Summary collapse

METHOD_CONNECT =
"CONNECT"
VALID_PSEUDO_HEADERS =

Known HTTP/3 request pseudo-headers (RFC 9114 §4.3.1)

%w[:method :scheme :authority :path :protocol].freeze
VALID_PSEUDO_SET =
VALID_PSEUDO_HEADERS.each_with_object({}) { |h, s| s[h] = true }.freeze
FORBIDDEN_HEADERS =

Connection-specific headers forbidden in HTTP/3 (RFC 9114 §4.2)

%w[connection transfer-encoding keep-alive upgrade proxy-connection te].freeze
FORBIDDEN_SET =
FORBIDDEN_HEADERS.each_with_object({}) { |h, s| s[h] = true }.freeze
HEADERS_CACHE =

Cache for validated header results: payload → headers hash Only used when no custom limits are set (max_header_count, max_header_size)

{}
HEADERS_CACHE_MAX =
256
PARSE_CACHE =

Class-level parse result cache

{}
PARSE_CACHE_MAX =
128
PARSE_OID_CACHE =

Object-id fast-path for reparse (integer key = faster hash lookup)

{}
PARSE_OID_CACHE_MAX =
128

Constants inherited from FrameParser

FrameParser::CONTROL_ONLY_SET, FrameParser::DEFAULT_DECODER, FrameParser::EMPTY_BODY

Instance Attribute Summary

Attributes inherited from FrameParser

#bytes_consumed, #headers, #trailers

Instance Method Summary collapse

Methods inherited from FrameParser

#body, #frames

Constructor Details

#initialize(data, **opts) ⇒ RequestParser

Returns a new instance of RequestParser.



31
32
33
34
35
36
37
38
39
# File 'lib/quicsilver/protocol/request_parser.rb', line 31

def initialize(data, **opts)
  decoder = opts.delete(:decoder) || DEFAULT_DECODER
  super(decoder: decoder, max_body_size: opts[:max_body_size],
        max_header_size: opts[:max_header_size],
        max_header_count: opts[:max_header_count],
        max_frame_payload_size: opts[:max_frame_payload_size])
  @data = data
  @use_parse_cache = @decoder.equal?(DEFAULT_DECODER) && !@max_body_size && !@max_header_size && !@max_header_count && !@max_frame_payload_size
end

Instance Method Details

#parseObject



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/quicsilver/protocol/request_parser.rb', line 89

def parse
  # Fast path: full parse result cache for default decoder with no limits
  if @use_parse_cache
    cached = PARSE_CACHE[@data]
    if cached
      @headers = cached[0]
      @frames = cached[1]
      @cached_body_str = cached[2]
      return
    end
  end

  parse!
  cache_result if @use_parse_cache
end

#priorityObject

The parsed priority from the ‘priority` header (RFC 9218). Returns a Priority with defaults if no header present.



12
13
14
# File 'lib/quicsilver/protocol/request_parser.rb', line 12

def priority
  @priority ||= Priority.parse(@headers["priority"])
end

#reparse(data) ⇒ Object

Combined reset + parse for maximum throughput (single method call) Cache values stored as [headers, frames, body_str] for fast index access



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/quicsilver/protocol/request_parser.rb', line 53

def reparse(data)
  @data = data
  # Fastest path: same data object as last time — skip all cache lookups
  return if data.equal?(@last_data) && @headers

  if @use_parse_cache
    oid = data.object_id
    cached = PARSE_OID_CACHE[oid]
    unless cached
      cached = PARSE_CACHE[data]
      PARSE_OID_CACHE[oid] = cached if cached && PARSE_OID_CACHE.size < PARSE_OID_CACHE_MAX
    end
    if cached
      @headers = cached[0]
      @frames = cached[1]
      @cached_body_str = cached[2]
      @last_data = data
      return
    end
  end
  @headers = {}
  @trailers = {}
  @frames = nil
  @body = nil
  @cached_body_str = nil
  parse!
  cache_result if @use_parse_cache
end

#reset(data) ⇒ Object

Reset parser with new data for object reuse (avoids allocation overhead)



42
43
44
45
46
47
48
49
# File 'lib/quicsilver/protocol/request_parser.rb', line 42

def reset(data)
  @data = data
  @headers = {}
  @trailers = {}
  @frames = nil
  @body = nil
  @cached_body_str = nil
end

#to_rack_env(stream_info = {}) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/quicsilver/protocol/request_parser.rb', line 160

def to_rack_env(stream_info = {})
  return nil if @headers.empty?

  method = @headers[":method"]

  if method == METHOD_CONNECT
    return nil unless @headers[":authority"]
  else
    return nil unless method && @headers[":scheme"] && @headers[":path"]
  end

  path_full = @headers[":path"] || ""
  path, query = path_full.split("?", 2)

  authority = @headers[":authority"] || "localhost:4433"
  host, port = authority.split(":", 2)
  port ||= "4433"

  env = {
    "REQUEST_METHOD" => method,
    "PATH_INFO" => path || "",
    "QUERY_STRING" => query || "",
    "SERVER_NAME" => host,
    "SERVER_PORT" => port,
    "SERVER_PROTOCOL" => "HTTP/3",
    "rack.version" => [1, 3],
    "rack.url_scheme" => @headers[":scheme"] || "https",
    "rack.input" => body,
    "rack.errors" => $stderr,
    "rack.multithread" => true,
    "rack.multiprocess" => false,
    "rack.run_once" => false,
    "rack.hijack?" => false,
    "SCRIPT_NAME" => "",
    "CONTENT_LENGTH" => body.size.to_s,
  }

  if @headers[":authority"]
    env["HTTP_HOST"] = @headers[":authority"]
  end

  @headers.each do |name, value|
    next if name.start_with?(":")
    key = name.upcase.tr("-", "_")
    if key == "CONTENT_TYPE"
      env["CONTENT_TYPE"] = value
    elsif key == "CONTENT_LENGTH"
      env["CONTENT_LENGTH"] = value
    else
      env["HTTP_#{key}"] = value
    end
  end

  env
end

#validate_headers!Object

Validate pseudo-header semantics per RFC 9114 §4.3.1. Call after parse to check CONNECT rules, required headers, host/:authority consistency.



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/quicsilver/protocol/request_parser.rb', line 125

def validate_headers!
  return if @headers.empty?

  method = @headers[":method"]

  if method == METHOD_CONNECT && @headers[":protocol"]
    # Extended CONNECT (RFC 9220) — requires :scheme, :path, :authority, :protocol
    raise Protocol::MessageError, "Extended CONNECT must include :scheme" unless @headers[":scheme"]
    raise Protocol::MessageError, "Extended CONNECT must include :path" unless @headers[":path"]
    raise Protocol::MessageError, "Extended CONNECT must include :authority" unless @headers[":authority"]
  elsif method == METHOD_CONNECT
    # Regular CONNECT (RFC 9114 §4.4)
    raise Protocol::MessageError, "CONNECT request must include :authority" unless @headers[":authority"]
    raise Protocol::MessageError, "CONNECT request must not include :scheme" if @headers[":scheme"]
    raise Protocol::MessageError, "CONNECT request must not include :path" if @headers[":path"]
  else
    raise Protocol::MessageError, "Request missing required pseudo-header :method" unless method
    raise Protocol::MessageError, "Request missing required pseudo-header :scheme" unless @headers[":scheme"]
    raise Protocol::MessageError, "Request missing required pseudo-header :path" unless @headers[":path"]

    # RFC 9114 §4.3.1: schemes with mandatory authority (http/https) require :authority or host
    scheme = @headers[":scheme"]
    if %w[http https].include?(scheme) && !@headers[":authority"] && !@headers["host"]
      raise Protocol::MessageError, "Request with #{scheme} scheme must include :authority or host"
    end
  end

  # Host and :authority consistency (RFC 9114 §4.3.1)
  if @headers[":authority"] && @headers["host"]
    unless @headers[":authority"] == @headers["host"]
      raise Protocol::MessageError, ":authority and host header must be consistent"
    end
  end
end