puma 3.11.4 → 4.2.0

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +130 -1
  3. data/README.md +100 -44
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/restart.md +4 -2
  7. data/docs/systemd.md +27 -9
  8. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  9. data/ext/puma_http11/extconf.rb +8 -0
  10. data/ext/puma_http11/http11_parser.c +37 -62
  11. data/ext/puma_http11/http11_parser_common.rl +3 -3
  12. data/ext/puma_http11/mini_ssl.c +96 -5
  13. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  14. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +21 -4
  15. data/lib/puma/accept_nonblock.rb +7 -1
  16. data/lib/puma/app/status.rb +35 -29
  17. data/lib/puma/binder.rb +47 -11
  18. data/lib/puma/cli.rb +21 -7
  19. data/lib/puma/client.rb +227 -191
  20. data/lib/puma/cluster.rb +70 -31
  21. data/lib/puma/commonlogger.rb +2 -0
  22. data/lib/puma/configuration.rb +6 -3
  23. data/lib/puma/const.rb +24 -18
  24. data/lib/puma/control_cli.rb +33 -14
  25. data/lib/puma/convenient.rb +2 -0
  26. data/lib/puma/delegation.rb +2 -0
  27. data/lib/puma/detect.rb +2 -0
  28. data/lib/puma/dsl.rb +308 -76
  29. data/lib/puma/events.rb +6 -1
  30. data/lib/puma/io_buffer.rb +3 -6
  31. data/lib/puma/jruby_restart.rb +2 -0
  32. data/lib/puma/launcher.rb +102 -55
  33. data/lib/puma/minissl.rb +41 -19
  34. data/lib/puma/null_io.rb +2 -0
  35. data/lib/puma/plugin/tmp_restart.rb +2 -0
  36. data/lib/puma/plugin.rb +7 -2
  37. data/lib/puma/rack/builder.rb +4 -1
  38. data/lib/puma/rack/urlmap.rb +2 -0
  39. data/lib/puma/rack_default.rb +2 -0
  40. data/lib/puma/reactor.rb +220 -34
  41. data/lib/puma/runner.rb +14 -4
  42. data/lib/puma/server.rb +82 -40
  43. data/lib/puma/single.rb +15 -3
  44. data/lib/puma/state_file.rb +2 -0
  45. data/lib/puma/tcp_logger.rb +2 -0
  46. data/lib/puma/thread_pool.rb +59 -36
  47. data/lib/puma/util.rb +2 -6
  48. data/lib/puma.rb +8 -0
  49. data/lib/rack/handler/puma.rb +6 -3
  50. data/tools/docker/Dockerfile +16 -0
  51. data/tools/jungle/init.d/puma +6 -6
  52. data/tools/trickletest.rb +0 -1
  53. metadata +22 -10
  54. data/lib/puma/compat.rb +0 -14
  55. data/lib/puma/daemon_ext.rb +0 -31
  56. data/lib/puma/java_io_buffer.rb +0 -45
  57. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
data/lib/puma/binder.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'socket'
3
5
 
@@ -40,7 +42,7 @@ module Puma
40
42
  @ios = []
41
43
  end
42
44
 
43
- attr_reader :listeners, :ios
45
+ attr_reader :ios
44
46
 
45
47
  def env(sock)
46
48
  @envs.fetch(sock, @proto_env)
@@ -48,7 +50,6 @@ module Puma
48
50
 
49
51
  def close
50
52
  @ios.each { |i| i.close }
51
- @unix_paths.each { |i| File.unlink i }
52
53
  end
53
54
 
54
55
  def import_from_env
@@ -90,19 +91,28 @@ module Puma
90
91
  case uri.scheme
91
92
  when "tcp"
92
93
  if fd = @inherited_fds.delete(str)
93
- logger.log "* Inherited #{str}"
94
94
  io = inherit_tcp_listener uri.host, uri.port, fd
95
+ logger.log "* Inherited #{str}"
95
96
  elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
96
- logger.log "* Activated #{str}"
97
97
  io = inherit_tcp_listener uri.host, uri.port, sock
98
+ logger.log "* Activated #{str}"
98
99
  else
