raptor 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/raptor/http1.rb CHANGED
@@ -43,6 +43,7 @@ module Raptor
43
43
  BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
44
44
  CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
45
45
  INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
46
+ NOT_IMPLEMENTED_RESPONSE = "HTTP/1.1 501 Not Implemented\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
46
47
  CHUNKED_TERMINATOR = "0\r\n\r\n"
47
48
 
48
49
  CONNECTION_CLOSE = "close"
@@ -75,31 +76,29 @@ module Raptor
75
76
  !http_host || http_host.empty? || http_host.include?(",")
76
77
  end
77
78
 
78
- # Returns true when the message framing shows a request-smuggling vector
79
- # per RFC 9112 section 6.3: a `Transfer-Encoding` where `chunked` is
80
- # missing, not the final encoding, or duplicated; a `Transfer-Encoding`
81
- # paired with a `Content-Length`; or a `Content-Length` containing any
82
- # non-digit character.
79
+ # Rejects ambiguous message framing per RFC 9112 section 6.3 and
80
+ # transfer codings other than a single `chunked`.
83
81
  #
84
82
  # @param env [Hash] the Rack environment after header parsing
85
- # @return [Boolean]
83
+ # @return [Symbol, nil] `:malformed`, `:unsupported_encoding`, or nil
86
84
  #
87
- # @rbs (Hash[String, untyped] env) -> bool
88
- def self.request_smuggling?(env)
85
+ # @rbs (Hash[String, untyped] env) -> Symbol?
86
+ def self.framing_error(env)
89
87
  transfer_encoding = env[HTTP_TRANSFER_ENCODING]
90
88
  content_length = env[Http::CONTENT_LENGTH]
91
89
 
92
90
  if transfer_encoding
93
- return true if content_length
91
+ return :malformed if content_length
94
92
 
95
- encodings = transfer_encoding.downcase.split(",").map(&:strip)
96
- return true if encodings.last != TRANSFER_ENCODING_CHUNKED
97
- return true if encodings.count(TRANSFER_ENCODING_CHUNKED) > 1
93
+ encodings = transfer_encoding.downcase.split(",", -1).map(&:strip)
94
+ return :malformed if encodings.any?(&:empty?) || encodings.last != TRANSFER_ENCODING_CHUNKED
95
+ return :malformed if encodings.count(TRANSFER_ENCODING_CHUNKED) > 1
96
+ return :unsupported_encoding if encodings.size > 1
98
97
  elsif content_length
99
- return true if content_length.match?(/[^\d]/)
98
+ return :malformed unless content_length.match?(/\A[0-9]+\z/)
100
99
  end
101
100
 
102
- false
101
+ nil
103
102
  end
104
103
 
105
104
  # Returns any pipelined bytes following the request, or nil if none remain.
@@ -144,9 +143,9 @@ module Raptor
144
143
  # Advances an HTTP/1.x request parse from the state hash's buffered
145
144
  # bytes. A complete well-formed request returns with `:complete` set
146
145
  # plus populated `:env` and `:body`, and records any bytes after it in
147
- # the parse data's `:pipelined`; malformed or oversized input flips
148
- # `:malformed` or `:too_large`; incomplete input returns the state
149
- # so the reactor can wait for more.
146
+ # the parse data's `:pipelined`. Rejected input sets `:malformed`,
147
+ # `:too_large`, or `:unsupported_encoding`; incomplete input returns
148
+ # the state so the reactor can wait for more.
150
149
  #
151
150
  # @param data [Hash] the current parse state
152
151
  # @param env_template [Hash] the Rack env template to seed the request with
@@ -170,8 +169,9 @@ module Raptor
170
169
  parse_data[:parse_count] += 1
171
170
 
172
171
  message = if parser.finished?
173
- if invalid_host?(env) || request_smuggling?(env)
174
- data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
172
+ error = invalid_host?(env) ? :malformed : framing_error(env)
173
+ if error
174
+ data.merge(env: env, body: nil, parse_data: parse_data, complete: true, error => true)
175
175
  elsif parser.has_body?
176
176
  body_buffer = data[:buffer].byteslice(nread..-1) || ""
177
177
 
@@ -325,6 +325,9 @@ module Raptor
325
325
  when :malformed
326
326
  reject_malformed(socket)
327
327
  return
328
+ when :unsupported_encoding
329
+ reject_unsupported_encoding(socket)
330
+ return
328
331
  end
