raptor 0.2.0 → 0.4.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 +23 -0
- data/README.md +16 -16
- data/ext/raptor_http2/raptor_http2.c +1 -0
- data/lib/rackup/handler/raptor.rb +24 -11
- data/lib/raptor/binder.rb +1 -0
- data/lib/raptor/cli.rb +100 -1
- data/lib/raptor/cluster.rb +96 -26
- data/lib/raptor/http2.rb +333 -44
- data/lib/raptor/reactor.rb +67 -27
- data/lib/raptor/request.rb +196 -69
- data/lib/raptor/server.rb +112 -36
- data/lib/raptor/version.rb +1 -1
- data/lib/raptor.rb +3 -3
- data/sig/generated/raptor/cli.rbs +51 -0
- data/sig/generated/raptor/cluster.rbs +31 -3
- data/sig/generated/raptor/http2.rbs +130 -8
- data/sig/generated/raptor/reactor.rbs +22 -0
- data/sig/generated/raptor/request.rbs +75 -22
- data/sig/generated/raptor/server.rbs +51 -12
- data/sig/generated/raptor.rbs +3 -3
- metadata +1 -1
data/lib/raptor/reactor.rb
CHANGED
|
@@ -76,6 +76,7 @@ module Raptor
|
|
|
76
76
|
# @rbs @socket_to_state: Hash[TCPSocket, Hash[Symbol, untyped]]
|
|
77
77
|
# @rbs @id_to_timeout: Hash[Integer, TimeoutClient]
|
|
78
78
|
# @rbs @id_to_writer: Hash[Integer, untyped]
|
|
79
|
+
# @rbs @id_to_flow_control: Hash[Integer, untyped]
|
|
79
80
|
|
|
80
81
|
# Creates a new Reactor instance.
|
|
81
82
|
#
|
|
@@ -101,6 +102,7 @@ module Raptor
|
|
|
101
102
|
@socket_to_state = {}
|
|
102
103
|
@id_to_timeout = {}
|
|
103
104
|
@id_to_writer = {}
|
|
105
|
+
@id_to_flow_control = {}
|
|
104
106
|
end
|
|
105
107
|
|
|
106
108
|
# Starts the reactor's main event loop in a new thread.
|
|
@@ -117,33 +119,38 @@ module Raptor
|
|
|
117
119
|
Thread.current.name = self.class.name
|
|
118
120
|
|
|
119
121
|
until @queue.closed? && @queue.empty?
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
122
|
+
begin
|
|
123
|
+
timeout = @timeouts.min&.timeout(Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
124
|
+
@selector.select(timeout) do |monitor|
|
|
125
|
+
wakeup!(monitor.value)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
129
|
+
expired = []
|
|
130
|
+
@timeouts.traverse do |to_client|
|
|
131
|
+
break unless to_client.timeout(now) == 0
|
|
132
|
+
|
|
133
|
+
expired << to_client
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
expired.each do |to_client|
|
|
137
|
+
@timeouts.delete!(to_client)
|
|
138
|
+
id = to_client.client_data[:id]
|
|
139
|
+
@id_to_timeout.delete(id)
|
|
140
|
+
socket = @id_to_socket[id]
|
|
141
|
+
next unless socket
|
|
142
|
+
|
|
143
|
+
@selector.deregister(socket)
|
|
144
|
+
socket.write(TIMEOUT_RESPONSE) rescue nil
|
|
145
|
+
cleanup(socket)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
until @queue.empty?
|
|
149
|
+
register(@queue.pop)
|
|
150
|
+
end
|
|
151
|
+
rescue => error
|
|
152
|
+
warn "#{Thread.current.name} rescued:"
|
|
153
|
+
warn error.full_message
|
|
147
154
|
end
|
|
148
155
|
end
|
|
149
156
|
|
|
@@ -163,9 +170,11 @@ module Raptor
|
|
|
163
170
|
socket = state[:socket]
|
|
164
171
|
state.delete(:socket)
|
|
165
172
|
writer = state.delete(:writer)
|
|
173
|
+
flow_control = state.delete(:flow_control)
|
|
166
174
|
@id_to_socket[state[:id]] = socket
|
|
167
175
|
@socket_to_state[socket] = state
|
|
168
176
|
@id_to_writer[state[:id]] = writer if writer
|
|
177
|
+
@id_to_flow_control[state[:id]] = flow_control if flow_control
|
|
169
178
|
|
|
170
179
|
read_and_queue_for_parse(socket, state)
|
|
171
180
|
end
|
|
@@ -262,6 +271,18 @@ module Raptor
|
|
|
262
271
|
@id_to_writer[id]
|
|
263
272
|
end
|
|
264
273
|
|
|
274
|
+
# Returns the flow controller associated with a given connection, if one
|
|
275
|
+
# was supplied when the connection was added. Used by HTTP/2 stream
|
|
276
|
+
# dispatchers to honour the peer's flow-control windows.
|
|
277
|
+
#
|
|
278
|
+
# @param id [Integer] unique client identifier
|
|
279
|
+
# @return [Object, nil] the flow controller, if found
|
|
280
|
+
#
|
|
281
|
+
# @rbs (Integer id) -> untyped?
|
|
282
|
+
def flow_control_for(id)
|
|
283
|
+
@id_to_flow_control[id]
|
|
284
|
+
end
|
|
285
|
+
|
|
265
286
|
# Updates connection state for an HTTP/2 connection after frame processing.
|
|
266
287
|
#
|
|
267
288
|
# Re-registers the socket with the selector for further reads and stores
|
|
@@ -282,6 +303,24 @@ module Raptor
|
|
|
282
303
|
socket.close
|
|
283
304
|
end
|
|
284
305
|
|
|
306
|
+
# Closes the socket for the given connection and drops all reactor state
|
|
307
|
+
# associated with it. Used to terminate HTTP/2 connections after sending
|
|
308
|
+
# a GOAWAY frame.
|
|
309
|
+
#
|
|
310
|
+
# @param id [Integer] unique client identifier
|
|
311
|
+
# @return [void]
|
|
312
|
+
#
|
|
313
|
+
# @rbs (Integer id) -> void
|
|
314
|
+
def close_connection(id)
|
|
315
|
+
socket = @id_to_socket.delete(id)
|
|
316
|
+
return unless socket
|
|
317
|
+
|
|
318
|
+
@socket_to_state.delete(socket)
|
|
319
|
+
@id_to_writer.delete(id)
|
|
320
|
+
@id_to_flow_control.delete(id)
|
|
321
|
+
socket.close rescue nil
|
|
322
|
+
end
|
|
323
|
+
|
|
285
324
|
# Initiates reactor shutdown.
|
|
286
325
|
#
|
|
287
326
|
# Closes the registration queue and wakes up the selector to begin
|
|
@@ -385,6 +424,7 @@ module Raptor
|
|
|
385
424
|
state = @socket_to_state.delete(socket)
|
|
386
425
|
@id_to_socket.delete(state[:id])
|
|
387
426
|
@id_to_writer.delete(state[:id])
|
|
427
|
+
@id_to_flow_control.delete(state[:id])
|
|
388
428
|
socket.close
|
|
389
429
|
end
|
|
390
430
|
|
data/lib/raptor/request.rb
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
require "socket"
|
|
5
5
|
require "stringio"
|
|
6
|
+
require "tempfile"
|
|
6
7
|
|
|
8
|
+
require "atomic-ruby/atomic_boolean"
|
|
7
9
|
require "rack"
|
|
8
10
|
|
|
9
11
|
require_relative "raptor_http"
|
|
@@ -38,7 +40,9 @@ module Raptor
|
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
STATUS_WITH_NO_ENTITY_BODY = Set.new([204, 304, *100..199]).freeze
|
|
41
|
-
|
|
43
|
+
BAD_REQUEST_RESPONSE = "HTTP/1.1 400 Bad Request\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"
|
|
45
|
+
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
42
46
|
|
|
43
47
|
CONNECTION_CLOSE = "close"
|
|
44
48
|
CONNECTION_KEEPALIVE = "keep-alive"
|
|
@@ -61,46 +65,97 @@ module Raptor
|
|
|
61
65
|
|
|
62
66
|
# Decodes a chunked transfer-encoded body buffer.
|
|
63
67
|
#
|
|
64
|
-
# Returns the decoded bytes and a
|
|
65
|
-
# zero-length chunk was found
|
|
66
|
-
#
|
|
68
|
+
# Returns the decoded bytes and a state symbol: `:complete` when the
|
|
69
|
+
# terminating zero-length chunk was found, `:too_large` when the decoded
|
|
70
|
+
# size would exceed `max_size`, or `:incomplete` otherwise.
|
|
67
71
|
#
|
|
68
72
|
# @param buffer [String] the raw body buffer to decode
|
|
69
|
-
# @
|
|
73
|
+
# @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
|
|
74
|
+
# @return [Array(String, Symbol)] decoded body and completion state
|
|
70
75
|
#
|
|
71
|
-
# @rbs (String buffer) -> [String,
|
|
72
|
-
def self.decode_chunked(buffer)
|
|
76
|
+
# @rbs (String buffer, ?Integer? max_size) -> [String, Symbol]
|
|
77
|
+
def self.decode_chunked(buffer, max_size = nil)
|
|
73
78
|
decoded = String.new
|
|
74
79
|
offset = 0
|
|
75
80
|
|
|
76
81
|
while offset < buffer.bytesize
|
|
77
82
|
crlf = buffer.index("\r\n", offset)
|
|
78
|
-
return [decoded,
|
|
83
|
+
return [decoded, :incomplete] unless crlf
|
|
79
84
|
|
|
80
85
|
chunk_size = buffer.byteslice(offset, crlf - offset).to_i(16)
|
|
81
|
-
return [decoded,
|
|
86
|
+
return [decoded, :complete] if chunk_size == 0
|
|
87
|
+
return [decoded, :too_large] if max_size && decoded.bytesize + chunk_size > max_size
|
|
82
88
|
|
|
83
89
|
offset = crlf + 2
|
|
84
90
|
decoded << buffer.byteslice(offset, chunk_size)
|
|
85
91
|
offset += chunk_size + 2
|
|
86
92
|
end
|
|
87
93
|
|
|
88
|
-
[decoded,
|
|
94
|
+
[decoded, :incomplete]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Writes a string to the socket, retrying on partial writes and flow control blocks.
|
|
98
|
+
#
|
|
99
|
+
# Uses write_nonblock with `WRITE_TIMEOUT` to avoid blocking the thread
|
|
100
|
+
# indefinitely on slow clients.
|
|
101
|
+
#
|
|
102
|
+
# @param socket [TCPSocket] the socket to write to
|
|
103
|
+
# @param string [String] the data to write
|
|
104
|
+
# @return [void]
|
|
105
|
+
# @raise [WriteError] if the socket is not writable within the timeout or raises IOError
|
|
106
|
+
#
|
|
107
|
+
# @rbs (TCPSocket socket, String string) -> void
|
|
108
|
+
def self.socket_write(socket, string)
|
|
109
|
+
bytes = 0
|
|
110
|
+
byte_size = string.bytesize
|
|
111
|
+
|
|
112
|
+
while bytes < byte_size
|
|
113
|
+
begin
|
|
114
|
+
bytes += socket.write_nonblock(bytes.zero? ? string : string.byteslice(bytes..-1))
|
|
115
|
+
rescue IO::WaitWritable
|
|
116
|
+
raise WriteError unless socket.wait_writable(WRITE_TIMEOUT)
|
|
117
|
+
retry
|
|
118
|
+
rescue IOError
|
|
119
|
+
raise WriteError
|
|
120
|
+
end
|
|
121
|
+
end
|
|
89
122
|
end
|
|
90
123
|
|
|
91
124
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
92
125
|
# @rbs @server_port: Integer
|
|
126
|
+
# @rbs @max_body_size: Integer?
|
|
127
|
+
# @rbs @body_spool_threshold: Integer?
|
|
128
|
+
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
129
|
+
# @rbs @running: AtomicBoolean
|
|
93
130
|
|
|
94
131
|
# Creates a new Request handler.
|
|
95
132
|
#
|
|
96
133
|
# @param app [#call] the Rack application to dispatch complete requests to
|
|
97
134
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
135
|
+
# @param client_options [Hash] client limits configuration
|
|
136
|
+
# @option client_options [Integer, nil] :max_body_size maximum request body size in bytes
|
|
137
|
+
# @option client_options [Integer, nil] :body_spool_threshold spool bodies larger than this to a tempfile
|
|
138
|
+
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
98
139
|
# @return [void]
|
|
99
140
|
#
|
|
100
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port) -> void
|
|
101
|
-
def initialize(app, server_port)
|
|
141
|
+
# @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
|
|
142
|
+
def initialize(app, server_port, client_options: {}, on_error: nil)
|
|
102
143
|
@app = app
|
|
103
144
|
@server_port = server_port
|
|
145
|
+
@max_body_size = client_options[:max_body_size]
|
|
146
|
+
@body_spool_threshold = client_options[:body_spool_threshold]
|
|
147
|
+
@on_error = on_error
|
|
148
|
+
@running = AtomicBoolean.new(true)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Signals eager keep-alive loops to stop processing further requests on
|
|
152
|
+
# their connections. In-flight requests complete normally.
|
|
153
|
+
#
|
|
154
|
+
# @return [void]
|
|
155
|
+
#
|
|
156
|
+
# @rbs () -> void
|
|
157
|
+
def shutdown
|
|
158
|
+
@running.make_false
|
|
104
159
|
end
|
|
105
160
|
|
|
106
161
|
# Eagerly reads and parses the first request on a freshly accepted
|
|
@@ -141,7 +196,12 @@ module Raptor
|
|
|
141
196
|
|
|
142
197
|
parser = HttpParser.new
|
|
143
198
|
env = {}
|
|
144
|
-
nread =
|
|
199
|
+
nread = begin
|
|
200
|
+
parser.execute(env, buffer, 0)
|
|
201
|
+
rescue HttpParserError
|
|
202
|
+
reject_malformed(socket)
|
|
203
|
+
return
|
|
204
|
+
end
|
|
145
205
|
parse_data = { parse_count: 1, content_length: parser.content_length }
|
|
146
206
|
|
|
147
207
|
body = nil
|
|
@@ -149,12 +209,21 @@ module Raptor
|
|
|
149
209
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
150
210
|
return
|
|
151
211
|
elsif parser.has_body?
|
|
212
|
+
if @max_body_size && parser.content_length > @max_body_size
|
|
213
|
+
reject_oversized(socket)
|
|
214
|
+
return
|
|
215
|
+
end
|
|
216
|
+
|
|
152
217
|
body = buffer.byteslice(nread..-1) || ""
|
|
153
218
|
|
|
154
219
|
if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
155
|
-
body,
|
|
156
|
-
|
|
220
|
+
body, chunked_state = Request.decode_chunked(body, @max_body_size)
|
|
221
|
+
case chunked_state
|
|
222
|
+
when :complete
|
|
157
223
|
env.delete(HTTP_TRANSFER_ENCODING)
|
|
224
|
+
when :too_large
|
|
225
|
+
reject_oversized(socket)
|
|
226
|
+
return
|
|
158
227
|
else
|
|
159
228
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
160
229
|
return
|
|
@@ -180,12 +249,18 @@ module Raptor
|
|
|
180
249
|
#
|
|
181
250
|
# @rbs () -> ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
182
251
|
def http_parser_worker
|
|
252
|
+
max_body_size = @max_body_size
|
|
253
|
+
|
|
183
254
|
proc do |data|
|
|
184
255
|
next Raptor::Http2.process_frames(data) if data[:protocol] == :http2
|
|
185
256
|
|
|
186
257
|
parser = Raptor::HttpParser.new
|
|
187
258
|
env = {}
|
|
188
|
-
nread =
|
|
259
|
+
nread = begin
|
|
260
|
+
parser.execute(env, data[:buffer], 0)
|
|
261
|
+
rescue Raptor::HttpParserError
|
|
262
|
+
next Ractor.make_shareable(data.merge(complete: true, malformed: true))
|
|
263
|
+
end
|
|
189
264
|
parse_data = if data[:parse_data]
|
|
190
265
|
data[:parse_data].dup
|
|
191
266
|
else
|
|
@@ -197,12 +272,17 @@ module Raptor
|
|
|
197
272
|
if parser.has_body?
|
|
198
273
|
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
199
274
|
|
|
200
|
-
if
|
|
201
|
-
|
|
275
|
+
if max_body_size && parser.content_length > max_body_size
|
|
276
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
277
|
+
elsif env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
278
|
+
decoded_body, chunked_state = Raptor::Request.decode_chunked(body_buffer, max_body_size)
|
|
202
279
|
|
|
203
|
-
|
|
280
|
+
case chunked_state
|
|
281
|
+
when :complete
|
|
204
282
|
env.delete(HTTP_TRANSFER_ENCODING)
|
|
205
283
|
data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
|
|
284
|
+
when :too_large
|
|
285
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
206
286
|
else
|
|
207
287
|
data.merge(env: env, parse_data: parse_data)
|
|
208
288
|
end
|
|
@@ -233,6 +313,18 @@ module Raptor
|
|
|
233
313
|
#
|
|
234
314
|
# @rbs (Hash[Symbol, untyped] parsed_request, Reactor reactor, AtomicThreadPool thread_pool) -> void
|
|
235
315
|
def handle_parsed_request(parsed_request, reactor, thread_pool)
|
|
316
|
+
if parsed_request[:too_large]
|
|
317
|
+
socket = reactor.remove(parsed_request[:id])
|
|
318
|
+
reject_oversized(socket) if socket
|
|
319
|
+
return
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
if parsed_request[:malformed]
|
|
323
|
+
socket = reactor.remove(parsed_request[:id])
|
|
324
|
+
reject_malformed(socket) if socket
|
|
325
|
+
return
|
|
326
|
+
end
|
|
327
|
+
|
|
236
328
|
unless parsed_request[:complete]
|
|
237
329
|
reactor.update_state(parsed_request)
|
|
238
330
|
else
|
|
@@ -322,10 +414,18 @@ module Raptor
|
|
|
322
414
|
keep_alive && !hijacked
|
|
323
415
|
rescue => error
|
|
324
416
|
call_response_finished(rack_env, status, headers, error) if rack_env
|
|
325
|
-
socket.write(
|
|
417
|
+
socket.write(INTERNAL_SERVER_ERROR_RESPONSE) rescue nil unless response_started || hijacked
|
|
326
418
|
keep_alive = false
|
|
327
|
-
|
|
419
|
+
|
|
420
|
+
if @on_error
|
|
421
|
+
@on_error.call(rack_env, error) rescue nil
|
|
422
|
+
else
|
|
423
|
+
raise
|
|
424
|
+
end
|
|
328
425
|
ensure
|
|
426
|
+
rack_input = rack_env && rack_env[Rack::RACK_INPUT]
|
|
427
|
+
rack_input.close! rescue nil if rack_input.respond_to?(:close!)
|
|
428
|
+
|
|
329
429
|
unless hijacked || keep_alive
|
|
330
430
|
socket.close rescue nil
|
|
331
431
|
end
|
|
@@ -350,6 +450,11 @@ module Raptor
|
|
|
350
450
|
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, Integer request_count, String remote_addr, String url_scheme) -> void
|
|
351
451
|
def eager_keepalive(socket, id, reactor, thread_pool, request_count, remote_addr, url_scheme)
|
|
352
452
|
loop do
|
|
453
|
+
unless @running.true?
|
|
454
|
+
socket.close rescue nil
|
|
455
|
+
return
|
|
456
|
+
end
|
|
457
|
+
|
|
353
458
|
unless socket.wait_readable(KEEPALIVE_READ_TIMEOUT)
|
|
354
459
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
355
460
|
return
|
|
@@ -374,7 +479,12 @@ module Raptor
|
|
|
374
479
|
|
|
375
480
|
parser = HttpParser.new
|
|
376
481
|
env = {}
|
|
377
|
-
nread =
|
|
482
|
+
nread = begin
|
|
483
|
+
parser.execute(env, buffer, 0)
|
|
484
|
+
rescue HttpParserError
|
|
485
|
+
reject_malformed(socket)
|
|
486
|
+
return
|
|
487
|
+
end
|
|
378
488
|
parse_data = { parse_count: 1, content_length: parser.content_length }
|
|
379
489
|
|
|
380
490
|
body = nil
|
|
@@ -459,6 +569,30 @@ module Raptor
|
|
|
459
569
|
reactor.update_state(Ractor.make_shareable(state))
|
|
460
570
|
end
|
|
461
571
|
|
|
572
|
+
# Writes a 413 response and closes the socket. Used when a request body
|
|
573
|
+
# exceeds the configured maximum size.
|
|
574
|
+
#
|
|
575
|
+
# @param socket [TCPSocket] the client socket
|
|
576
|
+
# @return [void]
|
|
577
|
+
#
|
|
578
|
+
# @rbs (TCPSocket socket) -> void
|
|
579
|
+
def reject_oversized(socket)
|
|
580
|
+
socket.write(CONTENT_TOO_LARGE_RESPONSE) rescue nil
|
|
581
|
+
socket.close rescue nil
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# Writes a 400 response and closes the socket. Used when the HTTP parser
|
|
585
|
+
# rejects the request line or headers.
|
|
586
|
+
#
|
|
587
|
+
# @param socket [TCPSocket] the client socket
|
|
588
|
+
# @return [void]
|
|
589
|
+
#
|
|
590
|
+
# @rbs (TCPSocket socket) -> void
|
|
591
|
+
def reject_malformed(socket)
|
|
592
|
+
socket.write(BAD_REQUEST_RESPONSE) rescue nil
|
|
593
|
+
socket.close rescue nil
|
|
594
|
+
end
|
|
595
|
+
|
|
462
596
|
# Builds a Rack environment hash from parsed HTTP request data.
|
|
463
597
|
#
|
|
464
598
|
# Populates all required Rack env keys including rack.* keys, REMOTE_ADDR,
|
|
@@ -476,7 +610,7 @@ module Raptor
|
|
|
476
610
|
def build_rack_env(env, parse_data, body, socket, remote_addr: "127.0.0.1", url_scheme: HTTP_SCHEME)
|
|
477
611
|
env[Rack::RACK_VERSION] = Rack::VERSION
|
|
478
612
|
env[Rack::RACK_URL_SCHEME] = url_scheme
|
|
479
|
-
env[Rack::RACK_INPUT] = (body
|
|
613
|
+
env[Rack::RACK_INPUT] = build_rack_input(body)
|
|
480
614
|
env[Rack::RACK_ERRORS] = $stderr
|
|
481
615
|
env[Rack::RACK_RESPONSE_FINISHED] = []
|
|
482
616
|
|
|
@@ -520,6 +654,26 @@ module Raptor
|
|
|
520
654
|
env
|
|
521
655
|
end
|
|
522
656
|
|
|
657
|
+
# Builds the `rack.input` IO object for the request body. Returns an
|
|
658
|
+
# in-memory StringIO for bodies up to the spool threshold, or a Tempfile
|
|
659
|
+
# for larger bodies to bound per-worker memory.
|
|
660
|
+
#
|
|
661
|
+
# @param body [String, nil] decoded request body
|
|
662
|
+
# @return [IO] an IO-like object positioned at the start of the body
|
|
663
|
+
#
|
|
664
|
+
# @rbs (String? body) -> IO
|
|
665
|
+
def build_rack_input(body)
|
|
666
|
+
if body && @body_spool_threshold && body.bytesize > @body_spool_threshold
|
|
667
|
+
tempfile = Tempfile.new("raptor-body")
|
|
668
|
+
tempfile.binmode
|
|
669
|
+
tempfile.write(body)
|
|
670
|
+
tempfile.rewind
|
|
671
|
+
tempfile
|
|
672
|
+
else
|
|
673
|
+
(body ? StringIO.new(body) : StringIO.new).set_encoding(Encoding::ASCII_8BIT)
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
|
|
523
677
|
# Determines whether the connection should be kept alive after the response.
|
|
524
678
|
#
|
|
525
679
|
# Returns false if the request limit has been reached. For HTTP/1.1, keep-alive
|
|
@@ -568,7 +722,7 @@ module Raptor
|
|
|
568
722
|
end
|
|
569
723
|
response << "\r\n"
|
|
570
724
|
|
|
571
|
-
socket_write(socket, response)
|
|
725
|
+
Request.socket_write(socket, response)
|
|
572
726
|
end
|
|
573
727
|
|
|
574
728
|
# Writes a complete HTTP response to the socket.
|
|
@@ -701,7 +855,7 @@ module Raptor
|
|
|
701
855
|
def write_hijacked_response(socket, response, headers, response_hijack)
|
|
702
856
|
response << format_headers(headers)
|
|
703
857
|
response << "\r\n"
|
|
704
|
-
socket_write(socket, response)
|
|
858
|
+
Request.socket_write(socket, response)
|
|
705
859
|
uncork_socket(socket)
|
|
706
860
|
response_hijack.call(socket)
|
|
707
861
|
end
|
|
@@ -726,7 +880,7 @@ module Raptor
|
|
|
726
880
|
|
|
727
881
|
response << format_headers(headers)
|
|
728
882
|
response << "\r\n"
|
|
729
|
-
socket_write(socket, response)
|
|
883
|
+
Request.socket_write(socket, response)
|
|
730
884
|
end
|
|
731
885
|
|
|
732
886
|
# Writes a complete response with a body.
|
|
@@ -748,7 +902,7 @@ module Raptor
|
|
|
748
902
|
if body.respond_to?(:call)
|
|
749
903
|
response << format_headers(headers)
|
|
750
904
|
response << "\r\n"
|
|
751
|
-
socket_write(socket, response)
|
|
905
|
+
Request.socket_write(socket, response)
|
|
752
906
|
uncork_socket(socket)
|
|
753
907
|
body.call(socket)
|
|
754
908
|
return
|
|
@@ -785,7 +939,7 @@ module Raptor
|
|
|
785
939
|
raise TypeError, "body must respond to each, to_ary, or to_path"
|
|
786
940
|
end
|
|
787
941
|
|
|
788
|
-
socket_write(socket, "0\r\n\r\n") if use_chunked
|
|
942
|
+
Request.socket_write(socket, "0\r\n\r\n") if use_chunked
|
|
789
943
|
end
|
|
790
944
|
|
|
791
945
|
# Calculates content length from an array or file body without consuming it.
|
|
@@ -825,15 +979,15 @@ module Raptor
|
|
|
825
979
|
def write_file_body(socket, response, path, content_length, use_chunked)
|
|
826
980
|
File.open(path, "rb") do |file|
|
|
827
981
|
if use_chunked
|
|
828
|
-
socket_write(socket, response)
|
|
982
|
+
Request.socket_write(socket, response)
|
|
829
983
|
while (chunk = file.read(FILE_CHUNK_SIZE))
|
|
830
|
-
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
984
|
+
Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
831
985
|
end
|
|
832
986
|
elsif content_length && content_length < BODY_BUFFER_THRESHOLD
|
|
833
987
|
response << file.read(content_length)
|
|
834
|
-
socket_write(socket, response)
|
|
988
|
+
Request.socket_write(socket, response)
|
|
835
989
|
else
|
|
836
|
-
socket_write(socket, response)
|
|
990
|
+
Request.socket_write(socket, response)
|
|
837
991
|
IO.copy_stream(file, socket)
|
|
838
992
|
end
|
|
839
993
|
end
|
|
@@ -876,12 +1030,12 @@ module Raptor
|
|
|
876
1030
|
|
|
877
1031
|
if use_chunked
|
|
878
1032
|
response << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
|
879
|
-
socket_write(socket, response)
|
|
1033
|
+
Request.socket_write(socket, response)
|
|
880
1034
|
elsif chunk.bytesize < BODY_BUFFER_THRESHOLD
|
|
881
|
-
socket_write(socket, response << chunk)
|
|
1035
|
+
Request.socket_write(socket, response << chunk)
|
|
882
1036
|
else
|
|
883
|
-
socket_write(socket, response)
|
|
884
|
-
socket_write(socket, chunk)
|
|
1037
|
+
Request.socket_write(socket, response)
|
|
1038
|
+
Request.socket_write(socket, chunk)
|
|
885
1039
|
end
|
|
886
1040
|
end
|
|
887
1041
|
|
|
@@ -897,13 +1051,13 @@ module Raptor
|
|
|
897
1051
|
# @rbs (TCPSocket socket, String response, Array[String] body_array, bool use_chunked) -> void
|
|
898
1052
|
def write_multiple_chunks(socket, response, body_array, use_chunked)
|
|
899
1053
|
if use_chunked
|
|
900
|
-
socket_write(socket, response)
|
|
1054
|
+
Request.socket_write(socket, response)
|
|
901
1055
|
body_array.each do |chunk|
|
|
902
1056
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
903
1057
|
|
|
904
1058
|
next if chunk.empty?
|
|
905
1059
|
|
|
906
|
-
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
1060
|
+
Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
907
1061
|
end
|
|
908
1062
|
else
|
|
909
1063
|
body_array.each do |chunk|
|
|
@@ -911,7 +1065,7 @@ module Raptor
|
|
|
911
1065
|
|
|
912
1066
|
response << chunk
|
|
913
1067
|
end
|
|
914
|
-
socket_write(socket, response)
|
|
1068
|
+
Request.socket_write(socket, response)
|
|
915
1069
|
end
|
|
916
1070
|
end
|
|
917
1071
|
|
|
@@ -927,13 +1081,13 @@ module Raptor
|
|
|
927
1081
|
# @rbs (TCPSocket socket, String response, untyped body, bool use_chunked) -> void
|
|
928
1082
|
def write_enumerable_body(socket, response, body, use_chunked)
|
|
929
1083
|
if use_chunked
|
|
930
|
-
socket_write(socket, response)
|
|
1084
|
+
Request.socket_write(socket, response)
|
|
931
1085
|
body.each do |chunk|
|
|
932
1086
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
933
1087
|
|
|
934
1088
|
next if chunk.empty?
|
|
935
1089
|
|
|
936
|
-
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
1090
|
+
Request.socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
937
1091
|
end
|
|
938
1092
|
else
|
|
939
1093
|
body.each do |chunk|
|
|
@@ -941,7 +1095,7 @@ module Raptor
|
|
|
941
1095
|
|
|
942
1096
|
response << chunk
|
|
943
1097
|
end
|
|
944
|
-
socket_write(socket, response)
|
|
1098
|
+
Request.socket_write(socket, response)
|
|
945
1099
|
end
|
|
946
1100
|
end
|
|
947
1101
|
|
|
@@ -1014,33 +1168,6 @@ module Raptor
|
|
|
1014
1168
|
end
|
|
1015
1169
|
end
|
|
1016
1170
|
|
|
1017
|
-
# Writes a string to the socket, retrying on partial writes and flow control blocks.
|
|
1018
|
-
#
|
|
1019
|
-
# Uses write_nonblock with a 5-second writable timeout to avoid blocking the
|
|
1020
|
-
# thread indefinitely on slow clients.
|
|
1021
|
-
#
|
|
1022
|
-
# @param socket [TCPSocket] the socket to write to
|
|
1023
|
-
# @param string [String] the data to write
|
|
1024
|
-
# @return [void]
|
|
1025
|
-
# @raise [WriteError] if the socket is not writable within the timeout or raises IOError
|
|
1026
|
-
#
|
|
1027
|
-
# @rbs (TCPSocket socket, String string) -> void
|
|
1028
|
-
def socket_write(socket, string)
|
|
1029
|
-
bytes = 0
|
|
1030
|
-
byte_size = string.bytesize
|
|
1031
|
-
|
|
1032
|
-
while bytes < byte_size
|
|
1033
|
-
begin
|
|
1034
|
-
bytes += socket.write_nonblock(bytes.zero? ? string : string.byteslice(bytes..-1))
|
|
1035
|
-
rescue IO::WaitWritable
|
|
1036
|
-
raise WriteError unless socket.wait_writable(WRITE_TIMEOUT)
|
|
1037
|
-
retry
|
|
1038
|
-
rescue IOError
|
|
1039
|
-
raise WriteError
|
|
1040
|
-
end
|
|
1041
|
-
end
|
|
1042
|
-
end
|
|
1043
|
-
|
|
1044
1171
|
if Socket.const_defined?(:TCP_CORK)
|
|
1045
1172
|
# Enables TCP_CORK on the socket to batch outgoing packets into fewer segments.
|
|
1046
1173
|
#
|