99
100
  params = Util.parse_query uri.query
100
101
 
101
102
  opt = params.key?('low_latency')
102
103
  bak = params.fetch('backlog', 1024).to_i
103
104
 
104
- logger.log "* Listening on #{str}"
105
105
  io = add_tcp_listener uri.host, uri.port, opt, bak
106
+
107
+ @ios.each do |i|
108
+ addr = if i.local_address.ipv6?
109
+ "[#{i.local_address.ip_unpack[0]}]:#{i.local_address.ip_unpack[1]}"
110
+ else
111
+ i.local_address.ip_unpack.join(':')
112
+ end
113
+
114
+ logger.log "* Listening on tcp://#{addr}"
115
+ end
106
116
  end
107
117
 
108
118
  @listeners << [str, io] if io
@@ -110,14 +120,12 @@ module Puma
110
120
  path = "#{uri.host}#{uri.path}".gsub("%20", " ")
111
121
 
112
122
  if fd = @inherited_fds.delete(str)
113
- logger.log "* Inherited #{str}"
114
123
  io = inherit_unix_listener path, fd
124
+ logger.log "* Inherited #{str}"
115
125
  elsif sock = @activated_sockets.delete([ :unix, path ])
116
- logger.log "* Activated #{str}"
117
126
  io = inherit_unix_listener path, sock
127
+ logger.log "* Activated #{str}"
118
128
  else
119
- logger.log "* Listening on #{str}"
120
-
121
129
  umask = nil
122
130
  mode = nil
123
131
  backlog = 1024
@@ -139,6 +147,7 @@ module Puma
139
147
  end
140
148
 
141
149
  io = add_unix_listener path, umask, mode, backlog
150
+ logger.log "* Listening on #{str}"
142
151
  end
143
152
 
144
153
  @listeners << [str, io]
@@ -162,6 +171,7 @@ module Puma
162
171
  end
163
172
 
164
173
  ctx.keystore_pass = params['keystore-pass']
174
+ ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
165
175
  else
166
176
  unless params['key']
167
177
  @events.error "Please specify the SSL key via 'key='"
@@ -182,8 +192,12 @@ module Puma
182
192
  end
183
193
 
184
194
  ctx.ca = params['ca'] if params['ca']
195
+ ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
185
196
  end
186
197
 
198
+ ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
199
+ ctx.no_tlsv1_1 = true if params['no_tlsv1_1'] == 'true'
200
+
187
201
  if params['verify_mode']
188
202
  ctx.verify_mode = case params['verify_mode']
189
203
  when "peer"
@@ -202,11 +216,11 @@ module Puma
202
216
  logger.log "* Inherited #{str}"
203
217
  io = inherit_ssl_listener fd, ctx
204
218
  elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
205
- logger.log "* Activated #{str}"
206
219
  io = inherit_ssl_listener sock, ctx
220
+ logger.log "* Activated #{str}"
207
221
  else
208
- logger.log "* Listening on #{str}"
209
222
  io = add_ssl_listener uri.host, uri.port, ctx
223
+ logger.log "* Listening on #{str}"
210
224
  end
211
225
 
212
226
  @listeners << [str, io] if io
@@ -313,6 +327,7 @@ module Puma
313
327
  s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
314
328
  s.listen backlog
315
329
 
330
+
316
331
  ssl = MiniSSL::Server.new s, ctx
317
332
  env = @proto_env.dup
318
333
  env[HTTPS_KEY] = HTTPS
@@ -399,5 +414,26 @@ module Puma
399
414
  s
400
415
  end
401
416
 
417
+ def close_listeners
418
+ @listeners.each do |l, io|
419
+ io.close
420
+ uri = URI.parse(l)
421
+ next unless uri.scheme == 'unix'
422
+ File.unlink("#{uri.host}#{uri.path}")
423
+ end
424
+ end
425
+
426
+ def close_unix_paths
427
+ @unix_paths.each { |up| File.unlink(up) if File.exist? up }
428
+ end
429
+
430
+ def redirects_for_restart
431
+ redirects = {:close_others => true}
432
+ @listeners.each_with_index do |(l, io), i|
433
+ ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
434
+ redirects[io.to_i] = io.to_i
435
+ end
436
+ redirects
437
+ end
402
438
  end
