raptor 0.22.3 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/Dockerfile +10 -2
- data/README.md +183 -61
- data/docs/assets/profile_picture.png +0 -0
- data/docs/assets/startup_output.svg +37 -0
- data/docs/raptor-vs-puma.md +104 -424
- data/docs/talk.md +1388 -0
- data/ext/raptor_bpf/reuseport_select.bpf.c +18 -9
- data/ext/raptor_http/extconf.rb +2 -0
- data/ext/raptor_http/raptor_http.c +165 -234
- data/ext/raptor_http2/raptor_http2.c +272 -223
- data/ext/raptor_native/raptor_native.c +38 -20
- data/lib/rackup/handler/raptor.rb +14 -12
- data/lib/raptor/binder.rb +12 -12
- data/lib/raptor/cli.rb +19 -18
- data/lib/raptor/cluster.rb +120 -74
- data/lib/raptor/control_server.rb +5 -2
- data/lib/raptor/detached_body.rb +6 -2
- data/lib/raptor/http.rb +7 -6
- data/lib/raptor/http1.rb +281 -234
- data/lib/raptor/http2.rb +356 -276
- data/lib/raptor/plugin.rb +98 -0
- data/lib/raptor/reactor.rb +138 -148
- data/lib/raptor/reuseport_bpf.rb +62 -38
- data/lib/raptor/server.rb +45 -34
- data/lib/raptor/stats.rb +15 -3
- data/lib/raptor/thread_locals.rb +4 -3
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/rackup/handler/raptor.rbs +2 -1
- data/sig/generated/raptor/binder.rbs +9 -9
- data/sig/generated/raptor/cli.rbs +6 -6
- data/sig/generated/raptor/cluster.rbs +18 -5
- data/sig/generated/raptor/control_server.rbs +2 -1
- data/sig/generated/raptor/detached_body.rbs +5 -1
- data/sig/generated/raptor/http.rbs +3 -5
- data/sig/generated/raptor/http1.rbs +119 -102
- data/sig/generated/raptor/http2.rbs +204 -190
- data/sig/generated/raptor/plugin.rbs +52 -0
- data/sig/generated/raptor/reactor.rbs +107 -118
- data/sig/generated/raptor/reuseport_bpf.rbs +54 -17
- data/sig/generated/raptor/server.rbs +26 -26
- data/sig/generated/raptor/stats.rbs +3 -2
- data/sig/generated/raptor/thread_locals.rbs +4 -3
- metadata +6 -2
- data/docs/brisrails-talk.md +0 -2398
data/lib/raptor/http1.rb
CHANGED
|
@@ -11,6 +11,7 @@ require "rack"
|
|
|
11
11
|
require_relative "detached_body"
|
|
12
12
|
require_relative "http"
|
|
13
13
|
require_relative "raptor_http"
|
|
14
|
+
require_relative "server"
|
|
14
15
|
require_relative "thread_locals"
|
|
15
16
|
|
|
16
17
|
module Raptor
|
|
@@ -29,29 +30,33 @@ module Raptor
|
|
|
29
30
|
|
|
30
31
|
HTTP_10 = "HTTP/1.0"
|
|
31
32
|
HTTP_11 = "HTTP/1.1"
|
|
32
|
-
STATUS_LINE_CACHE_10 = Hash.new do |
|
|
33
|
+
STATUS_LINE_CACHE_10 = Hash.new do |cache, status|
|
|
33
34
|
reason = Rack::Utils::HTTP_STATUS_CODES[status]
|
|
34
|
-
|
|
35
|
+
cache[status] = "#{HTTP_10} #{status}#{reason ? " #{reason}" : ""}\r\n".freeze
|
|
35
36
|
end
|
|
36
|
-
STATUS_LINE_CACHE_11 = Hash.new do |
|
|
37
|
+
STATUS_LINE_CACHE_11 = Hash.new do |cache, status|
|
|
37
38
|
reason = Rack::Utils::HTTP_STATUS_CODES[status]
|
|
38
|
-
|
|
39
|
+
cache[status] = "#{HTTP_11} #{status}#{reason ? " #{reason}" : ""}\r\n".freeze
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
CONTINUE_RESPONSE = "HTTP/1.1 100 Continue\r\n\r\n"
|
|
42
43
|
BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
43
44
|
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
44
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"
|
|
47
|
+
CHUNKED_TERMINATOR = "0\r\n\r\n"
|
|
45
48
|
|
|
46
49
|
CONNECTION_CLOSE = "close"
|
|
47
50
|
CONNECTION_KEEPALIVE = "keep-alive"
|
|
48
51
|
EXPECT_100_CONTINUE = "100-continue"
|
|
49
52
|
TRANSFER_ENCODING_CHUNKED = "chunked"
|
|
50
|
-
CHUNKED_TERMINATOR = "0\r\n\r\n"
|
|
51
53
|
|
|
52
54
|
HTTP_CONNECTION = "HTTP_CONNECTION"
|
|
53
55
|
HTTP_EXPECT = "HTTP_EXPECT"
|
|
54
56
|
HTTP_TRANSFER_ENCODING = "HTTP_TRANSFER_ENCODING"
|
|
57
|
+
HTTP_X_FORWARDED_PROTO = "HTTP_X_FORWARDED_PROTO"
|
|
58
|
+
HTTP_X_FORWARDED_SCHEME = "HTTP_X_FORWARDED_SCHEME"
|
|
59
|
+
HTTP_X_FORWARDED_SSL = "HTTP_X_FORWARDED_SSL"
|
|
55
60
|
RACK_HEADER_PREFIX = "rack."
|
|
56
61
|
RACK_HIJACKED = "rack.hijacked"
|
|
57
62
|
RACK_HIJACK_IO = "rack.hijack_io"
|
|
@@ -71,31 +76,40 @@ module Raptor
|
|
|
71
76
|
!http_host || http_host.empty? || http_host.include?(",")
|
|
72
77
|
end
|
|
73
78
|
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
# missing, not the final encoding, or duplicated; a `Transfer-Encoding`
|
|
77
|
-
# paired with a `Content-Length`; or a `Content-Length` containing any
|
|
78
|
-
# non-digit character.
|
|
79
|
+
# Rejects ambiguous message framing per RFC 9112 section 6.3 and
|
|
80
|
+
# transfer codings other than a single `chunked`.
|
|
79
81
|
#
|
|
80
82
|
# @param env [Hash] the Rack environment after header parsing
|
|
81
|
-
# @return [
|
|
83
|
+
# @return [Symbol, nil] `:malformed`, `:unsupported_encoding`, or nil
|
|
82
84
|
#
|
|
83
|
-
# @rbs (Hash[String, untyped] env) ->
|
|
84
|
-
def self.
|
|
85
|
+
# @rbs (Hash[String, untyped] env) -> Symbol?
|
|
86
|
+
def self.framing_error(env)
|
|
85
87
|
transfer_encoding = env[HTTP_TRANSFER_ENCODING]
|
|
86
88
|
content_length = env[Http::CONTENT_LENGTH]
|
|
87
89
|
|
|
88
90
|
if transfer_encoding
|
|
89
|
-
return
|
|
91
|
+
return :malformed if content_length
|
|
90
92
|
|
|
91
|
-
encodings = transfer_encoding.downcase.split(",").map(&:strip)
|
|
92
|
-
return
|
|
93
|
-
return
|
|
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
|
|
94
97
|
elsif content_length
|
|
95
|
-
return
|
|
98
|
+
return :malformed unless content_length.match?(/\A[0-9]+\z/)
|
|
96
99
|
end
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns any pipelined bytes following the request, or nil if none remain.
|
|
105
|
+
#
|
|
106
|
+
# @param buffer [String] the raw request bytes
|
|
107
|
+
# @param offset [Integer] the byte offset where the request ends
|
|
108
|
+
# @return [String, nil]
|
|
109
|
+
#
|
|
110
|
+
# @rbs (String buffer, Integer offset) -> String?
|
|
111
|
+
def self.pipelined_bytes(buffer, offset)
|
|
112
|
+
buffer.byteslice(offset..-1) if offset < buffer.bytesize
|
|
99
113
|
end
|
|
100
114
|
|
|
101
115
|
# Encodes one HTTP/1.1 response chunk.
|
|
@@ -128,9 +142,10 @@ module Raptor
|
|
|
128
142
|
|
|
129
143
|
# Advances an HTTP/1.x request parse from the state hash's buffered
|
|
130
144
|
# bytes. A complete well-formed request returns with `:complete` set
|
|
131
|
-
# plus populated `:env` and `:body
|
|
132
|
-
#
|
|
133
|
-
#
|
|
145
|
+
# plus populated `:env` and `:body`, and records any bytes after it in
|
|
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.
|
|
134
149
|
#
|
|
135
150
|
# @param data [Hash] the current parse state
|
|
136
151
|
# @param env_template [Hash] the Rack env template to seed the request with
|
|
@@ -139,11 +154,11 @@ module Raptor
|
|
|
139
154
|
#
|
|
140
155
|
# @rbs (Hash[Symbol, untyped] data, Hash[String, untyped] env_template, Integer? max_body_size) -> Hash[Symbol, untyped]
|
|
141
156
|
def self.parse(data, env_template, max_body_size)
|
|
142
|
-
parser =
|
|
157
|
+
parser = HttpParser.new
|
|
143
158
|
env = env_template.dup
|
|
144
159
|
nread = begin
|
|
145
160
|
parser.execute(env, data[:buffer], 0)
|
|
146
|
-
rescue
|
|
161
|
+
rescue HttpParserError
|
|
147
162
|
return Ractor.make_shareable(data.merge(complete: true, malformed: true))
|
|
148
163
|
end
|
|
149
164
|
parse_data = if data[:parse_data]
|
|
@@ -154,19 +169,21 @@ module Raptor
|
|
|
154
169
|
parse_data[:parse_count] += 1
|
|
155
170
|
|
|
156
171
|
message = if parser.finished?
|
|
157
|
-
|
|
158
|
-
|
|
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)
|
|
159
175
|
elsif parser.has_body?
|
|
160
176
|
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
161
177
|
|
|
162
178
|
if max_body_size && parser.content_length > max_body_size
|
|
163
179
|
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
164
180
|
elsif parser.chunked?
|
|
165
|
-
decoded_body, chunked_state = HttpParser.decode_chunked(body_buffer, max_body_size)
|
|
181
|
+
decoded_body, chunked_state, consumed = HttpParser.decode_chunked(body_buffer, max_body_size)
|
|
166
182
|
|
|
167
183
|
case chunked_state
|
|
168
184
|
when :complete
|
|
169
185
|
env.delete(HTTP_TRANSFER_ENCODING)
|
|
186
|
+
parse_data[:pipelined] = pipelined_bytes(body_buffer, consumed)
|
|
170
187
|
data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
|
|
171
188
|
when :too_large
|
|
172
189
|
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
@@ -178,9 +195,11 @@ module Raptor
|
|
|
178
195
|
elsif parser.content_length > body_buffer.bytesize
|
|
179
196
|
data.merge(env: env, parse_data: parse_data)
|
|
180
197
|
else
|
|
181
|
-
|
|
198
|
+
parse_data[:pipelined] = pipelined_bytes(body_buffer, parser.content_length)
|
|
199
|
+
data.merge(env: env, body: body_buffer.byteslice(0, parser.content_length), parse_data: parse_data, complete: true)
|
|
182
200
|
end
|
|
183
201
|
else
|
|
202
|
+
parse_data[:pipelined] = pipelined_bytes(data[:buffer], nread)
|
|
184
203
|
data.merge(env: env, body: nil, parse_data: parse_data, complete: true)
|
|
185
204
|
end
|
|
186
205
|
else
|
|
@@ -190,7 +209,6 @@ module Raptor
|
|
|
190
209
|
end
|
|
191
210
|
|
|
192
211
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
193
|
-
# @rbs @server_port: Integer
|
|
194
212
|
# @rbs @server_port_string: String
|
|
195
213
|
# @rbs @write_timeout: Integer
|
|
196
214
|
# @rbs @max_body_size: Integer?
|
|
@@ -199,7 +217,7 @@ module Raptor
|
|
|
199
217
|
# @rbs @access_log_io: IO?
|
|
200
218
|
# @rbs @clean_thread_locals: bool
|
|
201
219
|
# @rbs @clean_fiber_locals: bool
|
|
202
|
-
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void
|
|
220
|
+
# @rbs @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
|
|
203
221
|
# @rbs @running: AtomicBoolean
|
|
204
222
|
# @rbs @env_template: Hash[String, untyped]
|
|
205
223
|
|
|
@@ -219,10 +237,9 @@ module Raptor
|
|
|
219
237
|
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
220
238
|
# @return [void]
|
|
221
239
|
#
|
|
222
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?connection_options: Hash[Symbol, untyped], ?http1_options: Hash[Symbol, untyped], ?access_log_io: IO?, ?clean_thread_locals: bool, ?clean_fiber_locals: bool, ?on_error: ^(Hash[String, untyped]?, Exception) -> void
|
|
240
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?connection_options: Hash[Symbol, untyped], ?http1_options: Hash[Symbol, untyped], ?access_log_io: IO?, ?clean_thread_locals: bool, ?clean_fiber_locals: bool, ?on_error: (^(Hash[String, untyped]?, Exception) -> void)?) -> void
|
|
223
241
|
def initialize(app, server_port, connection_options: {}, http1_options: {}, access_log_io: nil, clean_thread_locals: true, clean_fiber_locals: false, on_error: nil)
|
|
224
242
|
@app = app
|
|
225
|
-
@server_port = server_port
|
|
226
243
|
@server_port_string = server_port.to_s.freeze
|
|
227
244
|
@write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
|
|
228
245
|
@max_body_size = connection_options[:max_body_size]
|
|
@@ -242,8 +259,7 @@ module Raptor
|
|
|
242
259
|
}.freeze
|
|
243
260
|
end
|
|
244
261
|
|
|
245
|
-
#
|
|
246
|
-
# handler's env template and body-size limit into the worker proc.
|
|
262
|
+
# Returns a Ractor-safe proc that parses HTTP/1.x requests.
|
|
247
263
|
#
|
|
248
264
|
# @return [Proc]
|
|
249
265
|
#
|
|
@@ -252,32 +268,6 @@ module Raptor
|
|
|
252
268
|
Http.parser_worker(@env_template, @max_body_size)
|
|
253
269
|
end
|
|
254
270
|
|
|
255
|
-
# Instance-level wrapper around {Http.socket_write} that applies the
|
|
256
|
-
# configured `write_timeout`.
|
|
257
|
-
#
|
|
258
|
-
# @param socket [TCPSocket] the socket to write to
|
|
259
|
-
# @param string [String] the data to write
|
|
260
|
-
# @return [void]
|
|
261
|
-
# @raise [Http::WriteError] if the socket is not writable within the timeout or raises IOError
|
|
262
|
-
#
|
|
263
|
-
# @rbs (TCPSocket socket, String string) -> void
|
|
264
|
-
def socket_write(socket, string)
|
|
265
|
-
Http.socket_write(socket, string, timeout: @write_timeout)
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# Instance-level wrapper around {Http.socket_writev} that applies the
|
|
269
|
-
# configured `write_timeout`.
|
|
270
|
-
#
|
|
271
|
-
# @param socket [TCPSocket] the socket to write to
|
|
272
|
-
# @param strings [Array<String>] the buffers to write in order
|
|
273
|
-
# @return [void]
|
|
274
|
-
# @raise [Http::WriteError] if the socket is not writable within the timeout or raises IOError
|
|
275
|
-
#
|
|
276
|
-
# @rbs (TCPSocket socket, Array[String] strings) -> void
|
|
277
|
-
def socket_writev(socket, strings)
|
|
278
|
-
Http.socket_writev(socket, strings, timeout: @write_timeout)
|
|
279
|
-
end
|
|
280
|
-
|
|
281
271
|
# Signals eager keep-alive loops to stop processing further requests on
|
|
282
272
|
# their connections. In-flight requests complete normally.
|
|
283
273
|
#
|
|
@@ -293,10 +283,10 @@ module Raptor
|
|
|
293
283
|
# when complete. Falls back to the reactor when more data is needed.
|
|
294
284
|
#
|
|
295
285
|
# @param socket [TCPSocket] the freshly accepted client socket
|
|
296
|
-
# @param id [Integer] unique
|
|
286
|
+
# @param id [Integer] unique connection identifier
|
|
297
287
|
# @param reactor [Reactor] the reactor for fallback registration
|
|
298
288
|
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
299
|
-
# @param remote_addr [String] client IP address
|
|
289
|
+
# @param remote_addr [String] the client IP address
|
|
300
290
|
# @param url_scheme [String] "http" or "https"
|
|
301
291
|
# @return [void]
|
|
302
292
|
#
|
|
@@ -319,18 +309,12 @@ module Raptor
|
|
|
319
309
|
return
|
|
320
310
|
end
|
|
321
311
|
|
|
322
|
-
|
|
312
|
+
unless parser.finished?
|
|
323
313
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
324
314
|
return
|
|
325
|
-
elsif Http1.invalid_host?(env) || Http1.request_smuggling?(env)
|
|
326
|
-
reject_malformed(socket)
|
|
327
|
-
return
|
|
328
|
-
elsif parser.has_body? && @max_body_size && parser.content_length > @max_body_size
|
|
329
|
-
reject_oversized(socket)
|
|
330
|
-
return
|
|
331
315
|
end
|
|
332
316
|
|
|
333
|
-
body = extract_body(buffer, env, parser, nread
|
|
317
|
+
body = extract_body(buffer, env, parse_data, parser, nread)
|
|
334
318
|
case body
|
|
335
319
|
when :incomplete
|
|
336
320
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
@@ -341,6 +325,9 @@ module Raptor
|
|
|
341
325
|
when :malformed
|
|
342
326
|
reject_malformed(socket)
|
|
343
327
|
return
|
|
328
|
+
when :unsupported_encoding
|
|
329
|
+
reject_unsupported_encoding(socket)
|
|
330
|
+
return
|
|
344
331
|
end
|
|
345
332
|
|
|
346
333
|
thread_pool << proc do
|
|
@@ -370,10 +357,13 @@ module Raptor
|
|
|
370
357
|
return
|
|
371
358
|
end
|
|
372
359
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
+
|
|
366
|
+
if parsed_request[:complete]
|
|
377
367
|
socket = reactor.remove(parsed_request[:id])
|
|
378
368
|
request_count = (parsed_request[:request_count] || 0) + 1
|
|
379
369
|
remote_addr = parsed_request[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
|
|
@@ -393,6 +383,115 @@ module Raptor
|
|
|
393
383
|
url_scheme
|
|
394
384
|
)
|
|
395
385
|
end
|
|
386
|
+
else
|
|
387
|
+
parsed_request = send_continue_if_expected(parsed_request, reactor)
|
|
388
|
+
reactor.update_state(parsed_request)
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Processes kept-alive requests on application threads, yielding to queued
|
|
393
|
+
# work and returning idle or incomplete connections to the reactor.
|
|
394
|
+
#
|
|
395
|
+
# @param socket [TCPSocket] the client socket
|
|
396
|
+
# @param id [Integer] unique connection identifier
|
|
397
|
+
# @param reactor [Reactor] the reactor for fallback registration
|
|
398
|
+
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
399
|
+
# @param request_count [Integer] number of requests handled on this connection
|
|
400
|
+
# @param remote_addr [String] the client IP address
|
|
401
|
+
# @param url_scheme [String] "http" or "https"
|
|
402
|
+
# @param resumed [Boolean] whether the reactor queued the connection once its next request became readable
|
|
403
|
+
# @param pipelined [String, nil] bytes the client sent after the previous request
|
|
404
|
+
# @return [void]
|
|
405
|
+
#
|
|
406
|
+
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, Integer request_count, String remote_addr, String url_scheme, ?resumed: bool, ?pipelined: String?) -> void
|
|
407
|
+
def eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme, resumed: false, pipelined: nil)
|
|
408
|
+
loop do
|
|
409
|
+
unless @running.true?
|
|
410
|
+
socket.close rescue nil
|
|
411
|
+
return
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
if pipelined
|
|
415
|
+
buffer = pipelined
|
|
416
|
+
else
|
|
417
|
+
unless resumed || socket.wait_readable(0)
|
|
418
|
+
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
419
|
+
return
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
begin
|
|
423
|
+
buffer = read_into_thread_buffer(socket)
|
|
424
|
+
rescue IO::WaitReadable
|
|
425
|
+
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
426
|
+
return
|
|
427
|
+
rescue EOFError, IOError, SystemCallError
|
|
428
|
+
socket.close rescue nil
|
|
429
|
+
return
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
env, parse_data, nread, parser = begin
|
|
434
|
+
parse_next_request(buffer)
|
|
435
|
+
rescue HttpParserError
|
|
436
|
+
reject_malformed(socket)
|
|
437
|
+
return
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
unless parser.finished?
|
|
441
|
+
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
|
|
442
|
+
return
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
body = extract_body(buffer, env, parse_data, parser, nread)
|
|
446
|
+
case body
|
|
447
|
+
when :incomplete
|
|
448
|
+
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
|
|
449
|
+
return
|
|
450
|
+
when :too_large
|
|
451
|
+
reject_oversized(socket)
|
|
452
|
+
return
|
|
453
|
+
when :malformed
|
|
454
|
+
reject_malformed(socket)
|
|
455
|
+
return
|
|
456
|
+
when :unsupported_encoding
|
|
457
|
+
reject_unsupported_encoding(socket)
|
|
458
|
+
return
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
request_count += 1
|
|
462
|
+
|
|
463
|
+
if !resumed && thread_pool.queue_size.positive?
|
|
464
|
+
thread_pool << proc do
|
|
465
|
+
process_client(
|
|
466
|
+
socket,
|
|
467
|
+
id,
|
|
468
|
+
env,
|
|
469
|
+
parse_data,
|
|
470
|
+
body,
|
|
471
|
+
reactor,
|
|
472
|
+
thread_pool,
|
|
473
|
+
request_count,
|
|
474
|
+
remote_addr,
|
|
475
|
+
url_scheme
|
|
476
|
+
)
|
|
477
|
+
end
|
|
478
|
+
return
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
resumed = false
|
|
482
|
+
pipelined = parse_data[:pipelined]
|
|
483
|
+
keep_alive = process_request(
|
|
484
|
+
socket,
|
|
485
|
+
id,
|
|
486
|
+
env,
|
|
487
|
+
parse_data,
|
|
488
|
+
body,
|
|
489
|
+
reactor,
|
|
490
|
+
request_count,
|
|
491
|
+
remote_addr,
|
|
492
|
+
url_scheme
|
|
493
|
+
)
|
|
494
|
+
return unless keep_alive
|
|
396
495
|
end
|
|
397
496
|
end
|
|
398
497
|
|
|
@@ -400,11 +499,12 @@ module Raptor
|
|
|
400
499
|
|
|
401
500
|
# Reads pending bytes off `socket` into the thread-local read buffer,
|
|
402
501
|
# draining any additional SSL-buffered bytes so `pending` is empty on
|
|
403
|
-
# return.
|
|
404
|
-
# underlying `read_nonblock` does.
|
|
502
|
+
# return.
|
|
405
503
|
#
|
|
406
504
|
# @param socket [TCPSocket] the socket to read from
|
|
407
505
|
# @return [String] the thread-local buffer, freshly populated
|
|
506
|
+
# @raise [IO::WaitReadable] if no bytes are ready to read
|
|
507
|
+
# @raise [EOFError, IOError, SystemCallError] if the connection is closed or fails
|
|
408
508
|
#
|
|
409
509
|
# @rbs (TCPSocket socket) -> String
|
|
410
510
|
def read_into_thread_buffer(socket)
|
|
@@ -418,12 +518,11 @@ module Raptor
|
|
|
418
518
|
buffer
|
|
419
519
|
end
|
|
420
520
|
|
|
421
|
-
# Runs a fresh HTTP/1.x parse against `buffer
|
|
422
|
-
# `[env, parse_data, nread, parser]`. Raises `HttpParserError` on
|
|
423
|
-
# malformed input.
|
|
521
|
+
# Runs a fresh HTTP/1.x parse against `buffer`.
|
|
424
522
|
#
|
|
425
523
|
# @param buffer [String] the raw request bytes
|
|
426
|
-
# @return [Array(Hash, Hash, Integer, HttpParser)]
|
|
524
|
+
# @return [Array(Hash, Hash, Integer, HttpParser)] the env, parse data, bytes read, and parser
|
|
525
|
+
# @raise [HttpParserError] if the request is malformed
|
|
427
526
|
#
|
|
428
527
|
# @rbs (String buffer) -> [Hash[String, untyped], Hash[Symbol, untyped], Integer, HttpParser]
|
|
429
528
|
def parse_next_request(buffer)
|
|
@@ -435,51 +534,55 @@ module Raptor
|
|
|
435
534
|
[env, parse_data, nread, parser]
|
|
436
535
|
end
|
|
437
536
|
|
|
438
|
-
#
|
|
439
|
-
# `String` (or `nil` when the request has no body), or one of
|
|
440
|
-
# `:incomplete`, `:too_large`, `:malformed` when the caller must
|
|
441
|
-
# fall back or reject.
|
|
537
|
+
# Validates request framing and extracts the body, preserving pipelined bytes.
|
|
442
538
|
#
|
|
443
539
|
# @param buffer [String] the raw request bytes
|
|
444
540
|
# @param env [Hash] the Rack environment being built
|
|
541
|
+
# @param parse_data [Hash] metadata from the parsing pass
|
|
445
542
|
# @param parser [HttpParser] the parser holding the finished parse state
|
|
446
543
|
# @param nread [Integer] the byte offset where the body begins in `buffer`
|
|
447
|
-
# @
|
|
448
|
-
#
|
|
544
|
+
# @return [String, nil, Symbol] body, nil when absent, or `:incomplete`,
|
|
545
|
+
# `:too_large`, `:malformed`, or `:unsupported_encoding`
|
|
449
546
|
#
|
|
450
|
-
# @rbs (String buffer, Hash[String, untyped] env, HttpParser parser, Integer nread
|
|
451
|
-
def extract_body(buffer, env, parser, nread
|
|
452
|
-
return
|
|
547
|
+
# @rbs (String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, HttpParser parser, Integer nread) -> (String | Symbol)?
|
|
548
|
+
def extract_body(buffer, env, parse_data, parser, nread)
|
|
549
|
+
return :malformed if Http1.invalid_host?(env)
|
|
550
|
+
error = Http1.framing_error(env)
|
|
551
|
+
return error if error
|
|
552
|
+
|
|
553
|
+
unless parser.has_body?
|
|
554
|
+
parse_data[:pipelined] = Http1.pipelined_bytes(buffer, nread)
|
|
555
|
+
return
|
|
556
|
+
end
|
|
453
557
|
|
|
454
|
-
|
|
558
|
+
return :too_large if @max_body_size && parser.content_length > @max_body_size
|
|
455
559
|
|
|
456
560
|
if parser.chunked?
|
|
457
|
-
|
|
561
|
+
body, chunked_state, consumed = HttpParser.decode_chunked(buffer.byteslice(nread..-1), @max_body_size)
|
|
562
|
+
return chunked_state unless chunked_state == :complete
|
|
458
563
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
body
|
|
464
|
-
else
|
|
465
|
-
chunked_state
|
|
466
|
-
end
|
|
467
|
-
elsif parser.content_length > body.bytesize
|
|
564
|
+
env.delete(HTTP_TRANSFER_ENCODING)
|
|
565
|
+
parse_data[:pipelined] = Http1.pipelined_bytes(buffer, nread + consumed)
|
|
566
|
+
body
|
|
567
|
+
elsif nread + parser.content_length > buffer.bytesize
|
|
468
568
|
:incomplete
|
|
469
569
|
else
|
|
470
|
-
|
|
570
|
+
parse_data[:pipelined] = Http1.pipelined_bytes(buffer, nread + parser.content_length)
|
|
571
|
+
buffer.byteslice(nread, parser.content_length)
|
|
471
572
|
end
|
|
472
573
|
end
|
|
473
574
|
|
|
474
|
-
# Returns true
|
|
475
|
-
# RFC
|
|
575
|
+
# Returns true when the request expects a 100 Continue response per
|
|
576
|
+
# RFC 9110 section 10.1.1.
|
|
476
577
|
#
|
|
477
578
|
# @param env [Hash] the parsed Rack environment (possibly incomplete)
|
|
478
579
|
# @return [Boolean]
|
|
479
580
|
#
|
|
480
581
|
# @rbs (Hash[String, untyped] env) -> bool
|
|
481
582
|
def expects_100_continue?(env)
|
|
482
|
-
|
|
583
|
+
return false unless env[Rack::SERVER_PROTOCOL] == HTTP_11
|
|
584
|
+
|
|
585
|
+
env[HTTP_EXPECT]&.casecmp?(EXPECT_100_CONTINUE) || false
|
|
483
586
|
end
|
|
484
587
|
|
|
485
588
|
# Sends an HTTP 100 Continue response when the client requested
|
|
@@ -508,35 +611,35 @@ module Raptor
|
|
|
508
611
|
# if keep-alive, eagerly reading subsequent requests inline.
|
|
509
612
|
#
|
|
510
613
|
# @param socket [TCPSocket] the client socket
|
|
511
|
-
# @param id [Integer] unique
|
|
614
|
+
# @param id [Integer] unique connection identifier
|
|
512
615
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
513
616
|
# @param parse_data [Hash] metadata from the parsing pass
|
|
514
617
|
# @param body [String, nil] decoded request body
|
|
515
618
|
# @param reactor [Reactor] the reactor managing the client connection
|
|
516
619
|
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
517
620
|
# @param request_count [Integer] number of requests handled on this connection
|
|
518
|
-
# @param remote_addr [String] client IP address
|
|
621
|
+
# @param remote_addr [String] the client IP address
|
|
519
622
|
# @param url_scheme [String] "http" or "https"
|
|
520
623
|
# @return [void]
|
|
521
624
|
#
|
|
522
625
|
# @rbs (TCPSocket socket, Integer id, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Reactor reactor, AtomicThreadPool thread_pool, Integer request_count, String remote_addr, String url_scheme) -> void
|
|
523
626
|
def process_client(socket, id, env, parse_data, body, reactor, thread_pool, request_count, remote_addr, url_scheme)
|
|
524
627
|
keep_alive = process_request(socket, id, env, parse_data, body, reactor, request_count, remote_addr, url_scheme)
|
|
525
|
-
eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme) if keep_alive
|
|
628
|
+
eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme, pipelined: parse_data[:pipelined]) if keep_alive
|
|
526
629
|
end
|
|
527
630
|
|
|
528
|
-
# Processes a single request
|
|
529
|
-
#
|
|
530
|
-
#
|
|
631
|
+
# Processes a single request, isolating fiber and thread locals as
|
|
632
|
+
# configured, and returns whether the connection stays open for another
|
|
633
|
+
# request.
|
|
531
634
|
#
|
|
532
635
|
# @param socket [TCPSocket] the client socket
|
|
533
|
-
# @param id [Integer] unique
|
|
636
|
+
# @param id [Integer] unique connection identifier
|
|
534
637
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
535
638
|
# @param parse_data [Hash] metadata from the parsing pass
|
|
536
639
|
# @param body [String, nil] decoded request body
|
|
537
640
|
# @param reactor [Reactor] the reactor managing the client connection
|
|
538
641
|
# @param request_count [Integer] number of requests handled on this connection
|
|
539
|
-
# @param remote_addr [String] client IP address
|
|
642
|
+
# @param remote_addr [String] the client IP address
|
|
540
643
|
# @param url_scheme [String] "http" or "https"
|
|
541
644
|
# @return [Boolean] true if the connection should be kept alive
|
|
542
645
|
#
|
|
@@ -554,13 +657,13 @@ module Raptor
|
|
|
554
657
|
# Calls the Rack app and writes its response for one request.
|
|
555
658
|
#
|
|
556
659
|
# @param socket [TCPSocket] the client socket
|
|
557
|
-
# @param id [Integer] unique
|
|
660
|
+
# @param id [Integer] unique connection identifier
|
|
558
661
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
559
662
|
# @param parse_data [Hash] metadata from the parsing pass
|
|
560
663
|
# @param body [String, nil] decoded request body
|
|
561
664
|
# @param reactor [Reactor] the reactor managing the client connection
|
|
562
665
|
# @param request_count [Integer] number of requests handled on this connection
|
|
563
|
-
# @param remote_addr [String] client IP address
|
|
666
|
+
# @param remote_addr [String] the client IP address
|
|
564
667
|
# @param url_scheme [String] "http" or "https"
|
|
565
668
|
# @return [Boolean] true if the connection should be kept alive
|
|
566
669
|
#
|
|
@@ -599,7 +702,14 @@ module Raptor
|
|
|
599
702
|
socket,
|
|
600
703
|
id,
|
|
601
704
|
body,
|
|
602
|
-
{
|
|
705
|
+
{
|
|
706
|
+
chunked: chunked,
|
|
707
|
+
keep_alive: keep_alive,
|
|
708
|
+
request_count: request_count,
|
|
709
|
+
remote_addr: remote_addr,
|
|
710
|
+
url_scheme: url_scheme,
|
|
711
|
+
pipelined: parse_data[:pipelined]
|
|
712
|
+
},
|
|
603
713
|
finished
|
|
604
714
|
)
|
|
605
715
|
else
|
|
@@ -637,14 +747,14 @@ module Raptor
|
|
|
637
747
|
#
|
|
638
748
|
# @param socket [TCPSocket] the client socket
|
|
639
749
|
# @param rack_env [Hash, nil] the Rack environment, if it was built
|
|
640
|
-
# @param status [
|
|
750
|
+
# @param status [Object, nil] the status returned by the app, if any
|
|
641
751
|
# @param headers [Hash, nil] the headers returned by the app, if any
|
|
642
752
|
# @param error [Exception] the exception raised
|
|
643
753
|
# @param response_started [Boolean] whether any response bytes have been written
|
|
644
754
|
# @param hijacked [Boolean] whether the app took over the socket
|
|
645
755
|
# @return [void]
|
|
646
756
|
#
|
|
647
|
-
# @rbs (TCPSocket socket, Hash[String, untyped]? rack_env,
|
|
757
|
+
# @rbs (TCPSocket socket, Hash[String, untyped]? rack_env, untyped status, Hash[String, String | Array[String]]? headers, Exception error, response_started: bool, hijacked: bool) -> void
|
|
648
758
|
def handle_app_error(socket, rack_env, status, headers, error, response_started:, hijacked:)
|
|
649
759
|
Http.call_response_finished(rack_env, status, headers, error)
|
|
650
760
|
socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked
|
|
@@ -656,112 +766,22 @@ module Raptor
|
|
|
656
766
|
end
|
|
657
767
|
end
|
|
658
768
|
|
|
659
|
-
# Reads and processes subsequent requests inline on a kept-alive
|
|
660
|
-
# connection. Returns the connection to the reactor when no data is ready
|
|
661
|
-
# or a request is incomplete, and queues complete requests behind work
|
|
662
|
-
# that is already waiting.
|
|
663
|
-
#
|
|
664
|
-
# @param socket [TCPSocket] the client socket
|
|
665
|
-
# @param id [Integer] unique client identifier
|
|
666
|
-
# @param reactor [Reactor] the reactor for fallback registration
|
|
667
|
-
# @param thread_pool [AtomicThreadPool] thread pool for deprioritization
|
|
668
|
-
# @param request_count [Integer] number of requests handled on this connection
|
|
669
|
-
# @param remote_addr [String] client IP address
|
|
670
|
-
# @param url_scheme [String] "http" or "https"
|
|
671
|
-
# @return [void]
|
|
672
|
-
#
|
|
673
|
-
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, Integer request_count, String remote_addr, String url_scheme) -> void
|
|
674
|
-
def eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme)
|
|
675
|
-
loop do
|
|
676
|
-
unless @running.true?
|
|
677
|
-
socket.close rescue nil
|
|
678
|
-
return
|
|
679
|
-
end
|
|
680
|
-
|
|
681
|
-
unless socket.wait_readable(0)
|
|
682
|
-
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
683
|
-
return
|
|
684
|
-
end
|
|
685
|
-
|
|
686
|
-
begin
|
|
687
|
-
buffer = read_into_thread_buffer(socket)
|
|
688
|
-
rescue IO::WaitReadable
|
|
689
|
-
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
690
|
-
return
|
|
691
|
-
rescue EOFError, IOError, SystemCallError
|
|
692
|
-
socket.close rescue nil
|
|
693
|
-
return
|
|
694
|
-
end
|
|
695
|
-
|
|
696
|
-
env, parse_data, nread, parser = begin
|
|
697
|
-
parse_next_request(buffer)
|
|
698
|
-
rescue HttpParserError
|
|
699
|
-
reject_malformed(socket)
|
|
700
|
-
return
|
|
701
|
-
end
|
|
702
|
-
|
|
703
|
-
if !parser.finished?
|
|
704
|
-
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
|
|
705
|
-
return
|
|
706
|
-
end
|
|
707
|
-
|
|
708
|
-
body = extract_body(buffer, env, parser, nread, decode_chunked: false)
|
|
709
|
-
if body == :incomplete
|
|
710
|
-
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
|
|
711
|
-
return
|
|
712
|
-
end
|
|
713
|
-
|
|
714
|
-
request_count += 1
|
|
715
|
-
|
|
716
|
-
if thread_pool.queue_size.positive?
|
|
717
|
-
thread_pool << proc do
|
|
718
|
-
process_client(
|
|
719
|
-
socket,
|
|
720
|
-
id,
|
|
721
|
-
env,
|
|
722
|
-
parse_data,
|
|
723
|
-
body,
|
|
724
|
-
reactor,
|
|
725
|
-
thread_pool,
|
|
726
|
-
request_count,
|
|
727
|
-
remote_addr,
|
|
728
|
-
url_scheme
|
|
729
|
-
)
|
|
730
|
-
end
|
|
731
|
-
return
|
|
732
|
-
end
|
|
733
|
-
|
|
734
|
-
keep_alive = process_request(
|
|
735
|
-
socket,
|
|
736
|
-
id,
|
|
737
|
-
env,
|
|
738
|
-
parse_data,
|
|
739
|
-
body,
|
|
740
|
-
reactor,
|
|
741
|
-
request_count,
|
|
742
|
-
remote_addr,
|
|
743
|
-
url_scheme
|
|
744
|
-
)
|
|
745
|
-
return unless keep_alive
|
|
746
|
-
end
|
|
747
|
-
end
|
|
748
|
-
|
|
749
769
|
# Re-registers a socket with the reactor for further processing when
|
|
750
770
|
# an incomplete request is received on the fast path.
|
|
751
771
|
#
|
|
752
772
|
# @param socket [TCPSocket] the client socket
|
|
753
|
-
# @param id [Integer] unique
|
|
773
|
+
# @param id [Integer] unique connection identifier
|
|
754
774
|
# @param buffer [String] the partial request data already read
|
|
755
775
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
756
776
|
# @param parse_data [Hash] metadata from the parsing pass
|
|
757
777
|
# @param reactor [Reactor] the reactor to re-register with
|
|
758
778
|
# @param request_count [Integer] number of requests handled on this connection
|
|
759
|
-
# @param remote_addr [String] client IP address
|
|
779
|
+
# @param remote_addr [String] the client IP address
|
|
760
780
|
# @param url_scheme [String] "http" or "https"
|
|
761
781
|
# @param persisted [Boolean] whether the connection has already completed at least one request
|
|
762
782
|
# @return [void]
|
|
763
783
|
#
|
|
764
|
-
# @rbs (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme, persisted: bool) -> void
|
|
784
|
+
# @rbs (TCPSocket socket, Integer id, String buffer, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, Reactor reactor, Integer request_count, String remote_addr, String url_scheme, ?persisted: bool) -> void
|
|
765
785
|
def fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme, persisted: true)
|
|
766
786
|
continued = expects_100_continue?(env)
|
|
767
787
|
socket_write(socket, CONTINUE_RESPONSE) rescue nil if continued
|
|
@@ -803,13 +823,24 @@ module Raptor
|
|
|
803
823
|
socket.close rescue nil
|
|
804
824
|
end
|
|
805
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
|
+
|
|
806
837
|
# Builds a Rack environment hash from parsed HTTP request data.
|
|
807
838
|
#
|
|
808
839
|
# @param env [Hash] partial env hash from the HTTP parser
|
|
809
840
|
# @param parse_data [Hash] metadata from the parsing pass, including content_length
|
|
810
841
|
# @param body [String, nil] decoded request body, or nil if no body
|
|
811
842
|
# @param socket [TCPSocket] the client socket, used for hijack support
|
|
812
|
-
# @param remote_addr [String] client IP address
|
|
843
|
+
# @param remote_addr [String] the client IP address
|
|
813
844
|
# @param url_scheme [String] "http" or "https"
|
|
814
845
|
# @return [Hash] fully populated Rack environment hash
|
|
815
846
|
#
|
|
@@ -833,7 +864,7 @@ module Raptor
|
|
|
833
864
|
if scheme_end
|
|
834
865
|
authority_end = request_uri.index("/", scheme_end + 3) || request_uri.bytesize
|
|
835
866
|
path_and_query = request_uri.byteslice(authority_end..-1) || ""
|
|
836
|
-
if query_delim = path_and_query.index("?")
|
|
867
|
+
if (query_delim = path_and_query.index("?"))
|
|
837
868
|
env[Rack::PATH_INFO] = query_delim.zero? ? "/" : path_and_query.byteslice(0, query_delim)
|
|
838
869
|
env[Rack::QUERY_STRING] = path_and_query.byteslice(query_delim + 1..-1)
|
|
839
870
|
else
|
|
@@ -849,7 +880,6 @@ module Raptor
|
|
|
849
880
|
end
|
|
850
881
|
|
|
851
882
|
env[Http::REMOTE_ADDR] = remote_addr
|
|
852
|
-
env[Http::HTTP_VERSION] = env[Rack::SERVER_PROTOCOL]
|
|
853
883
|
|
|
854
884
|
behind_tls_proxy = (url_scheme == Server::HTTP_SCHEME) && forwarded_https?(env)
|
|
855
885
|
env[Rack::RACK_URL_SCHEME] = behind_tls_proxy ? Server::HTTPS_SCHEME : url_scheme
|
|
@@ -884,7 +914,8 @@ module Raptor
|
|
|
884
914
|
|
|
885
915
|
# Builds the `rack.input` IO object for the request body. Returns an
|
|
886
916
|
# in-memory StringIO for bodies up to the spool threshold, or a Tempfile
|
|
887
|
-
# for larger bodies
|
|
917
|
+
# for larger bodies. The complete body is already buffered at this point,
|
|
918
|
+
# so spooling does not bound peak receive memory.
|
|
888
919
|
#
|
|
889
920
|
# @param body [String, nil] decoded request body
|
|
890
921
|
# @return [IO] an IO-like object positioned at the start of the body
|
|
@@ -911,10 +942,10 @@ module Raptor
|
|
|
911
942
|
#
|
|
912
943
|
# @rbs (Hash[String, untyped] env) -> bool
|
|
913
944
|
def forwarded_https?(env)
|
|
914
|
-
proto = env[
|
|
945
|
+
proto = env[HTTP_X_FORWARDED_PROTO] || env[HTTP_X_FORWARDED_SCHEME]
|
|
915
946
|
return true if proto && proto.split(",").first&.strip&.casecmp?(Server::HTTPS_SCHEME)
|
|
916
947
|
|
|
917
|
-
env[
|
|
948
|
+
env[HTTP_X_FORWARDED_SSL]&.casecmp?("on") || false
|
|
918
949
|
end
|
|
919
950
|
|
|
920
951
|
# Returns true when the connection should be kept alive after the
|
|
@@ -928,13 +959,10 @@ module Raptor
|
|
|
928
959
|
def keep_alive?(env, request_count)
|
|
929
960
|
return false if request_count >= @max_keepalive_requests
|
|
930
961
|
|
|
931
|
-
|
|
962
|
+
connection_tokens = env[HTTP_CONNECTION].to_s.downcase.split(",").map(&:strip)
|
|
963
|
+
return false if connection_tokens.include?(CONNECTION_CLOSE)
|
|
932
964
|
|
|
933
|
-
|
|
934
|
-
!connection_header&.casecmp?(CONNECTION_CLOSE)
|
|
935
|
-
else
|
|
936
|
-
connection_header&.casecmp?(CONNECTION_KEEPALIVE) || false
|
|
937
|
-
end
|
|
965
|
+
env[Rack::SERVER_PROTOCOL] == HTTP_11 || connection_tokens.include?(CONNECTION_KEEPALIVE)
|
|
938
966
|
end
|
|
939
967
|
|
|
940
968
|
# Sends an HTTP 103 Early Hints response, skipping any entries with
|
|
@@ -1037,14 +1065,13 @@ module Raptor
|
|
|
1037
1065
|
# @raise [TypeError] if status is not an Integer
|
|
1038
1066
|
# @raise [ArgumentError] if status is less than 100
|
|
1039
1067
|
#
|
|
1040
|
-
# @rbs (
|
|
1068
|
+
# @rbs (untyped status) -> void
|
|
1041
1069
|
def validate_status(status)
|
|
1042
1070
|
raise TypeError, "status must be an Integer" unless status.is_a?(Integer)
|
|
1043
|
-
|
|
1044
1071
|
raise ArgumentError, "status must be >= 100" unless status >= 100
|
|
1045
1072
|
end
|
|
1046
1073
|
|
|
1047
|
-
# Returns a
|
|
1074
|
+
# Returns a normalized copy of the response headers with lowercased
|
|
1048
1075
|
# keys and illegal/`rack.*`/`status` entries dropped.
|
|
1049
1076
|
#
|
|
1050
1077
|
# @param headers [Hash] raw headers from the Rack application
|
|
@@ -1083,7 +1110,6 @@ module Raptor
|
|
|
1083
1110
|
def validate_headers(headers, status, no_entity_body)
|
|
1084
1111
|
if no_entity_body
|
|
1085
1112
|
raise ArgumentError, "content-type must not be present for status #{status}" if headers.key?(Rack::CONTENT_TYPE)
|
|
1086
|
-
|
|
1087
1113
|
raise ArgumentError, "content-length must not be present for status #{status}" if headers.key?(Rack::CONTENT_LENGTH)
|
|
1088
1114
|
end
|
|
1089
1115
|
end
|
|
@@ -1103,8 +1129,8 @@ module Raptor
|
|
|
1103
1129
|
response
|
|
1104
1130
|
end
|
|
1105
1131
|
|
|
1106
|
-
# Writes the response headers
|
|
1107
|
-
#
|
|
1132
|
+
# Writes the response headers and hands the raw socket to the hijack
|
|
1133
|
+
# callback.
|
|
1108
1134
|
#
|
|
1109
1135
|
# @param socket [TCPSocket] the client socket
|
|
1110
1136
|
# @param response [String] the status line accumulated so far
|
|
@@ -1117,7 +1143,6 @@ module Raptor
|
|
|
1117
1143
|
HttpParser.format_headers(response, headers)
|
|
1118
1144
|
response << "\r\n"
|
|
1119
1145
|
socket_write(socket, response)
|
|
1120
|
-
uncork_socket(socket)
|
|
1121
1146
|
response_hijack.call(socket)
|
|
1122
1147
|
end
|
|
1123
1148
|
|
|
@@ -1152,10 +1177,11 @@ module Raptor
|
|
|
1152
1177
|
# @param body [Object] the response body
|
|
1153
1178
|
# @param http_version [String] "HTTP/1.1" or "HTTP/1.0"
|
|
1154
1179
|
# @return [void]
|
|
1180
|
+
# @raise [TypeError] if the body responds to none of `each`, `to_ary`, or `to_path`
|
|
1155
1181
|
#
|
|
1156
1182
|
# @rbs (TCPSocket socket, String response, Hash[String, String | Array[String]] headers, untyped body, String http_version) -> void
|
|
1157
1183
|
def write_full_response(socket, response, headers, body, http_version)
|
|
1158
|
-
if body.respond_to?(:call)
|
|
1184
|
+
if body.respond_to?(:call) && !body.respond_to?(:each)
|
|
1159
1185
|
HttpParser.format_headers(response, headers)
|
|
1160
1186
|
response << "\r\n"
|
|
1161
1187
|
socket_write(socket, response)
|
|
@@ -1211,8 +1237,6 @@ module Raptor
|
|
|
1211
1237
|
array.sum { |chunk| chunk.is_a?(String) ? chunk.bytesize : 0 }
|
|
1212
1238
|
elsif body.respond_to?(:to_path) && (path = body.to_path) && File.readable?(path)
|
|
1213
1239
|
File.size(path)
|
|
1214
|
-
else
|
|
1215
|
-
nil
|
|
1216
1240
|
end
|
|
1217
1241
|
end
|
|
1218
1242
|
|
|
@@ -1352,8 +1376,31 @@ module Raptor
|
|
|
1352
1376
|
end
|
|
1353
1377
|
end
|
|
1354
1378
|
|
|
1355
|
-
#
|
|
1356
|
-
#
|
|
1379
|
+
# Writes response bytes within the configured write timeout.
|
|
1380
|
+
#
|
|
1381
|
+
# @param socket [TCPSocket] the socket to write to
|
|
1382
|
+
# @param string [String] the data to write
|
|
1383
|
+
# @return [void]
|
|
1384
|
+
# @raise [Http::WriteError] if the socket is not writable within the timeout or raises IOError
|
|
1385
|
+
#
|
|
1386
|
+
# @rbs (TCPSocket socket, String string) -> void
|
|
1387
|
+
def socket_write(socket, string)
|
|
1388
|
+
Http.socket_write(socket, string, timeout: @write_timeout)
|
|
1389
|
+
end
|
|
1390
|
+
|
|
1391
|
+
# Writes response buffers in order within the configured write timeout.
|
|
1392
|
+
#
|
|
1393
|
+
# @param socket [TCPSocket] the socket to write to
|
|
1394
|
+
# @param strings [Array<String>] the buffers to write in order
|
|
1395
|
+
# @return [void]
|
|
1396
|
+
# @raise [Http::WriteError] if the socket is not writable within the timeout or raises IOError
|
|
1397
|
+
#
|
|
1398
|
+
# @rbs (TCPSocket socket, Array[String] strings) -> void
|
|
1399
|
+
def socket_writev(socket, strings)
|
|
1400
|
+
Http.socket_writev(socket, strings, timeout: @write_timeout)
|
|
1401
|
+
end
|
|
1402
|
+
|
|
1403
|
+
# Writes a completed request to the access log.
|
|
1357
1404
|
#
|
|
1358
1405
|
# @param env [Hash] the Rack environment
|
|
1359
1406
|
# @param status [Integer] the response status code
|