puma 3.8.2 → 4.3.12
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.
- checksums.yaml +5 -5
- data/History.md +305 -0
- data/LICENSE +0 -0
- data/README.md +162 -224
- data/bin/puma-wild +0 -0
- data/docs/architecture.md +37 -0
- data/{DEPLOYMENT.md → docs/deployment.md} +24 -4
- data/docs/images/puma-connection-flow-no-reactor.png +0 -0
- data/docs/images/puma-connection-flow.png +0 -0
- data/docs/images/puma-general-arch.png +0 -0
- data/docs/nginx.md +0 -0
- data/docs/plugins.md +38 -0
- data/docs/restart.md +41 -0
- data/docs/signals.md +56 -3
- data/docs/systemd.md +130 -37
- data/docs/tcp_mode.md +96 -0
- data/ext/puma_http11/PumaHttp11Service.java +2 -0
- data/ext/puma_http11/ext_help.h +0 -0
- data/ext/puma_http11/extconf.rb +21 -0
- data/ext/puma_http11/http11_parser.c +134 -144
- data/ext/puma_http11/http11_parser.h +0 -0
- data/ext/puma_http11/http11_parser.java.rl +21 -37
- data/ext/puma_http11/http11_parser.rl +12 -10
- data/ext/puma_http11/http11_parser_common.rl +4 -4
- data/ext/puma_http11/io_buffer.c +0 -0
- data/ext/puma_http11/mini_ssl.c +165 -34
- data/ext/puma_http11/org/jruby/puma/Http11.java +106 -114
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +85 -101
- data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +30 -6
- data/ext/puma_http11/puma_http11.c +3 -0
- data/lib/puma/accept_nonblock.rb +7 -1
- data/lib/puma/app/status.rb +42 -26
- data/lib/puma/binder.rb +57 -74
- data/lib/puma/cli.rb +26 -7
- data/lib/puma/client.rb +307 -191
- data/lib/puma/cluster.rb +78 -34
- data/lib/puma/commonlogger.rb +2 -0
- data/lib/puma/configuration.rb +24 -16
- data/lib/puma/const.rb +41 -20
- data/lib/puma/control_cli.rb +46 -19
- data/lib/puma/detect.rb +2 -0
- data/lib/puma/dsl.rb +329 -68
- data/lib/puma/events.rb +6 -2
- data/lib/puma/io_buffer.rb +3 -6
- data/lib/puma/jruby_restart.rb +2 -1
- data/lib/puma/launcher.rb +125 -61
- data/lib/puma/minissl/context_builder.rb +76 -0
- data/lib/puma/minissl.rb +85 -28
- data/lib/puma/null_io.rb +2 -0
- data/lib/puma/plugin/tmp_restart.rb +2 -1
- data/lib/puma/plugin.rb +7 -2
- data/lib/puma/rack/builder.rb +4 -1
- data/lib/puma/rack/urlmap.rb +2 -0
- data/lib/puma/rack_default.rb +2 -0
- data/lib/puma/reactor.rb +224 -34
- data/lib/puma/runner.rb +27 -6
- data/lib/puma/server.rb +212 -68
- data/lib/puma/single.rb +16 -5
- data/lib/puma/state_file.rb +2 -0
- data/lib/puma/tcp_logger.rb +2 -0
- data/lib/puma/thread_pool.rb +67 -36
- data/lib/puma/util.rb +2 -6
- data/lib/puma.rb +16 -0
- data/lib/rack/handler/puma.rb +16 -5
- data/tools/docker/Dockerfile +16 -0
- data/tools/jungle/README.md +12 -2
- data/tools/jungle/init.d/README.md +2 -0
- data/tools/jungle/init.d/puma +8 -8
- data/tools/jungle/init.d/run-puma +1 -1
- data/tools/jungle/rc.d/README.md +74 -0
- data/tools/jungle/rc.d/puma +61 -0
- data/tools/jungle/rc.d/puma.conf +10 -0
- data/tools/jungle/upstart/README.md +0 -0
- data/tools/jungle/upstart/puma-manager.conf +0 -0
- data/tools/jungle/upstart/puma.conf +0 -0
- data/tools/trickletest.rb +1 -2
- metadata +32 -93
- data/.github/issue_template.md +0 -20
- data/Gemfile +0 -12
- data/Manifest.txt +0 -78
- data/Rakefile +0 -158
- data/Release.md +0 -9
- data/gemfiles/2.1-Gemfile +0 -12
- data/lib/puma/compat.rb +0 -14
- data/lib/puma/convenient.rb +0 -23
- data/lib/puma/daemon_ext.rb +0 -31
- data/lib/puma/delegation.rb +0 -11
- data/lib/puma/java_io_buffer.rb +0 -45
- data/lib/puma/rack/backports/uri/common_193.rb +0 -33
- data/puma.gemspec +0 -52
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
|
@@ -7,8 +9,8 @@ class IO
|
|
7
9
|
end
|
8
10
|
|
9
11
|
require 'puma/detect'
|
10
|
-
require 'puma/delegation'
|
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,9 +23,41 @@ module Puma
|
|
21
23
|
|
22
24
|
class ConnectionError < RuntimeError; end
|
23
25
|
|
26
|
+
class HttpParserError501 < IOError; end
|
27
|
+
|
28
|
+
# An instance of this class represents a unique request from a client.
|
29
|
+
# For example, this could be a web request from a browser or from CURL.
|
30
|
+
#
|
31
|
+
# An instance of `Puma::Client` can be used as if it were an IO object
|
32
|
+
# by the reactor. The reactor is expected to call `#to_io`
|
33
|
+
# on any non-IO objects it polls. For example, nio4r internally calls
|
34
|
+
# `IO::try_convert` (which may call `#to_io`) when a new socket is
|
35
|
+
# registered.
|
36
|
+
#
|
37
|
+
# Instances of this class are responsible for knowing if
|
38
|
+
# the header and body are fully buffered via the `try_to_finish` method.
|
39
|
+
# They can be used to "time out" a response via the `timeout_at` reader.
|
40
|
+
#
|
24
41
|
class Client
|
42
|
+
|
43
|
+
# this tests all values but the last, which must be chunked
|
44
|
+
ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
|
45
|
+
|
46
|
+
# chunked body validation
|
47
|
+
CHUNK_SIZE_INVALID = /[^\h]/.freeze
|
48
|
+
CHUNK_VALID_ENDING = "\r\n".freeze
|
49
|
+
|
50
|
+
# Content-Length header value validation
|
51
|
+
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
52
|
+
|
53
|
+
TE_ERR_MSG = 'Invalid Transfer-Encoding'
|
54
|
+
|
55
|
+
# The object used for a request with no body. All requests with
|
56
|
+
# no body share this one object since it has no state.
|
57
|
+
EmptyBody = NullIO.new
|
58
|
+
|
25
59
|
include Puma::Const
|
26
|
-
extend
|
60
|
+
extend Forwardable
|
27
61
|
|
28
62
|
def initialize(io, env=nil)
|
29
63
|
@io = io
|
@@ -41,6 +75,7 @@ module Puma
|
|
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
|
|
@@ -51,6 +86,10 @@ module Puma
|
|
51
86
|
|
52
87
|
@peerip = nil
|
53
88
|
@remote_addr_header = nil
|
89
|
+
|
90
|
+
@body_remain = 0
|
91
|
+
|
92
|
+
@in_last_chunk = false
|
54
93
|
end
|
55
94
|
|
56
95
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
@@ -60,7 +99,7 @@ module Puma
|
|
60
99
|
|
61
100
|
attr_accessor :remote_addr_header
|
62
101
|
|
63
|
-
|
102
|
+
def_delegators :@io, :closed?
|
64
103
|
|
65
104
|
def inspect
|
66
105
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
@@ -89,6 +128,9 @@ module Puma
|
|
89
128
|
@tempfile = nil
|
90
129
|
@parsed_bytes = 0
|
91
130
|
@ready = false
|
131
|
+
@body_remain = 0
|
132
|
+
@peerip = nil
|
133
|
+
@in_last_chunk = false
|
92
134
|
|
93
135
|
if @buffer
|
94
136
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
@@ -101,175 +143,25 @@ module Puma
|
|
101
143
|
end
|
102
144
|
|
103
145
|
return false
|
104
|
-
elsif fast_check &&
|
105
|
-
IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
|
106
|
-
return try_to_finish
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
def close
|
111
|
-
begin
|
112
|
-
@io.close
|
113
|
-
rescue IOError
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
# The object used for a request with no body. All requests with
|
118
|
-
# no body share this one object since it has no state.
|
119
|
-
EmptyBody = NullIO.new
|
120
|
-
|
121
|
-
def setup_chunked_body(body)
|
122
|
-
@chunked_body = true
|
123
|
-
@partial_part_left = 0
|
124
|
-
@prev_chunk = ""
|
125
|
-
|
126
|
-
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
127
|
-
@body.binmode
|
128
|
-
@tempfile = @body
|
129
|
-
|
130
|
-
return decode_chunk(body)
|
131
|
-
end
|
132
|
-
|
133
|
-
def decode_chunk(chunk)
|
134
|
-
if @partial_part_left > 0
|
135
|
-
if @partial_part_left <= chunk.size
|
136
|
-
@body << chunk[0..(@partial_part_left-3)] # skip the \r\n
|
137
|
-
chunk = chunk[@partial_part_left..-1]
|
138
|
-
else
|
139
|
-
@body << chunk
|
140
|
-
@partial_part_left -= chunk.size
|
141
|
-
return false
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
if @prev_chunk.empty?
|
146
|
-
io = StringIO.new(chunk)
|
147
146
|
else
|
148
|
-
io = StringIO.new(@prev_chunk+chunk)
|
149
|
-
@prev_chunk = ""
|
150
|
-
end
|
151
|
-
|
152
|
-
while !io.eof?
|
153
|
-
line = io.gets
|
154
|
-
if line.end_with?("\r\n")
|
155
|
-
len = line.strip.to_i(16)
|
156
|
-
if len == 0
|
157
|
-
@body.rewind
|
158
|
-
rest = io.read
|
159
|
-
@buffer = rest.empty? ? nil : rest
|
160
|
-
@requests_served += 1
|
161
|
-
@ready = true
|
162
|
-
return true
|
163
|
-
end
|
164
|
-
|
165
|
-
len += 2
|
166
|
-
|
167
|
-
part = io.read(len)
|
168
|
-
|
169
|
-
unless part
|
170
|
-
@partial_part_left = len
|
171
|
-
next
|
172
|
-
end
|
173
|
-
|
174
|
-
got = part.size
|
175
|
-
|
176
|
-
case
|
177
|
-
when got == len
|
178
|
-
@body << part[0..-3] # to skip the ending \r\n
|
179
|
-
when got <= len - 2
|
180
|
-
@body << part
|
181
|
-
@partial_part_left = len - part.size
|
182
|
-
when got == len - 1 # edge where we get just \r but not \n
|
183
|
-
@body << part[0..-2]
|
184
|
-
@partial_part_left = len - part.size
|
185
|
-
end
|
186
|
-
else
|
187
|
-
@prev_chunk = line
|
188
|
-
return false
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
return false
|
193
|
-
end
|
194
|
-
|
195
|
-
def read_chunked_body
|
196
|
-
while true
|
197
147
|
begin
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
# No chunk means a closed socket
|
206
|
-
unless chunk
|
207
|
-
@body.close
|
208
|
-
@buffer = nil
|
209
|
-
@requests_served += 1
|
210
|
-
@ready = true
|
211
|
-
raise EOFError
|
148
|
+
if fast_check &&
|
149
|
+
IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
|
150
|
+
return try_to_finish
|
151
|
+
end
|
152
|
+
rescue IOError
|
153
|
+
# swallow it
|
212
154
|
end
|
213
155
|
|
214
|
-
return true if decode_chunk(chunk)
|
215
156
|
end
|
216
157
|
end
|
217
158
|
|
218
|
-
def
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
@io.flush
|
224
|
-
end
|
225
|
-
|
226
|
-
@read_header = false
|
227
|
-
|
228
|
-
body = @parser.body
|
229
|
-
|
230
|
-
te = @env[TRANSFER_ENCODING2]
|
231
|
-
|
232
|
-
if te && CHUNKED.casecmp(te) == 0
|
233
|
-
return setup_chunked_body(body)
|
234
|
-
end
|
235
|
-
|
236
|
-
@chunked_body = false
|
237
|
-
|
238
|
-
cl = @env[CONTENT_LENGTH]
|
239
|
-
|
240
|
-
unless cl
|
241
|
-
@buffer = body.empty? ? nil : body
|
242
|
-
@body = EmptyBody
|
243
|
-
@requests_served += 1
|
244
|
-
@ready = true
|
245
|
-
return true
|
246
|
-
end
|
247
|
-
|
248
|
-
remain = cl.to_i - body.bytesize
|
249
|
-
|
250
|
-
if remain <= 0
|
251
|
-
@body = StringIO.new(body)
|
252
|
-
@buffer = nil
|
253
|
-
@requests_served += 1
|
254
|
-
@ready = true
|
255
|
-
return true
|
256
|
-
end
|
257
|
-
|
258
|
-
if remain > MAX_BODY
|
259
|
-
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
260
|
-
@body.binmode
|
261
|
-
@tempfile = @body
|
262
|
-
else
|
263
|
-
# The body[0,0] trick is to get an empty string in the same
|
264
|
-
# encoding as body.
|
265
|
-
@body = StringIO.new body[0,0]
|
159
|
+
def close
|
160
|
+
begin
|
161
|
+
@io.close
|
162
|
+
rescue IOError
|
163
|
+
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
|
266
164
|
end
|
267
|
-
|
268
|
-
@body.write body
|
269
|
-
|
270
|
-
@body_remain = remain
|
271
|
-
|
272
|
-
return false
|
273
165
|
end
|
274
166
|
|
275
167
|
def try_to_finish
|
@@ -277,12 +169,19 @@ module Puma
|
|
277
169
|
|
278
170
|
begin
|
279
171
|
data = @io.read_nonblock(CHUNK_SIZE)
|
280
|
-
rescue
|
172
|
+
rescue IO::WaitReadable
|
281
173
|
return false
|
282
|
-
rescue SystemCallError, IOError
|
174
|
+
rescue SystemCallError, IOError, EOFError
|
283
175
|
raise ConnectionError, "Connection error detected during read"
|
284
176
|
end
|
285
177
|
|
178
|
+
# No data means a closed socket
|
179
|
+
unless data
|
180
|
+
@buffer = nil
|
181
|
+
set_ready
|
182
|
+
raise EOFError
|
183
|
+
end
|
184
|
+
|
286
185
|
if @buffer
|
287
186
|
@buffer << data
|
288
187
|
else
|
@@ -312,6 +211,13 @@ module Puma
|
|
312
211
|
raise e
|
313
212
|
end
|
314
213
|
|
214
|
+
# No data means a closed socket
|
215
|
+
unless data
|
216
|
+
@buffer = nil
|
217
|
+
set_ready
|
218
|
+
raise EOFError
|
219
|
+
end
|
220
|
+
|
315
221
|
if @buffer
|
316
222
|
@buffer << data
|
317
223
|
else
|
@@ -358,6 +264,108 @@ module Puma
|
|
358
264
|
true
|
359
265
|
end
|
360
266
|
|
267
|
+
def write_error(status_code)
|
268
|
+
begin
|
269
|
+
@io << ERROR_RESPONSE[status_code]
|
270
|
+
rescue StandardError
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def peerip
|
275
|
+
return @peerip if @peerip
|
276
|
+
|
277
|
+
if @remote_addr_header
|
278
|
+
hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
|
279
|
+
@peerip = hdr
|
280
|
+
return hdr
|
281
|
+
end
|
282
|
+
|
283
|
+
@peerip ||= @io.peeraddr.last
|
284
|
+
end
|
285
|
+
|
286
|
+
private
|
287
|
+
|
288
|
+
def setup_body
|
289
|
+
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
290
|
+
|
291
|
+
if @env[HTTP_EXPECT] == CONTINUE
|
292
|
+
# TODO allow a hook here to check the headers before
|
293
|
+
# going forward
|
294
|
+
@io << HTTP_11_100
|
295
|
+
@io.flush
|
296
|
+
end
|
297
|
+
|
298
|
+
@read_header = false
|
299
|
+
|
300
|
+
body = @parser.body
|
301
|
+
|
302
|
+
te = @env[TRANSFER_ENCODING2]
|
303
|
+
if te
|
304
|
+
te_lwr = te.downcase
|
305
|
+
if te.include? ','
|
306
|
+
te_ary = te_lwr.split ','
|
307
|
+
te_count = te_ary.count CHUNKED
|
308
|
+
te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
|
309
|
+
if te_ary.last == CHUNKED && te_count == 1 && te_valid
|
310
|
+
@env.delete TRANSFER_ENCODING2
|
311
|
+
return setup_chunked_body body
|
312
|
+
elsif te_count >= 1
|
313
|
+
raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
|
314
|
+
elsif !te_valid
|
315
|
+
raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
316
|
+
end
|
317
|
+
elsif te_lwr == CHUNKED
|
318
|
+
@env.delete TRANSFER_ENCODING2
|
319
|
+
return setup_chunked_body body
|
320
|
+
elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
|
321
|
+
raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
|
322
|
+
else
|
323
|
+
raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
@chunked_body = false
|
328
|
+
|
329
|
+
cl = @env[CONTENT_LENGTH]
|
330
|
+
|
331
|
+
if cl
|
332
|
+
# cannot contain characters that are not \d
|
333
|
+
if cl =~ CONTENT_LENGTH_VALUE_INVALID
|
334
|
+
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
335
|
+
end
|
336
|
+
else
|
337
|
+
@buffer = body.empty? ? nil : body
|
338
|
+
@body = EmptyBody
|
339
|
+
set_ready
|
340
|
+
return true
|
341
|
+
end
|
342
|
+
|
343
|
+
remain = cl.to_i - body.bytesize
|
344
|
+
|
345
|
+
if remain <= 0
|
346
|
+
@body = StringIO.new(body)
|
347
|
+
@buffer = nil
|
348
|
+
set_ready
|
349
|
+
return true
|
350
|
+
end
|
351
|
+
|
352
|
+
if remain > MAX_BODY
|
353
|
+
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
354
|
+
@body.binmode
|
355
|
+
@tempfile = @body
|
356
|
+
else
|
357
|
+
# The body[0,0] trick is to get an empty string in the same
|
358
|
+
# encoding as body.
|
359
|
+
@body = StringIO.new body[0,0]
|
360
|
+
end
|
361
|
+
|
362
|
+
@body.write body
|
363
|
+
|
364
|
+
@body_remain = remain
|
365
|
+
|
366
|
+
return false
|
367
|
+
end
|
368
|
+
|
361
369
|
def read_body
|
362
370
|
if @chunked_body
|
363
371
|
return read_chunked_body
|
@@ -375,7 +383,7 @@ module Puma
|
|
375
383
|
|
376
384
|
begin
|
377
385
|
chunk = @io.read_nonblock(want)
|
378
|
-
rescue
|
386
|
+
rescue IO::WaitReadable
|
379
387
|
return false
|
380
388
|
rescue SystemCallError, IOError
|
381
389
|
raise ConnectionError, "Connection error detected during read"
|
@@ -385,8 +393,7 @@ module Puma
|
|
385
393
|
unless chunk
|
386
394
|
@body.close
|
387
395
|
@buffer = nil
|
388
|
-
|
389
|
-
@ready = true
|
396
|
+
set_ready
|
390
397
|
raise EOFError
|
391
398
|
end
|
392
399
|
|
@@ -395,8 +402,7 @@ module Puma
|
|
395
402
|
if remain <= 0
|
396
403
|
@body.rewind
|
397
404
|
@buffer = nil
|
398
|
-
|
399
|
-
@ready = true
|
405
|
+
set_ready
|
400
406
|
return true
|
401
407
|
end
|
402
408
|
|
@@ -405,37 +411,147 @@ module Puma
|
|
405
411
|
false
|
406
412
|
end
|
407
413
|
|
408
|
-
def
|
409
|
-
|
410
|
-
|
411
|
-
|
414
|
+
def read_chunked_body
|
415
|
+
while true
|
416
|
+
begin
|
417
|
+
chunk = @io.read_nonblock(4096)
|
418
|
+
rescue IO::WaitReadable
|
419
|
+
return false
|
420
|
+
rescue SystemCallError, IOError
|
421
|
+
raise ConnectionError, "Connection error detected during read"
|
422
|
+
end
|
423
|
+
|
424
|
+
# No chunk means a closed socket
|
425
|
+
unless chunk
|
426
|
+
@body.close
|
427
|
+
@buffer = nil
|
428
|
+
set_ready
|
429
|
+
raise EOFError
|
430
|
+
end
|
431
|
+
|
432
|
+
if decode_chunk(chunk)
|
433
|
+
@env[CONTENT_LENGTH] = @chunked_content_length
|
434
|
+
return true
|
435
|
+
end
|
412
436
|
end
|
413
437
|
end
|
414
438
|
|
415
|
-
def
|
416
|
-
|
417
|
-
|
418
|
-
|
439
|
+
def setup_chunked_body(body)
|
440
|
+
@chunked_body = true
|
441
|
+
@partial_part_left = 0
|
442
|
+
@prev_chunk = ""
|
443
|
+
|
444
|
+
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
445
|
+
@body.binmode
|
446
|
+
@tempfile = @body
|
447
|
+
|
448
|
+
@chunked_content_length = 0
|
449
|
+
|
450
|
+
if decode_chunk(body)
|
451
|
+
@env[CONTENT_LENGTH] = @chunked_content_length
|
452
|
+
return true
|
419
453
|
end
|
420
454
|
end
|
421
455
|
|
422
|
-
def
|
423
|
-
|
424
|
-
@io << ERROR_500_RESPONSE
|
425
|
-
rescue StandardError
|
426
|
-
end
|
456
|
+
def write_chunk(str)
|
457
|
+
@chunked_content_length += @body.write(str)
|
427
458
|
end
|
428
459
|
|
429
|
-
def
|
430
|
-
|
460
|
+
def decode_chunk(chunk)
|
461
|
+
if @partial_part_left > 0
|
462
|
+
if @partial_part_left <= chunk.size
|
463
|
+
if @partial_part_left > 2
|
464
|
+
write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
|
465
|
+
end
|
466
|
+
chunk = chunk[@partial_part_left..-1]
|
467
|
+
@partial_part_left = 0
|
468
|
+
else
|
469
|
+
write_chunk(chunk) if @partial_part_left > 2 # don't include the last \r\n
|
470
|
+
@partial_part_left -= chunk.size
|
471
|
+
return false
|
472
|
+
end
|
473
|
+
end
|
431
474
|
|
432
|
-
if @
|
433
|
-
|
434
|
-
|
435
|
-
|
475
|
+
if @prev_chunk.empty?
|
476
|
+
io = StringIO.new(chunk)
|
477
|
+
else
|
478
|
+
io = StringIO.new(@prev_chunk+chunk)
|
479
|
+
@prev_chunk = ""
|
436
480
|
end
|
437
481
|
|
438
|
-
|
482
|
+
while !io.eof?
|
483
|
+
line = io.gets
|
484
|
+
if line.end_with?("\r\n")
|
485
|
+
# Puma doesn't process chunk extensions, but should parse if they're
|
486
|
+
# present, which is the reason for the semicolon regex
|
487
|
+
chunk_hex = line.strip[/\A[^;]+/]
|
488
|
+
if chunk_hex =~ CHUNK_SIZE_INVALID
|
489
|
+
raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
|
490
|
+
end
|
491
|
+
len = chunk_hex.to_i(16)
|
492
|
+
if len == 0
|
493
|
+
@in_last_chunk = true
|
494
|
+
@body.rewind
|
495
|
+
rest = io.read
|
496
|
+
last_crlf_size = "\r\n".bytesize
|
497
|
+
if rest.bytesize < last_crlf_size
|
498
|
+
@buffer = nil
|
499
|
+
@partial_part_left = last_crlf_size - rest.bytesize
|
500
|
+
return false
|
501
|
+
else
|
502
|
+
@buffer = rest[last_crlf_size..-1]
|
503
|
+
@buffer = nil if @buffer.empty?
|
504
|
+
set_ready
|
505
|
+
return true
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
len += 2
|
510
|
+
|
511
|
+
part = io.read(len)
|
512
|
+
|
513
|
+
unless part
|
514
|
+
@partial_part_left = len
|
515
|
+
next
|
516
|
+
end
|
517
|
+
|
518
|
+
got = part.size
|
519
|
+
|
520
|
+
case
|
521
|
+
when got == len
|
522
|
+
# proper chunked segment must end with "\r\n"
|
523
|
+
if part.end_with? CHUNK_VALID_ENDING
|
524
|
+
write_chunk(part[0..-3]) # to skip the ending \r\n
|
525
|
+
else
|
526
|
+
raise HttpParserError, "Chunk size mismatch"
|
527
|
+
end
|
528
|
+
when got <= len - 2
|
529
|
+
write_chunk(part)
|
530
|
+
@partial_part_left = len - part.size
|
531
|
+
when got == len - 1 # edge where we get just \r but not \n
|
532
|
+
write_chunk(part[0..-2])
|
533
|
+
@partial_part_left = len - part.size
|
534
|
+
end
|
535
|
+
else
|
536
|
+
@prev_chunk = line
|
537
|
+
return false
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
if @in_last_chunk
|
542
|
+
set_ready
|
543
|
+
true
|
544
|
+
else
|
545
|
+
false
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
def set_ready
|
550
|
+
if @body_read_start
|
551
|
+
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
|
552
|
+
end
|
553
|
+
@requests_served += 1
|
554
|
+
@ready = true
|
439
555
|
end
|
440
556
|
end
|
441
557
|
end
|