403
439
  end
data/lib/puma/cli.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
4
  require 'uri'
3
5
 
@@ -84,6 +86,14 @@ module Puma
84
86
  raise UnsupportedOption
85
87
  end
86
88
 
89
+ def configure_control_url(command_line_arg)
90
+ if command_line_arg
91
+ @control_url = command_line_arg
92
+ elsif Puma.jruby?
93
+ unsupported "No default url available on JRuby"
94
+ end
95
+ end
96
+
87
97
  # Build the OptionParser object to handle the available options.
88
98
  #
89
99
 
@@ -98,13 +108,13 @@ module Puma
98
108
  file_config.load arg
99
109
  end
100
110
 
101
- o.on "--control URL", "The bind url to use for the control server",
102
- "Use 'auto' to use temp unix server" do |arg|
103
- if arg
104
- @control_url = arg
105
- elsif Puma.jruby?
106
- unsupported "No default url available on JRuby"
107
- end
111
+ o.on "--control-url URL", "The bind url to use for the control server. Use 'auto' to use temp unix server" do |arg|
112
+ configure_control_url(arg)
113
+ end
114
+
115
+ # alias --control-url for backwards-compatibility
116
+ o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
117
+ configure_control_url(arg)
108
118
  end
109
119
 
110
120
  o.on "--control-token TOKEN",
@@ -151,6 +161,10 @@ module Puma
151
161
  user_config.prune_bundler
152
162
  end
153
163
 
164
+ o.on "--extra-runtime-dependencies GEM1,GEM2", "Defines any extra needed gems when using --prune-bundler" do |arg|
165
+ c.extra_runtime_dependencies arg.split(',')
166
+ end
167
+
154
168
  o.on "-q", "--quiet", "Do not log requests internally (default true)" do
155
169
  user_config.quiet
156
170
  end
data/lib/puma/client.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class IO
2
4
  # We need to use this for a jruby work around on both 1.8 and 1.9.
3
5
  # So this either creates the constant (on 1.8), or harmlessly
@@ -21,7 +23,23 @@ module Puma
21
23
 
22
24
  class ConnectionError < RuntimeError; end
23
25
 
26
+ # An instance of this class represents a unique request from a client.
27
+ # For example, this could be a web request from a browser or from CURL.
28
+ #
29
+ # An instance of `Puma::Client` can be used as if it were an IO object
30
+ # by the reactor. The reactor is expected to call `#to_io`
31
+ # on any non-IO objects it polls. For example, nio4r internally calls
32
+ # `IO::try_convert` (which may call `#to_io`) when a new socket is
33
+ # registered.
34
+ #
35
+ # Instances of this class are responsible for knowing if
36
+ # the header and body are fully buffered via the `try_to_finish` method.
37
+ # They can be used to "time out" a response via the `timeout_at` reader.
24
38
  class Client
39
+ # The object used for a request with no body. All requests with
40
+ # no body share this one object since it has no state.
41
+ EmptyBody = NullIO.new
42
+
25
43
  include Puma::Const
26
44
  extend Puma::Delegation
27
45
 
@@ -41,6 +59,7 @@ module Puma
41
59
  @ready = false
42
60
 
43
61
  @body = nil
62
+ @body_read_start = nil
44
63
  @buffer = nil
45
64
  @tempfile = nil
46
65
 
@@ -51,6 +70,10 @@ module Puma
51
70
 
52
71
  @peerip = nil
53
72
  @remote_addr_header = nil
73
+
74
+ @body_remain = 0
75
+
76
+ @in_last_chunk = false
54
77
  end
55
78
 
56
79
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -89,6 +112,9 @@ module Puma
89
112
  @tempfile = nil
90
113
  @parsed_bytes = 0
91
114
  @ready = false
115
+ @body_remain = 0
116
+ @peerip = nil
117
+ @in_last_chunk = false
92
118
 
93
119
  if @buffer
94
120
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
@@ -101,9 +127,16 @@ module Puma
101
127
  end
