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