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.
- checksums.yaml +4 -4
- data/History.md +1806 -451
- data/LICENSE +23 -20
- data/README.md +217 -65
- 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/{tools → docs}/jungle/rc.d/README.md +1 -1
- data/{tools → docs}/jungle/rc.d/puma +2 -2
- data/docs/kubernetes.md +66 -0
- data/docs/nginx.md +2 -2
- 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 +94 -120
- 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 +103 -117
- 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 +3 -3
- data/ext/puma_http11/http11_parser_common.rl +6 -6
- data/ext/puma_http11/mini_ssl.c +389 -99
- 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 +248 -92
- data/ext/puma_http11/puma_http11.c +49 -57
- data/lib/puma/app/status.rb +71 -49
- data/lib/puma/binder.rb +244 -150
- data/lib/puma/cli.rb +38 -34
- data/lib/puma/client.rb +388 -244
- data/lib/puma/cluster/worker.rb +180 -0
- data/lib/puma/cluster/worker_handle.rb +97 -0
- data/lib/puma/cluster.rb +261 -243
- data/lib/puma/commonlogger.rb +21 -14
- data/lib/puma/configuration.rb +116 -88
- data/lib/puma/const.rb +154 -104
- data/lib/puma/control_cli.rb +115 -70
- data/lib/puma/detect.rb +33 -2
- data/lib/puma/dsl.rb +764 -134
- data/lib/puma/error_logger.rb +113 -0
- data/lib/puma/events.rb +16 -112
- data/lib/puma/io_buffer.rb +42 -5
- data/lib/puma/jruby_restart.rb +2 -59
- data/lib/puma/json_serialization.rb +96 -0
- data/lib/puma/launcher/bundle_pruner.rb +104 -0
- data/lib/puma/launcher.rb +184 -133
- data/lib/puma/log_writer.rb +147 -0
- data/lib/puma/minissl/context_builder.rb +93 -0
- data/lib/puma/minissl.rb +263 -70
- data/lib/puma/null_io.rb +18 -1
- data/lib/puma/plugin/systemd.rb +90 -0
- data/lib/puma/plugin/tmp_restart.rb +3 -1
- data/lib/puma/plugin.rb +7 -13
- data/lib/puma/rack/builder.rb +9 -11
- data/lib/puma/rack/urlmap.rb +2 -0
- data/lib/puma/rack_default.rb +21 -4
- data/lib/puma/reactor.rb +93 -315
- data/lib/puma/request.rb +671 -0
- data/lib/puma/runner.rb +94 -69
- data/lib/puma/sd_notify.rb +149 -0
- data/lib/puma/server.rb +327 -772
- data/lib/puma/single.rb +20 -74
- data/lib/puma/state_file.rb +45 -8
- data/lib/puma/thread_pool.rb +146 -92
- data/lib/puma/util.rb +22 -10
- data/lib/puma.rb +60 -5
- data/lib/rack/handler/puma.rb +116 -90
- data/tools/Dockerfile +16 -0
- data/tools/trickletest.rb +0 -1
- metadata +54 -32
- 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 -25
- data/lib/puma/daemon_ext.rb +0 -33
- data/lib/puma/delegation.rb +0 -13
- data/lib/puma/java_io_buffer.rb +0 -47
- data/lib/puma/rack/backports/uri/common_193.rb +0 -33
- data/lib/puma/tcp_logger.rb +0 -41
- data/tools/jungle/README.md +0 -19
- data/tools/jungle/init.d/README.md +0 -61
- 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/{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
|
-
|
12
|
-
|
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.
|
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
|
-
#
|
31
|
-
#
|
32
|
-
#
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
135
|
+
!(@read_header || @read_proxy)
|
91
136
|
end
|
92
137
|
|
93
138
|
def set_timeout(val)
|
94
|
-
@timeout_at =
|
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
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
190
|
+
rescue IOError, Errno::EBADF
|
191
|
+
Puma::Util.purge_interrupt_queue
|
128
192
|
end
|
129
193
|
end
|
130
194
|
|
131
|
-
#
|
132
|
-
#
|
133
|
-
|
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
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
@
|
141
|
-
|
142
|
-
|
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
|
145
|
-
end
|
231
|
+
return read_body if in_data_phase
|
146
232
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
160
|
-
|
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
|
-
|
163
|
-
@prev_chunk = ""
|
253
|
+
@buffer = data
|
164
254
|
end
|
165
255
|
|
166
|
-
|
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
|
-
|
258
|
+
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
181
259
|
|
182
|
-
|
260
|
+
if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
|
261
|
+
@http_content_length_limit_exceeded = true
|
262
|
+
end
|
183
263
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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
|
-
|
271
|
+
false
|
272
|
+
end
|
190
273
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
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
|
-
|
306
|
+
@peerip ||= @io.peeraddr.last
|
208
307
|
end
|
209
308
|
|
210
|
-
def
|
211
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
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
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
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
|
255
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
448
|
-
|
449
|
-
|
450
|
-
|
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
|
455
|
-
|
456
|
-
|
457
|
-
|
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
|
-
|
462
|
-
|
463
|
-
|
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
|
469
|
-
|
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 @
|
472
|
-
|
473
|
-
|
474
|
-
|
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
|
-
|
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
|