puma 3.12.6 → 5.3.2

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 +1400 -451
  3. data/LICENSE +23 -20
  4. data/README.md +131 -60
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +24 -19
  7. data/docs/compile_options.md +19 -0
  8. data/docs/deployment.md +38 -13
  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 +20 -10
  20. data/docs/rails_dev_mode.md +29 -0
  21. data/docs/restart.md +47 -22
  22. data/docs/signals.md +7 -6
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +48 -70
  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 +27 -0
  28. data/ext/puma_http11/http11_parser.c +81 -108
  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 +1 -1
  32. data/ext/puma_http11/http11_parser_common.rl +3 -3
  33. data/ext/puma_http11/mini_ssl.c +254 -91
  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 +89 -106
  37. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +92 -22
  38. data/ext/puma_http11/puma_http11.c +34 -50
  39. data/lib/puma.rb +54 -0
  40. data/lib/puma/app/status.rb +68 -49
  41. data/lib/puma/binder.rb +191 -139
  42. data/lib/puma/cli.rb +15 -15
  43. data/lib/puma/client.rb +247 -226
  44. data/lib/puma/cluster.rb +221 -212
  45. data/lib/puma/cluster/worker.rb +183 -0
  46. data/lib/puma/cluster/worker_handle.rb +90 -0
  47. data/lib/puma/commonlogger.rb +2 -2
  48. data/lib/puma/configuration.rb +58 -51
  49. data/lib/puma/const.rb +32 -20
  50. data/lib/puma/control_cli.rb +109 -67
  51. data/lib/puma/detect.rb +24 -3
  52. data/lib/puma/dsl.rb +519 -121
  53. data/lib/puma/error_logger.rb +104 -0
  54. data/lib/puma/events.rb +55 -31
  55. data/lib/puma/io_buffer.rb +7 -5
  56. data/lib/puma/jruby_restart.rb +0 -58
  57. data/lib/puma/json.rb +96 -0
  58. data/lib/puma/launcher.rb +178 -68
  59. data/lib/puma/minissl.rb +147 -48
  60. data/lib/puma/minissl/context_builder.rb +79 -0
  61. data/lib/puma/null_io.rb +13 -1
  62. data/lib/puma/plugin.rb +6 -12
  63. data/lib/puma/plugin/tmp_restart.rb +2 -0
  64. data/lib/puma/queue_close.rb +26 -0
  65. data/lib/puma/rack/builder.rb +2 -4
  66. data/lib/puma/rack/urlmap.rb +2 -0
  67. data/lib/puma/rack_default.rb +2 -0
  68. data/lib/puma/reactor.rb +85 -316
  69. data/lib/puma/request.rb +467 -0
  70. data/lib/puma/runner.rb +31 -52
  71. data/lib/puma/server.rb +275 -726
  72. data/lib/puma/single.rb +11 -67
  73. data/lib/puma/state_file.rb +8 -3
  74. data/lib/puma/systemd.rb +46 -0
  75. data/lib/puma/thread_pool.rb +129 -81
  76. data/lib/puma/util.rb +13 -6
  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 -28
  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/cli.rb CHANGED
@@ -80,7 +80,7 @@ module Puma
80
80
  @launcher.run
81
81
  end
82
82
 
83
- private
83
+ private
84
84
  def unsupported(str)
85
85
  @events.error(str)
86
86
  raise UnsupportedOption
@@ -104,6 +104,10 @@ module Puma
104
104
  user_config.bind arg
105
105
  end
106
106
 
107
+ o.on "--bind-to-activated-sockets [only]", "Bind to all activated sockets" do |arg|
108
+ user_config.bind_to_activated_sockets(arg || true)
109
+ end
110
+
107
111
  o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
108
112
  file_config.load arg
109
113
  end
@@ -112,21 +116,11 @@ module Puma
112
116
  configure_control_url(arg)
113
117
  end
114
118
 
115
- # alias --control-url for backwards-compatibility
116
- o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
117
- configure_control_url(arg)
118
- end
119
-
120
119
  o.on "--control-token TOKEN",
121
120
  "The token to use as authentication for the control server" do |arg|
122
121
  @control_options[:auth_token] = arg
123
122
  end
124
123
 
125
- o.on "-d", "--daemon", "Daemonize the server into the background" do
126
- user_config.daemonize
127
- user_config.quiet
128
- end
129
-
130
124
  o.on "--debug", "Log lowlevel debugging information" do
