puma 3.12.1 → 5.6.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1553 -447
  3. data/LICENSE +23 -20
  4. data/README.md +175 -63
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +59 -21
  7. data/docs/compile_options.md +21 -0
  8. data/docs/deployment.md +69 -58
  9. data/docs/fork_worker.md +33 -0
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/jungle/README.md +9 -0
  14. data/{tools → docs}/jungle/rc.d/README.md +1 -1
  15. data/{tools → docs}/jungle/rc.d/puma +2 -2
  16. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  17. data/docs/kubernetes.md +66 -0
  18. data/docs/nginx.md +1 -1
  19. data/docs/plugins.md +22 -12
  20. data/docs/rails_dev_mode.md +28 -0
  21. data/docs/restart.md +47 -22
  22. data/docs/signals.md +13 -11
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +95 -120
  25. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  26. data/ext/puma_http11/ext_help.h +1 -1
  27. data/ext/puma_http11/extconf.rb +51 -1
  28. data/ext/puma_http11/http11_parser.c +105 -117
  29. data/ext/puma_http11/http11_parser.h +1 -1
  30. data/ext/puma_http11/http11_parser.java.rl +22 -38
  31. data/ext/puma_http11/http11_parser.rl +4 -2
  32. data/ext/puma_http11/http11_parser_common.rl +4 -4
  33. data/ext/puma_http11/mini_ssl.c +319 -96
  34. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  35. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  36. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +84 -99
  37. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +120 -65
  38. data/ext/puma_http11/puma_http11.c +35 -51
  39. data/lib/puma/app/status.rb +68 -49
  40. data/lib/puma/binder.rb +234 -137
  41. data/lib/puma/cli.rb +28 -18
  42. data/lib/puma/client.rb +343 -230
  43. data/lib/puma/cluster/worker.rb +173 -0
  44. data/lib/puma/cluster/worker_handle.rb +94 -0
  45. data/lib/puma/cluster.rb +247 -232
  46. data/lib/puma/commonlogger.rb +2 -2
  47. data/lib/puma/configuration.rb +61 -51
  48. data/lib/puma/const.rb +42 -21
  49. data/lib/puma/control_cli.rb +109 -67
  50. data/lib/puma/detect.rb +29 -2
  51. data/lib/puma/dsl.rb +615 -123
  52. data/lib/puma/error_logger.rb +104 -0
  53. data/lib/puma/events.rb +55 -31
  54. data/lib/puma/io_buffer.rb +7 -5
  55. data/lib/puma/jruby_restart.rb +0 -58
  56. data/lib/puma/json_serialization.rb +96 -0
  57. data/lib/puma/launcher.rb +182 -69
  58. data/lib/puma/minissl/context_builder.rb +81 -0
  59. data/lib/puma/minissl.rb +161 -61
  60. data/lib/puma/null_io.rb +13 -1
  61. data/lib/puma/plugin/tmp_restart.rb +2 -0
  62. data/lib/puma/plugin.rb +7 -13
  63. data/lib/puma/queue_close.rb +26 -0
  64. data/lib/puma/rack/builder.rb +3 -5
  65. data/lib/puma/rack/urlmap.rb +2 -0
  66. data/lib/puma/rack_default.rb +2 -0
  67. data/lib/puma/reactor.rb +85 -316
  68. data/lib/puma/request.rb +472 -0
  69. data/lib/puma/runner.rb +48 -55
  70. data/lib/puma/server.rb +303 -695
  71. data/lib/puma/single.rb +11 -67
  72. data/lib/puma/state_file.rb +47 -8
  73. data/lib/puma/systemd.rb +46 -0
  74. data/lib/puma/thread_pool.rb +132 -82
  75. data/lib/puma/util.rb +21 -7
  76. data/lib/puma.rb +54 -0
  77. data/lib/rack/handler/puma.rb +5 -6
  78. data/tools/Dockerfile +16 -0
  79. data/tools/trickletest.rb +0 -1
  80. metadata +45 -29
  81. data/ext/puma_http11/io_buffer.c +0 -155
  82. data/lib/puma/accept_nonblock.rb +0 -23
  83. data/lib/puma/compat.rb +0 -14
  84. data/lib/puma/convenient.rb +0 -25
  85. data/lib/puma/daemon_ext.rb +0 -33
  86. data/lib/puma/delegation.rb +0 -13
  87. data/lib/puma/java_io_buffer.rb +0 -47
  88. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  89. data/lib/puma/tcp_logger.rb +0 -41
  90. data/tools/jungle/README.md +0 -19
  91. data/tools/jungle/init.d/README.md +0 -61
  92. data/tools/jungle/init.d/puma +0 -421
  93. data/tools/jungle/init.d/run-puma +0 -18
  94. data/tools/jungle/upstart/README.md +0 -61
  95. data/tools/jungle/upstart/puma-manager.conf +0 -31
  96. data/tools/jungle/upstart/puma.conf +0 -69
