puma 3.12.1 → 5.6.7

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