tiny-fast-gem 0.0.1

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 (99) hide show
  1. checksums.yaml +7 -0
  2. data/puma-8.0.2/History.md +3334 -0
  3. data/puma-8.0.2/LICENSE +29 -0
  4. data/puma-8.0.2/README.md +484 -0
  5. data/puma-8.0.2/bin/puma +10 -0
  6. data/puma-8.0.2/bin/puma-wild +25 -0
  7. data/puma-8.0.2/bin/pumactl +12 -0
  8. data/puma-8.0.2/docs/5.0-Upgrade.md +98 -0
  9. data/puma-8.0.2/docs/6.0-Upgrade.md +56 -0
  10. data/puma-8.0.2/docs/7.0-Upgrade.md +52 -0
  11. data/puma-8.0.2/docs/8.0-Upgrade.md +100 -0
  12. data/puma-8.0.2/docs/architecture.md +74 -0
  13. data/puma-8.0.2/docs/compile_options.md +55 -0
  14. data/puma-8.0.2/docs/deployment.md +137 -0
  15. data/puma-8.0.2/docs/fork_worker.md +41 -0
  16. data/puma-8.0.2/docs/grpc.md +62 -0
  17. data/puma-8.0.2/docs/images/favicon.svg +1 -0
  18. data/puma-8.0.2/docs/images/puma-connection-flow-no-reactor.png +0 -0
  19. data/puma-8.0.2/docs/images/puma-connection-flow.png +0 -0
  20. data/puma-8.0.2/docs/images/puma-general-arch.png +0 -0
  21. data/puma-8.0.2/docs/images/running-puma.svg +1 -0
  22. data/puma-8.0.2/docs/images/standard-logo.svg +1 -0
  23. data/puma-8.0.2/docs/java_options.md +54 -0
  24. data/puma-8.0.2/docs/jungle/README.md +9 -0
  25. data/puma-8.0.2/docs/jungle/rc.d/README.md +74 -0
  26. data/puma-8.0.2/docs/jungle/rc.d/puma +61 -0
  27. data/puma-8.0.2/docs/jungle/rc.d/puma.conf +10 -0
  28. data/puma-8.0.2/docs/kubernetes.md +73 -0
  29. data/puma-8.0.2/docs/nginx.md +80 -0
  30. data/puma-8.0.2/docs/plugins.md +42 -0
  31. data/puma-8.0.2/docs/rails_dev_mode.md +28 -0
  32. data/puma-8.0.2/docs/restart.md +65 -0
  33. data/puma-8.0.2/docs/signals.md +98 -0
  34. data/puma-8.0.2/docs/stats.md +148 -0
  35. data/puma-8.0.2/docs/systemd.md +253 -0
  36. data/puma-8.0.2/docs/testing_benchmarks_local_files.md +150 -0
  37. data/puma-8.0.2/docs/testing_test_rackup_ci_files.md +36 -0
  38. data/puma-8.0.2/ext/puma_http11/PumaHttp11Service.java +17 -0
  39. data/puma-8.0.2/ext/puma_http11/extconf.rb +65 -0
  40. data/puma-8.0.2/ext/puma_http11/http11_parser.c +1057 -0
  41. data/puma-8.0.2/ext/puma_http11/http11_parser.h +65 -0
  42. data/puma-8.0.2/ext/puma_http11/http11_parser.java.rl +131 -0
  43. data/puma-8.0.2/ext/puma_http11/http11_parser.rl +149 -0
  44. data/puma-8.0.2/ext/puma_http11/http11_parser_common.rl +54 -0
  45. data/puma-8.0.2/ext/puma_http11/mini_ssl.c +852 -0
  46. data/puma-8.0.2/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  47. data/puma-8.0.2/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  48. data/puma-8.0.2/ext/puma_http11/org/jruby/puma/Http11.java +321 -0
  49. data/puma-8.0.2/ext/puma_http11/org/jruby/puma/Http11Parser.java +441 -0
  50. data/puma-8.0.2/ext/puma_http11/org/jruby/puma/MiniSSL.java +509 -0
  51. data/puma-8.0.2/ext/puma_http11/puma_http11.c +499 -0
  52. data/puma-8.0.2/lib/puma/app/status.rb +104 -0
  53. data/puma-8.0.2/lib/puma/binder.rb +511 -0
  54. data/puma-8.0.2/lib/puma/cli.rb +245 -0
  55. data/puma-8.0.2/lib/puma/client.rb +756 -0
  56. data/puma-8.0.2/lib/puma/client_env.rb +171 -0
  57. data/puma-8.0.2/lib/puma/cluster/worker.rb +183 -0
  58. data/puma-8.0.2/lib/puma/cluster/worker_handle.rb +127 -0
  59. data/puma-8.0.2/lib/puma/cluster.rb +634 -0
  60. data/puma-8.0.2/lib/puma/cluster_accept_loop_delay.rb +91 -0
  61. data/puma-8.0.2/lib/puma/commonlogger.rb +115 -0
  62. data/puma-8.0.2/lib/puma/configuration.rb +522 -0
  63. data/puma-8.0.2/lib/puma/const.rb +308 -0
  64. data/puma-8.0.2/lib/puma/control_cli.rb +320 -0
  65. data/puma-8.0.2/lib/puma/detect.rb +58 -0
  66. data/puma-8.0.2/lib/puma/dsl.rb +1562 -0
  67. data/puma-8.0.2/lib/puma/error_logger.rb +115 -0
  68. data/puma-8.0.2/lib/puma/events.rb +72 -0
  69. data/puma-8.0.2/lib/puma/io_buffer.rb +50 -0
  70. data/puma-8.0.2/lib/puma/jruby_restart.rb +11 -0
  71. data/puma-8.0.2/lib/puma/json_serialization.rb +96 -0
  72. data/puma-8.0.2/lib/puma/launcher/bundle_pruner.rb +102 -0
  73. data/puma-8.0.2/lib/puma/launcher.rb +501 -0
  74. data/puma-8.0.2/lib/puma/log_writer.rb +153 -0
  75. data/puma-8.0.2/lib/puma/minissl/context_builder.rb +96 -0
  76. data/puma-8.0.2/lib/puma/minissl.rb +458 -0
  77. data/puma-8.0.2/lib/puma/null_io.rb +101 -0
  78. data/puma-8.0.2/lib/puma/plugin/systemd.rb +90 -0
  79. data/puma-8.0.2/lib/puma/plugin/tmp_restart.rb +36 -0
  80. data/puma-8.0.2/lib/puma/plugin.rb +111 -0
  81. data/puma-8.0.2/lib/puma/rack/builder.rb +297 -0
  82. data/puma-8.0.2/lib/puma/rack/urlmap.rb +93 -0
  83. data/puma-8.0.2/lib/puma/rack_default.rb +24 -0
  84. data/puma-8.0.2/lib/puma/reactor.rb +131 -0
  85. data/puma-8.0.2/lib/puma/response.rb +532 -0
  86. data/puma-8.0.2/lib/puma/runner.rb +211 -0
  87. data/puma-8.0.2/lib/puma/sd_notify.rb +146 -0
  88. data/puma-8.0.2/lib/puma/server.rb +773 -0
  89. data/puma-8.0.2/lib/puma/server_plugin_control.rb +32 -0
  90. data/puma-8.0.2/lib/puma/single.rb +72 -0
  91. data/puma-8.0.2/lib/puma/state_file.rb +69 -0
  92. data/puma-8.0.2/lib/puma/thread_pool.rb +517 -0
  93. data/puma-8.0.2/lib/puma/util.rb +134 -0
  94. data/puma-8.0.2/lib/puma.rb +88 -0
  95. data/puma-8.0.2/lib/rack/handler/puma.rb +144 -0
  96. data/puma-8.0.2/tools/Dockerfile +26 -0
  97. data/puma-8.0.2/tools/trickletest.rb +44 -0
  98. data/tiny-fast-gem.gemspec +12 -0
  99. metadata +138 -0
