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.
- checksums.yaml +5 -5
- data/History.md +1708 -422
- data/LICENSE +23 -20
- data/README.md +190 -64
- data/bin/puma-wild +3 -9
- data/docs/architecture.md +59 -21
- data/docs/compile_options.md +55 -0
- data/docs/deployment.md +69 -58
- data/docs/fork_worker.md +31 -0
- 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/jungle/README.md +9 -0
- data/docs/jungle/rc.d/README.md +74 -0
- data/docs/jungle/rc.d/puma +61 -0
- data/docs/jungle/rc.d/puma.conf +10 -0
- data/docs/kubernetes.md +66 -0
- data/docs/nginx.md +1 -1
- data/docs/plugins.md +22 -12
- data/docs/rails_dev_mode.md +28 -0
- data/docs/restart.md +47 -22
- data/docs/signals.md +13 -11
- data/docs/stats.md +142 -0
- data/docs/systemd.md +100 -115
- data/docs/testing_benchmarks_local_files.md +150 -0
- data/docs/testing_test_rackup_ci_files.md +36 -0
- data/ext/puma_http11/PumaHttp11Service.java +2 -2
- data/ext/puma_http11/ext_help.h +1 -1
- data/ext/puma_http11/extconf.rb +61 -3
- data/ext/puma_http11/http11_parser.c +106 -118
- data/ext/puma_http11/http11_parser.h +2 -2
- data/ext/puma_http11/http11_parser.java.rl +22 -38
- data/ext/puma_http11/http11_parser.rl +6 -4
- data/ext/puma_http11/http11_parser_common.rl +6 -6
- data/ext/puma_http11/mini_ssl.c +376 -93
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +84 -99
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +250 -88
- data/ext/puma_http11/puma_http11.c +49 -57
- data/lib/puma/app/status.rb +71 -49
- data/lib/puma/binder.rb +243 -148
- data/lib/puma/cli.rb +50 -35
- data/lib/puma/client.rb +369 -232
- data/lib/puma/cluster/worker.rb +175 -0
- data/lib/puma/cluster/worker_handle.rb +97 -0
- data/lib/puma/cluster.rb +268 -235
- data/lib/puma/commonlogger.rb +4 -2
- data/lib/puma/configuration.rb +116 -88
- data/lib/puma/const.rb +49 -30
- data/lib/puma/control_cli.rb +124 -77
- data/lib/puma/detect.rb +33 -2
- data/lib/puma/dsl.rb +685 -138
- data/lib/puma/error_logger.rb +112 -0
- data/lib/puma/events.rb +17 -111
- data/lib/puma/io_buffer.rb +34 -5
- data/lib/puma/jruby_restart.rb +4 -59
- data/lib/puma/json_serialization.rb +96 -0
- data/lib/puma/launcher/bundle_pruner.rb +104 -0
- data/lib/puma/launcher.rb +197 -130
- data/lib/puma/log_writer.rb +137 -0
- data/lib/puma/minissl/context_builder.rb +92 -0
- data/lib/puma/minissl.rb +256 -70
- data/lib/puma/null_io.rb +20 -1
- data/lib/puma/plugin/tmp_restart.rb +3 -1
- data/lib/puma/plugin.rb +9 -13
- data/lib/puma/rack/builder.rb +8 -9
- data/lib/puma/rack/urlmap.rb +2 -0
- data/lib/puma/rack_default.rb +3 -1
- data/lib/puma/reactor.rb +90 -187
- data/lib/puma/request.rb +607 -0
- data/lib/puma/runner.rb +94 -71
- data/lib/puma/server.rb +336 -703
- data/lib/puma/single.rb +27 -72
- data/lib/puma/state_file.rb +46 -7
- data/lib/puma/systemd.rb +47 -0
- data/lib/puma/thread_pool.rb +185 -91
- data/lib/puma/util.rb +23 -10
- data/lib/puma.rb +68 -3
- data/lib/rack/handler/puma.rb +17 -14
- data/tools/Dockerfile +16 -0
- data/tools/trickletest.rb +0 -1
- metadata +53 -30
- data/ext/puma_http11/io_buffer.c +0 -155
- data/lib/puma/accept_nonblock.rb +0 -23
- 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/lib/puma/tcp_logger.rb +0 -39
- data/tools/jungle/README.md +0 -13
- data/tools/jungle/init.d/README.md +0 -59
- data/tools/jungle/init.d/puma +0 -421
- data/tools/jungle/init.d/run-puma +0 -18
- data/tools/jungle/upstart/README.md +0 -61
- data/tools/jungle/upstart/puma-manager.conf +0 -31
- 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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
127
|
+
!(@read_header || @read_proxy)
|
78
128
|
end
|
79
129
|
|
80
130
|
def set_timeout(val)
|
81
|
-
@timeout_at =
|
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
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
180
|
+
rescue IOError, Errno::EBADF
|
181
|
+
Puma::Util.purge_interrupt_queue
|
115
182
|
end
|
116
183
|
end
|
117
184
|
|
118
|
-
#
|
119
|
-
#
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
128
|
-
|
129
|
-
@tempfile = @body
|
209
|
+
def try_to_finish
|
210
|
+
return read_body if in_data_phase
|
130
211
|
|
131
|
-
|
132
|
-
|
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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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 @
|
147
|
-
|
229
|
+
if @buffer
|
230
|
+
@buffer << data
|
148
231
|
else
|
149
|
-
|
150
|
-
@prev_chunk = ""
|
232
|
+
@buffer = data
|
151
233
|
end
|
152
234
|
|
153
|
-
|
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
|
-
|
237
|
+
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
167
238
|
|
168
|
-
|
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
|
-
|
171
|
-
|
172
|
-
next
|
173
|
-
end
|
246
|
+
false
|
247
|
+
end
|
174
248
|
|
175
|
-
|
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
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
281
|
+
@peerip ||= @io.peeraddr.last
|
194
282
|
end
|
195
283
|
|
196
|
-
def
|
197
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
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
|
-
|
234
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
426
|
-
|
427
|
-
|
428
|
-
|
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
|
433
|
-
|
434
|
-
|
435
|
-
|
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
|
-
|
440
|
-
|
441
|
-
|
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
|
447
|
-
|
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 @
|
450
|
-
|
451
|
-
|
452
|
-
|
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
|
-
|
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
|