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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +448 -8
  3. data/README.md +110 -51
  4. data/docs/5.0-Upgrade.md +98 -0
  5. data/docs/6.0-Upgrade.md +56 -0
  6. data/docs/7.0-Upgrade.md +52 -0
  7. data/docs/8.0-Upgrade.md +100 -0
  8. data/docs/deployment.md +58 -23
  9. data/docs/fork_worker.md +11 -1
  10. data/docs/grpc.md +62 -0
  11. data/docs/images/favicon.svg +1 -0
  12. data/docs/images/running-puma.svg +1 -0
  13. data/docs/images/standard-logo.svg +1 -0
  14. data/docs/java_options.md +54 -0
  15. data/docs/jungle/README.md +1 -1
  16. data/docs/kubernetes.md +11 -16
  17. data/docs/plugins.md +6 -2
  18. data/docs/restart.md +2 -2
  19. data/docs/signals.md +21 -21
  20. data/docs/stats.md +11 -5
  21. data/docs/systemd.md +14 -5
  22. data/ext/puma_http11/extconf.rb +20 -32
  23. data/ext/puma_http11/http11_parser.java.rl +51 -65
  24. data/ext/puma_http11/mini_ssl.c +29 -9
  25. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  26. data/ext/puma_http11/org/jruby/puma/Http11.java +194 -101
  27. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
  28. data/ext/puma_http11/puma_http11.c +125 -118
  29. data/lib/puma/app/status.rb +11 -3
  30. data/lib/puma/binder.rb +22 -12
  31. data/lib/puma/cli.rb +11 -9
  32. data/lib/puma/client.rb +233 -136
  33. data/lib/puma/client_env.rb +171 -0
  34. data/lib/puma/cluster/worker.rb +24 -21
  35. data/lib/puma/cluster/worker_handle.rb +38 -8
  36. data/lib/puma/cluster.rb +74 -48
  37. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  38. data/lib/puma/commonlogger.rb +3 -3
  39. data/lib/puma/configuration.rb +197 -64
  40. data/lib/puma/const.rb +23 -12
  41. data/lib/puma/control_cli.rb +11 -7
  42. data/lib/puma/detect.rb +13 -0
  43. data/lib/puma/dsl.rb +483 -127
  44. data/lib/puma/error_logger.rb +7 -5
  45. data/lib/puma/events.rb +25 -10
  46. data/lib/puma/io_buffer.rb +8 -4
  47. data/lib/puma/jruby_restart.rb +0 -16
  48. data/lib/puma/launcher/bundle_pruner.rb +3 -5
  49. data/lib/puma/launcher.rb +76 -59
  50. data/lib/puma/log_writer.rb +17 -11
  51. data/lib/puma/minissl/context_builder.rb +1 -0
  52. data/lib/puma/minissl.rb +1 -1
  53. data/lib/puma/null_io.rb +26 -0
  54. data/lib/puma/plugin/systemd.rb +3 -3
  55. data/lib/puma/rack/urlmap.rb +1 -1
  56. data/lib/puma/reactor.rb +19 -13
  57. data/lib/puma/{request.rb → response.rb} +57 -209
  58. data/lib/puma/runner.rb +15 -17
  59. data/lib/puma/sd_notify.rb +1 -4
  60. data/lib/puma/server.rb +200 -104
  61. data/lib/puma/server_plugin_control.rb +32 -0
  62. data/lib/puma/single.rb +7 -4
  63. data/lib/puma/state_file.rb +3 -2
  64. data/lib/puma/thread_pool.rb +179 -96
  65. data/lib/puma/util.rb +0 -7
  66. data/lib/puma.rb +10 -0
  67. data/lib/rack/handler/puma.rb +11 -8
  68. data/tools/Dockerfile +15 -5
  69. metadata +26 -16
  70. 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 represents a unique request from a client.
32
- # For example, this could be a web request from a browser or from CURL.
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
- # the header and body are fully buffered via the `try_to_finish` method.
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
- include Puma::Const
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 = false
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 (allows us to just put the Client object
137
- # into the env)
138
- def call
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(fast_check=true)
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 = false
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 = @parser.execute(@env, @buffer, @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
- if @buffer.include? "\r\n"
211
- if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
212
- if md[1]
213
- @peerip = md[1].split(" ")[0]
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
- @buffer = md.post_match
225
+ return false
216
226
  end
217
- # if the buffer has a \r\n but doesn't have a PROXY protocol
218
- # request, this is just HTTP from a non-PROXY client; move on
227
+
219
228
  @read_proxy = false
220
- return @buffer.size > 0
221
- else
222
- return false
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 = @parser.execute(@env, @buffer, @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
- return false unless @to_io.wait_readable(0)
287
- try_to_finish
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] || @io.peeraddr.last).split(/[\s,]/).first
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 ||= @io.peeraddr.last
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
- body = @parser.body
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 te_ary.last == CHUNKED && te_count == 1 && te_valid
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 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 = body.empty? ? nil : body
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
- remain = cl.to_i - body.bytesize
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
- @body = StringIO.new(body)
410
- @buffer = nil
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.new(Const::PUMA_TMP_BASE)
417
- @body.unlink
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 body[0,0] trick is to get an empty string in the same
422
- # encoding as body.
423
- @body = StringIO.new body[0,0]
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 body
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
- if remain > CHUNK_SIZE
443
- want = CHUNK_SIZE
444
- else
445
- want = remain
446
- end
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
- begin
449
- chunk = @io.read_nonblock(want, @read_buffer)
450
- rescue IO::WaitReadable
451
- return false
452
- rescue SystemCallError, IOError
453
- raise ConnectionError, "Connection error detected during read"
454
- end
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
- # No chunk means a closed socket
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
- return true
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(4096, @read_buffer)
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.new(Const::PUMA_TMP_BASE)
510
- @body.unlink
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 + chunk.size >= MAX_CHUNK_HEADER_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 above_http_content_limit(value)
656
- @http_content_length_limit&.< value
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