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