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