@@ -0,0 +1,756 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'detect'
4
+ require_relative 'io_buffer'
5
+ require_relative 'client_env'
6
+ require 'tempfile'
7
+
8
+ if Puma::IS_JRUBY
9
+ # We have to work around some OpenSSL buffer/io-readiness bugs
10
+ # so we pull it in regardless of if the user is binding
11
+ # to an SSL socket
12
+ require 'openssl'
13
+ end
14
+
15
+ module Puma
16
+
17
+ class ConnectionError < RuntimeError; end
18
+
19
+ class HttpParserError501 < IOError; end
20
+
21
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
22
+
23
+
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.
26
+ #
27
+ # An instance of `Puma::Client` can be used as if it were an IO object
28
+ # by the reactor. The reactor is expected to call `#to_io`
29
+ # on any non-IO objects it polls. For example, nio4r internally calls
30
+ # `IO::try_convert` (which may call `#to_io`) when a new socket is
31
+ # registered.
32
+ #
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.
36
+ # They can be used to "time out" a response via the `timeout_at` reader.
37
+ #
38
+ # Most of the code for env processing and verification is contained
39
+ # in `Puma::ClientEnv`, which is included.
40
+ #
41
+ class Client # :nodoc:
42
+
43
+ include ClientEnv
44
+
45
+ # this tests all values but the last, which must be chunked
46
+ ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
47
+
48
+ # chunked body validation
49
+ CHUNK_SIZE_INVALID = /[^\h]/.freeze
50
+ CHUNK_VALID_ENDING = Const::LINE_END
51
+ CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
52
+
53
+ # The maximum number of bytes we'll buffer looking for a valid
54
+ # chunk header.
55
+ MAX_CHUNK_HEADER_SIZE = 4096
56
+
57
+ # The maximum amount of excess data the client sends
58
+ # using chunk size extensions before we abort the connection.
59
+ MAX_CHUNK_EXCESS = 16 * 1024
60
+
61
+ # Content-Length header value validation
62
+ CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
63
+
64
+ TE_ERR_MSG = 'Invalid Transfer-Encoding'
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
+
71
+ # The object used for a request with no body. All requests with
72
+ # no body share this one object since it has no state.
73
+ EmptyBody = NullIO.new
74
+
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
82
+
83
+ def initialize(io, env=nil)
84
+ @io = io
85
+ @to_io = io.to_io
86
+ @io_buffer = IOBuffer.new
87
+ @proto_env = env
88
+ @env = env&.dup
89
+
90
+ @parser = HttpParser.new
91
+ @parsed_bytes = 0
92
+ @read_header = true
93
+ @read_proxy = false
94
+ @ready = false
95
+
96
+ @body = nil
97
+ @body_read_start = nil
98
+ @buffer = nil
99
+ @tempfile = nil
100
+
101
+ @timeout_at = nil
102
+
103
+ @requests_served = 0
104
+ @hijacked = false
105
+
106
+ @http_content_length_limit = nil
107
+ @http_content_length_limit_exceeded = nil
108
+ @error_status_code = nil
109
+
110
+ @peerip = nil
111
+ @peer_family = nil
112
+ @listener = nil
113
+ @remote_addr_header = nil
114
+ @expect_proxy_proto = false
115
+
116
+ @body_remain = 0
117
+
118
+ @in_last_chunk = false
119
+
120
+ # need unfrozen ASCII-8BIT, +'' is UTF-8
121
+ @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
122
+ end
123
+
124
+ # Remove in Puma 7?
125
+ def closed?
126
+ @to_io.closed?
127
+ end
128
+
129
+ # Test to see if io meets a bare minimum of functioning, @to_io needs to be
130
+ # used for MiniSSL::Socket
131
+ def io_ok?
132
+ @to_io.is_a?(::BasicSocket) && !closed?
133
+ end
134
+
135
+ # @!attribute [r] inspect
136
+ def inspect
137
+ "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
138
+ end
139
+
140
+ # For the full hijack protocol, `env['rack.hijack']` is set to
141
+ # `client.method :full_hijack`
142
+ def full_hijack
143
+ @hijacked = true
144
+ env[HIJACK_IO] ||= @io
145
+ end
146
+
147
+ # @!attribute [r] in_data_phase
148
+ def in_data_phase
149
+ !(@read_header || @read_proxy)
150
+ end
151
+
152
+ def set_timeout(val)
153
+ @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
154
+ end
155
+
156
+ # Number of seconds until the timeout elapses.
157
+ # @!attribute [r] timeout
158
+ def timeout
159
+ [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
160
+ end
161
+
162
+ def reset
163
+ @parser.reset
164
+ @io_buffer.reset
165
+ @read_header = true
166
+ @read_proxy = !!@expect_proxy_proto && @requests_served.zero?
167
+ @env = @proto_env.dup
168
+ @parsed_bytes = 0
169
+ @ready = false
170
+ @body_remain = 0
171
+ @peerip = nil if @remote_addr_header
172
+ @in_last_chunk = false
173
+ @http_content_length_limit_exceeded = nil
174
+ @error_status_code = nil
175
+ end
176
+
177
+ # only used with back-to-back requests contained in the buffer
178
+ def process_back_to_back_requests
179
+ if @buffer
180
+ return false unless try_to_parse_proxy_protocol
181
+
182
+ @parsed_bytes = parser_execute
183
+
184
+ @parser.finished? ? process_env_body : false
185
+ end
186
+ end
187
+
188
+ # if a client sends back-to-back requests, the buffer may contain one or more
189
+ # of them.
190
+ def has_back_to_back_requests?
191
+ !(@buffer.nil? || @buffer.empty?)
192
+ end
193
+
194
+ def close
195
+ tempfile_close
196
+ begin
197
+ @io.close
198
+ rescue IOError, Errno::EBADF
199
+ end
200
+ end
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
+
211
+ # If necessary, read the PROXY protocol from the buffer. Returns
212
+ # false if more data is needed.
213
+ def try_to_parse_proxy_protocol
214
+ if @read_proxy
215
+ if @expect_proxy_proto == :v1
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"
224
+ end
225
+ return false
226
+ end
227
+
228
+ @read_proxy = 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"
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
246
+ end
247
+ end
248
+ true
249
+ end
250
+
251
+ def try_to_finish
252
+ return read_body if in_data_phase
253
+
254
+ data = nil
255
+ begin
256
+ data = @io.read_nonblock(CHUNK_SIZE)
257
+ rescue IO::WaitReadable
258
+ return false
259
+ rescue EOFError
260
+ # Swallow error, don't log
261
+ rescue SystemCallError, IOError
262
+ raise ConnectionError, "Connection error detected during read"
263
+ end
264
+
265
+ # No data means a closed socket
266
+ unless data
267
+ @buffer = nil
268
+ set_ready
269
+ raise EOFError
270
+ end
271
+
272
+ if @buffer
273
+ @buffer << data
274
+ else
275
+ @buffer = data
276
+ end
277
+
278
+ return false unless try_to_parse_proxy_protocol
279
+
280
+ @parsed_bytes = parser_execute
281
+
282
+ @parser.finished? ? process_env_body : false
283
+ end
284
+
285
+ def eagerly_finish
286
+ return true if @ready
287
+ while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
288
+ return true if try_to_finish
289
+ end
290
+ false
291
+ end
292
+
293
+ def finish(timeout)
294
+ return if @ready
295
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
296
+ end
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
+
350
+ def timeout!
351
+ write_error(408) if in_data_phase
352
+ raise ConnectionError
353
+ end
354
+
355
+ def write_error(status_code)
356
+ begin
357
+ @io << ERROR_RESPONSE[status_code]
358
+ rescue StandardError
359
+ end
360
+ end
361
+
362
+ def peerip
363
+ return @peerip if @peerip
364
+
365
+ if @remote_addr_header
366
+ hdr = (@env[@remote_addr_header] || socket_peerip).split(/[\s,]/).first
367
+ @peerip = hdr
368
+ return hdr
369
+ end
370
+
371
+ @peerip ||= socket_peerip
372
+ end
373
+
374
+ def peer_family
375
+ return @peer_family if @peer_family
376
+
377
+ @peer_family ||= begin
378
+ @io.local_address.afamily
379
+ rescue
380
+ Socket::AF_INET
381
+ end
382
+ end
383
+
384
+ # Returns true if the persistent connection can be closed immediately
385
+ # without waiting for the configured idle/shutdown timeout.
386
+ # @version 5.0.0
387
+ #
388
+ def can_close?
389
+ # Allow connection to close if we're not in the middle of parsing a request.
390
+ @parsed_bytes == 0
391
+ end
392
+
393
+ def expect_proxy_proto=(val)
394
+ if val
395
+ if @read_header
396
+ @read_proxy = true
397
+ end
398
+ else
399
+ @read_proxy = false
400
+ end
401
+ @expect_proxy_proto = val
402
+ end
403
+
404
+ private
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
+ #
424
+ def setup_body
425
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
426
+
427
+ if @env[HTTP_EXPECT] == CONTINUE
428
+ # TODO allow a hook here to check the headers before going forward
429
+ @io << HTTP_11_100
430
+ @io.flush
431
+ end
432
+
433
+ @read_header = false
434
+
435
+ parser_body = @parser.body
436
+
437
+ te = @env[TRANSFER_ENCODING2]
438
+ if te
439
+ te_lwr = te.downcase
440
+ if te.include? ','
441
+ te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
442
+ te_count = te_ary.count CHUNKED
443
+ te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
444
+ if te_count > 1
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}'"
448
+ elsif !te_valid
449
+ raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
450
+ end
451
+ @env.delete TRANSFER_ENCODING2
452
+ return setup_chunked_body parser_body
453
+ elsif te_lwr == CHUNKED
454
+ @env.delete TRANSFER_ENCODING2
455
+ return setup_chunked_body parser_body
456
+ elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
457
+ raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
458
+ else
459
+ raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
460
+ end
461
+ end
462
+
463
+ @chunked_body = false
464
+
465
+ cl = @env[CONTENT_LENGTH]
466
+
467
+ if cl
468
+ # cannot contain characters that are not \d, or be empty
469
+ if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
470
+ @error_status_code = 400
471
+ @env[HTTP_CONNECTION] = 'close'
472
+ raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
473
+ end
474
+ else
475
+ @buffer = parser_body.empty? ? nil : parser_body
476
+ @body = EmptyBody
477
+ set_ready
478
+ return true
479
+ end
480
+
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
486
+
487
+ if remain <= 0
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
503
+ set_ready
504
+ return true
505
+ end
506
+
507
+ if remain > MAX_BODY
508
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
509
+ File.unlink @body.path unless IS_WINDOWS
510
+ @body.binmode
511
+ @tempfile = @body
512
+ else
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]
516
+ end
517
+
518
+ @body.write parser_body
519
+
520
+ @body_remain = remain
521
+
522
+ false
523
+ end
524
+
525
+ def read_body
526
+ if @chunked_body
527
+ return read_chunked_body
528
+ end
529
+
530
+ # Read an odd sized chunk so we can read even sized ones
531
+ # after this
532
+ remain = @body_remain
533
+
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
543
+
544
+ # No chunk means a closed socket
545
+ unless chunk
546
+ @body.close
547
+ @buffer = nil
548
+ set_ready
549
+ raise EOFError
550
+ end
551
+
552
+ remain -= @body.write(chunk)
553
+ end
554
+
555
+ if remain <= 0
556
+ @body.rewind
557
+ @buffer = nil
558
+ set_ready
559
+ true
560
+ else
561
+ @body_remain = remain
562
+ false
563
+ end
564
+ end
565
+
566
+ def read_chunked_body
567
+ while true
568
+ begin
569
+ chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
570
+ rescue IO::WaitReadable
571
+ return false
572
+ rescue SystemCallError, IOError
573
+ raise ConnectionError, "Connection error detected during read"
574
+ end
575
+
576
+ # No chunk means a closed socket
577
+ unless chunk
578
+ @body.close
579
+ @buffer = nil
580
+ set_ready
581
+ raise EOFError
582
+ end
583
+
584
+ if decode_chunk(chunk)
585
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
586
+ return true
587
+ end
588
+ end
589
+ end
590
+
591
+ def setup_chunked_body(body)
592
+ @chunked_body = true
593
+ @partial_part_left = 0
594
+ @prev_chunk = ""
595
+ @excess_cr = 0
596
+
597
+ @body = Tempfile.create(Const::PUMA_TMP_BASE)
598
+ File.unlink @body.path unless IS_WINDOWS
599
+ @body.binmode
600
+ @tempfile = @body
601
+ @chunked_content_length = 0
602
+
603
+ if decode_chunk(body)
604
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
605
+ return true
606
+ end
607
+ end
608
+
609
+ # @version 5.0.0
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
+
615
+ @chunked_content_length += @body.write(str)
616
+ end
617
+
618
+ def decode_chunk(chunk)
619
+ if @partial_part_left > 0
620
+ if @partial_part_left <= chunk.size
621
+ if @partial_part_left > 2
622
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
623
+ end
624
+ chunk = chunk[@partial_part_left..-1]
625
+ @partial_part_left = 0
626
+ else
627
+ if @partial_part_left > 2
628
+ if @partial_part_left == chunk.size + 1
629
+ # Don't include the last \r
630
+ write_chunk(chunk[0..(@partial_part_left-3)])
631
+ else
632
+ # don't include the last \r\n
633
+ write_chunk(chunk)
634
+ end
635
+ end
636
+ @partial_part_left -= chunk.size
637
+ return false
638
+ end
639
+ end
640
+
641
+ if @prev_chunk.empty?
642
+ io = StringIO.new(chunk)
643
+ else
644
+ io = StringIO.new(@prev_chunk+chunk)
645
+ @prev_chunk = ""
646
+ end
647
+
648
+ while !io.eof?
649
+ line = io.gets
650
+ if line.end_with?(CHUNK_VALID_ENDING)
651
+ # Puma doesn't process chunk extensions, but should parse if they're
652
+ # present, which is the reason for the semicolon regex
653
+ chunk_hex = line.strip[/\A[^;]+/]
654
+ if CHUNK_SIZE_INVALID.match? chunk_hex
655
+ raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
656
+ end
657
+ len = chunk_hex.to_i(16)
658
+ if len == 0
659
+ @in_last_chunk = true
660
+ @body.rewind
661
+ rest = io.read
662
+ if rest.bytesize < CHUNK_VALID_ENDING_SIZE
663
+ @buffer = nil
664
+ @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
665
+ return false
666
+ else
667
+ # if the next character is a CRLF, set buffer to everything after that CRLF
668
+ start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
669
+ CHUNK_VALID_ENDING_SIZE
670
+ else # we have started a trailer section, which we do not support. skip it!
671
+ rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
672
+ end
673
+
674
+ @buffer = rest[start_of_rest..-1]
675
+ @buffer = nil if @buffer.empty?
676
+ set_ready
677
+ return true
678
+ end
679
+ end
680
+
681
+ # Track the excess as a function of the size of the
682
+ # header vs the size of the actual data. Excess can
683
+ # go negative (and is expected to) when the body is
684
+ # significant.
685
+ # The additional of chunk_hex.size and 2 compensates
686
+ # for a client sending 1 byte in a chunked body over
687
+ # a long period of time, making sure that that client
688
+ # isn't accidentally eventually punished.
689
+ @excess_cr += (line.size - len - chunk_hex.size - 2)
690
+
691
+ if @excess_cr >= MAX_CHUNK_EXCESS
692
+ raise HttpParserError, "Maximum chunk excess detected"
693
+ end
694
+
695
+ len += 2
696
+
697
+ part = io.read(len)
698
+
699
+ unless part
700
+ @partial_part_left = len
701
+ next
702
+ end
703
+
704
+ got = part.size
705
+
706
+ case
707
+ when got == len
708
+ # proper chunked segment must end with "\r\n"
709
+ if part.end_with? CHUNK_VALID_ENDING
710
+ write_chunk(part[0..-3]) # to skip the ending \r\n
711
+ else
712
+ raise HttpParserError, "Chunk size mismatch"
713
+ end
714
+ when got <= len - 2
715
+ write_chunk(part)
716
+ @partial_part_left = len - part.size
717
+ when got == len - 1 # edge where we get just \r but not \n
718
+ write_chunk(part[0..-2])
719
+ @partial_part_left = len - part.size
720
+ end
721
+ else
722
+ if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
723
+ raise HttpParserError, "maximum size of chunk header exceeded"
724
+ end
725
+
726
+ @prev_chunk = line
727
+ return false
728
+ end
729
+ end
730
+
731
+ if @in_last_chunk
732
+ set_ready
733
+ true
734
+ else
735
+ false
736
+ end
737
+ end
738
+
739
+ def set_ready
740
+ if @body_read_start
741
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
742
+ end
743
+ @requests_served += 1
744
+ @ready = true
745
+ end
746
+
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"
754
+ end
755
+ end
756
+ end