data/lib/puma/client.rb CHANGED
@@ -9,8 +9,8 @@ class IO
9
9
  end
10
10
 
11
11
  require 'puma/detect'
12
- require 'puma/delegation'
13
12
  require 'tempfile'
13
+ require 'forwardable'
14
14
 
15
15
  if Puma::IS_JRUBY
16
16
  # We have to work around some OpenSSL buffer/io-readiness bugs
@@ -23,20 +23,41 @@ module Puma
23
23
 
24
24
  class ConnectionError < RuntimeError; end
25
25
 
26
+ class HttpParserError501 < IOError; end
27
+
26
28
  # An instance of this class represents a unique request from a client.
27
- # For example a web request from a browser or from CURL. This
29
+ # For example, this could be a web request from a browser or from CURL.
28
30
  #
29
31
  # An instance of `Puma::Client` can be used as if it were an IO object
30
- # for example it is passed into `IO.select` inside of the `Puma::Reactor`.
31
- # This is accomplished by the `to_io` method which gets called on any
32
- # non-IO objects being used with the IO api such as `IO.select.
32
+ # by the reactor. The reactor is expected to call `#to_io`
33
+ # on any non-IO objects it polls. For example, nio4r internally calls
34
+ # `IO::try_convert` (which may call `#to_io`) when a new socket is
35
+ # registered.
33
36
  #
34
37
  # Instances of this class are responsible for knowing if
35
38
  # the header and body are fully buffered via the `try_to_finish` method.
36
39
  # They can be used to "time out" a response via the `timeout_at` reader.
40
+ #
37
41
  class Client
42
+
43
+ # this tests all values but the last, which must be chunked
44
+ ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
45
+
46
+ # chunked body validation
47
+ CHUNK_SIZE_INVALID = /[^\h]/.freeze
48
+ CHUNK_VALID_ENDING = "\r\n".freeze
49
+
50
+ # Content-Length header value validation
51
+ CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
52
+
53
+ TE_ERR_MSG = 'Invalid Transfer-Encoding'
54
+
55
+ # The object used for a request with no body. All requests with
56
+ # no body share this one object since it has no state.
57
+ EmptyBody = NullIO.new
58
+
38
59
  include Puma::Const
39
- extend Puma::Delegation
60
+ extend Forwardable
40
61
 
41
62
  def initialize(io, env=nil)
42
63
  @io = io
@@ -51,9 +72,11 @@ module Puma
51
72
  @parser = HttpParser.new
52
73
  @parsed_bytes = 0
53
74
  @read_header = true
75
+ @read_proxy = false
54
76
  @ready = false
55
77
 
56
78
  @body = nil
79
+ @body_read_start = nil
57
80
  @buffer = nil
58
81
  @tempfile = nil
59
82
 
@@ -63,7 +86,13 @@ module Puma
63
86
  @hijacked = false
64
87
 
65
88
  @peerip = nil
89
+ @listener = nil
66
90
  @remote_addr_header = nil
