ed-precompiled_puma 7.0.4

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +3172 -0
  3. data/LICENSE +29 -0
  4. data/README.md +477 -0
  5. data/bin/puma +10 -0
  6. data/bin/puma-wild +25 -0
  7. data/bin/pumactl +12 -0
  8. data/docs/architecture.md +74 -0
  9. data/docs/compile_options.md +55 -0
  10. data/docs/deployment.md +102 -0
  11. data/docs/fork_worker.md +41 -0
  12. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  13. data/docs/images/puma-connection-flow.png +0 -0
  14. data/docs/images/puma-general-arch.png +0 -0
  15. data/docs/java_options.md +54 -0
  16. data/docs/jungle/README.md +9 -0
  17. data/docs/jungle/rc.d/README.md +74 -0
  18. data/docs/jungle/rc.d/puma +61 -0
  19. data/docs/jungle/rc.d/puma.conf +10 -0
  20. data/docs/kubernetes.md +80 -0
  21. data/docs/nginx.md +80 -0
  22. data/docs/plugins.md +42 -0
  23. data/docs/rails_dev_mode.md +28 -0
  24. data/docs/restart.md +65 -0
  25. data/docs/signals.md +98 -0
  26. data/docs/stats.md +148 -0
  27. data/docs/systemd.md +253 -0
  28. data/docs/testing_benchmarks_local_files.md +150 -0
  29. data/docs/testing_test_rackup_ci_files.md +36 -0
  30. data/ext/puma_http11/PumaHttp11Service.java +17 -0
  31. data/ext/puma_http11/ext_help.h +15 -0
  32. data/ext/puma_http11/extconf.rb +65 -0
  33. data/ext/puma_http11/http11_parser.c +1057 -0
  34. data/ext/puma_http11/http11_parser.h +65 -0
  35. data/ext/puma_http11/http11_parser.java.rl +145 -0
  36. data/ext/puma_http11/http11_parser.rl +149 -0
  37. data/ext/puma_http11/http11_parser_common.rl +54 -0
  38. data/ext/puma_http11/mini_ssl.c +852 -0
  39. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  40. data/ext/puma_http11/org/jruby/puma/Http11.java +257 -0
  41. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +455 -0
  42. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +509 -0
  43. data/ext/puma_http11/puma_http11.c +507 -0
  44. data/lib/puma/app/status.rb +96 -0
  45. data/lib/puma/binder.rb +511 -0
  46. data/lib/puma/cli.rb +245 -0
  47. data/lib/puma/client.rb +720 -0
  48. data/lib/puma/cluster/worker.rb +182 -0
  49. data/lib/puma/cluster/worker_handle.rb +127 -0
  50. data/lib/puma/cluster.rb +635 -0
  51. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  52. data/lib/puma/commonlogger.rb +115 -0
  53. data/lib/puma/configuration.rb +452 -0
  54. data/lib/puma/const.rb +307 -0
  55. data/lib/puma/control_cli.rb +320 -0
  56. data/lib/puma/detect.rb +47 -0
  57. data/lib/puma/dsl.rb +1480 -0
  58. data/lib/puma/error_logger.rb +115 -0
  59. data/lib/puma/events.rb +72 -0
  60. data/lib/puma/io_buffer.rb +50 -0
  61. data/lib/puma/jruby_restart.rb +11 -0
  62. data/lib/puma/json_serialization.rb +96 -0
  63. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  64. data/lib/puma/launcher.rb +496 -0
  65. data/lib/puma/log_writer.rb +147 -0
  66. data/lib/puma/minissl/context_builder.rb +96 -0
  67. data/lib/puma/minissl.rb +463 -0
  68. data/lib/puma/null_io.rb +101 -0
  69. data/lib/puma/plugin/systemd.rb +90 -0
  70. data/lib/puma/plugin/tmp_restart.rb +36 -0
  71. data/lib/puma/plugin.rb +111 -0
  72. data/lib/puma/rack/builder.rb +297 -0
  73. data/lib/puma/rack/urlmap.rb +93 -0
  74. data/lib/puma/rack_default.rb +24 -0
  75. data/lib/puma/reactor.rb +140 -0
  76. data/lib/puma/request.rb +701 -0
  77. data/lib/puma/runner.rb +211 -0
  78. data/lib/puma/sd_notify.rb +146 -0
  79. data/lib/puma/server.rb +734 -0
  80. data/lib/puma/single.rb +72 -0
  81. data/lib/puma/state_file.rb +69 -0
  82. data/lib/puma/thread_pool.rb +402 -0
  83. data/lib/puma/util.rb +134 -0
  84. data/lib/puma.rb +93 -0
  85. data/lib/rack/handler/puma.rb +144 -0
  86. data/tools/Dockerfile +18 -0
  87. data/tools/trickletest.rb +44 -0
  88. metadata +152 -0