131
125
  user_config.debug
132
126
  end
@@ -140,6 +134,12 @@ module Puma
140
134
  user_config.environment arg
141
135
  end
142
136
 
137
+ o.on "-f", "--fork-worker=[REQUESTS]", OptionParser::DecimalInteger,
138
+ "Fork new workers from existing worker. Cluster mode only",
139
+ "Auto-refork after REQUESTS (default 1000)" do |*args|
140
+ user_config.fork_worker(*args.compact)
141
+ end
142
+
143
143
  o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
144
144
  $LOAD_PATH.unshift(*arg.split(':'))
145
145
  end
@@ -161,6 +161,10 @@ module Puma
161
161
  user_config.prune_bundler
162
162
  end
163
163
 
164
+ o.on "--extra-runtime-dependencies GEM1,GEM2", "Defines any extra needed gems when using --prune-bundler" do |arg|
165
+ user_config.extra_runtime_dependencies arg.split(',')
166
+ end
167
+
164
168
  o.on "-q", "--quiet", "Do not log requests internally (default true)" do
165
169
  user_config.quiet
166
170
  end
@@ -188,10 +192,6 @@ module Puma
188
192
  end
189
193
  end
190
194
 
191
- o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
192
- user_config.tcp_mode!
193
- end
194
-
195
195
  o.on "--early-hints", "Enable early hints support" do
196
196
  user_config.early_hints
197
197
  end
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
@@ -24,19 +24,24 @@ module Puma
24
24
  class ConnectionError < RuntimeError; end
25
25
 
26
26
  # 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
27
+ # For example, this could be a web request from a browser or from CURL.
28
28
  #
29
29
  # 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.
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.
33
34
  #
34
35
  # Instances of this class are responsible for knowing if
35
36
  # the header and body are fully buffered via the `try_to_finish` method.
36
37
  # They can be used to "time out" a response via the `timeout_at` reader.
37
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
+
38
43
  include Puma::Const
39
- extend Puma::Delegation
44
+ extend Forwardable
40
45
 
41
46
  def initialize(io, env=nil)
42
47
  @io = io
@@ -54,6 +59,7 @@ module Puma
54
59
  @ready = false
55
60
 
56
61
  @body = nil
62
+ @body_read_start = nil
57
63
  @buffer = nil
58
64
  @tempfile = nil
59
65
 
@@ -63,7 +69,12 @@ module Puma
63
69
  @hijacked = false
64
70
 
65
71
  @peerip = nil
72
+ @listener = nil
66
73
  @remote_addr_header = nil
74
+
75
+ @body_remain = 0
76
+
77
+ @in_last_chunk = false
67
78
  end
68
79
 
69
80
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -71,10 +82,17 @@ module Puma
71
82
 
72
83
  attr_writer :peerip
73
84
 
74
- attr_accessor :remote_addr_header
85
+ attr_accessor :remote_addr_header, :listener
86
+
87
+ def_delegators :@io, :closed?
75
88
 
76
- forward :closed?, :@io
89
+ # Test to see if io meets a bare minimum of functioning, @to_io needs to be
90
+ # used for MiniSSL::Socket
91
+ def io_ok?
92
+ @to_io.is_a?(::BasicSocket) && !closed?
93
+ end
77
94
 
95
+ # @!attribute [r] inspect
78
96
  def inspect
79
97
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
80
98
  end
@@ -86,12 +104,18 @@ module Puma
86
104
  env[HIJACK_IO] ||= @io
87
105
  end
88
106
 
107
+ # @!attribute [r] in_data_phase
89
108
  def in_data_phase
90
109
  !@read_header
91
110
  end
92
111
 
93
112
  def set_timeout(val)
