puma 6.6.0 → 8.0.2
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/History.md +309 -5
- data/README.md +41 -42
- data/docs/5.0-Upgrade.md +98 -0
- data/docs/6.0-Upgrade.md +56 -0
- data/docs/7.0-Upgrade.md +52 -0
- data/docs/8.0-Upgrade.md +100 -0
- data/docs/deployment.md +58 -23
- data/docs/fork_worker.md +5 -5
- data/docs/grpc.md +62 -0
- data/docs/images/favicon.svg +1 -0
- data/docs/images/running-puma.svg +1 -0
- data/docs/images/standard-logo.svg +1 -0
- data/docs/jungle/README.md +1 -1
- data/docs/kubernetes.md +11 -16
- data/docs/plugins.md +2 -2
- data/docs/restart.md +2 -2
- data/docs/signals.md +21 -21
- data/docs/stats.md +4 -3
- data/docs/systemd.md +4 -4
- data/ext/puma_http11/extconf.rb +2 -17
- data/ext/puma_http11/http11_parser.java.rl +51 -65
- data/ext/puma_http11/mini_ssl.c +18 -8
- data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +174 -102
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
- data/ext/puma_http11/puma_http11.c +122 -118
- data/lib/puma/app/status.rb +10 -2
- data/lib/puma/binder.rb +12 -10
- data/lib/puma/cli.rb +4 -6
- data/lib/puma/client.rb +205 -131
- data/lib/puma/client_env.rb +171 -0
- data/lib/puma/cluster/worker.rb +17 -17
- data/lib/puma/cluster/worker_handle.rb +38 -7
- data/lib/puma/cluster.rb +44 -30
- data/lib/puma/cluster_accept_loop_delay.rb +91 -0
- data/lib/puma/commonlogger.rb +3 -3
- data/lib/puma/configuration.rb +173 -58
- data/lib/puma/const.rb +11 -11
- data/lib/puma/control_cli.rb +7 -3
- data/lib/puma/detect.rb +13 -0
- data/lib/puma/dsl.rb +225 -108
- data/lib/puma/error_logger.rb +3 -1
- data/lib/puma/events.rb +25 -10
- data/lib/puma/io_buffer.rb +8 -4
- data/lib/puma/launcher/bundle_pruner.rb +3 -5
- data/lib/puma/launcher.rb +57 -53
- data/lib/puma/log_writer.rb +8 -2
- data/lib/puma/minissl.rb +0 -1
- data/lib/puma/plugin/systemd.rb +3 -3
- data/lib/puma/rack/urlmap.rb +1 -1
- data/lib/puma/reactor.rb +19 -13
- data/lib/puma/{request.rb → response.rb} +56 -212
- data/lib/puma/runner.rb +9 -18
- data/lib/puma/server.rb +182 -97
- data/lib/puma/server_plugin_control.rb +32 -0
- data/lib/puma/single.rb +7 -4
- data/lib/puma/state_file.rb +3 -2
- data/lib/puma/thread_pool.rb +170 -99
- data/lib/puma/util.rb +0 -7
- data/lib/puma.rb +10 -0
- data/lib/rack/handler/puma.rb +3 -3
- data/tools/Dockerfile +15 -5
- metadata +19 -7
- data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/client.rb
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class IO
|
|
4
|
-
# We need to use this for a jruby work around on both 1.8 and 1.9.
|
|
5
|
-
# So this either creates the constant (on 1.8), or harmlessly
|
|
6
|
-
# reopens it (on 1.9).
|
|
7
|
-
module WaitReadable
|
|
8
|
-
end
|
|
9
|
-
end
|
|
10
|
-
|
|
11
3
|
require_relative 'detect'
|
|
12
4
|
require_relative 'io_buffer'
|
|
5
|
+
require_relative 'client_env'
|
|
13
6
|
require 'tempfile'
|
|
14
7
|
|
|
15
8
|
if Puma::IS_JRUBY
|
|
@@ -28,8 +21,8 @@ module Puma
|
|
|
28
21
|
#———————————————————————— DO NOT USE — this class is for internal use only ———
|
|
29
22
|
|
|
30
23
|
|
|
31
|
-
# An instance of this class
|
|
32
|
-
# For example, this could be
|
|
24
|
+
# An instance of this class wraps a connection/socket.
|
|
25
|
+
# For example, this could be an http request from a browser or from CURL.
|
|
33
26
|
#
|
|
34
27
|
# An instance of `Puma::Client` can be used as if it were an IO object
|
|
35
28
|
# by the reactor. The reactor is expected to call `#to_io`
|
|
@@ -37,12 +30,18 @@ module Puma
|
|
|
37
30
|
# `IO::try_convert` (which may call `#to_io`) when a new socket is
|
|
38
31
|
# registered.
|
|
39
32
|
#
|
|
40
|
-
# Instances of this class are responsible for knowing if
|
|
41
|
-
#
|
|
33
|
+
# Instances of this class are responsible for knowing if the request line,
|
|
34
|
+
# headers and body are fully buffered and verified via the `try_to_finish` method.
|
|
35
|
+
# All verification of each request is done in the `Client` object.
|
|
42
36
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
|
43
37
|
#
|
|
38
|
+
# Most of the code for env processing and verification is contained
|
|
39
|
+
# in `Puma::ClientEnv`, which is included.
|
|
40
|
+
#
|
|
44
41
|
class Client # :nodoc:
|
|
45
42
|
|
|
43
|
+
include ClientEnv
|
|
44
|
+
|
|
46
45
|
# this tests all values but the last, which must be chunked
|
|
47
46
|
ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
|
|
48
47
|
|
|
@@ -64,11 +63,22 @@ module Puma
|
|
|
64
63
|
|
|
65
64
|
TE_ERR_MSG = 'Invalid Transfer-Encoding'
|
|
66
65
|
|
|
66
|
+
# See:
|
|
67
|
+
# https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.1.1
|
|
68
|
+
# https://httpwg.org/specs/rfc9112.html#rfc.section.6.1
|
|
69
|
+
STRIP_OWS = /\A[ \t]+|[ \t]+\z/
|
|
70
|
+
|
|
67
71
|
# The object used for a request with no body. All requests with
|
|
68
72
|
# no body share this one object since it has no state.
|
|
69
73
|
EmptyBody = NullIO.new
|
|
70
74
|
|
|
71
|
-
|
|
75
|
+
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
|
76
|
+
:tempfile, :io_buffer, :http_content_length_limit_exceeded,
|
|
77
|
+
:requests_served, :error_status_code
|
|
78
|
+
|
|
79
|
+
attr_writer :peerip, :http_content_length_limit, :supported_http_methods
|
|
80
|
+
|
|
81
|
+
attr_accessor :remote_addr_header, :listener, :env_set_http_version
|
|
72
82
|
|
|
73
83
|
def initialize(io, env=nil)
|
|
74
84
|
@io = io
|
|
@@ -94,7 +104,8 @@ module Puma
|
|
|
94
104
|
@hijacked = false
|
|
95
105
|
|
|
96
106
|
@http_content_length_limit = nil
|
|
97
|
-
@http_content_length_limit_exceeded =
|
|
107
|
+
@http_content_length_limit_exceeded = nil
|
|
108
|
+
@error_status_code = nil
|
|
98
109
|
|
|
99
110
|
@peerip = nil
|
|
100
111
|
@peer_family = nil
|
|
@@ -110,13 +121,6 @@ module Puma
|
|
|
110
121
|
@read_buffer = String.new # rubocop: disable Performance/UnfreezeString
|
|
111
122
|
end
|
|
112
123
|
|
|
113
|
-
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
|
114
|
-
:tempfile, :io_buffer, :http_content_length_limit_exceeded
|
|
115
|
-
|
|
116
|
-
attr_writer :peerip, :http_content_length_limit
|
|
117
|
-
|
|
118
|
-
attr_accessor :remote_addr_header, :listener
|
|
119
|
-
|
|
120
124
|
# Remove in Puma 7?
|
|
121
125
|
def closed?
|
|
122
126
|
@to_io.closed?
|
|
@@ -133,9 +137,9 @@ module Puma
|
|
|
133
137
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
|
134
138
|
end
|
|
135
139
|
|
|
136
|
-
# For the hijack protocol
|
|
137
|
-
#
|
|
138
|
-
def
|
|
140
|
+
# For the full hijack protocol, `env['rack.hijack']` is set to
|
|
141
|
+
# `client.method :full_hijack`
|
|
142
|
+
def full_hijack
|
|
139
143
|
@hijacked = true
|
|
140
144
|
env[HIJACK_IO] ||= @io
|
|
141
145
|
end
|
|
@@ -150,53 +154,48 @@ module Puma
|
|
|
150
154
|
end
|
|
151
155
|
|
|
152
156
|
# Number of seconds until the timeout elapses.
|
|
157
|
+
# @!attribute [r] timeout
|
|
153
158
|
def timeout
|
|
154
159
|
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
155
160
|
end
|
|
156
161
|
|
|
157
|
-
def reset
|
|
162
|
+
def reset
|
|
158
163
|
@parser.reset
|
|
159
164
|
@io_buffer.reset
|
|
160
165
|
@read_header = true
|
|
161
|
-
@read_proxy = !!@expect_proxy_proto
|
|
166
|
+
@read_proxy = !!@expect_proxy_proto && @requests_served.zero?
|
|
162
167
|
@env = @proto_env.dup
|
|
163
168
|
@parsed_bytes = 0
|
|
164
169
|
@ready = false
|
|
165
170
|
@body_remain = 0
|
|
166
171
|
@peerip = nil if @remote_addr_header
|
|
167
172
|
@in_last_chunk = false
|
|
168
|
-
@http_content_length_limit_exceeded =
|
|
173
|
+
@http_content_length_limit_exceeded = nil
|
|
174
|
+
@error_status_code = nil
|
|
175
|
+
end
|
|
169
176
|
|
|
177
|
+
# only used with back-to-back requests contained in the buffer
|
|
178
|
+
def process_back_to_back_requests
|
|
170
179
|
if @buffer
|
|
171
180
|
return false unless try_to_parse_proxy_protocol
|
|
172
181
|
|
|
173
|
-
@parsed_bytes =
|
|
174
|
-
|
|
175
|
-
if @parser.finished?
|
|
176
|
-
return setup_body
|
|
177
|
-
elsif @parsed_bytes >= MAX_HEADER
|
|
178
|
-
raise HttpParserError,
|
|
179
|
-
"HEADER is longer than allowed, aborting client early."
|
|
180
|
-
end
|
|
182
|
+
@parsed_bytes = parser_execute
|
|
181
183
|
|
|
182
|
-
|
|
183
|
-
else
|
|
184
|
-
begin
|
|
185
|
-
if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
|
|
186
|
-
return try_to_finish
|
|
187
|
-
end
|
|
188
|
-
rescue IOError
|
|
189
|
-
# swallow it
|
|
190
|
-
end
|
|
184
|
+
@parser.finished? ? process_env_body : false
|
|
191
185
|
end
|
|
192
186
|
end
|
|
193
187
|
|
|
188
|
+
# if a client sends back-to-back requests, the buffer may contain one or more
|
|
189
|
+
# of them.
|
|
190
|
+
def has_back_to_back_requests?
|
|
191
|
+
!(@buffer.nil? || @buffer.empty?)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
194
|
def close
|
|
195
195
|
tempfile_close
|
|
196
196
|
begin
|
|
197
197
|
@io.close
|
|
198
198
|
rescue IOError, Errno::EBADF
|
|
199
|
-
Puma::Util.purge_interrupt_queue
|
|
200
199
|
end
|
|
201
200
|
end
|
|
202
201
|
|
|
@@ -214,37 +213,42 @@ module Puma
|
|
|
214
213
|
def try_to_parse_proxy_protocol
|
|
215
214
|
if @read_proxy
|
|
216
215
|
if @expect_proxy_proto == :v1
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
216
|
+
crlf_index = @buffer.index "\r\n"
|
|
217
|
+
|
|
218
|
+
unless crlf_index
|
|
219
|
+
if "PROXY ".start_with? @buffer
|
|
220
|
+
return false
|
|
221
|
+
elsif @buffer.start_with? "PROXY "
|
|
222
|
+
if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
|
|
223
|
+
raise ConnectionError, "PROXY protocol v1 line is too long"
|
|
221
224
|
end
|
|
222
|
-
|
|
225
|
+
return false
|
|
223
226
|
end
|
|
224
|
-
|
|
225
|
-
# request, this is just HTTP from a non-PROXY client; move on
|
|
227
|
+
|
|
226
228
|
@read_proxy = false
|
|
227
|
-
return
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
return true
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
|
|
233
|
+
raise ConnectionError, "PROXY protocol v1 line is too long"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
|
|
237
|
+
if md[1]
|
|
238
|
+
@peerip = md[1].split(" ")[0]
|
|
239
|
+
end
|
|
240
|
+
@buffer = md.post_match
|
|
230
241
|
end
|
|
242
|
+
# if the buffer has a \r\n but doesn't have a PROXY protocol
|
|
243
|
+
# request, this is just HTTP from a non-PROXY client; move on
|
|
244
|
+
@read_proxy = false
|
|
245
|
+
return @buffer.size > 0
|
|
231
246
|
end
|
|
232
247
|
end
|
|
233
248
|
true
|
|
234
249
|
end
|
|
235
250
|
|
|
236
251
|
def try_to_finish
|
|
237
|
-
if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
|
|
238
|
-
@http_content_length_limit_exceeded = true
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
if @http_content_length_limit_exceeded
|
|
242
|
-
@buffer = nil
|
|
243
|
-
@body = EmptyBody
|
|
244
|
-
set_ready
|
|
245
|
-
return true
|
|
246
|
-
end
|
|
247
|
-
|
|
248
252
|
return read_body if in_data_phase
|
|
249
253
|
|
|
250
254
|
data = nil
|
|
@@ -273,26 +277,17 @@ module Puma
|
|
|
273
277
|
|
|
274
278
|
return false unless try_to_parse_proxy_protocol
|
|
275
279
|
|
|
276
|
-
@parsed_bytes =
|
|
277
|
-
|
|
278
|
-
if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
|
|
279
|
-
@http_content_length_limit_exceeded = true
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
if @parser.finished?
|
|
283
|
-
return setup_body
|
|
284
|
-
elsif @parsed_bytes >= MAX_HEADER
|
|
285
|
-
raise HttpParserError,
|
|
286
|
-
"HEADER is longer than allowed, aborting client early."
|
|
287
|
-
end
|
|
280
|
+
@parsed_bytes = parser_execute
|
|
288
281
|
|
|
289
|
-
false
|
|
282
|
+
@parser.finished? ? process_env_body : false
|
|
290
283
|
end
|
|
291
284
|
|
|
292
285
|
def eagerly_finish
|
|
293
286
|
return true if @ready
|
|
294
|
-
|
|
295
|
-
|
|
287
|
+
while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
|
|
288
|
+
return true if try_to_finish
|
|
289
|
+
end
|
|
290
|
+
false
|
|
296
291
|
end
|
|
297
292
|
|
|
298
293
|
def finish(timeout)
|
|
@@ -300,6 +295,58 @@ module Puma
|
|
|
300
295
|
@to_io.wait_readable(timeout) || timeout! until try_to_finish
|
|
301
296
|
end
|
|
302
297
|
|
|
298
|
+
# Wraps `@parser.execute` and adds meaningful error messages
|
|
299
|
+
# @return [Integer] bytes of buffer read by parser
|
|
300
|
+
#
|
|
301
|
+
def parser_execute
|
|
302
|
+
ret = @parser.execute(@env, @buffer, @parsed_bytes)
|
|
303
|
+
|
|
304
|
+
if @env[REQUEST_METHOD] && @supported_http_methods != :any && !@supported_http_methods.key?(@env[REQUEST_METHOD])
|
|
305
|
+
raise HttpParserError501, "#{@env[REQUEST_METHOD]} method is not supported"
|
|
306
|
+
end
|
|
307
|
+
ret
|
|
308
|
+
rescue => e
|
|
309
|
+
@env[HTTP_CONNECTION] = 'close'
|
|
310
|
+
raise e unless HttpParserError === e && e.message.include?('non-SSL')
|
|
311
|
+
|
|
312
|
+
req, _ = @buffer.split "\r\n\r\n"
|
|
313
|
+
request_line, headers = req.split "\r\n", 2
|
|
314
|
+
|
|
315
|
+
# below checks for request issues and changes error message accordingly
|
|
316
|
+
if !@env.key? REQUEST_METHOD
|
|
317
|
+
if request_line.count(' ') != 2
|
|
318
|
+
# maybe this is an SSL connection ?
|
|
319
|
+
raise e
|
|
320
|
+
else
|
|
321
|
+
method = request_line[/\A[^ ]+/]
|
|
322
|
+
raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
|
|
323
|
+
end
|
|
324
|
+
elsif !@env.key? REQUEST_PATH
|
|
325
|
+
path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
|
|
326
|
+
raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
|
|
327
|
+
elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
|
|
328
|
+
query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
|
|
329
|
+
raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
|
|
330
|
+
elsif !@env.key? SERVER_PROTOCOL
|
|
331
|
+
# protocol is bad
|
|
332
|
+
text = request_line[/[^ ]*\z/]
|
|
333
|
+
raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
|
|
334
|
+
elsif !headers.empty?
|
|
335
|
+
# headers are bad
|
|
336
|
+
hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
|
|
337
|
+
raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# processes the `env` and the request body
|
|
342
|
+
def process_env_body
|
|
343
|
+
temp = setup_body
|
|
344
|
+
normalize_env
|
|
345
|
+
req_env_post_parse
|
|
346
|
+
raise HttpParserError if @error_status_code
|
|
347
|
+
temp
|
|
348
|
+
end
|
|
349
|
+
|
|
303
350
|
def timeout!
|
|
304
351
|
write_error(408) if in_data_phase
|
|
305
352
|
raise ConnectionError
|
|
@@ -316,12 +363,12 @@ module Puma
|
|
|
316
363
|
return @peerip if @peerip
|
|
317
364
|
|
|
318
365
|
if @remote_addr_header
|
|
319
|
-
hdr = (@env[@remote_addr_header] ||
|
|
366
|
+
hdr = (@env[@remote_addr_header] || socket_peerip).split(/[\s,]/).first
|
|
320
367
|
@peerip = hdr
|
|
321
368
|
return hdr
|
|
322
369
|
end
|
|
323
370
|
|
|
324
|
-
@peerip ||=
|
|
371
|
+
@peerip ||= socket_peerip
|
|
325
372
|
end
|
|
326
373
|
|
|
327
374
|
def peer_family
|
|
@@ -356,38 +403,56 @@ module Puma
|
|
|
356
403
|
|
|
357
404
|
private
|
|
358
405
|
|
|
406
|
+
IPV4_MAPPED_IPV6_PREFIX = "::ffff:"
|
|
407
|
+
private_constant :IPV4_MAPPED_IPV6_PREFIX
|
|
408
|
+
|
|
409
|
+
def socket_peerip
|
|
410
|
+
unmap_ipv6(@io.peeraddr.last)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Converts IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) back to
|
|
414
|
+
# their IPv4 form. These addresses appear when IPv4 clients connect to
|
|
415
|
+
# a dual-stack IPv6 socket.
|
|
416
|
+
def unmap_ipv6(addr)
|
|
417
|
+
addr.delete_prefix(IPV4_MAPPED_IPV6_PREFIX)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Checks the request `Transfer-Encoding` and/or `Content-Length` to see if
|
|
421
|
+
# they are valid. Raises errors if not, otherwise reads the body.
|
|
422
|
+
# @return [Boolean] true if the body can be completely read, false otherwise
|
|
423
|
+
#
|
|
359
424
|
def setup_body
|
|
360
425
|
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
361
426
|
|
|
362
427
|
if @env[HTTP_EXPECT] == CONTINUE
|
|
363
|
-
# TODO allow a hook here to check the headers before
|
|
364
|
-
# going forward
|
|
428
|
+
# TODO allow a hook here to check the headers before going forward
|
|
365
429
|
@io << HTTP_11_100
|
|
366
430
|
@io.flush
|
|
367
431
|
end
|
|
368
432
|
|
|
369
433
|
@read_header = false
|
|
370
434
|
|
|
371
|
-
|
|
435
|
+
parser_body = @parser.body
|
|
372
436
|
|
|
373
437
|
te = @env[TRANSFER_ENCODING2]
|
|
374
438
|
if te
|
|
375
439
|
te_lwr = te.downcase
|
|
376
440
|
if te.include? ','
|
|
377
|
-
te_ary = te_lwr.split
|
|
441
|
+
te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
|
|
378
442
|
te_count = te_ary.count CHUNKED
|
|
379
443
|
te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
|
|
380
|
-
if
|
|
381
|
-
@env.delete TRANSFER_ENCODING2
|
|
382
|
-
return setup_chunked_body body
|
|
383
|
-
elsif te_count >= 1
|
|
444
|
+
if te_count > 1
|
|
384
445
|
raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
|
|
446
|
+
elsif te_ary.last != CHUNKED
|
|
447
|
+
raise HttpParserError , "#{TE_ERR_MSG}, last value must be chunked: '#{te}'"
|
|
385
448
|
elsif !te_valid
|
|
386
449
|
raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
|
387
450
|
end
|
|
451
|
+
@env.delete TRANSFER_ENCODING2
|
|
452
|
+
return setup_chunked_body parser_body
|
|
388
453
|
elsif te_lwr == CHUNKED
|
|
389
454
|
@env.delete TRANSFER_ENCODING2
|
|
390
|
-
return setup_chunked_body
|
|
455
|
+
return setup_chunked_body parser_body
|
|
391
456
|
elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
|
|
392
457
|
raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
|
|
393
458
|
else
|
|
@@ -402,10 +467,12 @@ module Puma
|
|
|
402
467
|
if cl
|
|
403
468
|
# cannot contain characters that are not \d, or be empty
|
|
404
469
|
if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
|
|
470
|
+
@error_status_code = 400
|
|
471
|
+
@env[HTTP_CONNECTION] = 'close'
|
|
405
472
|
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
|
406
473
|
end
|
|
407
474
|
else
|
|
408
|
-
@buffer =
|
|
475
|
+
@buffer = parser_body.empty? ? nil : parser_body
|
|
409
476
|
@body = EmptyBody
|
|
410
477
|
set_ready
|
|
411
478
|
return true
|
|
@@ -413,23 +480,25 @@ module Puma
|
|
|
413
480
|
|
|
414
481
|
content_length = cl.to_i
|
|
415
482
|
|
|
416
|
-
|
|
483
|
+
raise_above_http_content_limit if @http_content_length_limit&.< content_length
|
|
484
|
+
|
|
485
|
+
remain = content_length - parser_body.bytesize
|
|
417
486
|
|
|
418
487
|
if remain <= 0
|
|
419
|
-
# Part of the
|
|
488
|
+
# Part of the parser_body is a pipelined request OR garbage. We'll deal with that later.
|
|
420
489
|
if content_length == 0
|
|
421
490
|
@body = EmptyBody
|
|
422
|
-
if
|
|
491
|
+
if parser_body.empty?
|
|
423
492
|
@buffer = nil
|
|
424
493
|
else
|
|
425
|
-
@buffer =
|
|
494
|
+
@buffer = parser_body
|
|
426
495
|
end
|
|
427
496
|
elsif remain == 0
|
|
428
|
-
@body = StringIO.new
|
|
497
|
+
@body = StringIO.new parser_body
|
|
429
498
|
@buffer = nil
|
|
430
499
|
else
|
|
431
|
-
@body = StringIO.new(
|
|
432
|
-
@buffer =
|
|
500
|
+
@body = StringIO.new(parser_body[0,content_length])
|
|
501
|
+
@buffer = parser_body[content_length..-1]
|
|
433
502
|
end
|
|
434
503
|
set_ready
|
|
435
504
|
return true
|
|
@@ -441,12 +510,12 @@ module Puma
|
|
|
441
510
|
@body.binmode
|
|
442
511
|
@tempfile = @body
|
|
443
512
|
else
|
|
444
|
-
# The
|
|
445
|
-
# encoding as
|
|
446
|
-
@body = StringIO.new
|
|
513
|
+
# The parser_body[0,0] trick is to get an empty string in the same
|
|
514
|
+
# encoding as parser_body.
|
|
515
|
+
@body = StringIO.new parser_body[0,0]
|
|
447
516
|
end
|
|
448
517
|
|
|
449
|
-
@body.write
|
|
518
|
+
@body.write parser_body
|
|
450
519
|
|
|
451
520
|
@body_remain = remain
|
|
452
521
|
|
|
@@ -462,40 +531,36 @@ module Puma
|
|
|
462
531
|
# after this
|
|
463
532
|
remain = @body_remain
|
|
464
533
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
534
|
+
# don't bother with reading zero bytes
|
|
535
|
+
unless remain.zero?
|
|
536
|
+
begin
|
|
537
|
+
chunk = @io.read_nonblock(remain.clamp(0, CHUNK_SIZE), @read_buffer)
|
|
538
|
+
rescue IO::WaitReadable
|
|
539
|
+
return false
|
|
540
|
+
rescue SystemCallError, IOError
|
|
541
|
+
raise ConnectionError, "Connection error detected during read"
|
|
542
|
+
end
|
|
470
543
|
|
|
471
|
-
|
|
472
|
-
chunk
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
544
|
+
# No chunk means a closed socket
|
|
545
|
+
unless chunk
|
|
546
|
+
@body.close
|
|
547
|
+
@buffer = nil
|
|
548
|
+
set_ready
|
|
549
|
+
raise EOFError
|
|
550
|
+
end
|
|
478
551
|
|
|
479
|
-
|
|
480
|
-
unless chunk
|
|
481
|
-
@body.close
|
|
482
|
-
@buffer = nil
|
|
483
|
-
set_ready
|
|
484
|
-
raise EOFError
|
|
552
|
+
remain -= @body.write(chunk)
|
|
485
553
|
end
|
|
486
554
|
|
|
487
|
-
remain -= @body.write(chunk)
|
|
488
|
-
|
|
489
555
|
if remain <= 0
|
|
490
556
|
@body.rewind
|
|
491
557
|
@buffer = nil
|
|
492
558
|
set_ready
|
|
493
|
-
|
|
559
|
+
true
|
|
560
|
+
else
|
|
561
|
+
@body_remain = remain
|
|
562
|
+
false
|
|
494
563
|
end
|
|
495
|
-
|
|
496
|
-
@body_remain = remain
|
|
497
|
-
|
|
498
|
-
false
|
|
499
564
|
end
|
|
500
565
|
|
|
501
566
|
def read_chunked_body
|
|
@@ -543,6 +608,10 @@ module Puma
|
|
|
543
608
|
|
|
544
609
|
# @version 5.0.0
|
|
545
610
|
def write_chunk(str)
|
|
611
|
+
if @http_content_length_limit&.< @chunked_content_length + str.bytesize
|
|
612
|
+
raise_above_http_content_limit
|
|
613
|
+
end
|
|
614
|
+
|
|
546
615
|
@chunked_content_length += @body.write(str)
|
|
547
616
|
end
|
|
548
617
|
|
|
@@ -675,8 +744,13 @@ module Puma
|
|
|
675
744
|
@ready = true
|
|
676
745
|
end
|
|
677
746
|
|
|
678
|
-
def
|
|
679
|
-
@
|
|
747
|
+
def raise_above_http_content_limit
|
|
748
|
+
@http_content_length_limit_exceeded = true
|
|
749
|
+
@buffer = nil
|
|
750
|
+
@body = EmptyBody
|
|
751
|
+
@error_status_code = 413
|
|
752
|
+
@env[HTTP_CONNECTION] = 'close'
|
|
753
|
+
raise HttpParserError, "Payload Too Large"
|
|
680
754
|
end
|
|
681
755
|
end
|
|
682
756
|
end
|