329
332
 
330
333
  thread_pool << proc do
@@ -354,6 +357,12 @@ module Raptor
354
357
  return
355
358
  end
356
359
 
360
+ if parsed_request[:unsupported_encoding]
361
+ socket = reactor.remove(parsed_request[:id])
362
+ reject_unsupported_encoding(socket) if socket
363
+ return
364
+ end
365
+
357
366
  if parsed_request[:complete]
358
367
  socket = reactor.remove(parsed_request[:id])
359
368
  request_count = (parsed_request[:request_count] || 0) + 1
@@ -444,6 +453,9 @@ module Raptor
444
453
  when :malformed
445
454
  reject_malformed(socket)
446
455
  return
456
+ when :unsupported_encoding
457
+ reject_unsupported_encoding(socket)
458
+ return
447
459
  end
448
460
 
449
461
  request_count += 1
@@ -530,11 +542,13 @@ module Raptor
530
542
  # @param parser [HttpParser] the parser holding the finished parse state
531
543
  # @param nread [Integer] the byte offset where the body begins in `buffer`
532
544
  # @return [String, nil, Symbol] body, nil when absent, or `:incomplete`,
533
- # `:too_large`, or `:malformed`
545
+ # `:too_large`, `:malformed`, or `:unsupported_encoding`
534
546
  #
535
547
  # @rbs (String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, HttpParser parser, Integer nread) -> (String | Symbol)?
536
548
  def extract_body(buffer, env, parse_data, parser, nread)
537
- return :malformed if Http1.invalid_host?(env) || Http1.request_smuggling?(env)
549
+ return :malformed if Http1.invalid_host?(env)
550
+ error = Http1.framing_error(env)
551
+ return error if error
538
552
 
539
553
  unless parser.has_body?
540
554
  parse_data[:pipelined] = Http1.pipelined_bytes(buffer, nread)
@@ -809,6 +823,17 @@ module Raptor
809
823
  socket.close rescue nil
810
824
  end
811
825
 
826
+ # Writes a 501 response and closes the socket.
827
+ #
828
+ # @param socket [TCPSocket] the client socket
829
+ # @return [void]
830
+ #
831
+ # @rbs (TCPSocket socket) -> void
832
+ def reject_unsupported_encoding(socket)
833
+ socket.write(NOT_IMPLEMENTED_RESPONSE) rescue nil
834
+ socket.close rescue nil
835
+ end
836
+
812
837
  # Builds a Rack environment hash from parsed HTTP request data.
813
838
  #
814
839
  # @param env [Hash] partial env hash from the HTTP parser
@@ -855,7 +880,6 @@ module Raptor
855
880
  end
856
881
 
857
882
  env[Http::REMOTE_ADDR] = remote_addr
858
- env[Http::HTTP_VERSION] = env[Rack::SERVER_PROTOCOL]
859
883
 
860
884
  behind_tls_proxy = (url_scheme == Server::HTTP_SCHEME) && forwarded_https?(env)
861
885
  env[Rack::RACK_URL_SCHEME] = behind_tls_proxy ? Server::HTTPS_SCHEME : url_scheme
@@ -890,7 +914,8 @@ module Raptor
890
914
 
891
915
  # Builds the `rack.input` IO object for the request body. Returns an
892
916
  # in-memory StringIO for bodies up to the spool threshold, or a Tempfile
893
- # for larger bodies to bound per-worker memory.
917
+ # for larger bodies. The complete body is already buffered at this point,
918
+ # so spooling does not bound peak receive memory.
894
919
  #
895
920
  # @param body [String, nil] decoded request body
896
921
  # @return [IO] an IO-like object positioned at the start of the body
@@ -934,13 +959,10 @@ module Raptor
934
959
  def keep_alive?(env, request_count)
935
960
  return false if request_count >= @max_keepalive_requests
936
961
 
937
- connection_header = env[HTTP_CONNECTION]
962
+ connection_tokens = env[HTTP_CONNECTION].to_s.downcase.split(",").map(&:strip)
963
+ return false if connection_tokens.include?(CONNECTION_CLOSE)
938
964
 
939
- if env[Rack::SERVER_PROTOCOL] == HTTP_11
940
- !connection_header&.casecmp?(CONNECTION_CLOSE)
941
- else
942
- connection_header&.casecmp?(CONNECTION_KEEPALIVE) || false
943
- end
965
+ env[Rack::SERVER_PROTOCOL] == HTTP_11 || connection_tokens.include?(CONNECTION_KEEPALIVE)
944
966
  end
