raptor 0.6.0 → 0.8.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 +148 -23
- data/lib/rackup/handler/raptor.rb +12 -2
- data/lib/raptor/binder.rb +122 -28
- data/lib/raptor/cli.rb +82 -21
- data/lib/raptor/cluster.rb +188 -32
- data/lib/raptor/http.rb +75 -0
- data/lib/raptor/{request.rb → http1.rb} +202 -81
- data/lib/raptor/http2.rb +149 -61
- data/lib/raptor/reactor.rb +22 -15
- data/lib/raptor/server.rb +57 -37
- 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 +72 -5
- data/sig/generated/raptor/cli.rbs +2 -3
- data/sig/generated/raptor/cluster.rbs +89 -13
- data/sig/generated/raptor/http.rbs +52 -0
- data/sig/generated/raptor/{request.rbs → http1.rbs} +107 -31
- data/sig/generated/raptor/http2.rbs +64 -14
- data/sig/generated/raptor/reactor.rbs +18 -11
- data/sig/generated/raptor/server.rbs +32 -18
- data/sig/generated/raptor/systemd.rbs +42 -0
- metadata +7 -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,11 @@ 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
|
-
WRITE_TIMEOUT = 5
|
|
24
25
|
KEEPALIVE_READ_TIMEOUT = 0.001
|
|
25
26
|
MAX_KEEPALIVE_REQUESTS = 100
|
|
26
27
|
|
|
@@ -35,16 +36,19 @@ module Raptor
|
|
|
35
36
|
h[status] = "HTTP/1.1 #{status}#{reason ? " #{reason}" : ""}\r\n".freeze
|
|
36
37
|
end
|
|
37
38
|
|
|
38
|
-
STATUS_WITH_NO_ENTITY_BODY =
|
|
39
|
+
STATUS_WITH_NO_ENTITY_BODY = [204, 304, *100..199].freeze
|
|
40
|
+
CONTINUE_RESPONSE = "HTTP/1.1 100 Continue\r\n\r\n"
|
|
39
41
|
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
42
|
CONTENT_TOO_LARGE_RESPONSE = "HTTP/1.1 413 Content Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
43
|
+
INTERNAL_SERVER_ERROR_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
42
44
|
|
|
43
45
|
CONNECTION_CLOSE = "close"
|
|
44
46
|
CONNECTION_KEEPALIVE = "keep-alive"
|
|
47
|
+
EXPECT_100_CONTINUE = "100-continue"
|
|
45
48
|
TRANSFER_ENCODING_CHUNKED = "chunked"
|
|
46
49
|
|
|
47
50
|
HTTP_CONNECTION = "HTTP_CONNECTION"
|
|
51
|
+
HTTP_EXPECT = "HTTP_EXPECT"
|
|
48
52
|
HTTP_TRANSFER_ENCODING = "HTTP_TRANSFER_ENCODING"
|
|
49
53
|
RACK_HEADER_PREFIX = "rack."
|
|
50
54
|
RACK_HIJACKED = "rack.hijacked"
|
|
@@ -53,17 +57,39 @@ module Raptor
|
|
|
53
57
|
ILLEGAL_HEADER_KEY_REGEX = /[\x00-\x20\(\)<>@,;:\\"\/\[\]\?=\{\}\x7F]/
|
|
54
58
|
ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
# Returns true when the message framing shows a request-smuggling vector
|
|
61
|
+
# per RFC 9112 section 6.3: a `Transfer-Encoding` where `chunked` is
|
|
62
|
+
# missing, not the final encoding, or duplicated; a `Transfer-Encoding`
|
|
63
|
+
# paired with a `Content-Length`; or a `Content-Length` containing any
|
|
64
|
+
# non-digit character.
|
|
65
|
+
#
|
|
66
|
+
# @param env [Hash] the Rack environment after header parsing
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
#
|
|
69
|
+
# @rbs (Hash[String, untyped] env) -> bool
|
|
70
|
+
def self.request_smuggling?(env)
|
|
71
|
+
transfer_encoding = env[HTTP_TRANSFER_ENCODING]
|
|
72
|
+
content_length = env[Http::CONTENT_LENGTH]
|
|
73
|
+
|
|
74
|
+
if transfer_encoding
|
|
75
|
+
return true if content_length
|
|
76
|
+
|
|
77
|
+
encodings = transfer_encoding.downcase.split(",").map(&:strip)
|
|
78
|
+
return true if encodings.last != TRANSFER_ENCODING_CHUNKED
|
|
79
|
+
return true if encodings.count(TRANSFER_ENCODING_CHUNKED) > 1
|
|
80
|
+
elsif content_length
|
|
81
|
+
return true if content_length.match?(/[^\d]/)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
false
|
|
60
85
|
end
|
|
61
86
|
|
|
62
87
|
# Decodes a chunked transfer-encoded body buffer.
|
|
63
88
|
#
|
|
64
89
|
# Returns the decoded bytes and a state symbol: `:complete` when the
|
|
65
90
|
# terminating zero-length chunk was found, `:too_large` when the decoded
|
|
66
|
-
# size would exceed `max_size`,
|
|
91
|
+
# size would exceed `max_size`, `:malformed` when chunk framing overhead
|
|
92
|
+
# exceeds `MAX_CHUNK_OVERHEAD`, or `:incomplete` otherwise.
|
|
67
93
|
#
|
|
68
94
|
# @param buffer [String] the raw body buffer to decode
|
|
69
95
|
# @param max_size [Integer, nil] maximum decoded body size, or nil for unlimited
|
|
@@ -73,6 +99,7 @@ module Raptor
|
|
|
73
99
|
def self.decode_chunked(buffer, max_size = nil)
|
|
74
100
|
decoded = String.new
|
|
75
101
|
offset = 0
|
|
102
|
+
overhead = 0
|
|
76
103
|
|
|
77
104
|
while offset < buffer.bytesize
|
|
78
105
|
crlf = buffer.index("\r\n", offset)
|
|
@@ -80,7 +107,10 @@ module Raptor
|
|
|
80
107
|
|
|
81
108
|
chunk_size = buffer.byteslice(offset, crlf - offset).to_i(16)
|
|
82
109
|
return [decoded, :complete] if chunk_size == 0
|
|
83
|
-
return [decoded, :too_large] if max_size && decoded.bytesize + chunk_size > max_size
|
|
110
|
+
return [decoded, :too_large] if max_size && (decoded.bytesize + chunk_size) > max_size
|
|
111
|
+
|
|
112
|
+
overhead += (crlf - offset) + 4
|
|
113
|
+
return [decoded, :malformed] if overhead > (decoded.bytesize + chunk_size + MAX_CHUNK_OVERHEAD)
|
|
84
114
|
|
|
85
115
|
offset = crlf + 2
|
|
86
116
|
decoded << buffer.byteslice(offset, chunk_size)
|
|
@@ -90,58 +120,56 @@ module Raptor
|
|
|
90
120
|
[decoded, :incomplete]
|
|
91
121
|
end
|
|
92
122
|
|
|
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
123
|
# @rbs @app: ^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped]
|
|
119
124
|
# @rbs @server_port: Integer
|
|
125
|
+
# @rbs @write_timeout: Integer
|
|
120
126
|
# @rbs @max_body_size: Integer?
|
|
121
127
|
# @rbs @body_spool_threshold: Integer?
|
|
128
|
+
# @rbs @max_keepalive_requests: Integer
|
|
129
|
+
# @rbs @access_log_io: IO?
|
|
122
130
|
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
123
131
|
# @rbs @running: AtomicBoolean
|
|
124
132
|
|
|
125
|
-
# Creates a new
|
|
133
|
+
# Creates a new Http1 handler.
|
|
126
134
|
#
|
|
127
135
|
# @param app [#call] the Rack application to dispatch complete requests to
|
|
128
136
|
# @param server_port [Integer] port number used to populate SERVER_PORT in the Rack env
|
|
129
|
-
# @param
|
|
130
|
-
# @option
|
|
131
|
-
# @option
|
|
137
|
+
# @param connection_options [Hash] per-connection settings shared across protocols
|
|
138
|
+
# @option connection_options [Integer] :write_timeout per-write socket timeout in seconds
|
|
139
|
+
# @option connection_options [Integer, nil] :max_body_size maximum request body size in bytes
|
|
140
|
+
# @option connection_options [Integer, nil] :body_spool_threshold spool bodies larger than this to a tempfile
|
|
141
|
+
# @param http1_options [Hash] HTTP/1.1-specific settings
|
|
142
|
+
# @option http1_options [Integer] :max_keepalive_requests maximum requests per HTTP/1.1 keep-alive connection
|
|
143
|
+
# @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
|
|
132
144
|
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
133
145
|
# @return [void]
|
|
134
146
|
#
|
|
135
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?
|
|
136
|
-
def initialize(app, server_port,
|
|
147
|
+
# @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
|
|
148
|
+
def initialize(app, server_port, connection_options: {}, http1_options: {}, access_log_io: nil, on_error: nil)
|
|
137
149
|
@app = app
|
|
138
150
|
@server_port = server_port
|
|
139
|
-
@
|
|
140
|
-
@
|
|
151
|
+
@write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
|
|
152
|
+
@max_body_size = connection_options[:max_body_size]
|
|
153
|
+
@body_spool_threshold = connection_options[:body_spool_threshold]
|
|
154
|
+
@max_keepalive_requests = http1_options[:max_keepalive_requests] || MAX_KEEPALIVE_REQUESTS
|
|
155
|
+
@access_log_io = access_log_io
|
|
141
156
|
@on_error = on_error
|
|
142
157
|
@running = AtomicBoolean.new(true)
|
|
143
158
|
end
|
|
144
159
|
|
|
160
|
+
# Instance-level wrapper around {Http.socket_write} that applies the
|
|
161
|
+
# configured `write_timeout`.
|
|
162
|
+
#
|
|
163
|
+
# @param socket [TCPSocket] the socket to write to
|
|
164
|
+
# @param string [String] the data to write
|
|
165
|
+
# @return [void]
|
|
166
|
+
# @raise [Http::WriteError] if the socket is not writable within the timeout or raises IOError
|
|
167
|
+
#
|
|
168
|
+
# @rbs (TCPSocket socket, String string) -> void
|
|
169
|
+
def socket_write(socket, string)
|
|
170
|
+
Http.socket_write(socket, string, timeout: @write_timeout)
|
|
171
|
+
end
|
|
172
|
+
|
|
145
173
|
# Signals eager keep-alive loops to stop processing further requests on
|
|
146
174
|
# their connections. In-flight requests complete normally.
|
|
147
175
|
#
|
|
@@ -202,6 +230,9 @@ module Raptor
|
|
|
202
230
|
if !parser.finished?
|
|
203
231
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
204
232
|
return
|
|
233
|
+
elsif Http1.request_smuggling?(env)
|
|
234
|
+
reject_malformed(socket)
|
|
235
|
+
return
|
|
205
236
|
elsif parser.has_body?
|
|
206
237
|
if @max_body_size && parser.content_length > @max_body_size
|
|
207
238
|
reject_oversized(socket)
|
|
@@ -211,13 +242,16 @@ module Raptor
|
|
|
211
242
|
body = buffer.byteslice(nread..-1) || ""
|
|
212
243
|
|
|
213
244
|
if env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
214
|
-
body, chunked_state =
|
|
245
|
+
body, chunked_state = Http1.decode_chunked(body, @max_body_size)
|
|
215
246
|
case chunked_state
|
|
216
247
|
when :complete
|
|
217
248
|
env.delete(HTTP_TRANSFER_ENCODING)
|
|
218
249
|
when :too_large
|
|
219
250
|
reject_oversized(socket)
|
|
220
251
|
return
|
|
252
|
+
when :malformed
|
|
253
|
+
reject_malformed(socket)
|
|
254
|
+
return
|
|
221
255
|
else
|
|
222
256
|
fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, 0, remote_addr, url_scheme, persisted: false)
|
|
223
257
|
return
|
|
@@ -263,13 +297,15 @@ module Raptor
|
|
|
263
297
|
parse_data[:parse_count] += 1
|
|
264
298
|
|
|
265
299
|
message = if parser.finished?
|
|
266
|
-
if
|
|
300
|
+
if Raptor::Http1.request_smuggling?(env)
|
|
301
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
|
|
302
|
+
elsif parser.has_body?
|
|
267
303
|
body_buffer = data[:buffer].byteslice(nread..-1) || ""
|
|
268
304
|
|
|
269
305
|
if max_body_size && parser.content_length > max_body_size
|
|
270
306
|
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
271
307
|
elsif env[HTTP_TRANSFER_ENCODING]&.include?(TRANSFER_ENCODING_CHUNKED)
|
|
272
|
-
decoded_body, chunked_state = Raptor::
|
|
308
|
+
decoded_body, chunked_state = Raptor::Http1.decode_chunked(body_buffer, max_body_size)
|
|
273
309
|
|
|
274
310
|
case chunked_state
|
|
275
311
|
when :complete
|
|
@@ -277,6 +313,8 @@ module Raptor
|
|
|
277
313
|
data.merge(env: env, body: decoded_body, parse_data: parse_data, complete: true)
|
|
278
314
|
when :too_large
|
|
279
315
|
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, too_large: true)
|
|
316
|
+
when :malformed
|
|
317
|
+
data.merge(env: env, body: nil, parse_data: parse_data, complete: true, malformed: true)
|
|
280
318
|
else
|
|
281
319
|
data.merge(env: env, parse_data: parse_data)
|
|
282
320
|
end
|
|
@@ -320,6 +358,7 @@ module Raptor
|
|
|
320
358
|
end
|
|
321
359
|
|
|
322
360
|
unless parsed_request[:complete]
|
|
361
|
+
parsed_request = send_continue_if_expected(parsed_request, reactor)
|
|
323
362
|
reactor.update_state(parsed_request)
|
|
324
363
|
else
|
|
325
364
|
socket = reactor.remove(parsed_request[:id])
|
|
@@ -346,6 +385,41 @@ module Raptor
|
|
|
346
385
|
|
|
347
386
|
private
|
|
348
387
|
|
|
388
|
+
# Returns true if the request expects a 100 Continue response per
|
|
389
|
+
# RFC 7231 section 5.1.1.
|
|
390
|
+
#
|
|
391
|
+
# @param env [Hash] the parsed Rack environment (possibly incomplete)
|
|
392
|
+
# @return [Boolean]
|
|
393
|
+
#
|
|
394
|
+
# @rbs (Hash[String, untyped] env) -> bool
|
|
395
|
+
def expects_100_continue?(env)
|
|
396
|
+
(env[Rack::SERVER_PROTOCOL] == HTTP_11) && env[HTTP_EXPECT]&.casecmp?(EXPECT_100_CONTINUE)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Sends an HTTP 100 Continue response when an HTTP/1.1 client requested
|
|
400
|
+
# `Expect: 100-continue` and the request body has not yet been received.
|
|
401
|
+
#
|
|
402
|
+
# Returns the state hash with `:continued` set when the response has been
|
|
403
|
+
# written. A write failure is silently ignored.
|
|
404
|
+
#
|
|
405
|
+
# @param state [Hash] the partially-parsed connection state
|
|
406
|
+
# @param reactor [Reactor] the reactor holding the connection's socket
|
|
407
|
+
# @return [Hash] the state, with `:continued` set if 100 was written
|
|
408
|
+
#
|
|
409
|
+
# @rbs (Hash[Symbol, untyped] state, Reactor reactor) -> Hash[Symbol, untyped]
|
|
410
|
+
def send_continue_if_expected(state, reactor)
|
|
411
|
+
return state if state[:continued]
|
|
412
|
+
|
|
413
|
+
env = state[:env]
|
|
414
|
+
return state unless env && expects_100_continue?(env)
|
|
415
|
+
|
|
416
|
+
socket = reactor.socket_for(state[:id])
|
|
417
|
+
return state unless socket
|
|
418
|
+
|
|
419
|
+
socket_write(socket, CONTINUE_RESPONSE) rescue nil
|
|
420
|
+
state.merge(continued: true)
|
|
421
|
+
end
|
|
422
|
+
|
|
349
423
|
# Processes a client connection by handling the current request and,
|
|
350
424
|
# if keep-alive, eagerly reading subsequent requests inline.
|
|
351
425
|
#
|
|
@@ -400,10 +474,12 @@ module Raptor
|
|
|
400
474
|
hijacked = headers.is_a?(Hash) && !!headers[Rack::RACK_HIJACK]
|
|
401
475
|
streaming = body.respond_to?(:call) && !body.respond_to?(:each)
|
|
402
476
|
keep_alive = (hijacked || streaming) ? false : keep_alive?(rack_env, request_count)
|
|
477
|
+
response_size = response_size(headers, body) unless hijacked
|
|
403
478
|
response_started = true
|
|
404
479
|
write_response(socket, rack_env, status, headers, body, keep_alive: keep_alive)
|
|
405
480
|
end
|
|
406
481
|
|
|
482
|
+
write_access_log(rack_env, status, response_size, remote_addr) if @access_log_io && !hijacked
|
|
407
483
|
call_response_finished(rack_env, status, headers, nil)
|
|
408
484
|
keep_alive && !hijacked
|
|
409
485
|
rescue => error
|
|
@@ -549,6 +625,9 @@ module Raptor
|
|
|
549
625
|
#
|
|
550
626
|
# @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
627
|
def fallback_to_reactor(socket, id, buffer, env, parse_data, reactor, request_count, remote_addr, url_scheme, persisted: true)
|
|
628
|
+
continued = expects_100_continue?(env)
|
|
629
|
+
socket_write(socket, CONTINUE_RESPONSE) rescue nil if continued
|
|
630
|
+
|
|
552
631
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
553
632
|
state = {
|
|
554
633
|
id: id,
|
|
@@ -560,6 +639,7 @@ module Raptor
|
|
|
560
639
|
url_scheme: url_scheme
|
|
561
640
|
}
|
|
562
641
|
state[:persisted] = true if persisted
|
|
642
|
+
state[:continued] = true if continued
|
|
563
643
|
reactor.update_state(Ractor.make_shareable(state))
|
|
564
644
|
end
|
|
565
645
|
|
|
@@ -603,7 +683,6 @@ module Raptor
|
|
|
603
683
|
# @rbs (Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, TCPSocket socket, ?remote_addr: String, ?url_scheme: String) -> Hash[String, untyped]
|
|
604
684
|
def build_rack_env(env, parse_data, body, socket, remote_addr: Server::DEFAULT_REMOTE_ADDR, url_scheme: Server::HTTP_SCHEME)
|
|
605
685
|
env[Rack::RACK_VERSION] = Rack::VERSION
|
|
606
|
-
env[Rack::RACK_URL_SCHEME] = url_scheme
|
|
607
686
|
env[Rack::RACK_INPUT] = build_rack_input(body)
|
|
608
687
|
env[Rack::RACK_ERRORS] = $stderr
|
|
609
688
|
env[Rack::RACK_RESPONSE_FINISHED] = []
|
|
@@ -625,10 +704,16 @@ module Raptor
|
|
|
625
704
|
env[Rack::QUERY_STRING] = "" unless env.key?(Rack::QUERY_STRING)
|
|
626
705
|
|
|
627
706
|
if (content_length = parse_data[:content_length]).positive?
|
|
628
|
-
env[
|
|
707
|
+
env[Http::CONTENT_LENGTH] = content_length.to_s
|
|
629
708
|
end
|
|
630
709
|
|
|
631
|
-
env[
|
|
710
|
+
env[Http::REMOTE_ADDR] = remote_addr
|
|
711
|
+
env[Http::SERVER_SOFTWARE] = Http::SERVER_SOFTWARE_VALUE
|
|
712
|
+
env[Http::HTTP_VERSION] = env[Rack::SERVER_PROTOCOL]
|
|
713
|
+
|
|
714
|
+
behind_tls_proxy = (url_scheme == Server::HTTP_SCHEME) && forwarded_https?(env)
|
|
715
|
+
env[Rack::RACK_URL_SCHEME] = behind_tls_proxy ? Server::HTTPS_SCHEME : url_scheme
|
|
716
|
+
default_port = behind_tls_proxy ? "443" : @server_port.to_s
|
|
632
717
|
|
|
633
718
|
http_host = env[Rack::HTTP_HOST]
|
|
634
719
|
if http_host
|
|
@@ -639,10 +724,10 @@ module Raptor
|
|
|
639
724
|
host, port = http_host.split(":", 2)
|
|
640
725
|
end
|
|
641
726
|
env[Rack::SERVER_NAME] ||= host
|
|
642
|
-
env[Rack::SERVER_PORT] ||= port ||
|
|
727
|
+
env[Rack::SERVER_PORT] ||= port || default_port
|
|
643
728
|
else
|
|
644
729
|
env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
|
|
645
|
-
env[Rack::SERVER_PORT] ||=
|
|
730
|
+
env[Rack::SERVER_PORT] ||= default_port
|
|
646
731
|
end
|
|
647
732
|
|
|
648
733
|
env
|
|
@@ -668,6 +753,21 @@ module Raptor
|
|
|
668
753
|
end
|
|
669
754
|
end
|
|
670
755
|
|
|
756
|
+
# Returns true when an upstream proxy signals that it terminated TLS for
|
|
757
|
+
# this request via `X-Forwarded-Proto`, `X-Forwarded-Scheme`, or
|
|
758
|
+
# `X-Forwarded-Ssl`. Only the first comma-separated value is consulted.
|
|
759
|
+
#
|
|
760
|
+
# @param env [Hash] the Rack environment
|
|
761
|
+
# @return [Boolean]
|
|
762
|
+
#
|
|
763
|
+
# @rbs (Hash[String, untyped] env) -> bool
|
|
764
|
+
def forwarded_https?(env)
|
|
765
|
+
proto = env["HTTP_X_FORWARDED_PROTO"] || env["HTTP_X_FORWARDED_SCHEME"]
|
|
766
|
+
return true if proto && proto.split(",").first&.strip&.casecmp?(Server::HTTPS_SCHEME)
|
|
767
|
+
|
|
768
|
+
env["HTTP_X_FORWARDED_SSL"]&.casecmp?("on") || false
|
|
769
|
+
end
|
|
770
|
+
|
|
671
771
|
# Determines whether the connection should be kept alive after the response.
|
|
672
772
|
#
|
|
673
773
|
# Returns false if the request limit has been reached. For HTTP/1.1, keep-alive
|
|
@@ -680,7 +780,7 @@ module Raptor
|
|
|
680
780
|
#
|
|
681
781
|
# @rbs (Hash[String, untyped] env, Integer request_count) -> bool
|
|
682
782
|
def keep_alive?(env, request_count)
|
|
683
|
-
return false if request_count >=
|
|
783
|
+
return false if request_count >= @max_keepalive_requests
|
|
684
784
|
|
|
685
785
|
connection_header = env[HTTP_CONNECTION]
|
|
686
786
|
|
|
@@ -716,7 +816,7 @@ module Raptor
|
|
|
716
816
|
end
|
|
717
817
|
response << "\r\n"
|
|
718
818
|
|
|
719
|
-
|
|
819
|
+
socket_write(socket, response)
|
|
720
820
|
end
|
|
721
821
|
|
|
722
822
|
# Writes a complete HTTP response to the socket.
|
|
@@ -849,7 +949,7 @@ module Raptor
|
|
|
849
949
|
def write_hijacked_response(socket, response, headers, response_hijack)
|
|
850
950
|
response << format_headers(headers)
|
|
851
951
|
response << "\r\n"
|
|
852
|
-
|
|
952
|
+
socket_write(socket, response)
|
|
853
953
|
uncork_socket(socket)
|
|
854
954
|
response_hijack.call(socket)
|
|
855
955
|
end
|
|
@@ -874,7 +974,7 @@ module Raptor
|
|
|
874
974
|
|
|
875
975
|
response << format_headers(headers)
|
|
876
976
|
response << "\r\n"
|
|
877
|
-
|
|
977
|
+
socket_write(socket, response)
|
|
878
978
|
end
|
|
879
979
|
|
|
880
980
|
# Writes a complete response with a body.
|
|
@@ -896,7 +996,7 @@ module Raptor
|
|
|
896
996
|
if body.respond_to?(:call)
|
|
897
997
|
response << format_headers(headers)
|
|
898
998
|
response << "\r\n"
|
|
899
|
-
|
|
999
|
+
socket_write(socket, response)
|
|
900
1000
|
uncork_socket(socket)
|
|
901
1001
|
body.call(socket)
|
|
902
1002
|
return
|
|
@@ -933,7 +1033,7 @@ module Raptor
|
|
|
933
1033
|
raise TypeError, "body must respond to each, to_ary, or to_path"
|
|
934
1034
|
end
|
|
935
1035
|
|
|
936
|
-
|
|
1036
|
+
socket_write(socket, "0\r\n\r\n") if use_chunked
|
|
937
1037
|
end
|
|
938
1038
|
|
|
939
1039
|
# Calculates content length from an array or file body without consuming it.
|
|
@@ -947,7 +1047,7 @@ module Raptor
|
|
|
947
1047
|
def calculate_content_length(body)
|
|
948
1048
|
if body.respond_to?(:to_ary)
|
|
949
1049
|
array = body.to_ary
|
|
950
|
-
return
|
|
1050
|
+
return unless array.is_a?(Array)
|
|
951
1051
|
|
|
952
1052
|
array.sum { |chunk| chunk.is_a?(String) ? chunk.bytesize : 0 }
|
|
953
1053
|
elsif body.respond_to?(:to_path) && (path = body.to_path) && File.readable?(path)
|
|
@@ -973,15 +1073,15 @@ module Raptor
|
|
|
973
1073
|
def write_file_body(socket, response, path, content_length, use_chunked)
|
|
974
1074
|
File.open(path, "rb") do |file|
|
|
975
1075
|
if use_chunked
|
|
976
|
-
|
|
1076
|
+
socket_write(socket, response)
|
|
977
1077
|
while (chunk = file.read(FILE_CHUNK_SIZE))
|
|
978
|
-
|
|
1078
|
+
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
979
1079
|
end
|
|
980
1080
|
elsif content_length && content_length < BODY_BUFFER_THRESHOLD
|
|
981
1081
|
response << file.read(content_length)
|
|
982
|
-
|
|
1082
|
+
socket_write(socket, response)
|
|
983
1083
|
else
|
|
984
|
-
|
|
1084
|
+
socket_write(socket, response)
|
|
985
1085
|
IO.copy_stream(file, socket)
|
|
986
1086
|
end
|
|
987
1087
|
end
|
|
@@ -1024,12 +1124,12 @@ module Raptor
|
|
|
1024
1124
|
|
|
1025
1125
|
if use_chunked
|
|
1026
1126
|
response << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
|
1027
|
-
|
|
1127
|
+
socket_write(socket, response)
|
|
1028
1128
|
elsif chunk.bytesize < BODY_BUFFER_THRESHOLD
|
|
1029
|
-
|
|
1129
|
+
socket_write(socket, response << chunk)
|
|
1030
1130
|
else
|
|
1031
|
-
|
|
1032
|
-
|
|
1131
|
+
socket_write(socket, response)
|
|
1132
|
+
socket_write(socket, chunk)
|
|
1033
1133
|
end
|
|
1034
1134
|
end
|
|
1035
1135
|
|
|
@@ -1045,13 +1145,13 @@ module Raptor
|
|
|
1045
1145
|
# @rbs (TCPSocket socket, String response, Array[String] body_array, bool use_chunked) -> void
|
|
1046
1146
|
def write_multiple_chunks(socket, response, body_array, use_chunked)
|
|
1047
1147
|
if use_chunked
|
|
1048
|
-
|
|
1148
|
+
socket_write(socket, response)
|
|
1049
1149
|
body_array.each do |chunk|
|
|
1050
1150
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1051
1151
|
|
|
1052
1152
|
next if chunk.empty?
|
|
1053
1153
|
|
|
1054
|
-
|
|
1154
|
+
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
1055
1155
|
end
|
|
1056
1156
|
else
|
|
1057
1157
|
body_array.each do |chunk|
|
|
@@ -1059,7 +1159,7 @@ module Raptor
|
|
|
1059
1159
|
|
|
1060
1160
|
response << chunk
|
|
1061
1161
|
end
|
|
1062
|
-
|
|
1162
|
+
socket_write(socket, response)
|
|
1063
1163
|
end
|
|
1064
1164
|
end
|
|
1065
1165
|
|
|
@@ -1075,13 +1175,13 @@ module Raptor
|
|
|
1075
1175
|
# @rbs (TCPSocket socket, String response, untyped body, bool use_chunked) -> void
|
|
1076
1176
|
def write_enumerable_body(socket, response, body, use_chunked)
|
|
1077
1177
|
if use_chunked
|
|
1078
|
-
|
|
1178
|
+
socket_write(socket, response)
|
|
1079
1179
|
body.each do |chunk|
|
|
1080
1180
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1081
1181
|
|
|
1082
1182
|
next if chunk.empty?
|
|
1083
1183
|
|
|
1084
|
-
|
|
1184
|
+
socket_write(socket, "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n")
|
|
1085
1185
|
end
|
|
1086
1186
|
else
|
|
1087
1187
|
body.each do |chunk|
|
|
@@ -1089,7 +1189,7 @@ module Raptor
|
|
|
1089
1189
|
|
|
1090
1190
|
response << chunk
|
|
1091
1191
|
end
|
|
1092
|
-
|
|
1192
|
+
socket_write(socket, response)
|
|
1093
1193
|
end
|
|
1094
1194
|
end
|
|
1095
1195
|
|
|
@@ -1127,16 +1227,10 @@ module Raptor
|
|
|
1127
1227
|
headers.each do |name, value|
|
|
1128
1228
|
next if illegal_header_key?(name)
|
|
1129
1229
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
next if illegal_header_value?(header_value.to_s)
|
|
1133
|
-
|
|
1134
|
-
result << "#{name}: #{header_value}\r\n"
|
|
1135
|
-
end
|
|
1136
|
-
else
|
|
1137
|
-
next if illegal_header_value?(value.to_s)
|
|
1230
|
+
Array(value).flat_map { |entry| entry.to_s.split("\n") }.each do |header_value|
|
|
1231
|
+
next if illegal_header_value?(header_value)
|
|
1138
1232
|
|
|
1139
|
-
result << "#{name}: #{
|
|
1233
|
+
result << "#{name}: #{header_value}\r\n"
|
|
1140
1234
|
end
|
|
1141
1235
|
end
|
|
1142
1236
|
result
|
|
@@ -1162,6 +1256,33 @@ module Raptor
|
|
|
1162
1256
|
end
|
|
1163
1257
|
end
|
|
1164
1258
|
|
|
1259
|
+
# Instance-level wrapper around {Http.write_access_log} that routes to
|
|
1260
|
+
# the configured `@access_log_io`.
|
|
1261
|
+
#
|
|
1262
|
+
# @param env [Hash] the Rack environment
|
|
1263
|
+
# @param status [Integer] the response status code
|
|
1264
|
+
# @param size [String] the response body size in bytes, or `-` if unknown
|
|
1265
|
+
# @param remote_addr [String] the client IP address
|
|
1266
|
+
# @return [void]
|
|
1267
|
+
#
|
|
1268
|
+
# @rbs (Hash[String, untyped] env, Integer status, String size, String remote_addr) -> void
|
|
1269
|
+
def write_access_log(env, status, size, remote_addr)
|
|
1270
|
+
Http.write_access_log(@access_log_io, env, status, size, remote_addr)
|
|
1271
|
+
end
|
|
1272
|
+
|
|
1273
|
+
# Returns the response body size as a String for the access log, taken
|
|
1274
|
+
# from the `content-length` header when set, computed from the body
|
|
1275
|
+
# otherwise, or `-` when the size cannot be determined upfront.
|
|
1276
|
+
#
|
|
1277
|
+
# @param headers [Hash] the response headers
|
|
1278
|
+
# @param body [Object] the response body
|
|
1279
|
+
# @return [String]
|
|
1280
|
+
#
|
|
1281
|
+
# @rbs (Hash[String, String | Array[String]] headers, untyped body) -> String
|
|
1282
|
+
def response_size(headers, body)
|
|
1283
|
+
headers[Rack::CONTENT_LENGTH] || calculate_content_length(body)&.to_s || "-"
|
|
1284
|
+
end
|
|
1285
|
+
|
|
1165
1286
|
if Socket.const_defined?(:TCP_CORK)
|
|
1166
1287
|
# Enables TCP_CORK on the socket to batch outgoing packets into fewer segments.
|
|
1167
1288
|
#
|