@@ -0,0 +1,720 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'detect'
4
+ require_relative 'io_buffer'
5
+ require 'tempfile'
6
+
7
+ if Puma::IS_JRUBY
8
+ # We have to work around some OpenSSL buffer/io-readiness bugs
9
+ # so we pull it in regardless of if the user is binding
10
+ # to an SSL socket
11
+ require 'openssl'
12
+ end
13
+
14
+ module Puma
15
+
16
+ class ConnectionError < RuntimeError; end
17
+
18
+ class HttpParserError501 < IOError; end
19
+
20
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
21
+
22
+
23
+ # An instance of this class represents a unique request from a client.
24
+ # For example, this could be a web request from a browser or from CURL.
25
+ #
26
+ # An instance of `Puma::Client` can be used as if it were an IO object
27
+ # by the reactor. The reactor is expected to call `#to_io`
28
+ # on any non-IO objects it polls. For example, nio4r internally calls
29
+ # `IO::try_convert` (which may call `#to_io`) when a new socket is
30
+ # registered.
31
+ #
32
+ # Instances of this class are responsible for knowing if
33
+ # the header and body are fully buffered via the `try_to_finish` method.
34
+ # They can be used to "time out" a response via the `timeout_at` reader.
35
+ #
36
+ class Client # :nodoc:
37
+
38
+ # this tests all values but the last, which must be chunked
39
+ ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
40
+
41
+ # chunked body validation
42
+ CHUNK_SIZE_INVALID = /[^\h]/.freeze
43
+ CHUNK_VALID_ENDING = Const::LINE_END
44
+ CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
45
+
46
+ # The maximum number of bytes we'll buffer looking for a valid
47
+ # chunk header.
48
+ MAX_CHUNK_HEADER_SIZE = 4096
49
+
50
+ # The maximum amount of excess data the client sends
51
+ # using chunk size extensions before we abort the connection.
52
+ MAX_CHUNK_EXCESS = 16 * 1024
53
+
54
+ # Content-Length header value validation
55
+ CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
56
+
57
+ TE_ERR_MSG = 'Invalid Transfer-Encoding'
58
+
59
+ # See:
60
+ # https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.1.1
61
+ # https://httpwg.org/specs/rfc9112.html#rfc.section.6.1
62
+ STRIP_OWS = /\A[ \t]+|[ \t]+\z/
63
+
64
+ # The object used for a request with no body. All requests with
65
+ # no body share this one object since it has no state.
66
+ EmptyBody = NullIO.new
67
+
68
+ include Puma::Const
69
+
70
+ def initialize(io, env=nil)
71
+ @io = io
72
+ @to_io = io.to_io
73
+ @io_buffer = IOBuffer.new
74
+ @proto_env = env
75
+ @env = env&.dup
76
+
77
+ @parser = HttpParser.new
78
+ @parsed_bytes = 0
79
+ @read_header = true
80
+ @read_proxy = false
81
+ @ready = false
82
+
83
+ @body = nil
84
+ @body_read_start = nil
85
+ @buffer = nil
86
+ @tempfile = nil
87
+
88
+ @timeout_at = nil
89
+
90
+ @requests_served = 0
91
+ @hijacked = false
92
+
93
+ @http_content_length_limit = nil
94
+ @http_content_length_limit_exceeded = false
95
+
96
+ @peerip = nil
97
+ @peer_family = nil
98
+ @listener = nil
99
+ @remote_addr_header = nil
100
+ @expect_proxy_proto = false
101
+
102
+ @body_remain = 0
103
+
104
+ @in_last_chunk = false
105
+
106
+ # need unfrozen ASCII-8BIT, +'' is UTF-8
107
+ @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
108
+ end
109
+
110
+ attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
111
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded,
112
+ :requests_served
113
+
114
+ attr_writer :peerip, :http_content_length_limit
115
+
116
+ attr_accessor :remote_addr_header, :listener
117
+
118
+ # Remove in Puma 7?
119
+ def closed?
120
+ @to_io.closed?
121
+ end
122
+
123
+ # Test to see if io meets a bare minimum of functioning, @to_io needs to be
124
+ # used for MiniSSL::Socket
125
+ def io_ok?
126
+ @to_io.is_a?(::BasicSocket) && !closed?
127
+ end
128
+
129
+ # @!attribute [r] inspect
130
+ def inspect
131
+ "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
132
+ end
133
+
134
+ # For the full hijack protocol, `env['rack.hijack']` is set to
135
+ # `client.method :full_hijack`
136
+ def full_hijack
137
+ @hijacked = true
138
+ env[HIJACK_IO] ||= @io
139
+ end
140
+
141
+ # @!attribute [r] in_data_phase
142
+ def in_data_phase
143
+ !(@read_header || @read_proxy)
144
+ end
145
+
146
+ def set_timeout(val)
147
+ @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
148
+ end
149
+
150
+ # Number of seconds until the timeout elapses.
151
+ # @!attribute [r] timeout
152
+ def timeout
153
+ [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
154
+ end
155
+
156
+ def reset
157
+ @parser.reset
158
+ @io_buffer.reset
159
+ @read_header = true
160
+ @read_proxy = !!@expect_proxy_proto
161
+ @env = @proto_env.dup
162
+ @parsed_bytes = 0
163
+ @ready = false
164
+ @body_remain = 0
165
+ @peerip = nil if @remote_addr_header
166
+ @in_last_chunk = false
167
+ @http_content_length_limit_exceeded = false
168
+ end
169
+
170
+ # only used with back-to-back requests contained in the buffer
171
+ def process_back_to_back_requests
172
+ if @buffer
173
+ return false unless try_to_parse_proxy_protocol
174
+
175
+ @parsed_bytes = parser_execute
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
+ end
184
+ end
185
+
186
+ # if a client sends back-to-back requests, the buffer may contain one or more
187
+ # of them.
188
+ def has_back_to_back_requests?
189
+ !(@buffer.nil? || @buffer.empty?)
190
+ end
191
+
192
+ def close
193
+ tempfile_close
194
+ begin
195
+ @io.close
196
+ rescue IOError, Errno::EBADF
197
+ end
198
+ end
199
+
200
+ def tempfile_close
201
+ tf_path = @tempfile&.path
202
+ @tempfile&.close
203
+ File.unlink(tf_path) if tf_path
204
+ @tempfile = nil
205
+ @body = nil
206
+ rescue Errno::ENOENT, IOError
207
+ end
208
+
209
+ # If necessary, read the PROXY protocol from the buffer. Returns
210
+ # false if more data is needed.
211
+ def try_to_parse_proxy_protocol
212
+ if @read_proxy
213
+ if @expect_proxy_proto == :v1
214
+ if @buffer.include? "\r\n"
215
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
216
+ if md[1]
217
+ @peerip = md[1].split(" ")[0]
218
+ end
219
+ @buffer = md.post_match
220
+ end
221
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
222
+ # request, this is just HTTP from a non-PROXY client; move on
223
+ @read_proxy = false
224
+ return @buffer.size > 0
225
+ else
226
+ return false
227
+ end
228
+ end
229
+ end
230
+ true
231
+ end
232
+
233
+ def try_to_finish
234
+ if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
235
+ @http_content_length_limit_exceeded = true
236
+ end
237
+
238
+ if @http_content_length_limit_exceeded
239
+ @buffer = nil
240
+ @body = EmptyBody
241
+ set_ready
242
+ return true
243
+ end
244
+
245
+ return read_body if in_data_phase
246
+
247
+ data = nil
248
+ begin
249
+ data = @io.read_nonblock(CHUNK_SIZE)
250
+ rescue IO::WaitReadable
251
+ return false
252
+ rescue EOFError
253
+ # Swallow error, don't log
254
+ rescue SystemCallError, IOError
255
+ raise ConnectionError, "Connection error detected during read"
256
+ end
257
+
258
+ # No data means a closed socket
259
+ unless data
260
+ @buffer = nil
261
+ set_ready
262
+ raise EOFError
263
+ end
264
+
265
+ if @buffer
266
+ @buffer << data
267
+ else
268
+ @buffer = data
269
+ end
270
+
271
+ return false unless try_to_parse_proxy_protocol
272
+
273
+ @parsed_bytes = parser_execute
274
+
275
+ if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
276
+ @http_content_length_limit_exceeded = true
277
+ end
278
+
279
+ if @parser.finished?
280
+ setup_body
281
+ elsif @parsed_bytes >= MAX_HEADER
282
+ raise HttpParserError,
283
+ "HEADER is longer than allowed, aborting client early."
284
+ else
285
+ false
286
+ end
287
+ end
288
+
289
+ def eagerly_finish
290
+ return true if @ready
291
+ while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
292
+ return true if try_to_finish
293
+ end
294
+ false
295
+ end
296
+
297
+ def finish(timeout)
298
+ return if @ready
299
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
300
+ end
301
+
302
+ # Wraps `@parser.execute` and adds meaningful error messages
303
+ # @return [Integer] bytes of buffer read by parser
304
+ #
305
+ def parser_execute
306
+ @parser.execute(@env, @buffer, @parsed_bytes)
307
+ rescue => e
308
+ @env[HTTP_CONNECTION] = 'close'
309
+ raise e unless HttpParserError === e && e.message.include?('non-SSL')
310
+
311
+ req, _ = @buffer.split "\r\n\r\n"
312
+ request_line, headers = req.split "\r\n", 2
313
+
314
+ # below checks for request issues and changes error message accordingly
315
+ if !@env.key? REQUEST_METHOD
316
+ if request_line.count(' ') != 2
317
+ # maybe this is an SSL connection ?
318
+ raise e
319
+ else
320
+ method = request_line[/\A[^ ]+/]
321
+ raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
322
+ end
323
+ elsif !@env.key? REQUEST_PATH
324
+ path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
325
+ raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
326
+ elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
327
+ query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
328
+ raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
329
+ elsif !@env.key? SERVER_PROTOCOL
330
+ # protocol is bad
331
+ text = request_line[/[^ ]*\z/]
332
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
333
+ elsif !headers.empty?
334
+ # headers are bad
335
+ hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
336
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
337
+ end
338
+ end
339
+
340
+ def timeout!
341
+ write_error(408) if in_data_phase
342
+ raise ConnectionError
343
+ end
344
+
345
+ def write_error(status_code)
346
+ begin
347
+ @io << ERROR_RESPONSE[status_code]
348
+ rescue StandardError
349
+ end
350
+ end
351
+
352
+ def peerip
353
+ return @peerip if @peerip
354
+
355
+ if @remote_addr_header
356
+ hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
357
+ @peerip = hdr
358
+ return hdr
359
+ end
360
+
361
+ @peerip ||= @io.peeraddr.last
362
+ end
363
+
364
+ def peer_family
365
+ return @peer_family if @peer_family
366
+
367
+ @peer_family ||= begin
368
+ @io.local_address.afamily
369
+ rescue
370
+ Socket::AF_INET
371
+ end
372
+ end
373
+
374
+ # Returns true if the persistent connection can be closed immediately
375
+ # without waiting for the configured idle/shutdown timeout.
376
+ # @version 5.0.0
377
+ #
378
+ def can_close?
379
+ # Allow connection to close if we're not in the middle of parsing a request.
380
+ @parsed_bytes == 0
381
+ end
382
+
383
+ def expect_proxy_proto=(val)
384
+ if val
385
+ if @read_header
386
+ @read_proxy = true
387
+ end
388
+ else
389
+ @read_proxy = false
390
+ end
391
+ @expect_proxy_proto = val
392
+ end
393
+
394
+ private
395
+
396
+ def setup_body
397
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
398
+
399
+ if @env[HTTP_EXPECT] == CONTINUE
400
+ # TODO allow a hook here to check the headers before
401
+ # going forward
402
+ @io << HTTP_11_100
403
+ @io.flush
404
+ end
405
+
406
+ @read_header = false
407
+
408
+ body = @parser.body
409
+
410
+ te = @env[TRANSFER_ENCODING2]
411
+ if te
412
+ te_lwr = te.downcase
413
+ if te.include? ','
414
+ te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
415
+ te_count = te_ary.count CHUNKED
416
+ te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
417
+ if te_count > 1
418
+ raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
419
+ elsif te_ary.last != CHUNKED
420
+ raise HttpParserError , "#{TE_ERR_MSG}, last value must be chunked: '#{te}'"
421
+ elsif !te_valid
422
+ raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
423
+ end
424
+ @env.delete TRANSFER_ENCODING2
425
+ return setup_chunked_body body
426
+ elsif te_lwr == CHUNKED
427
+ @env.delete TRANSFER_ENCODING2
428
+ return setup_chunked_body body
429
+ elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
430
+ raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
431
+ else
432
+ raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
433
+ end
434
+ end
435
+
436
+ @chunked_body = false
437
+
438
+ cl = @env[CONTENT_LENGTH]
439
+
440
+ if cl
441
+ # cannot contain characters that are not \d, or be empty
442
+ if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
443
+ raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
444
+ end
445
+ else
446
+ @buffer = body.empty? ? nil : body
447
+ @body = EmptyBody
448
+ set_ready
449
+ return true
450
+ end
451
+
452
+ content_length = cl.to_i
453
+
454
+ remain = content_length - body.bytesize
455
+
456
+ if remain <= 0
457
+ # Part of the body is a pipelined request OR garbage. We'll deal with that later.
458
+ if content_length == 0
459
+ @body = EmptyBody
460
+ if body.empty?
461
+ @buffer = nil
462
+ else
463
+ @buffer = body
464
+ end
465
+ elsif remain == 0
466
+ @body = StringIO.new body
467
+ @buffer = nil
468
+ else
469
+ @body = StringIO.new(body[0,content_length])
470
+ @buffer = body[content_length..-1]
471
+ end
472
+ set_ready
473
+ return true
474
+ end
475
+
476
+ if remain > MAX_BODY
477
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
478
+ File.unlink @body.path unless IS_WINDOWS
479
+ @body.binmode
480
+ @tempfile = @body
481
+ else
482
+ # The body[0,0] trick is to get an empty string in the same
483
+ # encoding as body.
484
+ @body = StringIO.new body[0,0]
485
+ end
486
+
487
+ @body.write body
488
+
489
+ @body_remain = remain
490
+
491
+ false
492
+ end
493
+
494
+ def read_body
495
+ if @chunked_body
496
+ return read_chunked_body
497
+ end
498
+
499
+ # Read an odd sized chunk so we can read even sized ones
500
+ # after this
501
+ remain = @body_remain
502
+
503
+ if remain > CHUNK_SIZE
504
+ want = CHUNK_SIZE
505
+ else
506
+ want = remain
507
+ end
508
+
509
+ begin
510
+ chunk = @io.read_nonblock(want, @read_buffer)
511
+ rescue IO::WaitReadable
512
+ return false
513
+ rescue SystemCallError, IOError
514
+ raise ConnectionError, "Connection error detected during read"
515
+ end
516
+
517
+ # No chunk means a closed socket
518
+ unless chunk
519
+ @body.close
520
+ @buffer = nil
521
+ set_ready
522
+ raise EOFError
523
+ end
524
+
525
+ remain -= @body.write(chunk)
526
+
527
+ if remain <= 0
528
+ @body.rewind
529
+ @buffer = nil
530
+ set_ready
531
+ return true
532
+ end
533
+
534
+ @body_remain = remain
535
+
536
+ false
537
+ end
538
+
539
+ def read_chunked_body
540
+ while true
541
+ begin
542
+ chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
543
+ rescue IO::WaitReadable
544
+ return false
545
+ rescue SystemCallError, IOError
546
+ raise ConnectionError, "Connection error detected during read"
547
+ end
548
+
549
+ # No chunk means a closed socket
550
+ unless chunk
551
+ @body.close
552
+ @buffer = nil
553
+ set_ready
554
+ raise EOFError
555
+ end
556
+
557
+ if decode_chunk(chunk)
558
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
559
+ return true
560
+ end
561
+ end
562
+ end
563
+
564
+ def setup_chunked_body(body)
565
+ @chunked_body = true
566
+ @partial_part_left = 0
567
+ @prev_chunk = ""
568
+ @excess_cr = 0
569
+
570
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
571
+ File.unlink @body.path unless IS_WINDOWS
572
+ @body.binmode
573
+ @tempfile = @body
574
+ @chunked_content_length = 0
575
+
576
+ if decode_chunk(body)
577
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
578
+ return true
579
+ end
580
+ end
581
+
582
+ # @version 5.0.0
583
+ def write_chunk(str)
584
+ @chunked_content_length += @body.write(str)
585
+ end
586
+
587
+ def decode_chunk(chunk)
588
+ if @partial_part_left > 0
589
+ if @partial_part_left <= chunk.size
590
+ if @partial_part_left > 2
591
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
592
+ end
593
+ chunk = chunk[@partial_part_left..-1]
594
+ @partial_part_left = 0
595
+ else
596
+ if @partial_part_left > 2
597
+ if @partial_part_left == chunk.size + 1
598
+ # Don't include the last \r
599
+ write_chunk(chunk[0..(@partial_part_left-3)])
600
+ else
601
+ # don't include the last \r\n
602
+ write_chunk(chunk)
603
+ end
604
+ end
605
+ @partial_part_left -= chunk.size
606
+ return false
607
+ end
608
+ end
609
+
610
+ if @prev_chunk.empty?
611
+ io = StringIO.new(chunk)
612
+ else
613
+ io = StringIO.new(@prev_chunk+chunk)
614
+ @prev_chunk = ""
615
+ end
616
+
617
+ while !io.eof?
618
+ line = io.gets
619
+ if line.end_with?(CHUNK_VALID_ENDING)
620
+ # Puma doesn't process chunk extensions, but should parse if they're
621
+ # present, which is the reason for the semicolon regex
622
+ chunk_hex = line.strip[/\A[^;]+/]
623
+ if CHUNK_SIZE_INVALID.match? chunk_hex
624
+ raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
625
+ end
626
+ len = chunk_hex.to_i(16)
627
+ if len == 0
628
+ @in_last_chunk = true
629
+ @body.rewind
630
+ rest = io.read
631
+ if rest.bytesize < CHUNK_VALID_ENDING_SIZE
632
+ @buffer = nil
633
+ @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
634
+ return false
635
+ else
636
+ # if the next character is a CRLF, set buffer to everything after that CRLF
637
+ start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
638
+ CHUNK_VALID_ENDING_SIZE
639
+ else # we have started a trailer section, which we do not support. skip it!
640
+ rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
641
+ end
642
+
643
+ @buffer = rest[start_of_rest..-1]
644
+ @buffer = nil if @buffer.empty?
645
+ set_ready
646
+ return true
647
+ end
648
+ end
649
+
650
+ # Track the excess as a function of the size of the
651
+ # header vs the size of the actual data. Excess can
652
+ # go negative (and is expected to) when the body is
653
+ # significant.
654
+ # The additional of chunk_hex.size and 2 compensates
655
+ # for a client sending 1 byte in a chunked body over
656
+ # a long period of time, making sure that that client
657
+ # isn't accidentally eventually punished.
658
+ @excess_cr += (line.size - len - chunk_hex.size - 2)
659
+
660
+ if @excess_cr >= MAX_CHUNK_EXCESS
661
+ raise HttpParserError, "Maximum chunk excess detected"
662
+ end
663
+
664
+ len += 2
665
+
666
+ part = io.read(len)
667
+
668
+ unless part
669
+ @partial_part_left = len
670
+ next
671
+ end
672
+
673
+ got = part.size
674
+
675
+ case
676
+ when got == len
677
+ # proper chunked segment must end with "\r\n"
678
+ if part.end_with? CHUNK_VALID_ENDING
679
+ write_chunk(part[0..-3]) # to skip the ending \r\n
680
+ else
681
+ raise HttpParserError, "Chunk size mismatch"
682
+ end
683
+ when got <= len - 2
684
+ write_chunk(part)
685
+ @partial_part_left = len - part.size
686
+ when got == len - 1 # edge where we get just \r but not \n
687
+ write_chunk(part[0..-2])
688
+ @partial_part_left = len - part.size
689
+ end
690
+ else
691
+ if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
692
+ raise HttpParserError, "maximum size of chunk header exceeded"
693
+ end
694
+
695
+ @prev_chunk = line
696
+ return false
697
+ end
698
+ end
699
+
700
+ if @in_last_chunk
701
+ set_ready
702
+ true
703
+ else
704
+ false
705
+ end
706
+ end
707
+
708
+ def set_ready
709
+ if @body_read_start
710
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
711
+ end
712
+ @requests_served += 1
713
+ @ready = true
714
+ end
715
+
716
+ def above_http_content_limit(value)
717
+ @http_content_length_limit&.< value
718
+ end
719
+ end
720
+ end