puma 6.4.3 → 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 +448 -8
- data/README.md +110 -51
- 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 +11 -1
- 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/java_options.md +54 -0
- data/docs/jungle/README.md +1 -1
- data/docs/kubernetes.md +11 -16
- data/docs/plugins.md +6 -2
- data/docs/restart.md +2 -2
- data/docs/signals.md +21 -21
- data/docs/stats.md +11 -5
- data/docs/systemd.md +14 -5
- data/ext/puma_http11/extconf.rb +20 -32
- data/ext/puma_http11/http11_parser.java.rl +51 -65
- data/ext/puma_http11/mini_ssl.c +29 -9
- data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +194 -101
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
- data/ext/puma_http11/puma_http11.c +125 -118
- data/lib/puma/app/status.rb +11 -3
- data/lib/puma/binder.rb +22 -12
- data/lib/puma/cli.rb +11 -9
- data/lib/puma/client.rb +233 -136
- data/lib/puma/client_env.rb +171 -0
- data/lib/puma/cluster/worker.rb +24 -21
- data/lib/puma/cluster/worker_handle.rb +38 -8
- data/lib/puma/cluster.rb +74 -48
- data/lib/puma/cluster_accept_loop_delay.rb +91 -0
- data/lib/puma/commonlogger.rb +3 -3
- data/lib/puma/configuration.rb +197 -64
- data/lib/puma/const.rb +23 -12
- data/lib/puma/control_cli.rb +11 -7
- data/lib/puma/detect.rb +13 -0
- data/lib/puma/dsl.rb +483 -127
- data/lib/puma/error_logger.rb +7 -5
- data/lib/puma/events.rb +25 -10
- data/lib/puma/io_buffer.rb +8 -4
- data/lib/puma/jruby_restart.rb +0 -16
- data/lib/puma/launcher/bundle_pruner.rb +3 -5
- data/lib/puma/launcher.rb +76 -59
- data/lib/puma/log_writer.rb +17 -11
- data/lib/puma/minissl/context_builder.rb +1 -0
- data/lib/puma/minissl.rb +1 -1
- data/lib/puma/null_io.rb +26 -0
- 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} +57 -209
- data/lib/puma/runner.rb +15 -17
- data/lib/puma/sd_notify.rb +1 -4
- data/lib/puma/server.rb +200 -104
- 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 +179 -96
- data/lib/puma/util.rb +0 -7
- data/lib/puma.rb +10 -0
- data/lib/rack/handler/puma.rb +11 -8
- data/tools/Dockerfile +15 -5
- metadata +26 -16
- 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,96 +154,104 @@ 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
|
-
@body = nil
|
|
164
|
-
@tempfile = nil
|
|
165
168
|
@parsed_bytes = 0
|
|
166
169
|
@ready = false
|
|
167
170
|
@body_remain = 0
|
|
168
171
|
@peerip = nil if @remote_addr_header
|
|
169
172
|
@in_last_chunk = false
|
|
170
|
-
@http_content_length_limit_exceeded =
|
|
173
|
+
@http_content_length_limit_exceeded = nil
|
|
174
|
+
@error_status_code = nil
|
|
175
|
+
end
|
|
171
176
|
|
|
177
|
+
# only used with back-to-back requests contained in the buffer
|
|
178
|
+
def process_back_to_back_requests
|
|
172
179
|
if @buffer
|
|
173
180
|
return false unless try_to_parse_proxy_protocol
|
|
174
181
|
|
|
175
|
-
@parsed_bytes =
|
|
176
|
-
|
|
177
|
-
if @parser.finished?
|
|
178
|
-
return setup_body
|
|
179
|
-
elsif @parsed_bytes >= MAX_HEADER
|
|
180
|
-
raise HttpParserError,
|
|
181
|
-
"HEADER is longer than allowed, aborting client early."
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
return false
|
|
185
|
-
else
|
|
186
|
-
begin
|
|
187
|
-
if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
|
|
188
|
-
return try_to_finish
|
|
189
|
-
end
|
|
190
|
-
rescue IOError
|
|
191
|
-
# swallow it
|
|
192
|
-
end
|
|
182
|
+
@parsed_bytes = parser_execute
|
|
193
183
|
|
|
184
|
+
@parser.finished? ? process_env_body : false
|
|
194
185
|
end
|
|
195
186
|
end
|
|
196
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
|
+
|
|
197
194
|
def close
|
|
195
|
+
tempfile_close
|
|
198
196
|
begin
|
|
199
197
|
@io.close
|
|
200
198
|
rescue IOError, Errno::EBADF
|
|
201
|
-
Puma::Util.purge_interrupt_queue
|
|
202
199
|
end
|
|
203
200
|
end
|
|
204
201
|
|
|
202
|
+
def tempfile_close
|
|
203
|
+
tf_path = @tempfile&.path
|
|
204
|
+
@tempfile&.close
|
|
205
|
+
File.unlink(tf_path) if tf_path
|
|
206
|
+
@tempfile = nil
|
|
207
|
+
@body = nil
|
|
208
|
+
rescue Errno::ENOENT, IOError
|
|
209
|
+
end
|
|
210
|
+
|
|
205
211
|
# If necessary, read the PROXY protocol from the buffer. Returns
|
|
206
212
|
# false if more data is needed.
|
|
207
213
|
def try_to_parse_proxy_protocol
|
|
208
214
|
if @read_proxy
|
|
209
215
|
if @expect_proxy_proto == :v1
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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"
|
|
214
224
|
end
|
|
215
|
-
|
|
225
|
+
return false
|
|
216
226
|
end
|
|
217
|
-
|
|
218
|
-
# request, this is just HTTP from a non-PROXY client; move on
|
|
227
|
+
|
|
219
228
|
@read_proxy = false
|
|
220
|
-
return
|
|
221
|
-
|
|
222
|
-
|
|
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"
|
|
223
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
|
|
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
|
|
224
246
|
end
|
|
225
247
|
end
|
|
226
248
|
true
|
|
227
249
|
end
|
|
228
250
|
|
|
229
251
|
def try_to_finish
|
|
230
|
-
if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
|
|
231
|
-
@http_content_length_limit_exceeded = true
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
if @http_content_length_limit_exceeded
|
|
235
|
-
@buffer = nil
|
|
236
|
-
@body = EmptyBody
|
|
237
|
-
set_ready
|
|
238
|
-
return true
|
|
239
|
-
end
|
|
240
|
-
|
|
241
252
|
return read_body if in_data_phase
|
|
242
253
|
|
|
254
|
+
data = nil
|
|
243
255
|
begin
|
|
244
256
|
data = @io.read_nonblock(CHUNK_SIZE)
|
|
245
257
|
rescue IO::WaitReadable
|
|
@@ -265,26 +277,17 @@ module Puma
|
|
|
265
277
|
|
|
266
278
|
return false unless try_to_parse_proxy_protocol
|
|
267
279
|
|
|
268
|
-
@parsed_bytes =
|
|
269
|
-
|
|
270
|
-
if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
|
|
271
|
-
@http_content_length_limit_exceeded = true
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
if @parser.finished?
|
|
275
|
-
return setup_body
|
|
276
|
-
elsif @parsed_bytes >= MAX_HEADER
|
|
277
|
-
raise HttpParserError,
|
|
278
|
-
"HEADER is longer than allowed, aborting client early."
|
|
279
|
-
end
|
|
280
|
+
@parsed_bytes = parser_execute
|
|
280
281
|
|
|
281
|
-
false
|
|
282
|
+
@parser.finished? ? process_env_body : false
|
|
282
283
|
end
|
|
283
284
|
|
|
284
285
|
def eagerly_finish
|
|
285
286
|
return true if @ready
|
|
286
|
-
|
|
287
|
-
|
|
287
|
+
while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
|
|
288
|
+
return true if try_to_finish
|
|
289
|
+
end
|
|
290
|
+
false
|
|
288
291
|
end
|
|
289
292
|
|
|
290
293
|
def finish(timeout)
|
|
@@ -292,6 +295,58 @@ module Puma
|
|
|
292
295
|
@to_io.wait_readable(timeout) || timeout! until try_to_finish
|
|
293
296
|
end
|
|
294
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
|
+
|
|
295
350
|
def timeout!
|
|
296
351
|
write_error(408) if in_data_phase
|
|
297
352
|
raise ConnectionError
|
|
@@ -308,12 +363,12 @@ module Puma
|
|
|
308
363
|
return @peerip if @peerip
|
|
309
364
|
|
|
310
365
|
if @remote_addr_header
|
|
311
|
-
hdr = (@env[@remote_addr_header] ||
|
|
366
|
+
hdr = (@env[@remote_addr_header] || socket_peerip).split(/[\s,]/).first
|
|
312
367
|
@peerip = hdr
|
|
313
368
|
return hdr
|
|
314
369
|
end
|
|
315
370
|
|
|
316
|
-
@peerip ||=
|
|
371
|
+
@peerip ||= socket_peerip
|
|
317
372
|
end
|
|
318
373
|
|
|
319
374
|
def peer_family
|
|
@@ -348,38 +403,56 @@ module Puma
|
|
|
348
403
|
|
|
349
404
|
private
|
|
350
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
|
+
#
|
|
351
424
|
def setup_body
|
|
352
425
|
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
353
426
|
|
|
354
427
|
if @env[HTTP_EXPECT] == CONTINUE
|
|
355
|
-
# TODO allow a hook here to check the headers before
|
|
356
|
-
# going forward
|
|
428
|
+
# TODO allow a hook here to check the headers before going forward
|
|
357
429
|
@io << HTTP_11_100
|
|
358
430
|
@io.flush
|
|
359
431
|
end
|
|
360
432
|
|
|
361
433
|
@read_header = false
|
|
362
434
|
|
|
363
|
-
|
|
435
|
+
parser_body = @parser.body
|
|
364
436
|
|
|
365
437
|
te = @env[TRANSFER_ENCODING2]
|
|
366
438
|
if te
|
|
367
439
|
te_lwr = te.downcase
|
|
368
440
|
if te.include? ','
|
|
369
|
-
te_ary = te_lwr.split
|
|
441
|
+
te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
|
|
370
442
|
te_count = te_ary.count CHUNKED
|
|
371
443
|
te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
|
|
372
|
-
if
|
|
373
|
-
@env.delete TRANSFER_ENCODING2
|
|
374
|
-
return setup_chunked_body body
|
|
375
|
-
elsif te_count >= 1
|
|
444
|
+
if te_count > 1
|
|
376
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}'"
|
|
377
448
|
elsif !te_valid
|
|
378
449
|
raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
|
379
450
|
end
|
|
451
|
+
@env.delete TRANSFER_ENCODING2
|
|
452
|
+
return setup_chunked_body parser_body
|
|
380
453
|
elsif te_lwr == CHUNKED
|
|
381
454
|
@env.delete TRANSFER_ENCODING2
|
|
382
|
-
return setup_chunked_body
|
|
455
|
+
return setup_chunked_body parser_body
|
|
383
456
|
elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
|
|
384
457
|
raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
|
|
385
458
|
else
|
|
@@ -394,36 +467,55 @@ module Puma
|
|
|
394
467
|
if cl
|
|
395
468
|
# cannot contain characters that are not \d, or be empty
|
|
396
469
|
if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
|
|
470
|
+
@error_status_code = 400
|
|
471
|
+
@env[HTTP_CONNECTION] = 'close'
|
|
397
472
|
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
|
398
473
|
end
|
|
399
474
|
else
|
|
400
|
-
@buffer =
|
|
475
|
+
@buffer = parser_body.empty? ? nil : parser_body
|
|
401
476
|
@body = EmptyBody
|
|
402
477
|
set_ready
|
|
403
478
|
return true
|
|
404
479
|
end
|
|
405
480
|
|
|
406
|
-
|
|
481
|
+
content_length = cl.to_i
|
|
482
|
+
|
|
483
|
+
raise_above_http_content_limit if @http_content_length_limit&.< content_length
|
|
484
|
+
|
|
485
|
+
remain = content_length - parser_body.bytesize
|
|
407
486
|
|
|
408
487
|
if remain <= 0
|
|
409
|
-
|
|
410
|
-
|
|
488
|
+
# Part of the parser_body is a pipelined request OR garbage. We'll deal with that later.
|
|
489
|
+
if content_length == 0
|
|
490
|
+
@body = EmptyBody
|
|
491
|
+
if parser_body.empty?
|
|
492
|
+
@buffer = nil
|
|
493
|
+
else
|
|
494
|
+
@buffer = parser_body
|
|
495
|
+
end
|
|
496
|
+
elsif remain == 0
|
|
497
|
+
@body = StringIO.new parser_body
|
|
498
|
+
@buffer = nil
|
|
499
|
+
else
|
|
500
|
+
@body = StringIO.new(parser_body[0,content_length])
|
|
501
|
+
@buffer = parser_body[content_length..-1]
|
|
502
|
+
end
|
|
411
503
|
set_ready
|
|
412
504
|
return true
|
|
413
505
|
end
|
|
414
506
|
|
|
415
507
|
if remain > MAX_BODY
|
|
416
|
-
@body = Tempfile.
|
|
417
|
-
@body.
|
|
508
|
+
@body = Tempfile.create(Const::PUMA_TMP_BASE)
|
|
509
|
+
File.unlink @body.path unless IS_WINDOWS
|
|
418
510
|
@body.binmode
|
|
419
511
|
@tempfile = @body
|
|
420
512
|
else
|
|
421
|
-
# The
|
|
422
|
-
# encoding as
|
|
423
|
-
@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]
|
|
424
516
|
end
|
|
425
517
|
|
|
426
|
-
@body.write
|
|
518
|
+
@body.write parser_body
|
|
427
519
|
|
|
428
520
|
@body_remain = remain
|
|
429
521
|
|
|
@@ -439,46 +531,42 @@ module Puma
|
|
|
439
531
|
# after this
|
|
440
532
|
remain = @body_remain
|
|
441
533
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
447
543
|
|
|
448
|
-
|
|
449
|
-
chunk
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
544
|
+
# No chunk means a closed socket
|
|
545
|
+
unless chunk
|
|
546
|
+
@body.close
|
|
547
|
+
@buffer = nil
|
|
548
|
+
set_ready
|
|
549
|
+
raise EOFError
|
|
550
|
+
end
|
|
455
551
|
|
|
456
|
-
|
|
457
|
-
unless chunk
|
|
458
|
-
@body.close
|
|
459
|
-
@buffer = nil
|
|
460
|
-
set_ready
|
|
461
|
-
raise EOFError
|
|
552
|
+
remain -= @body.write(chunk)
|
|
462
553
|
end
|
|
463
554
|
|
|
464
|
-
remain -= @body.write(chunk)
|
|
465
|
-
|
|
466
555
|
if remain <= 0
|
|
467
556
|
@body.rewind
|
|
468
557
|
@buffer = nil
|
|
469
558
|
set_ready
|
|
470
|
-
|
|
559
|
+
true
|
|
560
|
+
else
|
|
561
|
+
@body_remain = remain
|
|
562
|
+
false
|
|
471
563
|
end
|
|
472
|
-
|
|
473
|
-
@body_remain = remain
|
|
474
|
-
|
|
475
|
-
false
|
|
476
564
|
end
|
|
477
565
|
|
|
478
566
|
def read_chunked_body
|
|
479
567
|
while true
|
|
480
568
|
begin
|
|
481
|
-
chunk = @io.read_nonblock(
|
|
569
|
+
chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
|
|
482
570
|
rescue IO::WaitReadable
|
|
483
571
|
return false
|
|
484
572
|
rescue SystemCallError, IOError
|
|
@@ -506,8 +594,8 @@ module Puma
|
|
|
506
594
|
@prev_chunk = ""
|
|
507
595
|
@excess_cr = 0
|
|
508
596
|
|
|
509
|
-
@body = Tempfile.
|
|
510
|
-
@body.
|
|
597
|
+
@body = Tempfile.create(Const::PUMA_TMP_BASE)
|
|
598
|
+
File.unlink @body.path unless IS_WINDOWS
|
|
511
599
|
@body.binmode
|
|
512
600
|
@tempfile = @body
|
|
513
601
|
@chunked_content_length = 0
|
|
@@ -520,6 +608,10 @@ module Puma
|
|
|
520
608
|
|
|
521
609
|
# @version 5.0.0
|
|
522
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
|
+
|
|
523
615
|
@chunked_content_length += @body.write(str)
|
|
524
616
|
end
|
|
525
617
|
|
|
@@ -627,7 +719,7 @@ module Puma
|
|
|
627
719
|
@partial_part_left = len - part.size
|
|
628
720
|
end
|
|
629
721
|
else
|
|
630
|
-
if @prev_chunk.size +
|
|
722
|
+
if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
|
|
631
723
|
raise HttpParserError, "maximum size of chunk header exceeded"
|
|
632
724
|
end
|
|
633
725
|
|
|
@@ -652,8 +744,13 @@ module Puma
|
|
|
652
744
|
@ready = true
|
|
653
745
|
end
|
|
654
746
|
|
|
655
|
-
def
|
|
656
|
-
@
|
|
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"
|
|
657
754
|
end
|
|
658
755
|
end
|
|
659
756
|
end
|