puma 3.12.1 → 5.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +1414 -448
  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/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/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  14. data/docs/kubernetes.md +66 -0
  15. data/docs/nginx.md +1 -1
  16. data/docs/plugins.md +20 -10
  17. data/docs/rails_dev_mode.md +29 -0
  18. data/docs/restart.md +47 -22
  19. data/docs/signals.md +7 -6
  20. data/docs/stats.md +142 -0
  21. data/docs/systemd.md +48 -70
  22. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  23. data/ext/puma_http11/ext_help.h +1 -1
  24. data/ext/puma_http11/extconf.rb +27 -0
  25. data/ext/puma_http11/http11_parser.c +84 -109
  26. data/ext/puma_http11/http11_parser.h +1 -1
  27. data/ext/puma_http11/http11_parser.java.rl +22 -38
  28. data/ext/puma_http11/http11_parser.rl +4 -2
  29. data/ext/puma_http11/http11_parser_common.rl +3 -3
  30. data/ext/puma_http11/mini_ssl.c +254 -91
  31. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  32. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  33. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +89 -106
  34. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +92 -22
  35. data/ext/puma_http11/puma_http11.c +34 -50
  36. data/lib/puma.rb +54 -0
  37. data/lib/puma/app/status.rb +68 -49
  38. data/lib/puma/binder.rb +191 -139
  39. data/lib/puma/cli.rb +15 -15
  40. data/lib/puma/client.rb +257 -228
  41. data/lib/puma/cluster.rb +221 -212
  42. data/lib/puma/cluster/worker.rb +183 -0
  43. data/lib/puma/cluster/worker_handle.rb +90 -0
  44. data/lib/puma/commonlogger.rb +2 -2
  45. data/lib/puma/configuration.rb +58 -51
  46. data/lib/puma/const.rb +39 -19
  47. data/lib/puma/control_cli.rb +109 -67
  48. data/lib/puma/detect.rb +24 -3
  49. data/lib/puma/dsl.rb +519 -121
  50. data/lib/puma/error_logger.rb +104 -0
  51. data/lib/puma/events.rb +55 -31
  52. data/lib/puma/io_buffer.rb +7 -5
  53. data/lib/puma/jruby_restart.rb +0 -58
  54. data/lib/puma/json.rb +96 -0
  55. data/lib/puma/launcher.rb +178 -68
  56. data/lib/puma/minissl.rb +147 -48
  57. data/lib/puma/minissl/context_builder.rb +79 -0
  58. data/lib/puma/null_io.rb +13 -1
  59. data/lib/puma/plugin.rb +6 -12
  60. data/lib/puma/plugin/tmp_restart.rb +2 -0
  61. data/lib/puma/queue_close.rb +26 -0
  62. data/lib/puma/rack/builder.rb +2 -4
  63. data/lib/puma/rack/urlmap.rb +2 -0
  64. data/lib/puma/rack_default.rb +2 -0
  65. data/lib/puma/reactor.rb +85 -316
  66. data/lib/puma/request.rb +467 -0
  67. data/lib/puma/runner.rb +31 -52
  68. data/lib/puma/server.rb +282 -680
  69. data/lib/puma/single.rb +11 -67
  70. data/lib/puma/state_file.rb +8 -3
  71. data/lib/puma/systemd.rb +46 -0
  72. data/lib/puma/thread_pool.rb +129 -81
  73. data/lib/puma/util.rb +13 -6
  74. data/lib/rack/handler/puma.rb +5 -6
  75. data/tools/Dockerfile +16 -0
  76. data/tools/trickletest.rb +0 -1
  77. metadata +42 -26
  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
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
@@ -244,8 +262,16 @@ module Puma
244
262
 
245
263
  te = @env[TRANSFER_ENCODING2]
246
264
 
247
- if te && CHUNKED.casecmp(te) == 0
248
- return setup_chunked_body(body)
265
+ if te
266
+ if te.include?(",")
267
+ te.split(",").each do |part|
268
+ if CHUNKED.casecmp(part.strip) == 0
269
+ return setup_chunked_body(body)
270
+ end
271
+ end
272
+ elsif CHUNKED.casecmp(te) == 0
273
+ return setup_chunked_body(body)
274
+ end
249
275
  end
250
276
 
251
277
  @chunked_body = false
@@ -255,8 +281,7 @@ module Puma
255
281
  unless cl
256
282
  @buffer = body.empty? ? nil : body
257
283
  @body = EmptyBody
258
- @requests_served += 1
259
- @ready = true
284
+ set_ready
260
285
  return true
261
286
  end
262
287
 
@@ -265,13 +290,13 @@ module Puma
265
290
  if remain <= 0
266
291
  @body = StringIO.new(body)
267
292
  @buffer = nil
268
- @requests_served += 1
269
- @ready = true
293
+ set_ready
270
294
  return true
271
295
  end
272
296
 
273
297
  if remain > MAX_BODY
274
298
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
299
+ @body.unlink
275
300
  @body.binmode
276
301
  @tempfile = @body
277
302
  else
@@ -287,108 +312,6 @@ module Puma
287
312
  return false
288
313
  end
289
314
 
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
- false
325
- end
326
-
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
315
  def read_body
393
316
  if @chunked_body
394
317
  return read_chunked_body
@@ -406,7 +329,7 @@ module Puma
406
329
 
407
330
  begin
408
331
  chunk = @io.read_nonblock(want)
409
- rescue Errno::EAGAIN
332
+ rescue IO::WaitReadable
410
333
  return false
411
334
  rescue SystemCallError, IOError
412
335
  raise ConnectionError, "Connection error detected during read"
@@ -416,8 +339,7 @@ module Puma
416
339
  unless chunk
417
340
  @body.close
418
341
  @buffer = nil
419
- @requests_served += 1
420
- @ready = true
342
+ set_ready
421
343
  raise EOFError
422
344
  end
423
345
 
@@ -426,8 +348,7 @@ module Puma
426
348
  if remain <= 0
427
349
  @body.rewind
428
350
  @buffer = nil
429
- @requests_served += 1
430
- @ready = true
351
+ set_ready
431
352
  return true
432
353
  end
433
354
 
@@ -436,37 +357,145 @@ module Puma
436
357
  false
437
358
  end
438
359
 
439
- def write_400
440
- begin
441
- @io << ERROR_400_RESPONSE
442
- 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
443
382
  end
444
383
  end
445
384
 
446
- def write_408
447
- begin
448
- @io << ERROR_408_RESPONSE
449
- 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
450
399
  end
451
400
  end
452
401
 
453
- def write_500
454
- begin
455
- @io << ERROR_500_RESPONSE
456
- rescue StandardError
457
- end
402
+ # @version 5.0.0
403
+ def write_chunk(str)
404
+ @chunked_content_length += @body.write(str)
458
405
  end
459
406
 
460
- def peerip
461
- 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
462
429
 
463
- if @remote_addr_header
464
- hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
465
- @peerip = hdr
466
- return hdr
430
+ if @prev_chunk.empty?
431
+ io = StringIO.new(chunk)
432
+ else
433
+ io = StringIO.new(@prev_chunk+chunk)
434
+ @prev_chunk = ""
467
435
  end
468
436
 
469
- @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
470
499
  end
471
500
  end
472
501
  end