jun-puma 1.0.0-java

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