102
128
 
103
129
  return false
104
- elsif fast_check &&
105
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
106
- return try_to_finish
130
+ else
131
+ begin
132
+ if fast_check &&
133
+ IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
134
+ return try_to_finish
135
+ end
136
+ rescue IOError
137
+ # swallow it
138
+ end
139
+
107
140
  end
108
141
  end
109
142
 
@@ -115,164 +148,6 @@ module Puma
115
148
  end
116
149
  end
117
150
 
118
- # The object used for a request with no body. All requests with
119
- # no body share this one object since it has no state.
120
- EmptyBody = NullIO.new
121
-
122
- def setup_chunked_body(body)
123
- @chunked_body = true
124
- @partial_part_left = 0
125
- @prev_chunk = ""
126
-
127
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
128
- @body.binmode
129
- @tempfile = @body
130
-
131
- return decode_chunk(body)
132
- end
133
-
134
- def decode_chunk(chunk)
135
- if @partial_part_left > 0
136
- if @partial_part_left <= chunk.size
137
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
138
- chunk = chunk[@partial_part_left..-1]
139
- else
140
- @body << chunk
141
- @partial_part_left -= chunk.size
142
- return false
143
- end
144
- end
145
-
146
- if @prev_chunk.empty?
147
- io = StringIO.new(chunk)
148
- else
149
- io = StringIO.new(@prev_chunk+chunk)
150
- @prev_chunk = ""
151
- end
152
-
153
- while !io.eof?
154
- line = io.gets
155
- if line.end_with?("\r\n")
156
- len = line.strip.to_i(16)
157
- if len == 0
158
- @body.rewind
159
- rest = io.read
160
- @buffer = rest.empty? ? nil : rest
161
- @requests_served += 1
162
- @ready = true
163
- return true
164
- end
165
-
166
- len += 2
167
-
168
- part = io.read(len)
169
-
170
- unless part
171
- @partial_part_left = len
172
- next
173
- end
174
-
175
- got = part.size
176
-
177
- case
178
- when got == len
179
- @body << part[0..-3] # to skip the ending \r\n
180
- when got <= len - 2
181
- @body << part
182
- @partial_part_left = len - part.size
183
- when got == len - 1 # edge where we get just \r but not \n
184
- @body << part[0..-2]
185
- @partial_part_left = len - part.size
186
- end
187
- else
188
- @prev_chunk = line
189
- return false
190
- end
191
- end
192
-
193
- return false
194
- end
195
-
196
- def read_chunked_body
197
- while true
198
- begin
199
- chunk = @io.read_nonblock(4096)
200
- rescue Errno::EAGAIN
201
- return false
202
- rescue SystemCallError, IOError
203
- raise ConnectionError, "Connection error detected during read"
204
- end
205
-
206
- # No chunk means a closed socket
207
- unless chunk
208
- @body.close
209
- @buffer = nil
210
- @requests_served += 1
211
- @ready = true
212
- raise EOFError
213
- end
214
-
215
- return true if decode_chunk(chunk)
216
- end
217
- end
218
-
219
- def setup_body
220
- if @env[HTTP_EXPECT] == CONTINUE
221
- # TODO allow a hook here to check the headers before
222
- # going forward
223
- @io << HTTP_11_100
224
- @io.flush
225
- end
226
-
227
- @read_header = false
228
-
229
- body = @parser.body
230
-
231
- te = @env[TRANSFER_ENCODING2]
232
-
233
- if te && CHUNKED.casecmp(te) == 0
234
- return setup_chunked_body(body)
235
- end
236
-
237
- @chunked_body = false
238
-
239
- cl = @env[CONTENT_LENGTH]
240
-
241
- unless cl
242
- @buffer = body.empty? ? nil : body
243
- @body = EmptyBody
244
- @requests_served += 1
245
- @ready = true
246
- return true
247
- end
248
-
249
- remain = cl.to_i - body.bytesize
250
-
251
- if remain <= 0
252
- @body = StringIO.new(body)
253
- @buffer = nil
254
- @requests_served += 1
255
- @ready = true
256
- return true
257
- end
258
-
259
- if remain > MAX_BODY
260
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
261
- @body.binmode
262
- @tempfile = @body
263
- else
264
- # The body[0,0] trick is to get an empty string in the same
265
- # encoding as body.
266
- @body = StringIO.new body[0,0]
267
- end
268
-
269
- @body.write body
270
-
271
- @body_remain = remain
272
-
273
- return false
274
- end
275
-
276
151
  def try_to_finish