945
967
 
946
968
  # Sends an HTTP 103 Early Hints response, skipping any entries with
@@ -1159,7 +1181,7 @@ module Raptor
1159
1181
  #
1160
1182
  # @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, untyped body, String http_version) -> void
1161
1183
  def write_full_response(socket, response, headers, body, http_version)
1162
- if body.respond_to?(:call)
1184
+ if body.respond_to?(:call) && !body.respond_to?(:each)
1163
1185
  HttpParser.format_headers(response, headers)
1164
1186
  response << "\r\n"
1165
1187
  socket_write(socket, response)
data/lib/raptor/http2.rb CHANGED
@@ -455,7 +455,7 @@ module Raptor
455
455
  # updated connection state along with any outgoing protocol frames and
456
456
  # completed stream requests.
457
457
  #
458
- # @param data [Hash] the connection state including buffer and HPACK table
458
+ # @param data [Hash] the connection state including buffer, HPACK table and body-size limit
459
459
  # @return [Hash] updated state with outgoing_frames and completed_requests
460
460
  #
461
461
  # @rbs (Hash[Symbol, untyped] data) -> Hash[Symbol, untyped]
@@ -474,6 +474,7 @@ module Raptor
474
474
  preface_received = data[:http2_preface_received] || false
475
475
  last_client_stream_id = data[:http2_last_client_stream_id] || 0
476
476
  pending_headers = data[:http2_pending_headers]
477
+ max_body_size = data[:max_body_size]
477
478
  goaway_error = nil
478
479
 
479
480
  unless preface_received
@@ -586,12 +587,29 @@ module Raptor
586
587
  stream = streams[stream_id]
587
588
  next unless stream
588
589
 
590
+ if stream[:too_large]
591
+ streams.delete(stream_id) if frame[:flags].anybits?(FLAG_END_STREAM)
592
+ next
593
+ end
594
+
595
+ body = stream[:body] || ""
596
+ if max_body_size && body.bytesize + chunk.bytesize > max_body_size
597
+ headers = parser.encode_headers([[":status", "413"], ["content-length", "0"]])
598
+ outgoing_frames << parser.build_frame(:headers, FLAG_END_HEADERS | FLAG_END_STREAM, stream_id, headers)
599
+ if frame[:flags].anybits?(FLAG_END_STREAM)
600
+ streams.delete(stream_id)
601
+ else
602
+ streams[stream_id] = { too_large: true }
603
+ end
604
+ next
605
+ end
606
+
589
607
  stream_window_used = (stream[:window_used] || 0) + frame[:length]
590
608
  if stream_window_used >= DEFAULT_WINDOW_SIZE / 2 && frame[:flags].nobits?(FLAG_END_STREAM)
591
609
  outgoing_frames << parser.build_frame(:window_update, 0, stream_id, [stream_window_used].pack("N"))
592
610
  stream_window_used = 0
593
611
  end
594
- stream = stream.merge(body: (stream[:body] || "") + chunk, window_used: stream_window_used)
612
+ stream = stream.merge(body: body + chunk, window_used: stream_window_used)
595
613
 
596
614
  if frame[:flags].anybits?(FLAG_END_STREAM)
597
615
  stream_headers = stream[:headers] || []
@@ -714,11 +732,13 @@ module Raptor
714
732
  outgoing_frames << parser.build_frame(:rst_stream, 0, stream_id, [ERROR_PROTOCOL_ERROR].pack("N"))
715
733
  elsif trailers
716
734
  stream = streams.delete(stream_id)
717
- completed_requests << {
718
- stream_id: stream_id,
719
- headers: stream[:headers],
720
- body: stream[:body] || ""
721
- }
735
+ unless stream[:too_large]
736
+ completed_requests << {
737
+ stream_id: stream_id,
738
+ headers: stream[:headers],
739
+ body: stream[:body] || ""
740
+ }
741
+ end
722
742
  else
723
743
  streams, completed_requests = finalize_headers(streams, completed_requests, stream_id, decoded_headers, end_stream)
724
744
  end
@@ -783,6 +803,7 @@ module Raptor
783
803
  id: data[:id],
784
804
  protocol: :http2,
785
805
  buffer: buffer || "",
806
+ max_body_size: data[:max_body_size],
786
807
  hpack_table: hpack_table,