91
+ @expect_proxy_proto = false
92
+
93
+ @body_remain = 0
94
+
95
+ @in_last_chunk = false
67
96
  end
68
97
 
69
98
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -71,10 +100,17 @@ module Puma
71
100
 
72
101
  attr_writer :peerip
73
102
 
74
- attr_accessor :remote_addr_header
103
+ attr_accessor :remote_addr_header, :listener
75
104
 
76
- forward :closed?, :@io
105
+ def_delegators :@io, :closed?
106
+
107
+ # Test to see if io meets a bare minimum of functioning, @to_io needs to be
108
+ # used for MiniSSL::Socket
109
+ def io_ok?
110
+ @to_io.is_a?(::BasicSocket) && !closed?
111
+ end
77
112
 
113
+ # @!attribute [r] inspect
78
114
  def inspect
79
115
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
80
116
  end
@@ -86,24 +122,36 @@ module Puma
86
122
  env[HIJACK_IO] ||= @io
87
123
  end
88
124
 
125
+ # @!attribute [r] in_data_phase
89
126
  def in_data_phase
90
- !@read_header
127
+ !(@read_header || @read_proxy)
91
128
  end
92
129
 
93
130
  def set_timeout(val)
94
- @timeout_at = Time.now + val
131
+ @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
132
+ end
133
+
134
+ # Number of seconds until the timeout elapses.
135
+ def timeout
136
+ [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
95
137
  end
96
138
 
97
139
  def reset(fast_check=true)
98
140
  @parser.reset
99
141
  @read_header = true
142
+ @read_proxy = !!@expect_proxy_proto
100
143
  @env = @proto_env.dup
101
144
  @body = nil
102
145
  @tempfile = nil
103
146
  @parsed_bytes = 0
104
147
  @ready = false
148
+ @body_remain = 0
149
+ @peerip = nil if @remote_addr_header
150
+ @in_last_chunk = false
105
151
 
106
152
  if @buffer
153
+ return false unless try_to_parse_proxy_protocol
154
+
107
155
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
108
156
 
109
157
  if @parser.finished?
@@ -114,123 +162,150 @@ module Puma
114
162
  end
115
163
 
116
164
  return false
117
- elsif fast_check &&
118
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
119
- return try_to_finish
165
+ else
166
+ begin
167
+ if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
168
+ return try_to_finish
169
+ end
170
+ rescue IOError
171
+ # swallow it
172
+ end
173
+
120
174
  end
121
175
  end
122
176
 
123
177
  def close
124
178
  begin
125
179
  @io.close
126
- rescue IOError
127
- Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
180
+ rescue IOError, Errno::EBADF
181
+ Puma::Util.purge_interrupt_queue
128
182
  end
129
183
  end
130
184
 
131
- # The object used for a request with no body. All requests with
132
- # no body share this one object since it has no state.
133
- EmptyBody = NullIO.new
134
-
135
- def setup_chunked_body(body)
136
- @chunked_body = true
137
- @partial_part_left = 0
138
- @prev_chunk = ""
185
+ # If necessary, read the PROXY protocol from the buffer. Returns
186
+ # false if more data is needed.
187
+ def try_to_parse_proxy_protocol
188
+ if @read_proxy
189
+ if @expect_proxy_proto == :v1
190
+ if @buffer.include? "\r\n"
191
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
192
+ if md[1]
193
+ @peerip = md[1].split(" ")[0]
194
+ end
195
+ @buffer = md.post_match
196
+ end
197
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
198
+ # request, this is just HTTP from a non-PROXY client; move on
199
+ @read_proxy = false
200
+ return @buffer.size > 0
201
+ else
202
+ return false
203
+ end
204
+ end
205
+ end
206
+ true
207
+ end
139
208
 
140
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
141
- @body.binmode
142
- @tempfile = @body
209
+ def try_to_finish
210
+ return read_body if in_data_phase
143
211
 
144
- return decode_chunk(body)
145
- end
212
+ begin
213
+ data = @io.read_nonblock(CHUNK_SIZE)
214
+ rescue IO::WaitReadable
215
+ return false
216
+ rescue EOFError
217
+ # Swallow error, don't log
218
+ rescue SystemCallError, IOError
219
+ raise ConnectionError, "Connection error detected during read"
220
+ end
146
221
 
147
- def decode_chunk(chunk)
148
- if @partial_part_left > 0
149
- if @partial_part_left <= chunk.size
150
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
151
- chunk = chunk[@partial_part_left..-1]
152
- else
153
- @body << chunk
154
- @partial_part_left -= chunk.size
155
- return false
156
- end
222
+ # No data means a closed socket
223
+ unless data
224
+ @buffer = nil
225
+ set_ready
226
+ raise EOFError
157
227
  end
158
228
 
159
- if @prev_chunk.empty?
160
- io = StringIO.new(chunk)
229
+ if @buffer
230
+ @buffer << data
161
231
  else
162
- io = StringIO.new(@prev_chunk+chunk)
163
- @prev_chunk = ""
232
+ @buffer = data
164
233
  end
165
234
 
166
- while !io.eof?
167
- line = io.gets
168
- if line.end_with?("\r\n")
169
- len = line.strip.to_i(16)
170
- if len == 0
171
- @body.rewind
172
- rest = io.read
173
- rest = rest[2..-1] if rest.start_with?("\r\n")
174
- @buffer = rest.empty? ? nil : rest
175
- @requests_served += 1
176
- @ready = true
177
- return true
178
- end
235
+ return false unless try_to_parse_proxy_protocol
179
236
 
180
- len += 2
237
+ @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
181
238
 
182
- part = io.read(len)
239
+ if @parser.finished?
240
+ return setup_body
241
+ elsif @parsed_bytes >= MAX_HEADER
242
+ raise HttpParserError,
243
+ "HEADER is longer than allowed, aborting client early."
244
+ end
183
245
 
184
- unless part
185
- @partial_part_left = len
186
- next
187
- end
246
+ false
247
+ end
188
248
 
189
- got = part.size
249
+ def eagerly_finish
250
+ return true if @ready
251
+ return false unless @to_io.wait_readable(0)
252
+ try_to_finish
253
+ end
190
254
 
191
- case
192
- when got == len
193
- @body << part[0..-3] # to skip the ending \r\n
194
- when got <= len - 2
195
- @body << part
196
- @partial_part_left = len - part.size
197
- when got == len - 1 # edge where we get just \r but not \n
198
- @body << part[0..-2]
199
- @partial_part_left = len - part.size
200
- end
201
- else
202
- @prev_chunk = line
203
- return false
204
- end
255
+ def finish(timeout)
256
+ return if @ready
257
+ @to_io.wait_readable(timeout) || timeout! until try_to_finish
258
+ end
259
+
260
+ def timeout!
261
+ write_error(408) if in_data_phase
262
+ raise ConnectionError
263
+ end
264
+
265
+ def write_error(status_code)
266
+ begin
267
+ @io << ERROR_RESPONSE[status_code]
268
+ rescue StandardError
269
+ end
270
+ end
271
+
272
+ def peerip
273
+ return @peerip if @peerip
274
+
275
+ if @remote_addr_header
276
+ hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
277
+ @peerip = hdr
278
+ return hdr
205
279
  end
206
280
 
207
- return false
281
+ @peerip ||= @io.peeraddr.last
208
282
  end
209
283
 
210
- def read_chunked_body
211
- while true
212
- begin
213
- chunk = @io.read_nonblock(4096)
214
- rescue Errno::EAGAIN
215
- return false
216
- rescue SystemCallError, IOError
217
- raise ConnectionError, "Connection error detected during read"
218
- end
284
+ # Returns true if the persistent connection can be closed immediately
285
+ # without waiting for the configured idle/shutdown timeout.
286
+ # @version 5.0.0
287
+ #
288
+ def can_close?
289
+ # Allow connection to close if we're not in the middle of parsing a request.
290
+ @parsed_bytes == 0
291
+ end
219
292
 
220
- # No chunk means a closed socket
221
- unless chunk
222
- @body.close
223
- @buffer = nil
224
- @requests_served += 1
225
- @ready = true
226
- raise EOFError
293
+ def expect_proxy_proto=(val)
294
+ if val
295
+ if @read_header
296
+ @read_proxy = true
227
297
  end
228
-
229
- return true if decode_chunk(chunk)
298
+ else
299
+ @read_proxy = false
230
300
  end
301
+ @expect_proxy_proto = val
231
302
  end
232
303
 
304
+ private
305
+
233
306
  def setup_body
307
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
308
+
234
309
  if @env[HTTP_EXPECT] == CONTINUE
235
310
  # TODO allow a hook here to check the headers before
236
311
  # going forward
@@ -243,20 +318,43 @@ module Puma
243
318
  body = @parser.body
244
319
 
245
320
  te = @env[TRANSFER_ENCODING2]
246
-
247
- if te && CHUNKED.casecmp(te) == 0
248
- return setup_chunked_body(body)
321
+ if te
322
+ te_lwr = te.downcase
323
+ if te.include? ','
324
+ te_ary = te_lwr.split ','
325
+ te_count = te_ary.count CHUNKED
326
+ te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
327
+ if te_ary.last == CHUNKED && te_count == 1 && te_valid
328
+ @env.delete TRANSFER_ENCODING2
329
+ return setup_chunked_body body
330
+ elsif te_count >= 1
331
+ raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
332
+ elsif !te_valid
333
+ raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
334
+ end
335
+ elsif te_lwr == CHUNKED
336
+ @env.delete TRANSFER_ENCODING2
337
+ return setup_chunked_body body
338
+ elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
339
+ raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
340
+ else
341
+ raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
342
+ end
249
343
  end
250
344
 
251
345
  @chunked_body = false
252
346
 
253
347
  cl = @env[CONTENT_LENGTH]
254
348
 
255
- unless cl
349
+ if cl
350
+ # cannot contain characters that are not \d
351
+ if cl =~ CONTENT_LENGTH_VALUE_INVALID
352
+ raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
353
+ end
354
+ else
256
355
  @buffer = body.empty? ? nil : body
257
356
  @body = EmptyBody
258
- @requests_served += 1
259
- @ready = true
357
+ set_ready
260
358
  return true
261
359
  end
262
360
 
@@ -265,13 +363,13 @@ module Puma
265
363
  if remain <= 0
266
364
  @body = StringIO.new(body)
267
365
  @buffer = nil
268
- @requests_served += 1
269
- @ready = true
366
+ set_ready
270
367
  return true
271
368
  end
272
369
 
273
370
  if remain > MAX_BODY
274
371
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
372
+ @body.unlink
275
373
  @body.binmode
276
374
  @tempfile = @body
277
375
  else
@@ -284,111 +382,9 @@ module Puma
284
382
 
285
383
  @body_remain = remain
286
384
 
287
- return false
288
- end
289
-
290
- def try_to_finish
291
- return read_body unless @read_header
292
-
293
- begin
294
- data = @io.read_nonblock(CHUNK_SIZE)
295
- rescue Errno::EAGAIN
296
- return false
297
- rescue SystemCallError, IOError
298
- raise ConnectionError, "Connection error detected during read"
299
- end
300
-
301
- # No data means a closed socket
302
- unless data
303
- @buffer = nil
304
- @requests_served += 1
305
- @ready = true
306
- raise EOFError
307
- end
308
-
309
- if @buffer
310
- @buffer << data
311
- else
312
- @buffer = data
313
- end
314
-
315
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
316
-
317
- if @parser.finished?
318
- return setup_body
319
- elsif @parsed_bytes >= MAX_HEADER
320
- raise HttpParserError,
321
- "HEADER is longer than allowed, aborting client early."
322
- end
323
-
324
385
  false
325
386
  end
326
387
 
327
- if IS_JRUBY
328
- def jruby_start_try_to_finish
329
- return read_body unless @read_header
330
-
331
- begin
332
- data = @io.sysread_nonblock(CHUNK_SIZE)
333
- rescue OpenSSL::SSL::SSLError => e
334
- return false if e.kind_of? IO::WaitReadable
335
- raise e
336
- end
337
-
338
- # No data means a closed socket
339
- unless data
340
- @buffer = nil
341
- @requests_served += 1
342
- @ready = true
343
- raise EOFError
344
- end
345
-
346
- if @buffer
347
- @buffer << data
348
- else
349
- @buffer = data
350
- end
351
-
352
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
353
-
354
- if @parser.finished?
355
- return setup_body
356
- elsif @parsed_bytes >= MAX_HEADER
357
- raise HttpParserError,
358
- "HEADER is longer than allowed, aborting client early."
359
- end
360
-
361
- false
362
- end
363
-
364
- def eagerly_finish
365
- return true if @ready
366
-
367
- if @io.kind_of? OpenSSL::SSL::SSLSocket
368
- return true if jruby_start_try_to_finish
369
- end
370
-
371
- return false unless IO.select([@to_io], nil, nil, 0)
372
- try_to_finish
373
- end
374
-
375
- else
376
-
377
- def eagerly_finish
378
- return true if @ready
379
- return false unless IO.select([@to_io], nil, nil, 0)
380
- try_to_finish
381
- end
382
- end # IS_JRUBY
383
-
384
- def finish
385
- return true if @ready
386
- until try_to_finish
387
- IO.select([@to_io], nil, nil)
388
- end
389
- true
390
- end
391
-
392
388
  def read_body
393
389
  if @chunked_body
394
390
  return read_chunked_body
@@ -406,7 +402,7 @@ module Puma
406
402
 
407
403
  begin
408
404
  chunk = @io.read_nonblock(want)
409
- rescue Errno::EAGAIN
405
+ rescue IO::WaitReadable
410
406
  return false
411
407
  rescue SystemCallError, IOError
412
408
  raise ConnectionError, "Connection error detected during read"
@@ -416,8 +412,7 @@ module Puma
416
412
  unless chunk
417
413
  @body.close
418
414
  @buffer = nil
419
- @requests_served += 1
420
- @ready = true
415
+ set_ready
421
416
  raise EOFError
422
417
  end
423
418
 
@@ -426,8 +421,7 @@ module Puma
426
421
  if remain <= 0
427
422
  @body.rewind
428
423
  @buffer = nil
429
- @requests_served += 1
430
- @ready = true
424
+ set_ready
431
425
  return true
432
426
  end
433
427
 
@@ -436,37 +430,156 @@ module Puma
436
430
  false
437
431
  end
438
432
 
439
- def write_400
440
- begin
441
- @io << ERROR_400_RESPONSE
442
- rescue StandardError
433
+ def read_chunked_body
434
+ while true
435
+ begin
436
+ chunk = @io.read_nonblock(4096)
437
+ rescue IO::WaitReadable
438
+ return false
439
+ rescue SystemCallError, IOError
440
+ raise ConnectionError, "Connection error detected during read"
441
+ end
442
+
443
+ # No chunk means a closed socket
444
+ unless chunk
445
+ @body.close
446
+ @buffer = nil
447
+ set_ready
448
+ raise EOFError
449
+ end
450
+
451
+ if decode_chunk(chunk)
452
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
453
+ return true
454
+ end
443
455
  end
444
456
  end
445
457
 
446
- def write_408
447
- begin
448
- @io << ERROR_408_RESPONSE
449
- rescue StandardError
458
+ def setup_chunked_body(body)
459
+ @chunked_body = true
460
+ @partial_part_left = 0
461
+ @prev_chunk = ""
462
+
463
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
464
+ @body.unlink
465
+ @body.binmode
466
+ @tempfile = @body
467
+ @chunked_content_length = 0
468
+
469
+ if decode_chunk(body)
470
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
471
+ return true
450
472
  end
451
473
  end
452
474
 
453
- def write_500
454
- begin
455
- @io << ERROR_500_RESPONSE
456
- rescue StandardError
457
- end
475
+ # @version 5.0.0
476
+ def write_chunk(str)
477
+ @chunked_content_length += @body.write(str)
458
478
  end
459
479
 
460
- def peerip
461
- return @peerip if @peerip
480
+ def decode_chunk(chunk)
481
+ if @partial_part_left > 0
482
+ if @partial_part_left <= chunk.size
483
+ if @partial_part_left > 2
484
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
485
+ end
486
+ chunk = chunk[@partial_part_left..-1]
487
+ @partial_part_left = 0
488
+ else
489
+ if @partial_part_left > 2
490
+ if @partial_part_left == chunk.size + 1
491
+ # Don't include the last \r
492
+ write_chunk(chunk[0..(@partial_part_left-3)])
493
+ else
494
+ # don't include the last \r\n
495
+ write_chunk(chunk)
496
+ end
497
+ end
498
+ @partial_part_left -= chunk.size
499
+ return false
500
+ end
501
+ end
462
502
 
463
- if @remote_addr_header
464
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
465
- @peerip = hdr
466
- return hdr
503
+ if @prev_chunk.empty?
504
+ io = StringIO.new(chunk)
505
+ else
506
+ io = StringIO.new(@prev_chunk+chunk)
507
+ @prev_chunk = ""
467
508
  end
468
509
 
469
- @peerip ||= @io.peeraddr.last
510
+ while !io.eof?
511
+ line = io.gets
512
+ if line.end_with?("\r\n")
513
+ # Puma doesn't process chunk extensions, but should parse if they're
514
+ # present, which is the reason for the semicolon regex
515
+ chunk_hex = line.strip[/\A[^;]+/]
516
+ if chunk_hex =~ CHUNK_SIZE_INVALID
517
+ raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
518
+ end
519
+ len = chunk_hex.to_i(16)
520
+ if len == 0
521
+ @in_last_chunk = true
522
+ @body.rewind
523
+ rest = io.read
524
+ last_crlf_size = "\r\n".bytesize
525
+ if rest.bytesize < last_crlf_size
526
+ @buffer = nil
527
+ @partial_part_left = last_crlf_size - rest.bytesize
528
+ return false
529
+ else
530
+ @buffer = rest[last_crlf_size..-1]
531
+ @buffer = nil if @buffer.empty?
532
+ set_ready
533
+ return true
534
+ end
535
+ end
536
+
537
+ len += 2
538
+
539
+ part = io.read(len)
540
+
541
+ unless part
542
+ @partial_part_left = len
543
+ next
544
+ end
545
+
546
+ got = part.size
547
+
548
+ case
549
+ when got == len
550
+ # proper chunked segment must end with "\r\n"
551
+ if part.end_with? CHUNK_VALID_ENDING
552
+ write_chunk(part[0..-3]) # to skip the ending \r\n
553
+ else
554
+ raise HttpParserError, "Chunk size mismatch"
555
+ end
556
+ when got <= len - 2
557
+ write_chunk(part)
558
+ @partial_part_left = len - part.size
559
+ when got == len - 1 # edge where we get just \r but not \n
560
+ write_chunk(part[0..-2])
561
+ @partial_part_left = len - part.size
562
+ end
563
+ else
564
+ @prev_chunk = line
565
+ return false
566
+ end
567
+ end
568
+
569
+ if @in_last_chunk
570
+ set_ready
571
+ true
572
+ else
573
+ false
574
+ end
575
+ end
576
+
577
+ def set_ready
578
+ if @body_read_start
579
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
580
+ end
581
+ @requests_served += 1
582
+ @ready = true
470
583
  end
471
584
  end
472
585
  end