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