787
808
  http2_streams: streams,
788
809
  http2_window: connection_window,
@@ -806,6 +827,7 @@ module Raptor
806
827
  # @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
807
828
  # @rbs @server_port_string: String
808
829
  # @rbs @write_timeout: Integer
830
+ # @rbs @max_body_size: Integer?
809
831
  # @rbs @access_log_io: IO?
810
832
  # @rbs @clean_thread_locals: bool
811
833
  # @rbs @clean_fiber_locals: bool
@@ -819,6 +841,7 @@ module Raptor
819
841
  # @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
820
842
  # @param connection_options [Hash] per-connection settings shared across protocols
821
843
  # @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
844
+ # @option connection_options [Integer, nil] :max_body_size maximum request body size in bytes
822
845
  # @param http2_options [Hash] HTTP/2-specific settings
823
846
  # @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
824
847
  # @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
@@ -832,6 +855,7 @@ module Raptor
832
855
  @app = app
833
856
  @server_port_string = server_port.to_s.freeze
834
857
  @write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
858
+ @max_body_size = connection_options[:max_body_size]
835
859
  @access_log_io = access_log_io
836
860
  @clean_thread_locals = clean_thread_locals
837
861
  @clean_fiber_locals = clean_fiber_locals
@@ -902,6 +926,7 @@ module Raptor
902
926
  initial_state = {
903
927
  id: id,
904
928
  protocol: :http2,
929
+ max_body_size: @max_body_size,
905
930
  remote_addr: remote_addr,
906
931
  url_scheme: url_scheme
907
932
  }
@@ -1140,11 +1165,6 @@ module Raptor
1140
1165
  trailers: env[RESPONSE_TRAILERS],
1141
1166
  request_method: env[Rack::REQUEST_METHOD]
1142
1167
  ) { response_started = true }
1143
- if response_body.is_a?(DetachedBody)
1144
- response_body.finish(:closed)
1145
- elsif response_body.respond_to?(:close)
1146
- response_body.close
1147
- end
1148
1168
  write_access_log(env, status, response_size, remote_addr) if @access_log_io
1149
1169
  Http.call_response_finished(env, status, response_headers, nil)
1150
1170
  rescue StreamClosedError => error
@@ -1230,6 +1250,12 @@ module Raptor
1230
1250
  stream.close
1231
1251
 
1232
1252
  stream.bytes_written.to_s
1253
+ ensure
1254
+ if body.is_a?(DetachedBody)
1255
+ body.finish($! ? :cancelled : :closed)
1256
+ elsif body.respond_to?(:close)
1257
+ body.close
1258
+ end
1233
1259
  end
1234
1260
 
1235
1261
  # Sends an HTTP 103 Early Hints response.
@@ -1298,6 +1324,7 @@ module Raptor
1298
1324
  # @rbs (Array[[String, String]] headers, String body, TCPSocket | OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, remote_addr: String) -> Hash[String, untyped]
1299
1325
  def build_rack_env(headers, body, socket, writer, flow_control, stream_id, remote_addr:)
1300
1326
  env = {}
1327
+ underscored_headers = nil
1301
1328
 
1302
1329
  headers.each do |name, value|
1303
1330
  if name.start_with?(":")
@@ -1316,6 +1343,13 @@ module Raptor
1316
1343
  env[Http::CONTENT_LENGTH] = value
1317
1344
  else
1318
1345
  rack_key = "HTTP_#{name.upcase.tr("-", "_")}"
1346
+ if name.include?("_")
1347
+ next if env.key?(rack_key) && !underscored_headers&.key?(rack_key)
1348
+
1349
+ (underscored_headers ||= {})[rack_key] = true
1350
+ elsif underscored_headers&.delete(rack_key)
1351
+ env.delete(rack_key)
1352
+ end
1319
1353
  if (existing = env[rack_key])
1320
1354
  separator = rack_key == Rack::HTTP_COOKIE ? "; " : ", "
1321
1355
  env[rack_key] = "#{existing}#{separator}#{value}"
@@ -1345,7 +1379,6 @@ module Raptor
1345
1379
 
1346
1380
  env[Http::REMOTE_ADDR] = remote_addr
1347
1381
  env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
1348
- env[Http::HTTP_VERSION] = SERVER_PROTOCOL
1349
1382
  env[RESPONSE_TRAILERS] = {}
1350
1383
 
1351
1384
  populate_server_name_and_port(env)