94
- @timeout_at = Time.now + val
113
+ @timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
114
+ end
115
+
116
+ # Number of seconds until the timeout elapses.
117
+ def timeout
118
+ [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
95
119
  end
96
120
 
97
121
  def reset(fast_check=true)
@@ -102,6 +126,9 @@ module Puma
102
126
  @tempfile = nil
103
127
  @parsed_bytes = 0
104
128
  @ready = false
129
+ @body_remain = 0
130
+ @peerip = nil if @remote_addr_header
131
+ @in_last_chunk = false
105
132
 
106
133
  if @buffer
107
134
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
@@ -114,9 +141,16 @@ module Puma
114
141
  end
115
142
 
116
143
  return false
117
- elsif fast_check &&
118
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
119
- return try_to_finish
144
+ else
145
+ begin
146
+ if fast_check &&
147
+ IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
148
+ return try_to_finish
149
+ end
150
+ rescue IOError
151
+ # swallow it
152
+ end
153
+
120
154
  end
121
155
  end
122
156
 
@@ -128,109 +162,93 @@ module Puma
128
162
  end
129
163
  end
130
164
 
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 = ""
139
-
140
- @body = Tempfile.new(Const::PUMA_TMP_BASE)
141
- @body.binmode
142
- @tempfile = @body
165
+ def try_to_finish
166
+ return read_body unless @read_header
143
167
 
144
- return decode_chunk(body)
145
- end
168
+ begin
169
+ data = @io.read_nonblock(CHUNK_SIZE)
170
+ rescue IO::WaitReadable
171
+ return false
172
+ rescue EOFError
173
+ # Swallow error, don't log
174
+ rescue SystemCallError, IOError
175
+ raise ConnectionError, "Connection error detected during read"
176
+ end
146
177
 
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
178
+ # No data means a closed socket
179
+ unless data
180
+ @buffer = nil
181
+ set_ready
182
+ raise EOFError
157
183
  end
158
184
 
159
- if @prev_chunk.empty?
160
- io = StringIO.new(chunk)
185
+ if @buffer
186
+ @buffer << data
161
187
  else
162
- io = StringIO.new(@prev_chunk+chunk)
163
- @prev_chunk = ""
188
+ @buffer = data
164
189
  end
165
190
 
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
179
-
180
- len += 2
191
+ @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
181
192
 
182
- part = io.read(len)
193
+ if @parser.finished?
194
+ return setup_body
195
+ elsif @parsed_bytes >= MAX_HEADER
196
+ raise HttpParserError,
197
+ "HEADER is longer than allowed, aborting client early."
198
+ end
183
199
 
184
- unless part
185
- @partial_part_left = len
186
- next
187
- end
200
+ false
201
+ end
188
202
 
189
- got = part.size
203
+ def eagerly_finish
204
+ return true if @ready
205
+ return false unless IO.select([@to_io], nil, nil, 0)
206
+ try_to_finish
207
+ end
190
208
 
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
205
- end
209
+ def finish(timeout)
210
+ return if @ready
211
+ IO.select([@to_io], nil, nil, timeout) || timeout! until try_to_finish
212
+ end
206
213
 
207
- return false
214
+ def timeout!
215
+ write_error(408) if in_data_phase
216
+ raise ConnectionError
208
217
  end
209
218
 
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
219
+ def write_error(status_code)
220
+ begin
221
+ @io << ERROR_RESPONSE[status_code]
222
+ rescue StandardError
223
+ end
224
+ end
219
225
 
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
227
- end
226
+ def peerip
227
+ return @peerip if @peerip
228
228
 
229
- return true if decode_chunk(chunk)
229
+ if @remote_addr_header
230
+ hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
231
+ @peerip = hdr
232
+ return hdr
230
233
  end
234
+
235
+ @peerip ||= @io.peeraddr.last
231
236
  end
232
237
 
238
+ # Returns true if the persistent connection can be closed immediately
239
+ # without waiting for the configured idle/shutdown timeout.
240
+ # @version 5.0.0
241
+ #
242
+ def can_close?
243
+ # Allow connection to close if we're not in the middle of parsing a request.
244
+ @parsed_bytes == 0
245
+ end
246
+
247
+ private
248
+
233
249
  def setup_body
250
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
251
+
234
252
  if @env[HTTP_EXPECT] == CONTINUE
235
253
  # TODO allow a hook here to check the headers before
236
254
  # going forward
@@ -263,8 +281,7 @@ module Puma
263
281
  unless cl
264
282
  @buffer = body.empty? ? nil : body
265
283
  @body = EmptyBody
266
- @requests_served += 1
267
- @ready = true
284
+ set_ready
268
285
  return true
269
286
  end
270
287
 
@@ -273,13 +290,13 @@ module Puma
273
290
  if remain <= 0
274
291
  @body = StringIO.new(body)
275
292
  @buffer = nil
276
- @requests_served += 1
277
- @ready = true
293
+ set_ready
278
294
  return true
279
295
  end
280
296
 
281
297
  if remain > MAX_BODY
282
298
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
299
+ @body.unlink
283
300
  @body.binmode
284
301
  @tempfile = @body
285
302
  else
@@ -295,108 +312,6 @@ module Puma
295
312
  return false
296
313
  end
297
314
 
298
- def try_to_finish
299
- return read_body unless @read_header
300
-
301
- begin
302
- data = @io.read_nonblock(CHUNK_SIZE)
303
- rescue Errno::EAGAIN
304
- return false
305
- rescue SystemCallError, IOError
306
- raise ConnectionError, "Connection error detected during read"
307
- end
308
-
309
- # No data means a closed socket
310
- unless data
311
- @buffer = nil
312
- @requests_served += 1
313
- @ready = true
314
- raise EOFError
315
- end
316
-
317
- if @buffer
318
- @buffer << data
319
- else
320
- @buffer = data
321
- end
322
-
323
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
324
-
325
- if @parser.finished?
326
- return setup_body
327
- elsif @parsed_bytes >= MAX_HEADER
328
- raise HttpParserError,
329
- "HEADER is longer than allowed, aborting client early."
330
- end
331
-
332
- false
333
- end
334
-
335
- if IS_JRUBY
336
- def jruby_start_try_to_finish
337
- return read_body unless @read_header
338
-
339
- begin
340
- data = @io.sysread_nonblock(CHUNK_SIZE)
341
- rescue OpenSSL::SSL::SSLError => e
342
- return false if e.kind_of? IO::WaitReadable
343
- raise e
344
- end
345
-
346
- # No data means a closed socket
347
- unless data
348
- @buffer = nil
349
- @requests_served += 1
350
- @ready = true
351
- raise EOFError
352
- end
353
-
354
- if @buffer
355
- @buffer << data
356
- else
357
- @buffer = data
358
- end
359
-
360
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
361
-
362
- if @parser.finished?
363
- return setup_body
364
- elsif @parsed_bytes >= MAX_HEADER
365
- raise HttpParserError,
366
- "HEADER is longer than allowed, aborting client early."
367
- end
368
-
369
- false
370
- end
371
-
372
- def eagerly_finish
373
- return true if @ready
374
-
375
- if @io.kind_of? OpenSSL::SSL::SSLSocket
376
- return true if jruby_start_try_to_finish
377
- end
378
-
379
- return false unless IO.select([@to_io], nil, nil, 0)
380
- try_to_finish
381
- end
382
-
383
- else
384
-
385
- def eagerly_finish
386
- return true if @ready
387
- return false unless IO.select([@to_io], nil, nil, 0)
388
- try_to_finish
389
- end
390
- end # IS_JRUBY
391
-
392
- def finish
393
- return true if @ready
394
- until try_to_finish
395
- IO.select([@to_io], nil, nil)
396
- end
397
- true
398
- end
399
-
400
315
  def read_body
401
316
  if @chunked_body
402
317
  return read_chunked_body
@@ -414,7 +329,7 @@ module Puma
414
329
 
415
330
  begin
416
331
  chunk = @io.read_nonblock(want)
417
- rescue Errno::EAGAIN
332
+ rescue IO::WaitReadable
418
333
  return false
419
334
  rescue SystemCallError, IOError
420
335
  raise ConnectionError, "Connection error detected during read"
@@ -424,8 +339,7 @@ module Puma
424
339
  unless chunk
425
340
  @body.close
426
341
  @buffer = nil
427
- @requests_served += 1
428
- @ready = true
342
+ set_ready
429
343
  raise EOFError
430
344
  end
431
345
 
@@ -434,8 +348,7 @@ module Puma
434
348
  if remain <= 0
435
349
  @body.rewind
436
350
  @buffer = nil
437
- @requests_served += 1
438
- @ready = true
351
+ set_ready
439
352
  return true
440
353
  end
441
354
 
@@ -444,37 +357,145 @@ module Puma
444
357
  false
445
358
  end
446
359
 
447
- def write_400
448
- begin
449
- @io << ERROR_400_RESPONSE
450
- rescue StandardError
360
+ def read_chunked_body
361
+ while true
362
+ begin
363
+ chunk = @io.read_nonblock(4096)
364
+ rescue IO::WaitReadable
365
+ return false
366
+ rescue SystemCallError, IOError
367
+ raise ConnectionError, "Connection error detected during read"
368
+ end
369
+
370
+ # No chunk means a closed socket
371
+ unless chunk
372
+ @body.close
373
+ @buffer = nil
374
+ set_ready
375
+ raise EOFError
376
+ end
377
+
378
+ if decode_chunk(chunk)
379
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
380
+ return true
381
+ end
451
382
  end
452
383
  end
453
384
 
454
- def write_408
455
- begin
456
- @io << ERROR_408_RESPONSE
457
- rescue StandardError
385
+ def setup_chunked_body(body)
386
+ @chunked_body = true
387
+ @partial_part_left = 0
388
+ @prev_chunk = ""
389
+
390
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
391
+ @body.unlink
392
+ @body.binmode
393
+ @tempfile = @body
394
+ @chunked_content_length = 0
395
+
396
+ if decode_chunk(body)
397
+ @env[CONTENT_LENGTH] = @chunked_content_length.to_s
398
+ return true
458
399
  end
459
400
  end
460
401
 
461
- def write_500
462
- begin
463
- @io << ERROR_500_RESPONSE
464
- rescue StandardError
465
- end
402
+ # @version 5.0.0
403
+ def write_chunk(str)
404
+ @chunked_content_length += @body.write(str)
466
405
  end
467
406
 
468
- def peerip
469
- return @peerip if @peerip
407
+ def decode_chunk(chunk)
408
+ if @partial_part_left > 0
409
+ if @partial_part_left <= chunk.size
410
+ if @partial_part_left > 2
411
+ write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
412
+ end
413
+ chunk = chunk[@partial_part_left..-1]
414
+ @partial_part_left = 0
415
+ else
416
+ if @partial_part_left > 2
417
+ if @partial_part_left == chunk.size + 1
418
+ # Don't include the last \r
419
+ write_chunk(chunk[0..(@partial_part_left-3)])
420
+ else
421
+ # don't include the last \r\n
422
+ write_chunk(chunk)
423
+ end
424
+ end
425
+ @partial_part_left -= chunk.size
426
+ return false
427
+ end
428
+ end
470
429
 
471
- if @remote_addr_header
472
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
473
- @peerip = hdr
474
- return hdr
430
+ if @prev_chunk.empty?
431
+ io = StringIO.new(chunk)
432
+ else
433
+ io = StringIO.new(@prev_chunk+chunk)
434
+ @prev_chunk = ""
475
435
  end
476
436
 
477
- @peerip ||= @io.peeraddr.last
437
+ while !io.eof?
438
+ line = io.gets
439
+ if line.end_with?("\r\n")
440
+ len = line.strip.to_i(16)
441
+ if len == 0
442
+ @in_last_chunk = true
443
+ @body.rewind
444
+ rest = io.read
445
+ last_crlf_size = "\r\n".bytesize
446
+ if rest.bytesize < last_crlf_size
447
+ @buffer = nil
448
+ @partial_part_left = last_crlf_size - rest.bytesize
449
+ return false
450
+ else
451
+ @buffer = rest[last_crlf_size..-1]
452
+ @buffer = nil if @buffer.empty?
453
+ set_ready
454
+ return true
455
+ end
456
+ end
457
+
458
+ len += 2
459
+
460
+ part = io.read(len)
461
+
462
+ unless part
463
+ @partial_part_left = len
464
+ next
465
+ end
466
+
467
+ got = part.size
468
+
469
+ case
470
+ when got == len
471
+ write_chunk(part[0..-3]) # to skip the ending \r\n
472
+ when got <= len - 2
473
+ write_chunk(part)
474
+ @partial_part_left = len - part.size
475
+ when got == len - 1 # edge where we get just \r but not \n
476
+ write_chunk(part[0..-2])
477
+ @partial_part_left = len - part.size
478
+ end
479
+ else
480
+ @prev_chunk = line
481
+ return false
482
+ end
483
+ end
484
+
485
+ if @in_last_chunk
486
+ set_ready
487
+ true
488
+ else
489
+ false
490
+ end
491
+ end
492
+
493
+ def set_ready
494
+ if @body_read_start
495
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
496
+ end
497
+ @requests_served += 1
498
+ @ready = true
478
499
  end
479
500
  end
480
501
  end