277
152
  return read_body unless @read_header
278
153
 
@@ -280,15 +155,14 @@ module Puma
280
155
  data = @io.read_nonblock(CHUNK_SIZE)
281
156
  rescue Errno::EAGAIN
282
157
  return false
283
- rescue SystemCallError, IOError
158
+ rescue SystemCallError, IOError, EOFError
284
159
  raise ConnectionError, "Connection error detected during read"
285
160
  end
286
161
 
287
162
  # No data means a closed socket
288
163
  unless data
289
164
  @buffer = nil
290
- @requests_served += 1
291
- @ready = true
165
+ set_ready
292
166
  raise EOFError
293
167
  end
294
168
 
@@ -324,8 +198,7 @@ module Puma
324
198
  # No data means a closed socket
325
199
  unless data
326
200
  @buffer = nil
327
- @requests_served += 1
328
- @ready = true
201
+ set_ready
329
202
  raise EOFError
330
203
  end
331
204
 
@@ -375,6 +248,84 @@ module Puma
375
248
  true
376
249
  end
377
250
 
251
+ def write_error(status_code)
252
+ begin
253
+ @io << ERROR_RESPONSE[status_code]
254
+ rescue StandardError
255
+ end
256
+ end
257
+
258
+ def peerip
259
+ return @peerip if @peerip
260
+
261
+ if @remote_addr_header
262
+ hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
263
+ @peerip = hdr
264
+ return hdr
265
+ end
266
+
267
+ @peerip ||= @io.peeraddr.last
268
+ end
269
+
270
+ private
271
+
272
+ def setup_body
273
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
274
+
275
+ if @env[HTTP_EXPECT] == CONTINUE
276
+ # TODO allow a hook here to check the headers before
277
+ # going forward
278
+ @io << HTTP_11_100
279
+ @io.flush
280
+ end
281
+
282
+ @read_header = false
283
+
284
+ body = @parser.body
285
+
286
+ te = @env[TRANSFER_ENCODING2]
287
+
288
+ if te && CHUNKED.casecmp(te) == 0
289
+ return setup_chunked_body(body)
290
+ end
291
+
292
+ @chunked_body = false
293
+
294
+ cl = @env[CONTENT_LENGTH]
295
+
296
+ unless cl
297
+ @buffer = body.empty? ? nil : body
298
+ @body = EmptyBody
299
+ set_ready
300
+ return true
301
+ end
302
+
303
+ remain = cl.to_i - body.bytesize
304
+
305
+ if remain <= 0
306
+ @body = StringIO.new(body)
307
+ @buffer = nil
308
+ set_ready
309
+ return true
310
+ end
311
+
312
+ if remain > MAX_BODY
313
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
314
+ @body.binmode
315
+ @tempfile = @body
316
+ else
317
+ # The body[0,0] trick is to get an empty string in the same
318
+ # encoding as body.
319
+ @body = StringIO.new body[0,0]
320
+ end
321
+
322
+ @body.write body
323
+
324
+ @body_remain = remain
325
+
326
+ return false
327
+ end
328
+
378
329
  def read_body
379
330
  if @chunked_body
380
331
  return read_chunked_body
@@ -402,8 +353,7 @@ module Puma
402
353
  unless chunk
403
354
  @body.close
404
355
  @buffer = nil
405
- @requests_served += 1
406
- @ready = true
356
+ set_ready
407
357
  raise EOFError
408
358
  end
409
359
 
@@ -412,8 +362,7 @@ module Puma
412
362
  if remain <= 0
413
363
  @body.rewind
414
364
  @buffer = nil
415
- @requests_served += 1
416
- @ready = true
365
+ set_ready
417
366
  return true
