raptor 0.1.0 → 0.3.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 +16 -0
- data/README.md +21 -13
- data/lib/rackup/handler/raptor.rb +102 -0
- data/lib/raptor/binder.rb +1 -0
- data/lib/raptor/cli.rb +84 -1
- data/lib/raptor/cluster.rb +88 -23
- data/lib/raptor/http2.rb +99 -39
- data/lib/raptor/reactor.rb +12 -12
- data/lib/raptor/request.rb +197 -37
- data/lib/raptor/server.rb +22 -13
- data/lib/raptor/version.rb +1 -1
- data/lib/raptor.rb +3 -3
- data/sig/generated/rackup/handler/raptor.rbs +40 -0
- data/sig/generated/raptor/cli.rbs +37 -0
- data/sig/generated/raptor/cluster.rbs +32 -3
- data/sig/generated/raptor/http2.rbs +39 -11
- data/sig/generated/raptor/reactor.rbs +7 -5
- data/sig/generated/raptor/request.rbs +72 -8
- data/sig/generated/raptor/server.rbs +13 -7
- data/sig/generated/raptor.rbs +3 -3
- metadata +3 -1
data/lib/raptor/http2.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "stringio"
|
|
5
5
|
|
|
6
|
+
require "atomic-ruby/atom"
|
|
6
7
|
require "rack"
|
|
7
8
|
|
|
8
9
|
require_relative "raptor_http2"
|
|
@@ -16,6 +17,63 @@ module Raptor
|
|
|
16
17
|
# pipeline used by HTTP/1.1 connections.
|
|
17
18
|
#
|
|
18
19
|
class Http2
|
|
20
|
+
# Lock-free per-connection frame writer.
|
|
21
|
+
#
|
|
22
|
+
# Serializes concurrent socket writes from multiple stream workers
|
|
23
|
+
# without blocking any of them.
|
|
24
|
+
#
|
|
25
|
+
class Writer
|
|
26
|
+
IDLE = :idle
|
|
27
|
+
|
|
28
|
+
# @rbs @state: Atom
|
|
29
|
+
|
|
30
|
+
# Creates a new Writer.
|
|
31
|
+
#
|
|
32
|
+
# @rbs () -> void
|
|
33
|
+
def initialize
|
|
34
|
+
@state = Atom.new(IDLE)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Writes frames to the socket, coordinating with concurrent writers
|
|
38
|
+
# so that exactly one thread is actively writing at any time.
|
|
39
|
+
#
|
|
40
|
+
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
41
|
+
# @param frames [Array<String>] frame bytes to write in order
|
|
42
|
+
# @return [void]
|
|
43
|
+
#
|
|
44
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Array[String] frames) -> void
|
|
45
|
+
def write_frames(socket, frames)
|
|
46
|
+
return if frames.nil? || frames.empty?
|
|
47
|
+
|
|
48
|
+
claimed = false
|
|
49
|
+
@state.swap do |current|
|
|
50
|
+
if current.equal?(IDLE)
|
|
51
|
+
claimed = true
|
|
52
|
+
frames
|
|
53
|
+
else
|
|
54
|
+
claimed = false
|
|
55
|
+
current + frames
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return unless claimed
|
|
60
|
+
|
|
61
|
+
loop do
|
|
62
|
+
pending = nil
|
|
63
|
+
@state.swap do |current|
|
|
64
|
+
pending = current
|
|
65
|
+
current.empty? ? IDLE : []
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
break if pending.empty?
|
|
69
|
+
|
|
70
|
+
pending.each do |frame|
|
|
71
|
+
socket.write(frame) rescue nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
19
77
|
FLAG_END_STREAM = 0x1
|
|
20
78
|
FLAG_END_HEADERS = 0x4
|
|
21
79
|
FLAG_ACK = 0x1
|
|
@@ -27,17 +85,20 @@ module Raptor
|
|
|
27
85
|
|
|
28
86
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
29
87
|
# @rbs @server_port: Integer
|
|
88
|
+
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
30
89
|
|
|
31
90
|
# Creates a new Http2 handler.
|
|
32
91
|
#
|
|
33
92
|
# @param app [#call] the Rack application to dispatch requests to
|
|
34
93
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
94
|
+
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
35
95
|
# @return [void]
|
|
36
96
|
#
|
|
37
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
|
|
38
|
-
def initialize(app, server_port)
|
|
97
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
98
|
+
def initialize(app, server_port, on_error: nil)
|
|
39
99
|
@app = app
|
|
40
100
|
@server_port = server_port
|
|
101
|
+
@on_error = on_error
|
|
41
102
|
end
|
|
42
103
|
|
|
43
104
|
# Builds the initial server SETTINGS frame to send on connection establishment.
|
|
@@ -215,13 +276,9 @@ module Raptor
|
|
|
215
276
|
socket = reactor.socket_for(result[:id])
|
|
216
277
|
return unless socket
|
|
217
278
|
|
|
218
|
-
|
|
279
|
+
writer = reactor.writer_for(result[:id])
|
|
219
280
|
|
|
220
|
-
|
|
221
|
-
mutex.synchronize do
|
|
222
|
-
result[:outgoing_frames].each { |frame| socket.write(frame) rescue nil }
|
|
223
|
-
end
|
|
224
|
-
end
|
|
281
|
+
writer.write_frames(socket, result[:outgoing_frames])
|
|
225
282
|
|
|
226
283
|
reactor.update_http2_state(result)
|
|
227
284
|
|
|
@@ -231,7 +288,7 @@ module Raptor
|
|
|
231
288
|
|
|
232
289
|
thread_pool << proc do
|
|
233
290
|
dispatch_stream_request(
|
|
234
|
-
socket,
|
|
291
|
+
socket, writer, stream_id,
|
|
235
292
|
request[:headers], request[:body],
|
|
236
293
|
remote_addr: remote_addr
|
|
237
294
|
)
|
|
@@ -245,22 +302,27 @@ module Raptor
|
|
|
245
302
|
# the response back as HTTP/2 frames.
|
|
246
303
|
#
|
|
247
304
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
248
|
-
# @param
|
|
305
|
+
# @param writer [Writer] lock-free frame writer for the connection
|
|
249
306
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
250
307
|
# @param headers [Array<Array(String, String)>] request headers
|
|
251
308
|
# @param body [String] request body
|
|
252
309
|
# @param remote_addr [String] the client IP address
|
|
253
310
|
# @return [void]
|
|
254
311
|
#
|
|
255
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket,
|
|
256
|
-
def dispatch_stream_request(socket,
|
|
312
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
313
|
+
def dispatch_stream_request(socket, writer, stream_id, headers, body, remote_addr:)
|
|
257
314
|
env = build_rack_env(headers, body, remote_addr: remote_addr)
|
|
258
315
|
status, response_headers, response_body = @app.call(env)
|
|
259
316
|
|
|
260
|
-
write_http2_response(socket,
|
|
261
|
-
rescue
|
|
262
|
-
write_http2_error_response(socket,
|
|
263
|
-
|
|
317
|
+
write_http2_response(socket, writer, stream_id, status, response_headers, response_body)
|
|
318
|
+
rescue => error
|
|
319
|
+
write_http2_error_response(socket, writer, stream_id)
|
|
320
|
+
|
|
321
|
+
if @on_error
|
|
322
|
+
@on_error.call(env, error) rescue nil
|
|
323
|
+
else
|
|
324
|
+
raise
|
|
325
|
+
end
|
|
264
326
|
ensure
|
|
265
327
|
response_body.close if response_body.respond_to?(:close)
|
|
266
328
|
end
|
|
@@ -268,15 +330,15 @@ module Raptor
|
|
|
268
330
|
# Writes a Rack response as HTTP/2 frames to the socket.
|
|
269
331
|
#
|
|
270
332
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
271
|
-
# @param
|
|
333
|
+
# @param writer [Writer] lock-free frame writer for the connection
|
|
272
334
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
273
335
|
# @param status [Integer] HTTP status code
|
|
274
336
|
# @param headers [Hash] response headers from the Rack application
|
|
275
337
|
# @param body [Object] response body responding to each
|
|
276
338
|
# @return [void]
|
|
277
339
|
#
|
|
278
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket,
|
|
279
|
-
def write_http2_response(socket,
|
|
340
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id, Integer status, Hash[String, String | Array[String]] headers, untyped body) -> void
|
|
341
|
+
def write_http2_response(socket, writer, stream_id, status, headers, body)
|
|
280
342
|
parser = Http2Parser.new
|
|
281
343
|
|
|
282
344
|
header_pairs = [[":status", status.to_s]]
|
|
@@ -296,40 +358,38 @@ module Raptor
|
|
|
296
358
|
body_chunks = []
|
|
297
359
|
body.each { |chunk| body_chunks << chunk unless chunk.empty? }
|
|
298
360
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
361
|
+
frames = []
|
|
362
|
+
if body_chunks.empty?
|
|
363
|
+
frames << parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded_headers)
|
|
364
|
+
else
|
|
365
|
+
frames << parser.build_frame(:headers, FLAG_END_HEADERS, stream_id, encoded_headers)
|
|
304
366
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
end
|
|
367
|
+
last_index = body_chunks.size - 1
|
|
368
|
+
body_chunks.each_with_index do |chunk, index|
|
|
369
|
+
flags = index == last_index ? FLAG_END_STREAM : 0
|
|
370
|
+
frames << parser.build_frame(:data, flags, stream_id, chunk)
|
|
310
371
|
end
|
|
311
372
|
end
|
|
312
|
-
|
|
313
|
-
|
|
373
|
+
|
|
374
|
+
writer.write_frames(socket, frames)
|
|
314
375
|
end
|
|
315
376
|
|
|
316
377
|
# Writes a 500 error response as HTTP/2 frames.
|
|
317
378
|
#
|
|
318
379
|
# @param socket [OpenSSL::SSL::SSLSocket] the connection socket
|
|
319
|
-
# @param
|
|
380
|
+
# @param writer [Writer] lock-free frame writer for the connection
|
|
320
381
|
# @param stream_id [Integer] the HTTP/2 stream identifier
|
|
321
382
|
# @return [void]
|
|
322
383
|
#
|
|
323
|
-
# @rbs (OpenSSL::SSL::SSLSocket socket,
|
|
324
|
-
def write_http2_error_response(socket,
|
|
384
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, Integer stream_id) -> void
|
|
385
|
+
def write_http2_error_response(socket, writer, stream_id)
|
|
325
386
|
parser = Http2Parser.new
|
|
326
387
|
encoded = parser.encode_headers([[":status", "500"]])
|
|
327
388
|
|
|
328
|
-
|
|
329
|
-
socket
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
# Connection closed
|
|
389
|
+
writer.write_frames(
|
|
390
|
+
socket,
|
|
391
|
+
[parser.build_frame(:headers, FLAG_END_STREAM | FLAG_END_HEADERS, stream_id, encoded)]
|
|
392
|
+
)
|
|
333
393
|
end
|
|
334
394
|
|
|
335
395
|
# Builds a Rack environment hash from HTTP/2 headers and body.
|
data/lib/raptor/reactor.rb
CHANGED
|
@@ -75,7 +75,7 @@ module Raptor
|
|
|
75
75
|
# @rbs @id_to_socket: Hash[Integer, TCPSocket]
|
|
76
76
|
# @rbs @socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
|
|
77
77
|
# @rbs @id_to_timeout: Hash[Integer, TimeoutClient]
|
|
78
|
-
# @rbs @
|
|
78
|
+
# @rbs @id_to_writer: Hash[Integer, untyped]
|
|
79
79
|
|
|
80
80
|
# Creates a new Reactor instance.
|
|
81
81
|
#
|
|
@@ -100,7 +100,7 @@ module Raptor
|
|
|
100
100
|
@id_to_socket = {}
|
|
101
101
|
@socket_to_state = {}
|
|
102
102
|
@id_to_timeout = {}
|
|
103
|
-
@
|
|
103
|
+
@id_to_writer = {}
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
# Starts the reactor's main event loop in a new thread.
|
|
@@ -162,12 +162,10 @@ module Raptor
|
|
|
162
162
|
def add(state)
|
|
163
163
|
socket = state[:socket]
|
|
164
164
|
state.delete(:socket)
|
|
165
|
+
writer = state.delete(:writer)
|
|
165
166
|
@id_to_socket[state[:id]] = socket
|
|
166
167
|
@socket_to_state[socket] = state
|
|
167
|
-
|
|
168
|
-
if state[:protocol] == :http2
|
|
169
|
-
@id_to_mutex[state[:id]] = Mutex.new
|
|
170
|
-
end
|
|
168
|
+
@id_to_writer[state[:id]] = writer if writer
|
|
171
169
|
|
|
172
170
|
read_and_queue_for_parse(socket, state)
|
|
173
171
|
end
|
|
@@ -252,14 +250,16 @@ module Raptor
|
|
|
252
250
|
@id_to_socket[id]
|
|
253
251
|
end
|
|
254
252
|
|
|
255
|
-
# Returns the
|
|
253
|
+
# Returns the writer object associated with a given connection, if one
|
|
254
|
+
# was supplied when the connection was added. Used by protocol handlers
|
|
255
|
+
# that need to coordinate concurrent socket writes.
|
|
256
256
|
#
|
|
257
257
|
# @param id [Integer] unique client identifier
|
|
258
|
-
# @return [
|
|
258
|
+
# @return [Object, nil] the writer, if found
|
|
259
259
|
#
|
|
260
|
-
# @rbs (Integer id) ->
|
|
261
|
-
def
|
|
262
|
-
@
|
|
260
|
+
# @rbs (Integer id) -> untyped?
|
|
261
|
+
def writer_for(id)
|
|
262
|
+
@id_to_writer[id]
|
|
263
263
|
end
|
|
264
264
|
|
|
265
265
|
# Updates connection state for an HTTP/2 connection after frame processing.
|
|
@@ -384,7 +384,7 @@ module Raptor
|
|
|
384
384
|
def cleanup(socket)
|
|
385
385
|
state = @socket_to_state.delete(socket)
|
|
386
386
|
@id_to_socket.delete(state[:id])
|
|
387
|
-
@
|
|
387
|
+
@id_to_writer.delete(state[:id])
|
|
388
388
|
socket.close
|
|
389
389
|
end
|
|
390
390
|
|
data/lib/raptor/request.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "socket"
|
|
5
5
|
require "stringio"
|
|
6
|
+
require "tempfile"
|
|
6
7
|
|
|
7
8
|
require "rack"
|
|
8
9
|
|
|
@@ -20,7 +21,7 @@ module Raptor
|
|
|
20
21
|
class Request
|
|
21
22
|
BODY_BUFFER_THRESHOLD = 256 * 1024
|
|
22
23
|
FILE_CHUNK_SIZE = 64 * 1024
|
|
23
|
-
|
|
24
|
+
READ_BUFFER_SIZE = 64 * 1024
|
|
24
25
|
WRITE_TIMEOUT = 5
|
|
25
26
|
KEEPALIVE_READ_TIMEOUT = 0.001
|
|
26
27
|
MAX_KEEPALIVE_REQUESTS = 100
|
|
@@ -38,7 +39,8 @@ module Raptor
|
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
STATUS_WITH_NO_ENTITY_BODY = Set.new([204, 304, *100..199]).freeze
|
|
41
|
-
|
|
42
|
+
INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
43
|
+
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
42
44
|
|
|
43
45
|
CONNECTION_CLOSE = "close"
|
|
44
46
|
CONNECTION_KEEPALIVE = "keep-alive"
|
|
@@ -59,19 +61,136 @@ module Raptor
|
|
|
59
61
|
def message = "could not write response"
|
|
60
62
|
end
|
|
61
63
|
|
|
64
|
+
# Decodes a chunked transfer-encoded body buffer.
|
|
65
|
+
#
|
|
66
|
+
# Returns the decoded bytes and a state symbol: `:complete` when the
|
|
67
|
+
# terminating zero-length chunk was found, `:too_large` when the decoded
|
|
68
|
+
# size would exceed `max_size`, or `:incomplete` otherwise.
|
|
69
|
+
#
|
|
70
|
+
# @param buffer [String] the raw body buffer to decode
|
|
71
|
+
# @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
|
|
72
|
+
# @return [Array(String, Symbol)] decoded body and completion state
|
|
73
|
+
#
|
|
74
|
+
# @rbs (String buffer, ?Integer? max_size) -> [String, Symbol]
|
|
75
|
+
def self.decode_chunked(buffer, max_size = nil)
|
|
76
|
+
decoded = String.new
|
|
77
|
+
offset = 0
|
|
78
|
+
|
|
79
|
+
while offset < buffer.bytesize
|
|
80
|
+
crlf = buffer.index("\r\n", offset)
|
|
81
|
+
return [decoded, :incomplete] unless crlf
|
|
82
|
+
|
|
83
|
+
chunk_size = buffer.byteslice(offset, crlf - offset).to_i(16)
|
|
84
|
+
return [decoded, :complete] if chunk_size == 0
|
|
85
|
+
return [decoded, :too_large] if max_size && decoded.bytesize + chunk_size > max_size
|
|
86
|
+
|
|
87
|
+
offset = crlf + 2
|
|
88
|
+
decoded << buffer.byteslice(offset, chunk_size)
|
|
89
|
+
offset += chunk_size + 2
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
[decoded, :incomplete]
|
|
93
|
+
end
|
|
94
|
+
|
|
62
95
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
63
96
|
# @rbs @server_port: Integer
|
|
97
|
+
# @rbs @max_body_size: Integer?
|
|
98
|
+
# @rbs @body_spool_threshold: Integer?
|
|
99
|
+
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
64
100
|
|
|
65
101
|
# Creates a new Request handler.
|
|
66
102
|
#
|
|
67
103
|
# @param app [#call] the Rack application to dispatch complete requests to
|
|
68
104
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
105
|
+
# @param client_options [Hash] client limits configuration
|
|
106
|
+
# @option client_options [Integer, nil] :max_body_size maximum request body size in bytes
|
|
107
|
+
# @option client_options [Integer, nil] :body_spool_threshold spool bodies larger than this to a tempfile
|
|
108
|
+
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
69
109
|
# @return [void]
|
|
70
110
|
#
|
|
71
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
|
|
72
|
-
def initialize(app, server_port)
|
|
111
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?client_options: Hash[Symbol, untyped], ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
112
|
+
def initialize(app, server_port, client_options: {}, on_error: nil)
|
|
73
113
|
@app = app
|
|
74
114
|
@server_port = server_port
|
|
115
|
+
@max_body_size = client_options[:max_body_size]
|
|
116
|
+
@body_spool_threshold = client_options[:body_spool_threshold]
|
|
117
|
+
@on_error = on_error
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Eagerly reads and parses the first request on a freshly accepted
|
|
121
|
+
# connection on the server thread, dispatching directly to the thread pool
|
|
122
|
+
# when complete. Falls back to the reactor when more data is needed.
|
|
123
|
+
#
|
|
124
|
+
# @param socket [TCPSocket] the freshly accepted client socket
|
|
125
|
+
# @param id [Integer] unique client identifier
|
|
126
|
+
# @param reactor [Reactor] the reactor for fallback registration
|
|
127
|
+
# @param thread_pool [AtomicThreadPool] thread pool for application processing
|
|
128
|
+
# @param remote_addr [String] client IP address
|
|
129
|
+
# @param url_scheme [String] "http" or "https"
|
|
130
|
+
# @return [void]
|
|
131
|
+
#
|
|
132
|
+
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
133
|
+
def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
|
|
134
|
+
data = begin
|
|
135
|
+
socket.read_nonblock(READ_BUFFER_SIZE)
|
|
136
|
+
rescue IO::WaitReadable
|
|
137
|
+
reactor.add(
|
|
138
|
+
id: id,
|
|
139
|
+
socket: socket,
|
|
140
|
+
remote_addr: remote_addr,
|
|
141
|
+
url_scheme: url_scheme
|
|
142
|
+
)
|
|
143
|
+
return
|
|
144
|
+
rescue EOFError, IOError
|
|
145
|
+
socket.close rescue nil
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
buffer = String.new
|
|
150
|
+
buffer << data
|
|
151
|
+
|
|
152
|
+
while socket.respond_to?(:pending) && socket.pending > 0
|
|
153
|
+
buffer << socket.read_nonblock(socket.pending)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
parser = HttpParser.new
|
|
157
|
+
env = {}
|
|
158
|
+
nread = parser.execute(env, buffer, 0)
|
|
159
|
+
parse_data = { parse_count: 1, content_length: parser.content_length }
|
|
160
|
+
|
|
161
|
+
body = nil
|
|
162
|
+
if !parser.finished?
|
|
163
|
+
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
164
|
+
return
|
|
165
|
+
elsif parser.has_body?
|
|
166
|
+
if @max_body_size && parser.content_length > @max_body_size
|
|
167
|
+
reject_oversized(socket)
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
body = buffer.byteslice(nread..-1) || ""
|
|
172
|
+
|
|
173
|
+
if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
174
|
+
body, chunked_state = Request.decode_chunked(body, @max_body_size)
|
|
175
|
+
case chunked_state
|
|
176
|
+
when :complete
|
|
177
|
+
env.delete(HTTP_TRANSFER_ENCODING)
|
|
178
|
+
when :too_large
|
|
179
|
+
reject_oversized(socket)
|
|
180
|
+
return
|
|
181
|
+
else
|
|
182
|
+
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
elsif parser.content_length > body.bytesize
|
|
186
|
+
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
187
|
+
return
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
thread_pool << proc do
|
|
192
|
+
process_client(socket, id, env, parse_data, body, reactor, thread_pool, 1, remote_addr, url_scheme)
|
|
193
|
+
end
|
|
75
194
|
end
|
|
76
195
|
|
|
77
196
|
# Returns a Proc for HTTP parsing work in Ractor context.
|
|
@@ -84,6 +203,8 @@ module Raptor
|
|
|
84
203
|
#
|
|
85
204
|
# @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
86
205
|
def http_parser_worker
|
|
206
|
+
max_body_size = @max_body_size
|
|
207
|
+
|
|
87
208
|
proc do |data|
|
|
88
209
|
next Raptor::Http2.process_frames(data) if data[:protocol] == :http2
|
|
89
210
|
|
|
@@ -101,30 +222,17 @@ module Raptor
|
|
|
101
222
|
if parser.has_body?
|
|
102
223
|
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
103
224
|
|
|
104
|
-
if
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
225
|
+
if max_body_size && parser.content_length > max_body_size
|
|
226
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
227
|
+
elsif env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
228
|
+
decoded_body, chunked_state = Raptor::Request.decode_chunked(body_buffer, max_body_size)
|
|
108
229
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
break unless crlf
|
|
112
|
-
|
|
113
|
-
chunk_size = body_buffer.byteslice(offset, crlf - offset).to_i(16)
|
|
114
|
-
|
|
115
|
-
if chunk_size == 0
|
|
116
|
-
chunked_complete = true
|
|
117
|
-
break
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
offset = crlf + 2
|
|
121
|
-
decoded_body << body_buffer.byteslice(offset, chunk_size)
|
|
122
|
-
offset += chunk_size + 2
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
if chunked_complete
|
|
230
|
+
case chunked_state
|
|
231
|
+
when :complete
|
|
126
232
|
env.delete(HTTP_TRANSFER_ENCODING)
|
|
127
233
|
data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
|
|
234
|
+
when :too_large
|
|
235
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
128
236
|
else
|
|
129
237
|
data.merge(env: env, parse_data: parse_data)
|
|
130
238
|
end
|
|
@@ -155,6 +263,12 @@ module Raptor
|
|
|
155
263
|
#
|
|
156
264
|
# @rbs (Hash[Symbol, untyped] parsed_request, Reactor reactor, AtomicThreadPool thread_pool) -> void
|
|
157
265
|
def handle_parsed_request(parsed_request, reactor, thread_pool)
|
|
266
|
+
if parsed_request[:too_large]
|
|
267
|
+
socket = reactor.remove(parsed_request[:id])
|
|
268
|
+
reject_oversized(socket) if socket
|
|
269
|
+
return
|
|
270
|
+
end
|
|
271
|
+
|
|
158
272
|
unless parsed_request[:complete]
|
|
159
273
|
reactor.update_state(parsed_request)
|
|
160
274
|
else
|
|
@@ -244,10 +358,18 @@ module Raptor
|
|
|
244
358
|
keep_alive && !hijacked
|
|
245
359
|
rescue => error
|
|
246
360
|
call_response_finished(rack_env, status, headers, error) if rack_env
|
|
247
|
-
socket.write(
|
|
361
|
+
socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked
|
|
248
362
|
keep_alive = false
|
|
249
|
-
|
|
363
|
+
|
|
364
|
+
if @on_error
|
|
365
|
+
@on_error.call(rack_env, error) rescue nil
|
|
366
|
+
else
|
|
367
|
+
raise
|
|
368
|
+
end
|
|
250
369
|
ensure
|
|
370
|
+
rack_input = rack_env && rack_env[Rack::RACK_INPUT]
|
|
371
|
+
rack_input.close! rescue nil if rack_input.respond_to?(:close!)
|
|
372
|
+
|
|
251
373
|
unless hijacked || keep_alive
|
|
252
374
|
socket.close rescue nil
|
|
253
375
|
end
|
|
@@ -278,7 +400,7 @@ module Raptor
|
|
|
278
400
|
end
|
|
279
401
|
|
|
280
402
|
data = begin
|
|
281
|
-
socket.read_nonblock(
|
|
403
|
+
socket.read_nonblock(READ_BUFFER_SIZE)
|
|
282
404
|
rescue IO::WaitReadable
|
|
283
405
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
284
406
|
return
|
|
@@ -346,8 +468,12 @@ module Raptor
|
|
|
346
468
|
end
|
|
347
469
|
end
|
|
348
470
|
|
|
349
|
-
# Re-registers a socket with the reactor for further processing
|
|
350
|
-
#
|
|
471
|
+
# Re-registers a socket with the reactor for further processing when
|
|
472
|
+
# an incomplete request is received during eager accept or eager keep-alive.
|
|
473
|
+
#
|
|
474
|
+
# The persisted flag selects between persistent_data_timeout (for
|
|
475
|
+
# kept-alive connections awaiting the next request) and chunk_data_timeout
|
|
476
|
+
# (for fresh connections awaiting the rest of the first request).
|
|
351
477
|
#
|
|
352
478
|
# @param socket [TCPSocket] the client socket
|
|
353
479
|
# @param id [Integer] unique client identifier
|
|
@@ -358,21 +484,35 @@ module Raptor
|
|
|
358
484
|
# @param request_count [Integer] number of requests handled on this connection
|
|
359
485
|
# @param remote_addr [String] client IP address
|
|
360
486
|
# @param url_scheme [String] "http" or "https"
|
|
487
|
+
# @param persisted [Boolean] whether the connection has already completed at least one request
|
|
361
488
|
# @return [void]
|
|
362
489
|
#
|
|
363
|
-
# @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) -> void
|
|
364
|
-
def fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme)
|
|
490
|
+
# @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
|
|
491
|
+
def fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme, persisted: true)
|
|
365
492
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
366
|
-
|
|
493
|
+
state = {
|
|
367
494
|
id: id,
|
|
368
495
|
buffer: buffer,
|
|
369
496
|
env: env,
|
|
370
497
|
request_count: request_count,
|
|
371
498
|
parse_data: parse_data,
|
|
372
499
|
remote_addr: remote_addr,
|
|
373
|
-
url_scheme: url_scheme
|
|
374
|
-
|
|
375
|
-
|
|
500
|
+
url_scheme: url_scheme
|
|
501
|
+
}
|
|
502
|
+
state[:persisted] = true if persisted
|
|
503
|
+
reactor.update_state(Ractor.make_shareable(state))
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Writes a 413 response and closes the socket. Used when a request body
|
|
507
|
+
# exceeds the configured maximum size.
|
|
508
|
+
#
|
|
509
|
+
# @param socket [TCPSocket] the client socket
|
|
510
|
+
# @return [void]
|
|
511
|
+
#
|
|
512
|
+
# @rbs (TCPSocket socket) -> void
|
|
513
|
+
def reject_oversized(socket)
|
|
514
|
+
socket.write(CONTENT_TOO_LARGE_RESPONSE) rescue nil
|
|
515
|
+
socket.close rescue nil
|
|
376
516
|
end
|
|
377
517
|
|
|
378
518
|
# Builds a Rack environment hash from parsed HTTP request data.
|
|
@@ -392,7 +532,7 @@ module Raptor
|
|
|
392
532
|
def build_rack_env(env, parse_data, body, socket, remote_addr: "127.0.0.1", url_scheme: HTTP_SCHEME)
|
|
393
533
|
env[Rack::RACK_VERSION] = Rack::VERSION
|
|
394
534
|
env[Rack::RACK_URL_SCHEME] = url_scheme
|
|
395
|
-
env[Rack::RACK_INPUT] = (body
|
|
535
|
+
env[Rack::RACK_INPUT] = build_rack_input(body)
|
|
396
536
|
env[Rack::RACK_ERRORS] = $stderr
|
|
397
537
|
env[Rack::RACK_RESPONSE_FINISHED] = []
|
|
398
538
|
|
|
@@ -436,6 +576,26 @@ module Raptor
|
|
|
436
576
|
env
|
|
437
577
|
end
|
|
438
578
|
|
|
579
|
+
# Builds the `rack.input` IO object for the request body. Returns an
|
|
580
|
+
# in-memory StringIO for bodies up to the spool threshold, or a Tempfile
|
|
581
|
+
# for larger bodies to bound per-worker memory.
|
|
582
|
+
#
|
|
583
|
+
# @param body [String, nil] decoded request body
|
|
584
|
+
# @return [IO] an IO-like object positioned at the start of the body
|
|
585
|
+
#
|
|
586
|
+
# @rbs (String? body) -> IO
|
|
587
|
+
def build_rack_input(body)
|
|
588
|
+
if body && @body_spool_threshold && body.bytesize > @body_spool_threshold
|
|
589
|
+
tempfile = Tempfile.new("raptor-body")
|
|
590
|
+
tempfile.binmode
|
|
591
|
+
tempfile.write(body)
|
|
592
|
+
tempfile.rewind
|
|
593
|
+
tempfile
|
|
594
|
+
else
|
|
595
|
+
(body ? StringIO.new(body) : StringIO.new).set_encoding(Encoding::ASCII_8BIT)
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
439
599
|
# Determines whether the connection should be kept alive after the response.
|
|
440
600
|
#
|
|
441
601
|
# Returns false if the request limit has been reached. For HTTP/1.1, keep-alive
|