raptor 0.7.0 → 0.9.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/.buildkite/pipeline.yml +90 -32
- data/.dockerignore +5 -0
- data/CHANGELOG.md +30 -0
- data/Dockerfile +28 -0
- data/README.md +159 -27
- data/Rakefile +19 -1
- data/docs/raptor-vs-puma.md +557 -0
- data/ext/raptor_bpf/reuseport_select.bpf.c +63 -0
- data/ext/raptor_http/extconf.rb +1 -1
- data/ext/raptor_native/extconf.rb +7 -0
- data/ext/raptor_native/raptor_native.c +99 -0
- data/lib/rackup/handler/raptor.rb +12 -2
- data/lib/raptor/binder.rb +139 -31
- data/lib/raptor/cli.rb +83 -22
- data/lib/raptor/cluster.rb +214 -33
- data/lib/raptor/http.rb +116 -0
- data/lib/raptor/{request.rb → http1.rb} +228 -99
- data/lib/raptor/http2.rb +85 -40
- data/lib/raptor/reactor.rb +22 -15
- data/lib/raptor/reuseport_bpf.rb +108 -0
- data/lib/raptor/server.rb +92 -43
- data/lib/raptor/stats.rb +1 -1
- data/lib/raptor/systemd.rb +69 -0
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/binder.rbs +82 -5
- data/sig/generated/raptor/cli.rbs +2 -3
- data/sig/generated/raptor/cluster.rbs +91 -13
- data/sig/generated/raptor/http.rbs +65 -0
- data/sig/generated/raptor/{request.rbs → http1.rbs} +122 -35
- data/sig/generated/raptor/http2.rbs +46 -13
- data/sig/generated/raptor/reactor.rbs +18 -11
- data/sig/generated/raptor/reuseport_bpf.rbs +56 -0
- data/sig/generated/raptor/server.rbs +49 -21
- data/sig/generated/raptor/systemd.rbs +42 -0
- metadata +30 -3
|
@@ -8,6 +8,7 @@ require "tempfile"
|
|
|
8
8
|
require "atomic-ruby/atomic_boolean"
|
|
9
9
|
require "rack"
|
|
10
10
|
|
|
11
|
+
require_relative "http"
|
|
11
12
|
require_relative "raptor_http"
|
|
12
13
|
|
|
13
14
|
module Raptor
|
|
@@ -16,11 +17,12 @@ module Raptor
|
|
|
16
17
|
# with the reactor for requests that need more data before they
|
|
17
18
|
# can be handled.
|
|
18
19
|
#
|
|
19
|
-
class
|
|
20
|
+
class Http1
|
|
20
21
|
BODY_BUFFER_THRESHOLD = 256 * 1024
|
|
21
22
|
FILE_CHUNK_SIZE = 64 * 1024
|
|
23
|
+
MAX_CHUNK_OVERHEAD = 16 * 1024
|
|
22
24
|
READ_BUFFER_SIZE = 64 * 1024
|
|
23
|
-
|
|
25
|
+
RESPONSE_BUFFER_CAPACITY = 4 * 1024
|
|
24
26
|
KEEPALIVE_READ_TIMEOUT = 0.001
|
|
25
27
|
MAX_KEEPALIVE_REQUESTS = 100
|
|
26
28
|
|
|
@@ -36,15 +38,18 @@ module Raptor
|
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
STATUS_WITH_NO_ENTITY_BODY = [204, 304, *100..199].freeze
|
|
41
|
+
CONTINUE_RESPONSE = "HTTP/1.1 100 Continue\r\n\r\n"
|
|
39
42
|
BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
40
|
-
INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
41
43
|
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
44
|
+
INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
42
45
|
|
|
43
46
|
CONNECTION_CLOSE = "close"
|
|
44
47
|
CONNECTION_KEEPALIVE = "keep-alive"
|
|
48
|
+
EXPECT_100_CONTINUE = "100-continue"
|
|
45
49
|
TRANSFER_ENCODING_CHUNKED = "chunked"
|
|
46
50
|
|
|
47
51
|
HTTP_CONNECTION = "HTTP_CONNECTION"
|
|
52
|
+
HTTP_EXPECT = "HTTP_EXPECT"
|
|
48
53
|
HTTP_TRANSFER_ENCODING = "HTTP_TRANSFER_ENCODING"
|
|
49
54
|
RACK_HEADER_PREFIX = "rack."
|
|
50
55
|
RACK_HIJACKED = "rack.hijacked"
|
|
@@ -53,17 +58,39 @@ module Raptor
|
|
|
53
58
|
ILLEGAL_HEADER_KEY_REGEX = /[\x00-\x20\(\)<>@,;:\\"\/\[\]\?=\{\}\x7F]/
|
|
54
59
|
ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
# Returns true when the message framing shows a request-smuggling vector
|
|
62
|
+
# per RFC 9112 section 6.3: a `Transfer-Encoding` where `chunked` is
|
|
63
|
+
# missing, not the final encoding, or duplicated; a `Transfer-Encoding`
|
|
64
|
+
# paired with a `Content-Length`; or a `Content-Length` containing any
|
|
65
|
+
# non-digit character.
|
|
66
|
+
#
|
|
67
|
+
# @param env [Hash] the Rack environment after header parsing
|
|
68
|
+
# @return [Boolean]
|
|
69
|
+
#
|
|
70
|
+
# @rbs (Hash[String, untyped] env) -> bool
|
|
71
|
+
def self.request_smuggling?(env)
|
|
72
|
+
transfer_encoding = env[HTTP_TRANSFER_ENCODING]
|
|
73
|
+
content_length = env[Http::CONTENT_LENGTH]
|
|
74
|
+
|
|
75
|
+
if transfer_encoding
|
|
76
|
+
return true if content_length
|
|
77
|
+
|
|
78
|
+
encodings = transfer_encoding.downcase.split(",").map(&:strip)
|
|
79
|
+
return true if encodings.last != TRANSFER_ENCODING_CHUNKED
|
|
80
|
+
return true if encodings.count(TRANSFER_ENCODING_CHUNKED) > 1
|
|
81
|
+
elsif content_length
|
|
82
|
+
return true if content_length.match?(/[^\d]/)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
false
|
|
60
86
|
end
|
|
61
87
|
|
|
62
88
|
# Decodes a chunked transfer-encoded body buffer.
|
|
63
89
|
#
|
|
64
90
|
# Returns the decoded bytes and a state symbol: `:complete` when the
|
|
65
91
|
# terminating zero-length chunk was found, `:too_large` when the decoded
|
|
66
|
-
# size would exceed `max_size`,
|
|
92
|
+
# size would exceed `max_size`, `:malformed` when chunk framing overhead
|
|
93
|
+
# exceeds `MAX_CHUNK_OVERHEAD`, or `:incomplete` otherwise.
|
|
67
94
|
#
|
|
68
95
|
# @param buffer [String] the raw body buffer to decode
|
|
69
96
|
# @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
|
|
@@ -73,6 +100,7 @@ module Raptor
|
|
|
73
100
|
def self.decode_chunked(buffer, max_size = nil)
|
|
74
101
|
decoded = String.new
|
|
75
102
|
offset = 0
|
|
103
|
+
overhead = 0
|
|
76
104
|
|
|
77
105
|
while offset < buffer.bytesize
|
|
78
106
|
crlf = buffer.index("\r\n", offset)
|
|
@@ -80,7 +108,10 @@ module Raptor
|
|
|
80
108
|
|
|
81
109
|
chunk_size = buffer.byteslice(offset, crlf - offset).to_i(16)
|
|
82
110
|
return [decoded, :complete] if chunk_size == 0
|
|
83
|
-
return [decoded, :too_large] if max_size && decoded.bytesize + chunk_size > max_size
|
|
111
|
+
return [decoded, :too_large] if max_size && (decoded.bytesize + chunk_size) > max_size
|
|
112
|
+
|
|
113
|
+
overhead += (crlf - offset) + 4
|
|
114
|
+
return [decoded, :malformed] if overhead > (decoded.bytesize + chunk_size + MAX_CHUNK_OVERHEAD)
|
|
84
115
|
|
|
85
116
|
offset = crlf + 2
|
|
86
117
|
decoded << buffer.byteslice(offset, chunk_size)
|
|
@@ -90,58 +121,69 @@ module Raptor
|
|
|
90
121
|
[decoded, :incomplete]
|
|
91
122
|
end
|
|
92
123
|
|
|
93
|
-
# Writes `string` in full, retrying on partial writes. Bounded by
|
|
94
|
-
# `WRITE_TIMEOUT` so a slow client can't pin the writing thread.
|
|
95
|
-
#
|
|
96
|
-
# @param socket [TCPSocket] the socket to write to
|
|
97
|
-
# @param string [String] the data to write
|
|
98
|
-
# @return [void]
|
|
99
|
-
# @raise [WriteError] if the socket is not writable within the timeout or raises IOError
|
|
100
|
-
#
|
|
101
|
-
# @rbs (TCPSocket socket, String string) -> void
|
|
102
|
-
def self.socket_write(socket, string)
|
|
103
|
-
bytes = 0
|
|
104
|
-
byte_size = string.bytesize
|
|
105
|
-
|
|
106
|
-
while bytes < byte_size
|
|
107
|
-
begin
|
|
108
|
-
bytes += socket.write_nonblock(bytes.zero? ? string : string.byteslice(bytes..-1))
|
|
109
|
-
rescue IO::WaitWritable
|
|
110
|
-
raise WriteError unless socket.wait_writable(WRITE_TIMEOUT)
|
|
111
|
-
retry
|
|
112
|
-
rescue IOError
|
|
113
|
-
raise WriteError
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
124
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
119
125
|
# @rbs @server_port: Integer
|
|
126
|
+
# @rbs @write_timeout: Integer
|
|
120
127
|
# @rbs @max_body_size: Integer?
|
|
121
128
|
# @rbs @body_spool_threshold: Integer?
|
|
129
|
+
# @rbs @max_keepalive_requests: Integer
|
|
130
|
+
# @rbs @access_log_io: IO?
|
|
122
131
|
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
123
132
|
# @rbs @running: AtomicBoolean
|
|
124
133
|
|
|
125
|
-
# Creates a new
|
|
134
|
+
# Creates a new Http1 handler.
|
|
126
135
|
#
|
|
127
136
|
# @param app [#call] the Rack application to dispatch complete requests to
|
|
128
137
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
129
|
-
# @param
|
|
130
|
-
# @option
|
|
131
|
-
# @option
|
|
138
|
+
# @param connection_options [Hash] per-connection settings shared across protocols
|
|
139
|
+
# @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
|
|
140
|
+
# @option connection_options [Integer, nil] :max_body_size maximum request body size in bytes
|
|
141
|
+
# @option connection_options [Integer, nil] :body_spool_threshold spool bodies larger than this to a tempfile
|
|
142
|
+
# @param http1_options [Hash] HTTP/1.1-specific settings
|
|
143
|
+
# @option http1_options [Integer] :max_keepalive_requests maximum requests per HTTP/1.1 keep-alive connection
|
|
144
|
+
# @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
|
|
132
145
|
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
133
146
|
# @return [void]
|
|
134
147
|
#
|
|
135
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?
|
|
136
|
-
def initialize(app, server_port,
|
|
148
|
+
# @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?, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
149
|
+
def initialize(app, server_port, connection_options: {}, http1_options: {}, access_log_io: nil, on_error: nil)
|
|
137
150
|
@app = app
|
|
138
151
|
@server_port = server_port
|
|
139
|
-
@
|
|
140
|
-
@
|
|
152
|
+
@write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
|
|
153
|
+
@max_body_size = connection_options[:max_body_size]
|
|
154
|
+
@body_spool_threshold = connection_options[:body_spool_threshold]
|
|
155
|
+
@max_keepalive_requests = http1_options[:max_keepalive_requests] || MAX_KEEPALIVE_REQUESTS
|
|
156
|
+
@access_log_io = access_log_io
|
|
141
157
|
@on_error = on_error
|
|
142
158
|
@running = AtomicBoolean.new(true)
|
|
143
159
|
end
|
|
144
160
|
|
|
161
|
+
# Instance-level wrapper around {Http.socket_write} that applies the
|
|
162
|
+
# configured `write_timeout`.
|
|
163
|
+
#
|
|
164
|
+
# @param socket [TCPSocket] the socket to write to
|
|
165
|
+
# @param string [String] the data to write
|
|
166
|
+
# @return [void]
|
|
167
|
+
# @raise [Http::WriteError] if the socket is not writable within the timeout or raises IOError
|
|
168
|
+
#
|
|
169
|
+
# @rbs (TCPSocket socket, String string) -> void
|
|
170
|
+
def socket_write(socket, string)
|
|
171
|
+
Http.socket_write(socket, string, timeout: @write_timeout)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Instance-level wrapper around {Http.socket_writev} that applies the
|
|
175
|
+
# configured `write_timeout`.
|
|
176
|
+
#
|
|
177
|
+
# @param socket [TCPSocket] the socket to write to
|
|
178
|
+
# @param strings [Array<String>] the buffers to write in order
|
|
179
|
+
# @return [void]
|
|
180
|
+
# @raise [Http::WriteError] if the socket is not writable within the timeout or raises IOError
|
|
181
|
+
#
|
|
182
|
+
# @rbs (TCPSocket socket, Array[String] strings) -> void
|
|
183
|
+
def socket_writev(socket, strings)
|
|
184
|
+
Http.socket_writev(socket, strings, timeout: @write_timeout)
|
|
185
|
+
end
|
|
186
|
+
|
|
145
187
|
# Signals eager keep-alive loops to stop processing further requests on
|
|
146
188
|
# their connections. In-flight requests complete normally.
|
|
147
189
|
#
|
|
@@ -166,8 +208,10 @@ module Raptor
|
|
|
166
208
|
#
|
|
167
209
|
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
168
210
|
def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
|
|
169
|
-
|
|
170
|
-
|
|
211
|
+
buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
|
|
212
|
+
|
|
213
|
+
begin
|
|
214
|
+
socket.read_nonblock(READ_BUFFER_SIZE, buffer)
|
|
171
215
|
rescue IO::WaitReadable
|
|
172
216
|
reactor.add(
|
|
173
217
|
id: id,
|
|
@@ -181,9 +225,6 @@ module Raptor
|
|
|
181
225
|
return
|
|
182
226
|
end
|
|
183
227
|
|
|
184
|
-
buffer = String.new
|
|
185
|
-
buffer << data
|
|
186
|
-
|
|
187
228
|
while socket.respond_to?(:pending) && socket.pending > 0
|
|
188
229
|
buffer << socket.read_nonblock(socket.pending)
|
|
189
230
|
end
|
|
@@ -202,6 +243,9 @@ module Raptor
|
|
|
202
243
|
if !parser.finished?
|
|
203
244
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
204
245
|
return
|
|
246
|
+
elsif Http1.request_smuggling?(env)
|
|
247
|
+
reject_malformed(socket)
|
|
248
|
+
return
|
|
205
249
|
elsif parser.has_body?
|
|
206
250
|
if @max_body_size && parser.content_length > @max_body_size
|
|
207
251
|
reject_oversized(socket)
|
|
@@ -211,13 +255,16 @@ module Raptor
|
|
|
211
255
|
body = buffer.byteslice(nread..-1) || ""
|
|
212
256
|
|
|
213
257
|
if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
214
|
-
body, chunked_state =
|
|
258
|
+
body, chunked_state = Http1.decode_chunked(body, @max_body_size)
|
|
215
259
|
case chunked_state
|
|
216
260
|
when :complete
|
|
217
261
|
env.delete(HTTP_TRANSFER_ENCODING)
|
|
218
262
|
when :too_large
|
|
219
263
|
reject_oversized(socket)
|
|
220
264
|
return
|
|
265
|
+
when :malformed
|
|
266
|
+
reject_malformed(socket)
|
|
267
|
+
return
|
|
221
268
|
else
|
|
222
269
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
223
270
|
return
|
|
@@ -263,13 +310,15 @@ module Raptor
|
|
|
263
310
|
parse_data[:parse_count] += 1
|
|
264
311
|
|
|
265
312
|
message = if parser.finished?
|
|
266
|
-
if
|
|
313
|
+
if Raptor::Http1.request_smuggling?(env)
|
|
314
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
|
|
315
|
+
elsif parser.has_body?
|
|
267
316
|
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
268
317
|
|
|
269
318
|
if max_body_size && parser.content_length > max_body_size
|
|
270
319
|
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
271
320
|
elsif env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
272
|
-
decoded_body, chunked_state = Raptor::
|
|
321
|
+
decoded_body, chunked_state = Raptor::Http1.decode_chunked(body_buffer, max_body_size)
|
|
273
322
|
|
|
274
323
|
case chunked_state
|
|
275
324
|
when :complete
|
|
@@ -277,6 +326,8 @@ module Raptor
|
|
|
277
326
|
data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
|
|
278
327
|
when :too_large
|
|
279
328
|
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
329
|
+
when :malformed
|
|
330
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
|
|
280
331
|
else
|
|
281
332
|
data.merge(env: env, parse_data: parse_data)
|
|
282
333
|
end
|
|
@@ -320,6 +371,7 @@ module Raptor
|
|
|
320
371
|
end
|
|
321
372
|
|
|
322
373
|
unless parsed_request[:complete]
|
|
374
|
+
parsed_request = send_continue_if_expected(parsed_request, reactor)
|
|
323
375
|
reactor.update_state(parsed_request)
|
|
324
376
|
else
|
|
325
377
|
socket = reactor.remove(parsed_request[:id])
|
|
@@ -346,6 +398,41 @@ module Raptor
|
|
|
346
398
|
|
|
347
399
|
private
|
|
348
400
|
|
|
401
|
+
# Returns true if the request expects a 100 Continue response per
|
|
402
|
+
# RFC 7231 section 5.1.1.
|
|
403
|
+
#
|
|
404
|
+
# @param env [Hash] the parsed Rack environment (possibly incomplete)
|
|
405
|
+
# @return [Boolean]
|
|
406
|
+
#
|
|
407
|
+
# @rbs (Hash[String, untyped] env) -> bool
|
|
408
|
+
def expects_100_continue?(env)
|
|
409
|
+
(env[Rack::SERVER_PROTOCOL] == HTTP_11) && env[HTTP_EXPECT]&.casecmp?(EXPECT_100_CONTINUE)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Sends an HTTP 100 Continue response when an HTTP/1.1 client requested
|
|
413
|
+
# `Expect: 100-continue` and the request body has not yet been received.
|
|
414
|
+
#
|
|
415
|
+
# Returns the state hash with `:continued` set when the response has been
|
|
416
|
+
# written. A write failure is silently ignored.
|
|
417
|
+
#
|
|
418
|
+
# @param state [Hash] the partially-parsed connection state
|
|
419
|
+
# @param reactor [Reactor] the reactor holding the connection's socket
|
|
420
|
+
# @return [Hash] the state, with `:continued` set if 100 was written
|
|
421
|
+
#
|
|
422
|
+
# @rbs (Hash[Symbol, untyped] state, Reactor reactor) -> Hash[Symbol, untyped]
|
|
423
|
+
def send_continue_if_expected(state, reactor)
|
|
424
|
+
return state if state[:continued]
|
|
425
|
+
|
|
426
|
+
env = state[:env]
|
|
427
|
+
return state unless env && expects_100_continue?(env)
|
|
428
|
+
|
|
429
|
+
socket = reactor.socket_for(state[:id])
|
|
430
|
+
return state unless socket
|
|
431
|
+
|
|
432
|
+
socket_write(socket, CONTINUE_RESPONSE) rescue nil
|
|
433
|
+
state.merge(continued: true)
|
|
434
|
+
end
|
|
435
|
+
|
|
349
436
|
# Processes a client connection by handling the current request and,
|
|
350
437
|
# if keep-alive, eagerly reading subsequent requests inline.
|
|
351
438
|
#
|
|
@@ -400,10 +487,12 @@ module Raptor
|
|
|
400
487
|
hijacked = headers.is_a?(Hash) && !!headers[Rack::RACK_HIJACK]
|
|
401
488
|
streaming = body.respond_to?(:call) && !body.respond_to?(:each)
|
|
402
489
|
keep_alive = (hijacked || streaming) ? false : keep_alive?(rack_env, request_count)
|
|
490
|
+
response_size = response_size(headers, body) unless hijacked
|
|
403
491
|
response_started = true
|
|
404
492
|
write_response(socket, rack_env, status, headers, body, keep_alive: keep_alive)
|
|
405
493
|
end
|
|
406
494
|
|
|
495
|
+
write_access_log(rack_env, status, response_size, remote_addr) if @access_log_io && !hijacked
|
|
407
496
|
call_response_finished(rack_env, status, headers, nil)
|
|
408
497
|
keep_alive && !hijacked
|
|
409
498
|
rescue => error
|
|
@@ -454,8 +543,10 @@ module Raptor
|
|
|
454
543
|
return
|
|
455
544
|
end
|
|
456
545
|
|
|
457
|
-
|
|
458
|
-
|
|
546
|
+
buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
|
|
547
|
+
|
|
548
|
+
begin
|
|
549
|
+
socket.read_nonblock(READ_BUFFER_SIZE, buffer)
|
|
459
550
|
rescue IO::WaitReadable
|
|
460
551
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
461
552
|
return
|
|
@@ -464,9 +555,6 @@ module Raptor
|
|
|
464
555
|
return
|
|
465
556
|
end
|
|
466
557
|
|
|
467
|
-
buffer = String.new
|
|
468
|
-
buffer << data
|
|
469
|
-
|
|
470
558
|
while socket.respond_to?(:pending) && socket.pending > 0
|
|
471
559
|
buffer << socket.read_nonblock(socket.pending)
|
|
472
560
|
end
|
|
@@ -549,10 +637,13 @@ module Raptor
|
|
|
549
637
|
#
|
|
550
638
|
# @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
|
|
551
639
|
def fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme, persisted: true)
|
|
640
|
+
continued = expects_100_continue?(env)
|
|
641
|
+
socket_write(socket, CONTINUE_RESPONSE) rescue nil if continued
|
|
642
|
+
|
|
552
643
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
553
644
|
state = {
|
|
554
645
|
id: id,
|
|
555
|
-
buffer: buffer,
|
|
646
|
+
buffer: buffer.dup,
|
|
556
647
|
env: env,
|
|
557
648
|
request_count: request_count,
|
|
558
649
|
parse_data: parse_data,
|
|
@@ -560,6 +651,7 @@ module Raptor
|
|
|
560
651
|
url_scheme: url_scheme
|
|
561
652
|
}
|
|
562
653
|
state[:persisted] = true if persisted
|
|
654
|
+
state[:continued] = true if continued
|
|
563
655
|
reactor.update_state(Ractor.make_shareable(state))
|
|
564
656
|
end
|
|
565
657
|
|
|
@@ -603,7 +695,6 @@ module Raptor
|
|
|
603
695
|
# @rbs (Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, TCPSocket socket, ?remote_addr: String, ?url_scheme: String) -> Hash[String, untyped]
|
|
604
696
|
def build_rack_env(env, parse_data, body, socket, remote_addr: Server::DEFAULT_REMOTE_ADDR, url_scheme: Server::HTTP_SCHEME)
|
|
605
697
|
env[Rack::RACK_VERSION] = Rack::VERSION
|
|
606
|
-
env[Rack::RACK_URL_SCHEME] = url_scheme
|
|
607
698
|
env[Rack::RACK_INPUT] = build_rack_input(body)
|
|
608
699
|
env[Rack::RACK_ERRORS] = $stderr
|
|
609
700
|
env[Rack::RACK_RESPONSE_FINISHED] = []
|
|
@@ -625,10 +716,16 @@ module Raptor
|
|
|
625
716
|
env[Rack::QUERY_STRING] = "" unless env.key?(Rack::QUERY_STRING)
|
|
626
717
|
|
|
627
718
|
if (content_length = parse_data[:content_length]).positive?
|
|
628
|
-
env[
|
|
719
|
+
env[Http::CONTENT_LENGTH] = content_length.to_s
|
|
629
720
|
end
|
|
630
721
|
|
|
631
|
-
env[
|
|
722
|
+
env[Http::REMOTE_ADDR] = remote_addr
|
|
723
|
+
env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
|
|
724
|
+
env[Http::HTTP_VERSION] = env[Rack::SERVER_PROTOCOL]
|
|
725
|
+
|
|
726
|
+
behind_tls_proxy = (url_scheme == Server::HTTP_SCHEME) && forwarded_https?(env)
|
|
727
|
+
env[Rack::RACK_URL_SCHEME] = behind_tls_proxy ? Server::HTTPS_SCHEME : url_scheme
|
|
728
|
+
default_port = behind_tls_proxy ? "443" : @server_port.to_s
|
|
632
729
|
|
|
633
730
|
http_host = env[Rack::HTTP_HOST]
|
|
634
731
|
if http_host
|
|
@@ -639,10 +736,10 @@ module Raptor
|
|
|
639
736
|
host, port = http_host.split(":", 2)
|
|
640
737
|
end
|
|
641
738
|
env[Rack::SERVER_NAME] ||= host
|
|
642
|
-
env[Rack::SERVER_PORT] ||= port ||
|
|
739
|
+
env[Rack::SERVER_PORT] ||= port || default_port
|
|
643
740
|
else
|
|
644
741
|
env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
|
|
645
|
-
env[Rack::SERVER_PORT] ||=
|
|
742
|
+
env[Rack::SERVER_PORT] ||= default_port
|
|
646
743
|
end
|
|
647
744
|
|
|
648
745
|
env
|
|
@@ -668,6 +765,21 @@ module Raptor
|
|
|
668
765
|
end
|
|
669
766
|
end
|
|
670
767
|
|
|
768
|
+
# Returns true when an upstream proxy signals that it terminated TLS for
|
|
769
|
+
# this request via `X-Forwarded-Proto`, `X-Forwarded-Scheme`, or
|
|
770
|
+
# `X-Forwarded-Ssl`. Only the first comma-separated value is consulted.
|
|
771
|
+
#
|
|
772
|
+
# @param env [Hash] the Rack environment
|
|
773
|
+
# @return [Boolean]
|
|
774
|
+
#
|
|
775
|
+
# @rbs (Hash[String, untyped] env) -> bool
|
|
776
|
+
def forwarded_https?(env)
|
|
777
|
+
proto = env["HTTP_X_FORWARDED_PROTO"] || env["HTTP_X_FORWARDED_SCHEME"]
|
|
778
|
+
return true if proto && proto.split(",").first&.strip&.casecmp?(Server::HTTPS_SCHEME)
|
|
779
|
+
|
|
780
|
+
env["HTTP_X_FORWARDED_SSL"]&.casecmp?("on") || false
|
|
781
|
+
end
|
|
782
|
+
|
|
671
783
|
# Determines whether the connection should be kept alive after the response.
|
|
672
784
|
#
|
|
673
785
|
# Returns false if the request limit has been reached. For HTTP/1.1, keep-alive
|
|
@@ -680,7 +792,7 @@ module Raptor
|
|
|
680
792
|
#
|
|
681
793
|
# @rbs (Hash[String, untyped] env, Integer request_count) -> bool
|
|
682
794
|
def keep_alive?(env, request_count)
|
|
683
|
-
return false if request_count >=
|
|
795
|
+
return false if request_count >= @max_keepalive_requests
|
|
684
796
|
|
|
685
797
|
connection_header = env[HTTP_CONNECTION]
|
|
686
798
|
|
|
@@ -716,7 +828,7 @@ module Raptor
|
|
|
716
828
|
end
|
|
717
829
|
response << "\r\n"
|
|
718
830
|
|
|
719
|
-
|
|
831
|
+
socket_write(socket, response)
|
|
720
832
|
end
|
|
721
833
|
|
|
722
834
|
# Writes a complete HTTP response to the socket.
|
|
@@ -822,7 +934,8 @@ module Raptor
|
|
|
822
934
|
end
|
|
823
935
|
end
|
|
824
936
|
|
|
825
|
-
#
|
|
937
|
+
# Returns the HTTP status line for `status`. Callers must not retain the
|
|
938
|
+
# returned string across further calls on the same thread.
|
|
826
939
|
#
|
|
827
940
|
# @param http_version [String] "HTTP/1.1" or "HTTP/1.0"
|
|
828
941
|
# @param status [Integer] HTTP status code
|
|
@@ -831,7 +944,10 @@ module Raptor
|
|
|
831
944
|
# @rbs (String http_version, Integer status) -> String
|
|
832
945
|
def build_status_line(http_version, status)
|
|
833
946
|
cache = http_version == HTTP_11 ? STATUS_LINE_CACHE_11 : STATUS_LINE_CACHE_10
|
|
834
|
-
|
|
947
|
+
response = (Thread.current[:raptor_response_buffer] ||= String.new(capacity: RESPONSE_BUFFER_CAPACITY))
|
|
948
|
+
response.clear
|
|
949
|
+
response << cache[status]
|
|
950
|
+
response
|
|
835
951
|
end
|
|
836
952
|
|
|
837
953
|
# Writes response headers and delegates body writing to the hijack callback.
|
|
@@ -849,7 +965,7 @@ module Raptor
|
|
|
849
965
|
def write_hijacked_response(socket, response, headers, response_hijack)
|
|
850
966
|
response << format_headers(headers)
|
|
851
967
|
response << "\r\n"
|
|
852
|
-
|
|
968
|
+
socket_write(socket, response)
|
|
853
969
|
uncork_socket(socket)
|
|
854
970
|
response_hijack.call(socket)
|
|
855
971
|
end
|
|
@@ -874,7 +990,7 @@ module Raptor
|
|
|
874
990
|
|
|
875
991
|
response << format_headers(headers)
|
|
876
992
|
response << "\r\n"
|
|
877
|
-
|
|
993
|
+
socket_write(socket, response)
|
|
878
994
|
end
|
|
879
995
|
|
|
880
996
|
# Writes a complete response with a body.
|
|
@@ -896,7 +1012,7 @@ module Raptor
|
|
|
896
1012
|
if body.respond_to?(:call)
|
|
897
1013
|
response << format_headers(headers)
|
|
898
1014
|
response << "\r\n"
|
|
899
|
-
|
|
1015
|
+
socket_write(socket, response)
|
|
900
1016
|
uncork_socket(socket)
|
|
901
1017
|
body.call(socket)
|
|
902
1018
|
return
|
|
@@ -933,7 +1049,7 @@ module Raptor
|
|
|
933
1049
|
raise TypeError, "body must respond to each, to_ary, or to_path"
|
|
934
1050
|
end
|
|
935
1051
|
|
|
936
|
-
|
|
1052
|
+
socket_write(socket, "0\r\n\r\n") if use_chunked
|
|
937
1053
|
end
|
|
938
1054
|
|
|
939
1055
|
# Calculates content length from an array or file body without consuming it.
|
|
@@ -973,15 +1089,15 @@ module Raptor
|
|
|
973
1089
|
def write_file_body(socket, response, path, content_length, use_chunked)
|
|
974
1090
|
File.open(path, "rb") do |file|
|
|
975
1091
|
if use_chunked
|
|
976
|
-
|
|
1092
|
+
socket_write(socket, response)
|
|
977
1093
|
while (chunk = file.read(FILE_CHUNK_SIZE))
|
|
978
|
-
|
|
1094
|
+
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
979
1095
|
end
|
|
980
1096
|
elsif content_length && content_length < BODY_BUFFER_THRESHOLD
|
|
981
1097
|
response << file.read(content_length)
|
|
982
|
-
|
|
1098
|
+
socket_write(socket, response)
|
|
983
1099
|
else
|
|
984
|
-
|
|
1100
|
+
socket_write(socket, response)
|
|
985
1101
|
IO.copy_stream(file, socket)
|
|
986
1102
|
end
|
|
987
1103
|
end
|
|
@@ -1006,10 +1122,7 @@ module Raptor
|
|
|
1006
1122
|
end
|
|
1007
1123
|
end
|
|
1008
1124
|
|
|
1009
|
-
# Writes a single-element array body
|
|
1010
|
-
#
|
|
1011
|
-
# Small bodies are concatenated with the headers into one write to reduce
|
|
1012
|
-
# system call overhead.
|
|
1125
|
+
# Writes a single-element array body to the socket.
|
|
1013
1126
|
#
|
|
1014
1127
|
# @param socket [TCPSocket] the client socket
|
|
1015
1128
|
# @param response [String] headers already serialized, to be written before the body
|
|
@@ -1024,12 +1137,9 @@ module Raptor
|
|
|
1024
1137
|
|
|
1025
1138
|
if use_chunked
|
|
1026
1139
|
response << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
|
1027
|
-
|
|
1028
|
-
elsif chunk.bytesize < BODY_BUFFER_THRESHOLD
|
|
1029
|
-
Request.socket_write(socket, response << chunk)
|
|
1140
|
+
socket_write(socket, response)
|
|
1030
1141
|
else
|
|
1031
|
-
|
|
1032
|
-
Request.socket_write(socket, chunk)
|
|
1142
|
+
socket_writev(socket, [response, chunk])
|
|
1033
1143
|
end
|
|
1034
1144
|
end
|
|
1035
1145
|
|
|
@@ -1045,21 +1155,19 @@ module Raptor
|
|
|
1045
1155
|
# @rbs (TCPSocket socket, String response, Array[String] body_array, bool use_chunked) -> void
|
|
1046
1156
|
def write_multiple_chunks(socket, response, body_array, use_chunked)
|
|
1047
1157
|
if use_chunked
|
|
1048
|
-
|
|
1158
|
+
socket_write(socket, response)
|
|
1049
1159
|
body_array.each do |chunk|
|
|
1050
1160
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1051
1161
|
|
|
1052
1162
|
next if chunk.empty?
|
|
1053
1163
|
|
|
1054
|
-
|
|
1164
|
+
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
1055
1165
|
end
|
|
1056
1166
|
else
|
|
1057
1167
|
body_array.each do |chunk|
|
|
1058
1168
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1059
|
-
|
|
1060
|
-
response << chunk
|
|
1061
1169
|
end
|
|
1062
|
-
|
|
1170
|
+
socket_writev(socket, [response, *body_array])
|
|
1063
1171
|
end
|
|
1064
1172
|
end
|
|
1065
1173
|
|
|
@@ -1075,13 +1183,13 @@ module Raptor
|
|
|
1075
1183
|
# @rbs (TCPSocket socket, String response, untyped body, bool use_chunked) -> void
|
|
1076
1184
|
def write_enumerable_body(socket, response, body, use_chunked)
|
|
1077
1185
|
if use_chunked
|
|
1078
|
-
|
|
1186
|
+
socket_write(socket, response)
|
|
1079
1187
|
body.each do |chunk|
|
|
1080
1188
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1081
1189
|
|
|
1082
1190
|
next if chunk.empty?
|
|
1083
1191
|
|
|
1084
|
-
|
|
1192
|
+
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
1085
1193
|
end
|
|
1086
1194
|
else
|
|
1087
1195
|
body.each do |chunk|
|
|
@@ -1089,7 +1197,7 @@ module Raptor
|
|
|
1089
1197
|
|
|
1090
1198
|
response << chunk
|
|
1091
1199
|
end
|
|
1092
|
-
|
|
1200
|
+
socket_write(socket, response)
|
|
1093
1201
|
end
|
|
1094
1202
|
end
|
|
1095
1203
|
|
|
@@ -1127,16 +1235,10 @@ module Raptor
|
|
|
1127
1235
|
headers.each do |name, value|
|
|
1128
1236
|
next if illegal_header_key?(name)
|
|
1129
1237
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
next if illegal_header_value?(header_value.to_s)
|
|
1238
|
+
Array(value).flat_map { |entry| entry.to_s.split("\n") }.each do |header_value|
|
|
1239
|
+
next if illegal_header_value?(header_value)
|
|
1133
1240
|
|
|
1134
|
-
|
|
1135
|
-
end
|
|
1136
|
-
else
|
|
1137
|
-
next if illegal_header_value?(value.to_s)
|
|
1138
|
-
|
|
1139
|
-
result << "#{name}: #{value}\r\n"
|
|
1241
|
+
result << "#{name}: #{header_value}\r\n"
|
|
1140
1242
|
end
|
|
1141
1243
|
end
|
|
1142
1244
|
result
|
|
@@ -1162,6 +1264,33 @@ module Raptor
|
|
|
1162
1264
|
end
|
|
1163
1265
|
end
|
|
1164
1266
|
|
|
1267
|
+
# Instance-level wrapper around {Http.write_access_log} that routes to
|
|
1268
|
+
# the configured `@access_log_io`.
|
|
1269
|
+
#
|
|
1270
|
+
# @param env [Hash] the Rack environment
|
|
1271
|
+
# @param status [Integer] the response status code
|
|
1272
|
+
# @param size [String] the response body size in bytes, or `-` if unknown
|
|
1273
|
+
# @param remote_addr [String] the client IP address
|
|
1274
|
+
# @return [void]
|
|
1275
|
+
#
|
|
1276
|
+
# @rbs (Hash[String, untyped] env, Integer status, String size, String remote_addr) -> void
|
|
1277
|
+
def write_access_log(env, status, size, remote_addr)
|
|
1278
|
+
Http.write_access_log(@access_log_io, env, status, size, remote_addr)
|
|
1279
|
+
end
|
|
1280
|
+
|
|
1281
|
+
# Returns the response body size as a String for the access log, taken
|
|
1282
|
+
# from the `content-length` header when set, computed from the body
|
|
1283
|
+
# otherwise, or `-` when the size cannot be determined upfront.
|
|
1284
|
+
#
|
|
1285
|
+
# @param headers [Hash] the response headers
|
|
1286
|
+
# @param body [Object] the response body
|
|
1287
|
+
# @return [String]
|
|
1288
|
+
#
|
|
1289
|
+
# @rbs (Hash[String, String | Array[String]] headers, untyped body) -> String
|
|
1290
|
+
def response_size(headers, body)
|
|
1291
|
+
headers[Rack::CONTENT_LENGTH] || calculate_content_length(body)&.to_s || "-"
|
|
1292
|
+
end
|
|
1293
|
+
|
|
1165
1294
|
if Socket.const_defined?(:TCP_CORK)
|
|
1166
1295
|
# Enables TCP_CORK on the socket to batch outgoing packets into fewer segments.
|
|
1167
1296
|
#
|