@@ -0,0 +1,98 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ module Raptor
5
+ # An extension, loaded from `raptor/plugin/<name>` or
6
+ # registered explicitly by application code.
7
+ #
8
+ class Plugin
9
+ class UnknownPlugin < LoadError; end
10
+
11
+ # @rbs self.@registry: Hash[String, singleton(Plugin)]
12
+ @registry = {}
13
+
14
+ # Registers a plugin class under a name.
15
+ #
16
+ # @rbs (String | Symbol name, singleton(Plugin) plugin) -> void
17
+ def self.register(name, plugin)
18
+ @registry[name.to_s] = plugin
19
+ end
20
+
21
+ # Defines and registers a plugin without a named subclass.
22
+ #
23
+ # @rbs (String | Symbol name) { () -> void } -> singleton(Plugin)
24
+ def self.create(name, &block)
25
+ plugin = Class.new(self, &block)
26
+ register(name, plugin)
27
+ plugin
28
+ end
29
+
30
+ # Loads a registered plugin, requiring its conventional path if needed.
31
+ #
32
+ # @rbs (String | Symbol name) -> Plugin
33
+ def self.load(name)
34
+ name = name.to_s
35
+ path = "raptor/plugin/#{name}"
36
+ unless @registry.key?(name)
37
+ begin
38
+ require path
39
+ rescue LoadError => error
40
+ raise UnknownPlugin, "Unable to find plugin #{name.inspect}" if error.path == path
41
+
42
+ raise
43
+ end
44
+ end
45
+
46
+ plugin = @registry[name]
47
+ raise UnknownPlugin, "#{path} did not register plugin #{name.inspect}" unless plugin
48
+
49
+ plugin.new
50
+ end
51
+
52
+ # @rbs @threads: Array[Thread]?
53
+
54
+ # Starts in the master before workers fork. Override to open resources
55
+ # and append callbacks to the cluster's lifecycle hook arrays.
56
+ #
57
+ # @rbs (Cluster cluster) -> void
58
+ def start(cluster)
59
+ end
60
+
61
+ # Stops in the master after workers exit, including before hot restart.
62
+ # Also called if startup fails, so resources may be only partly opened.
63
+ #
64
+ # @rbs () -> void
65
+ def stop
66
+ end
67
+
68
+ # Runs a block on a master-owned thread. Raptor stops and joins these
69
+ # threads after calling `stop`; use `ensure` in the block for cleanup.
70
+ #
71
+ # @rbs () { () -> void } -> Thread
72
+ def in_background(&block)
73
+ @threads ||= []
74
+ @threads << Thread.new do
75
+ Thread.current.name = "Plugin #{self.class}"
76
+ block.call
77
+ end
78
+ @threads.last
79
+ end
80
+
81
+ # Calls the plugin's stop callback and joins its background threads.
82
+ #
83
+ # @rbs () -> void
84
+ def shutdown
85
+ stop
86
+ ensure
87
+ threads = @threads || []
88
+ threads.each(&:kill)
89
+ error = nil
90
+ threads.each do |thread|
91
+ thread.join
92
+ rescue StandardError => thread_error
93
+ error ||= thread_error
94
+ end
95
+ raise error if error
96
+ end
97
+ end
98
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.23.0"
5
+ VERSION = "0.24.0"
6
6
  end
@@ -66,6 +66,8 @@ module Raptor
66
66
 
67
67
  @binder: Binder
68
68
 
69
+ @plugins: Array[Plugin]
70
+
69
71
  @before_refork: Array[Proc]
70
72
 
71
73
  @before_worker_shutdown: Array[Proc]
@@ -152,6 +154,11 @@ module Raptor
152
154
 
153
155
  @drain_accept_queue: bool
154
156
 
157
+ # Returns the cluster configuration available to plugins.
158
+ #
159
+ # @return [Hash{Symbol => untyped}]
160
+ attr_reader options: Hash[Symbol, untyped]
161
+
155
162
  # Creates a new Cluster with the given options.
156
163
  #
157
164
  # @param options [Hash] cluster configuration options
@@ -180,6 +187,7 @@ module Raptor
180
187
  # @option options [Array<Proc>] :before_worker_boot procs called with the worker index before it begins serving
181
188
  # @option options [Array<Proc>] :before_worker_shutdown procs called with the worker index before its graceful shutdown
182
189
  # @option options [Array<Proc>] :before_refork procs called in a worker before it transitions to a seed