418
367
  end
419
368
 
@@ -422,37 +371,124 @@ module Puma
422
371
  false
423
372
  end
424
373
 
425
- def write_400
426
- begin
427
- @io << ERROR_400_RESPONSE
428
- rescue StandardError
374
+ def read_chunked_body
375
+ while true
376
+ begin
377
+ chunk = @io.read_nonblock(4096)
378
+ rescue IO::WaitReadable
379
+ return false
380
+ rescue SystemCallError, IOError
381
+ raise ConnectionError, "Connection error detected during read"
382
+ end
383
+
384
+ # No chunk means a closed socket
385
+ unless chunk
386
+ @body.close
387
+ @buffer = nil
388
+ set_ready
389
+ raise EOFError
390
+ end
391
+
392
+ return true if decode_chunk(chunk)
429
393
  end
430
394
  end
431
395
 
432
- def write_408
433
- begin
434
- @io << ERROR_408_RESPONSE
435
- rescue StandardError
436
- end
396
+ def setup_chunked_body(body)
397
+ @chunked_body = true
398
+ @partial_part_left = 0
399
+ @prev_chunk = ""
400
+
401
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
402
+ @body.binmode
403
+ @tempfile = @body
404
+
405
+ return decode_chunk(body)
437
406
  end
438
407
 
439
- def write_500
440
- begin
441
- @io << ERROR_500_RESPONSE
442
- rescue StandardError
408
+ def decode_chunk(chunk)
409
+ if @partial_part_left > 0
410
+ if @partial_part_left <= chunk.size
411
+ if @partial_part_left > 2
412
+ @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
413
+ end
414
+ chunk = chunk[@partial_part_left..-1]
415
+ @partial_part_left = 0
416
+ else
417
+ @body << chunk if @partial_part_left > 2 # don't include the last \r\n
418
+ @partial_part_left -= chunk.size
419
+ return false
420
+ end
443
421
  end
444
- end
445
422
 
446
- def peerip
447
- return @peerip if @peerip
423
+ if @prev_chunk.empty?
424
+ io = StringIO.new(chunk)
425
+ else
426
+ io = StringIO.new(@prev_chunk+chunk)
427
+ @prev_chunk = ""
428
+ end
448
429
 
449
- if @remote_addr_header
450
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
451
- @peerip = hdr
452
- return hdr
430
+ while !io.eof?
431
+ line = io.gets
432
+ if line.end_with?("\r\n")
433
+ len = line.strip.to_i(16)
434
+ if len == 0
435
+ @in_last_chunk = true
436
+ @body.rewind
437
+ rest = io.read
438
+ last_crlf_size = "\r\n".bytesize
439
+ if rest.bytesize < last_crlf_size
440
+ @buffer = nil
441
+ @partial_part_left = last_crlf_size - rest.bytesize
442
+ return false
443
+ else
444
+ @buffer = rest[last_crlf_size..-1]
445
+ @buffer = nil if @buffer.empty?
446
+ set_ready
447
+ return true
448
+ end
449
+ end
450
+
451
+ len += 2
452
+
453
+ part = io.read(len)
454
+
455
+ unless part
456
+ @partial_part_left = len
457
+ next
458
+ end
459
+
460
+ got = part.size
461
+
462
+ case
463
+ when got == len
464
+ @body << part[0..-3] # to skip the ending \r\n
465
+ when got <= len - 2
466
+ @body << part
467
+ @partial_part_left = len - part.size
468
+ when got == len - 1 # edge where we get just \r but not \n
469
+ @body << part[0..-2]
470
+ @partial_part_left = len - part.size
471
+ end
472
+ else
473
+ @prev_chunk = line
474
+ return false
475
+ end
453
476
  end
454
477
 
455
- @peerip ||= @io.peeraddr.last
478
+ if @in_last_chunk
479
+ set_ready
480
+ true
481
+ else
482
+ false
483
+ end
484
+ end
485
+
486
+ def set_ready
487
+ if @body_read_start
488
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
489
+ end
490
+ @requests_served += 1
491
+ @ready = true
456
492
  end
457
493
  end
458
494
  end