190
+ # @option options [Array<String, Symbol>] :plugins plugins to load and start in order
183
191
  # @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
184
192
  # @option options [String, nil] :control_url `unix://` URL serving cluster stats, or nil to disable
185
193
  # @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
@@ -210,17 +218,22 @@ module Raptor
210
218
  # @rbs () -> Array[Hash[Symbol, untyped]]
211
219
  def stats: () -> Array[Hash[Symbol, untyped]]
212
220
 
213
- private
214
-
215
- # Returns cluster statistics for the control server. For an adaptive
216
- # pool, `max_threads` is the number of threads currently running so
217
- # utilization reflects the capacity available at that moment.
221
+ # Returns cluster statistics for plugins and the control server.
222
+ # For an adaptive pool, `max_threads` is the number of threads currently
223
+ # running so utilization reflects the capacity available at that moment.
218
224
  #
219
225
  # @return [Hash] cluster statistics
220
226
  #
221
227
  # @rbs () -> Hash[Symbol, untyped]
222
228
  def stats_hash: () -> Hash[Symbol, untyped]
223
229
 
230
+ private
231
+
232
+ # Stops plugins in reverse startup order.
233
+ #
234
+ # @rbs () -> void
235
+ def stop_plugins: () -> void
236
+
224
237
  # @rbs (Numeric time) -> String
225
238
  def timestamp: (Numeric time) -> String
226
239
 
@@ -11,8 +11,6 @@ module Raptor
11
11
 
12
12
  CONTENT_TYPE: ::String
13
13
 
14
- HTTP_VERSION: ::String
15
-
16
14
  REMOTE_ADDR: ::String
17
15
 
18
16
  REQUEST_URI: ::String
@@ -34,6 +34,8 @@ module Raptor
34
34
 
35
35
  INTERNAL_SERVER_ERROR_RESPONSE: ::String
36
36
 
37
+ NOT_IMPLEMENTED_RESPONSE: ::String
38
+
37
39
  CHUNKED_TERMINATOR: ::String
38
40
 
39
41
  CONNECTION_CLOSE: ::String
@@ -72,17 +74,14 @@ module Raptor
72
74
  # @rbs (Hash[String, untyped] env) -> bool
73
75
  def self.invalid_host?: (Hash[String, untyped] env) -> bool
74
76
 
75
- # Returns true when the message framing shows a request-smuggling vector
76
- # per RFC 9112 section 6.3: a `Transfer-Encoding` where `chunked` is
77
- # missing, not the final encoding, or duplicated; a `Transfer-Encoding`
78
- # paired with a `Content-Length`; or a `Content-Length` containing any
79
- # non-digit character.
77
+ # Rejects ambiguous message framing per RFC 9112 section 6.3 and
78
+ # transfer codings other than a single `chunked`.
80
79
  #
81
80
  # @param env [Hash] the Rack environment after header parsing
82
- # @return [Boolean]
81
+ # @return [Symbol, nil] `:malformed`, `:unsupported_encoding`, or nil
83
82
  #
84
- # @rbs (Hash[String, untyped] env) -> bool
85
- def self.request_smuggling?: (Hash[String, untyped] env) -> bool
83
+ # @rbs (Hash[String, untyped] env) -> Symbol?
84
+ def self.framing_error: (Hash[String, untyped] env) -> Symbol?
86
85
 
87
86
  # Returns any pipelined bytes following the request, or nil if none remain.
88
87
  #
@@ -112,9 +111,9 @@ module Raptor
112
111
  # Advances an HTTP/1.x request parse from the state hash's buffered
113
112
  # bytes. A complete well-formed request returns with `:complete` set
114
113
  # plus populated `:env` and `:body`, and records any bytes after it in
115
- # the parse data's `:pipelined`; malformed or oversized input flips
116
- # `:malformed` or `:too_large`; incomplete input returns the state
117
- # so the reactor can wait for more.
114
+ # the parse data's `:pipelined`. Rejected input sets `:malformed`,
115
+ # `:too_large`, or `:unsupported_encoding`; incomplete input returns
116
+ # the state so the reactor can wait for more.
118
117
  #
119
118
  # @param data [Hash] the current parse state
120
119
  # @param env_template [Hash] the Rack env template to seed the request with
@@ -256,7 +255,7 @@ module Raptor
256
255
  # @param parser [HttpParser] the parser holding the finished parse state
257
256
  # @param nread [Integer] the byte offset where the body begins in `buffer`
258
257
  # @return [String, nil, Symbol] body, nil when absent, or `:incomplete`,
259
- # `:too_large`, or `:malformed`
258
+ # `:too_large`, `:malformed`, or `:unsupported_encoding`
260
259
  #
261
260
  # @rbs (String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, HttpParser parser, Integer nread) -> (String | Symbol)?
262
261
  def extract_body: (String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, HttpParser parser, Integer nread) -> (String | Symbol)?
@@ -384,6 +383,14 @@ module Raptor
384
383
  # @rbs (TCPSocket socket) -> void
385
384
  def reject_malformed: (TCPSocket socket) -> void
386
385
 
386
+ # Writes a 501 response and closes the socket.
387
+ #
388
+ # @param socket [TCPSocket] the client socket
389
+ # @return [void]
390
+ #
391
+ # @rbs (TCPSocket socket) -> void
392
+ def reject_unsupported_encoding: (TCPSocket socket) -> void
393
+
387
394
  # Builds a Rack environment hash from parsed HTTP request data.
388
395
  #
389
396
  # @param env [Hash] partial env hash from the HTTP parser
@@ -399,7 +406,8 @@ module Raptor
399
406
 
400
407
  # Builds the `rack.input` IO object for the request body. Returns an
401
408
  # in-memory StringIO for bodies up to the spool threshold, or a Tempfile
402
- # for larger bodies to bound per-worker memory.
409
+ # for larger bodies. The complete body is already buffered at this point,
410
+ # so spooling does not bound peak receive memory.
403
411
  #
404
412
  # @param body [String, nil] decoded request body
405
413
  # @return [IO] an IO-like object positioned at the start of the body
@@ -307,7 +307,7 @@ module Raptor
307
307
  # updated connection state along with any outgoing protocol frames and
308
308
  # completed stream requests.
309
309
  #
310
- # @param data [Hash] the connection state including buffer and HPACK table
310
+ # @param data [Hash] the connection state including buffer, HPACK table and body-size limit
311
311
  # @return [Hash] updated state with outgoing_frames and completed_requests
312
312
  #
313
313
  # @rbs (Hash[Symbol, untyped] data) -> Hash[Symbol, untyped]
@@ -381,23 +381,25 @@ module Raptor
381
381
  # @rbs (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[Integer] cancelled_streams, Array[[Integer, Integer]] window_updates, Array[String] ping_acknowledgements, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
382
382
  def self.build_result: (Hash[Symbol, untyped] data, String buffer, Array[untyped] hpack_table, Hash[Integer, Hash[Symbol, untyped]] streams, Array[String] outgoing_frames, Array[Hash[Symbol, untyped]] completed_requests, Array[Integer] cancelled_streams, Array[[ Integer, Integer ]] window_updates, Array[String] ping_acknowledgements, Integer? peer_initial_window_size, Integer connection_window, bool preface_received, Integer last_client_stream_id, Hash[Symbol, untyped]? pending_headers, bool close_connection) -> Hash[Symbol, untyped]
383
383
 
384
- @initial_settings_frame: String
384
+ @app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
385
385
 
386
- @running: AtomicBoolean
386
+ @server_port_string: String
387
387
 
388
- @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
388
+ @write_timeout: Integer
389
389
 
390
- @clean_fiber_locals: bool
390
+ @max_body_size: Integer?
391
+
392
+ @access_log_io: IO?
391
393
 
392
394
  @clean_thread_locals: bool
393
395
 
394
- @access_log_io: IO?
396
+ @clean_fiber_locals: bool
395
397
 
396
- @write_timeout: Integer
398
+ @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
397
399
 
398
- @server_port_string: String
400
+ @running: AtomicBoolean
399
401
 
400
- @app: ^(Hash[String, untyped]) -> [ Integer, Hash[String, String | Array[String]], untyped ]
402
+ @initial_settings_frame: String
401
403
 
402
404
  # Creates a new Http2 handler.
403
405
  #
@@ -405,6 +407,7 @@ module Raptor
405
407
  # @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
406
408
  # @param connection_options [Hash] per-connection settings shared across protocols
407
409
  # @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
410
+ # @option connection_options [Integer, nil] :max_body_size maximum request body size in bytes
408
411
  # @param http2_options [Hash] HTTP/2-specific settings
409
412
  # @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
410
413
  # @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable