puma 5.6.4 → 6.4.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.
- checksums.yaml +4 -4
- data/History.md +372 -6
- data/LICENSE +0 -0
- data/README.md +79 -29
- data/bin/puma-wild +1 -1
- data/docs/architecture.md +0 -0
- data/docs/compile_options.md +34 -0
- data/docs/deployment.md +0 -0
- data/docs/fork_worker.md +1 -3
- 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 +0 -0
- data/docs/jungle/rc.d/README.md +0 -0
- data/docs/jungle/rc.d/puma.conf +0 -0
- data/docs/kubernetes.md +12 -0
- data/docs/nginx.md +1 -1
- data/docs/plugins.md +0 -0
- data/docs/rails_dev_mode.md +0 -0
- data/docs/restart.md +1 -0
- data/docs/signals.md +0 -0
- data/docs/stats.md +0 -0
- data/docs/systemd.md +3 -6
- 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 +0 -0
- data/ext/puma_http11/ext_help.h +0 -0
- data/ext/puma_http11/extconf.rb +22 -10
- data/ext/puma_http11/http11_parser.c +1 -1
- data/ext/puma_http11/http11_parser.h +1 -1
- data/ext/puma_http11/http11_parser.java.rl +2 -2
- data/ext/puma_http11/http11_parser.rl +2 -2
- data/ext/puma_http11/http11_parser_common.rl +2 -2
- data/ext/puma_http11/mini_ssl.c +153 -27
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +167 -65
- data/ext/puma_http11/puma_http11.c +17 -9
- data/lib/puma/app/status.rb +7 -4
- data/lib/puma/binder.rb +51 -54
- data/lib/puma/cli.rb +16 -18
- data/lib/puma/client.rb +100 -26
- data/lib/puma/cluster/worker.rb +18 -11
- data/lib/puma/cluster/worker_handle.rb +4 -1
- data/lib/puma/cluster.rb +102 -40
- data/lib/puma/commonlogger.rb +21 -14
- data/lib/puma/configuration.rb +77 -59
- data/lib/puma/const.rb +129 -92
- data/lib/puma/control_cli.rb +33 -23
- data/lib/puma/detect.rb +7 -4
- data/lib/puma/dsl.rb +251 -53
- data/lib/puma/error_logger.rb +18 -9
- data/lib/puma/events.rb +6 -126
- data/lib/puma/io_buffer.rb +39 -4
- data/lib/puma/jruby_restart.rb +2 -1
- data/lib/puma/json_serialization.rb +0 -0
- data/lib/puma/launcher/bundle_pruner.rb +104 -0
- data/lib/puma/launcher.rb +113 -175
- data/lib/puma/log_writer.rb +147 -0
- data/lib/puma/minissl/context_builder.rb +26 -12
- data/lib/puma/minissl.rb +113 -15
- data/lib/puma/null_io.rb +21 -2
- data/lib/puma/plugin/systemd.rb +90 -0
- data/lib/puma/plugin/tmp_restart.rb +1 -1
- data/lib/puma/plugin.rb +0 -0
- data/lib/puma/rack/builder.rb +6 -6
- data/lib/puma/rack/urlmap.rb +1 -1
- data/lib/puma/rack_default.rb +19 -4
- data/lib/puma/reactor.rb +19 -10
- data/lib/puma/request.rb +365 -166
- data/lib/puma/runner.rb +56 -20
- data/lib/puma/sd_notify.rb +149 -0
- data/lib/puma/server.rb +137 -87
- data/lib/puma/single.rb +13 -11
- data/lib/puma/state_file.rb +4 -6
- data/lib/puma/thread_pool.rb +57 -19
- data/lib/puma/util.rb +12 -14
- data/lib/puma.rb +12 -11
- data/lib/rack/handler/puma.rb +113 -86
- data/tools/Dockerfile +2 -2
- data/tools/trickletest.rb +0 -0
- metadata +11 -6
- data/lib/puma/queue_close.rb +0 -26
- data/lib/puma/systemd.rb +0 -46
data/lib/puma/binder.rb
CHANGED
@@ -3,24 +3,15 @@
|
|
3
3
|
require 'uri'
|
4
4
|
require 'socket'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
require_relative 'const'
|
7
|
+
require_relative 'util'
|
8
|
+
require_relative 'configuration'
|
9
9
|
|
10
10
|
module Puma
|
11
11
|
|
12
12
|
if HAS_SSL
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
# Odd bug in 'pure Ruby' nio4r version 2.5.2, which installs with Ruby 2.3.
|
17
|
-
# NIO doesn't create any OpenSSL objects, but it rescues an OpenSSL error.
|
18
|
-
# The bug was that it did not require openssl.
|
19
|
-
# @todo remove when Ruby 2.3 support is dropped
|
20
|
-
#
|
21
|
-
if windows? && RbConfig::CONFIG['ruby_version'] == '2.3.0'
|
22
|
-
require 'openssl'
|
23
|
-
end
|
13
|
+
require_relative 'minissl'
|
14
|
+
require_relative 'minissl/context_builder'
|
24
15
|
end
|
25
16
|
|
26
17
|
class Binder
|
@@ -28,8 +19,8 @@ module Puma
|
|
28
19
|
|
29
20
|
RACK_VERSION = [1,6].freeze
|
30
21
|
|
31
|
-
def initialize(
|
32
|
-
@
|
22
|
+
def initialize(log_writer, conf = Configuration.new)
|
23
|
+
@log_writer = log_writer
|
33
24
|
@conf = conf
|
34
25
|
@listeners = []
|
35
26
|
@inherited_fds = {}
|
@@ -38,7 +29,7 @@ module Puma
|
|
38
29
|
|
39
30
|
@proto_env = {
|
40
31
|
"rack.version".freeze => RACK_VERSION,
|
41
|
-
"rack.errors".freeze =>
|
32
|
+
"rack.errors".freeze => log_writer.stderr,
|
42
33
|
"rack.multithread".freeze => conf.options[:max_threads] > 1,
|
43
34
|
"rack.multiprocess".freeze => conf.options[:workers] >= 1,
|
44
35
|
"rack.run_once".freeze => false,
|
@@ -51,14 +42,12 @@ module Puma
|
|
51
42
|
# infer properly.
|
52
43
|
|
53
44
|
"QUERY_STRING".freeze => "",
|
54
|
-
SERVER_PROTOCOL => HTTP_11,
|
55
45
|
SERVER_SOFTWARE => PUMA_SERVER_STRING,
|
56
46
|
GATEWAY_INTERFACE => CGI_VER
|
57
47
|
}
|
58
48
|
|
59
49
|
@envs = {}
|
60
50
|
@ios = []
|
61
|
-
localhost_authority
|
62
51
|
end
|
63
52
|
|
64
53
|
attr_reader :ios
|
@@ -80,7 +69,7 @@ module Puma
|
|
80
69
|
# @!attribute [r] connected_ports
|
81
70
|
# @version 5.0.0
|
82
71
|
def connected_ports
|
83
|
-
ios.map { |io| io.addr[1] }.uniq
|
72
|
+
t = ios.map { |io| io.addr[1] }; t.uniq!; t
|
84
73
|
end
|
85
74
|
|
86
75
|
# @version 5.0.0
|
@@ -98,7 +87,7 @@ module Puma
|
|
98
87
|
# @version 5.0.0
|
99
88
|
#
|
100
89
|
def create_activated_fds(env_hash)
|
101
|
-
@
|
90
|
+
@log_writer.debug "ENV['LISTEN_FDS'] #{ENV['LISTEN_FDS'].inspect} env_hash['LISTEN_PID'] #{env_hash['LISTEN_PID'].inspect}"
|
102
91
|
return [] unless env_hash['LISTEN_FDS'] && env_hash['LISTEN_PID'].to_i == $$
|
103
92
|
env_hash['LISTEN_FDS'].to_i.times do |index|
|
104
93
|
sock = TCPServer.for_fd(socket_activation_fd(index))
|
@@ -106,11 +95,11 @@ module Puma
|
|
106
95
|
[:unix, Socket.unpack_sockaddr_un(sock.getsockname)]
|
107
96
|
rescue ArgumentError # Try to parse as a port/ip
|
108
97
|
port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
|
109
|
-
addr = "[#{addr}]" if addr
|
98
|
+
addr = "[#{addr}]" if addr&.include? ':'
|
110
99
|
[:tcp, addr, port]
|
111
100
|
end
|
112
101
|
@activated_sockets[key] = sock
|
113
|
-
@
|
102
|
+
@log_writer.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
|
114
103
|
end
|
115
104
|
["LISTEN_FDS", "LISTEN_PID"] # Signal to remove these keys from ENV
|
116
105
|
end
|
@@ -152,29 +141,30 @@ module Puma
|
|
152
141
|
end
|
153
142
|
end
|
154
143
|
|
155
|
-
def parse(binds,
|
144
|
+
def parse(binds, log_writer = nil, log_msg = 'Listening')
|
145
|
+
log_writer ||= @log_writer
|
156
146
|
binds.each do |str|
|
157
147
|
uri = URI.parse str
|
158
148
|
case uri.scheme
|
159
149
|
when "tcp"
|
160
150
|
if fd = @inherited_fds.delete(str)
|
161
151
|
io = inherit_tcp_listener uri.host, uri.port, fd
|
162
|
-
|
152
|
+
log_writer.log "* Inherited #{str}"
|
163
153
|
elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
|
164
154
|
io = inherit_tcp_listener uri.host, uri.port, sock
|
165
|
-
|
155
|
+
log_writer.log "* Activated #{str}"
|
166
156
|
else
|
167
157
|
ios_len = @ios.length
|
168
158
|
params = Util.parse_query uri.query
|
169
159
|
|
170
|
-
|
160
|
+
low_latency = params.key?('low_latency') && params['low_latency'] != 'false'
|
171
161
|
backlog = params.fetch('backlog', 1024).to_i
|
172
162
|
|
173
|
-
io = add_tcp_listener uri.host, uri.port,
|
163
|
+
io = add_tcp_listener uri.host, uri.port, low_latency, backlog
|
174
164
|
|
175
165
|
@ios[ios_len..-1].each do |i|
|
176
166
|
addr = loc_addr_str i
|
177
|
-
|
167
|
+
log_writer.log "* #{log_msg} on http://#{addr}"
|
178
168
|
end
|
179
169
|
end
|
180
170
|
|
@@ -189,14 +179,14 @@ module Puma
|
|
189
179
|
end
|
190
180
|
|
191
181
|
if fd = @inherited_fds.delete(str)
|
192
|
-
@unix_paths << path unless abstract
|
182
|
+
@unix_paths << path unless abstract || File.exist?(path)
|
193
183
|
io = inherit_unix_listener path, fd
|
194
|
-
|
184
|
+
log_writer.log "* Inherited #{str}"
|
195
185
|
elsif sock = @activated_sockets.delete([ :unix, path ]) ||
|
196
186
|
@activated_sockets.delete([ :unix, File.realdirpath(path) ])
|
197
187
|
@unix_paths << path unless abstract || File.exist?(path)
|
198
188
|
io = inherit_unix_listener path, sock
|
199
|
-
|
189
|
+
log_writer.log "* Activated #{str}"
|
200
190
|
else
|
201
191
|
umask = nil
|
202
192
|
mode = nil
|
@@ -220,11 +210,12 @@ module Puma
|
|
220
210
|
|
221
211
|
@unix_paths << path unless abstract || File.exist?(path)
|
222
212
|
io = add_unix_listener path, umask, mode, backlog
|
223
|
-
|
213
|
+
log_writer.log "* #{log_msg} on #{str}"
|
224
214
|
end
|
225
215
|
|
226
216
|
@listeners << [str, io]
|
227
217
|
when "ssl"
|
218
|
+
cert_key = %w[cert key]
|
228
219
|
|
229
220
|
raise "Puma compiled without SSL support" unless HAS_SSL
|
230
221
|
|
@@ -233,49 +224,51 @@ module Puma
|
|
233
224
|
# If key and certs are not defined and localhost gem is required.
|
234
225
|
# localhost gem will be used for self signed
|
235
226
|
# Load localhost authority if not loaded.
|
236
|
-
|
227
|
+
# Ruby 3 `values_at` accepts an array, earlier do not
|
228
|
+
if params.values_at(*cert_key).all? { |v| v.to_s.empty? }
|
237
229
|
ctx = localhost_authority && localhost_authority_context
|
238
230
|
end
|
239
231
|
|
240
232
|
ctx ||=
|
241
233
|
begin
|
242
234
|
# Extract cert_pem and key_pem from options[:store] if present
|
243
|
-
|
244
|
-
if params[v]
|
235
|
+
cert_key.each do |v|
|
236
|
+
if params[v]&.start_with?('store:')
|
245
237
|
index = Integer(params.delete(v).split('store:').last)
|
246
238
|
params["#{v}_pem"] = @conf.options[:store][index]
|
247
239
|
end
|
248
240
|
end
|
249
|
-
MiniSSL::ContextBuilder.new(params, @
|
241
|
+
MiniSSL::ContextBuilder.new(params, @log_writer).context
|
250
242
|
end
|
251
243
|
|
252
244
|
if fd = @inherited_fds.delete(str)
|
253
|
-
|
245
|
+
log_writer.log "* Inherited #{str}"
|
254
246
|
io = inherit_ssl_listener fd, ctx
|
255
247
|
elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
|
256
248
|
io = inherit_ssl_listener sock, ctx
|
257
|
-
|
249
|
+
log_writer.log "* Activated #{str}"
|
258
250
|
else
|
259
251
|
ios_len = @ios.length
|
260
252
|
backlog = params.fetch('backlog', 1024).to_i
|
261
|
-
|
253
|
+
low_latency = params['low_latency'] != 'false'
|
254
|
+
io = add_ssl_listener uri.host, uri.port, ctx, low_latency, backlog
|
262
255
|
|
263
256
|
@ios[ios_len..-1].each do |i|
|
264
257
|
addr = loc_addr_str i
|
265
|
-
|
258
|
+
log_writer.log "* #{log_msg} on ssl://#{addr}?#{uri.query}"
|
266
259
|
end
|
267
260
|
end
|
268
261
|
|
269
262
|
@listeners << [str, io] if io
|
270
263
|
else
|
271
|
-
|
264
|
+
log_writer.error "Invalid URI: #{str}"
|
272
265
|
end
|
273
266
|
end
|
274
267
|
|
275
268
|
# If we inherited fds but didn't use them (because of a
|
276
269
|
# configuration change), then be sure to close them.
|
277
270
|
@inherited_fds.each do |str, fd|
|
278
|
-
|
271
|
+
log_writer.log "* Closing unused inherited connection: #{str}"
|
279
272
|
|
280
273
|
begin
|
281
274
|
IO.for_fd(fd).close
|
@@ -295,7 +288,7 @@ module Puma
|
|
295
288
|
fds = @ios.map(&:to_i)
|
296
289
|
@activated_sockets.each do |key, sock|
|
297
290
|
next if fds.include? sock.to_i
|
298
|
-
|
291
|
+
log_writer.log "* Closing unused activated socket: #{key.first}://#{key[1..-1].join ':'}"
|
299
292
|
begin
|
300
293
|
sock.close
|
301
294
|
rescue SystemCallError
|
@@ -319,7 +312,7 @@ module Puma
|
|
319
312
|
local_certificates_path = File.expand_path("~/.localhost")
|
320
313
|
[File.join(local_certificates_path, "localhost.key"), File.join(local_certificates_path, "localhost.crt")]
|
321
314
|
end
|
322
|
-
MiniSSL::ContextBuilder.new({ "key" => key_path, "cert" => crt_path }, @
|
315
|
+
MiniSSL::ContextBuilder.new({ "key" => key_path, "cert" => crt_path }, @log_writer).context
|
323
316
|
end
|
324
317
|
|
325
318
|
# Tell the server to listen on host +host+, port +port+.
|
@@ -337,7 +330,7 @@ module Puma
|
|
337
330
|
return
|
338
331
|
end
|
339
332
|
|
340
|
-
host = host[1..-2] if host
|
333
|
+
host = host[1..-2] if host&.start_with? '['
|
341
334
|
tcp_server = TCPServer.new(host, port)
|
342
335
|
|
343
336
|
if optimize_for_latency
|
@@ -371,7 +364,7 @@ module Puma
|
|
371
364
|
return
|
372
365
|
end
|
373
366
|
|
374
|
-
host = host[1..-2] if host
|
367
|
+
host = host[1..-2] if host&.start_with? '['
|
375
368
|
s = TCPServer.new(host, port)
|
376
369
|
if optimize_for_latency
|
377
370
|
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
@@ -457,11 +450,14 @@ module Puma
|
|
457
450
|
|
458
451
|
def close_listeners
|
459
452
|
@listeners.each do |l, io|
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
453
|
+
begin
|
454
|
+
io.close unless io.closed?
|
455
|
+
uri = URI.parse l
|
456
|
+
next unless uri.scheme == 'unix'
|
457
|
+
unix_path = "#{uri.host}#{uri.path}"
|
458
|
+
File.unlink unix_path if @unix_paths.include?(unix_path) && File.exist?(unix_path)
|
459
|
+
rescue Errno::EBADF
|
460
|
+
end
|
465
461
|
end
|
466
462
|
end
|
467
463
|
|
@@ -482,9 +478,10 @@ module Puma
|
|
482
478
|
|
483
479
|
# @!attribute [r] loopback_addresses
|
484
480
|
def loopback_addresses
|
485
|
-
Socket.ip_address_list.select do |addrinfo|
|
481
|
+
t = Socket.ip_address_list.select do |addrinfo|
|
486
482
|
addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
|
487
|
-
end
|
483
|
+
end
|
484
|
+
t.map! { |addrinfo| addrinfo.ip_address }; t.uniq!; t
|
488
485
|
end
|
489
486
|
|
490
487
|
def loc_addr_str(io)
|
data/lib/puma/cli.rb
CHANGED
@@ -3,11 +3,11 @@
|
|
3
3
|
require 'optparse'
|
4
4
|
require 'uri'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
require_relative '../puma'
|
7
|
+
require_relative 'configuration'
|
8
|
+
require_relative 'launcher'
|
9
|
+
require_relative 'const'
|
10
|
+
require_relative 'log_writer'
|
11
11
|
|
12
12
|
module Puma
|
13
13
|
class << self
|
@@ -21,19 +21,13 @@ module Puma
|
|
21
21
|
# Handles invoke a Puma::Server in a command line style.
|
22
22
|
#
|
23
23
|
class CLI
|
24
|
-
# @deprecated 6.0.0
|
25
|
-
KEYS_NOT_TO_PERSIST_IN_STATE = Launcher::KEYS_NOT_TO_PERSIST_IN_STATE
|
26
|
-
|
27
24
|
# Create a new CLI object using +argv+ as the command line
|
28
25
|
# arguments.
|
29
26
|
#
|
30
|
-
|
31
|
-
# this object will report status on.
|
32
|
-
#
|
33
|
-
def initialize(argv, events=Events.stdio)
|
27
|
+
def initialize(argv, log_writer = LogWriter.stdio, events = Events.new)
|
34
28
|
@debug = false
|
35
29
|
@argv = argv.dup
|
36
|
-
|
30
|
+
@log_writer = log_writer
|
37
31
|
@events = events
|
38
32
|
|
39
33
|
@conf = nil
|
@@ -69,7 +63,7 @@ module Puma
|
|
69
63
|
end
|
70
64
|
end
|
71
65
|
|
72
|
-
@launcher = Puma::Launcher.new(@conf, :events => @events, :argv => argv)
|
66
|
+
@launcher = Puma::Launcher.new(@conf, :log_writer => @log_writer, :events => @events, :argv => argv)
|
73
67
|
end
|
74
68
|
|
75
69
|
attr_reader :launcher
|
@@ -83,7 +77,7 @@ module Puma
|
|
83
77
|
|
84
78
|
private
|
85
79
|
def unsupported(str)
|
86
|
-
@
|
80
|
+
@log_writer.error(str)
|
87
81
|
raise UnsupportedOption
|
88
82
|
end
|
89
83
|
|
@@ -99,7 +93,7 @@ module Puma
|
|
99
93
|
#
|
100
94
|
|
101
95
|
def setup_options
|
102
|
-
@conf = Configuration.new do |user_config, file_config|
|
96
|
+
@conf = Configuration.new({}, {events: @events}) do |user_config, file_config|
|
103
97
|
@parser = OptionParser.new do |o|
|
104
98
|
o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
|
105
99
|
user_config.bind arg
|
@@ -150,9 +144,13 @@ module Puma
|
|
150
144
|
$LOAD_PATH.unshift(*arg.split(':'))
|
151
145
|
end
|
152
146
|
|
147
|
+
o.on "--idle-timeout SECONDS", "Number of seconds until the next request before automatic shutdown" do |arg|
|
148
|
+
user_config.idle_timeout arg
|
149
|
+
end
|
150
|
+
|
153
151
|
o.on "-p", "--port PORT", "Define the TCP port to bind to",
|
154
152
|
"Use -b for more advanced options" do |arg|
|
155
|
-
user_config.bind "tcp://#{Configuration::
|
153
|
+
user_config.bind "tcp://#{Configuration::DEFAULTS[:tcp_host]}:#{arg}"
|
156
154
|
end
|
157
155
|
|
158
156
|
o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg|
|
@@ -186,7 +184,7 @@ module Puma
|
|
186
184
|
end
|
187
185
|
|
188
186
|
o.on "-s", "--silent", "Do not log prompt messages other than errors" do
|
189
|
-
@
|
187
|
+
@log_writer = LogWriter.new(NullIO.new, $stderr)
|
190
188
|
end
|
191
189
|
|
192
190
|
o.on "-S", "--state PATH", "Where to store the state details" do |arg|
|
data/lib/puma/client.rb
CHANGED
@@ -8,9 +8,9 @@ class IO
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
|
11
|
+
require_relative 'detect'
|
12
|
+
require_relative 'io_buffer'
|
12
13
|
require 'tempfile'
|
13
|
-
require 'forwardable'
|
14
14
|
|
15
15
|
if Puma::IS_JRUBY
|
16
16
|
# We have to work around some OpenSSL buffer/io-readiness bugs
|
@@ -25,6 +25,9 @@ module Puma
|
|
25
25
|
|
26
26
|
class HttpParserError501 < IOError; end
|
27
27
|
|
28
|
+
#———————————————————————— DO NOT USE — this class is for internal use only ———
|
29
|
+
|
30
|
+
|
28
31
|
# An instance of this class represents a unique request from a client.
|
29
32
|
# For example, this could be a web request from a browser or from CURL.
|
30
33
|
#
|
@@ -38,14 +41,23 @@ module Puma
|
|
38
41
|
# the header and body are fully buffered via the `try_to_finish` method.
|
39
42
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
40
43
|
#
|
41
|
-
class Client
|
44
|
+
class Client # :nodoc:
|
42
45
|
|
43
46
|
# this tests all values but the last, which must be chunked
|
44
47
|
ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
|
45
48
|
|
46
49
|
# chunked body validation
|
47
50
|
CHUNK_SIZE_INVALID = /[^\h]/.freeze
|
48
|
-
CHUNK_VALID_ENDING =
|
51
|
+
CHUNK_VALID_ENDING = Const::LINE_END
|
52
|
+
CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
|
53
|
+
|
54
|
+
# The maximum number of bytes we'll buffer looking for a valid
|
55
|
+
# chunk header.
|
56
|
+
MAX_CHUNK_HEADER_SIZE = 4096
|
57
|
+
|
58
|
+
# The maximum amount of excess data the client sends
|
59
|
+
# using chunk size extensions before we abort the connection.
|
60
|
+
MAX_CHUNK_EXCESS = 16 * 1024
|
49
61
|
|
50
62
|
# Content-Length header value validation
|
51
63
|
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
@@ -57,17 +69,13 @@ module Puma
|
|
57
69
|
EmptyBody = NullIO.new
|
58
70
|
|
59
71
|
include Puma::Const
|
60
|
-
extend Forwardable
|
61
72
|
|
62
73
|
def initialize(io, env=nil)
|
63
74
|
@io = io
|
64
75
|
@to_io = io.to_io
|
76
|
+
@io_buffer = IOBuffer.new
|
65
77
|
@proto_env = env
|
66
|
-
|
67
|
-
@env = nil
|
68
|
-
else
|
69
|
-
@env = env.dup
|
70
|
-
end
|
78
|
+
@env = env&.dup
|
71
79
|
|
72
80
|
@parser = HttpParser.new
|
73
81
|
@parsed_bytes = 0
|
@@ -85,7 +93,11 @@ module Puma
|
|
85
93
|
@requests_served = 0
|
86
94
|
@hijacked = false
|
87
95
|
|
96
|
+
@http_content_length_limit = nil
|
97
|
+
@http_content_length_limit_exceeded = false
|
98
|
+
|
88
99
|
@peerip = nil
|
100
|
+
@peer_family = nil
|
89
101
|
@listener = nil
|
90
102
|
@remote_addr_header = nil
|
91
103
|
@expect_proxy_proto = false
|
@@ -93,16 +105,22 @@ module Puma
|
|
93
105
|
@body_remain = 0
|
94
106
|
|
95
107
|
@in_last_chunk = false
|
108
|
+
|
109
|
+
# need unfrozen ASCII-8BIT, +'' is UTF-8
|
110
|
+
@read_buffer = String.new # rubocop: disable Performance/UnfreezeString
|
96
111
|
end
|
97
112
|
|
98
113
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
99
|
-
:tempfile
|
114
|
+
:tempfile, :io_buffer, :http_content_length_limit_exceeded
|
100
115
|
|
101
|
-
attr_writer :peerip
|
116
|
+
attr_writer :peerip, :http_content_length_limit
|
102
117
|
|
103
118
|
attr_accessor :remote_addr_header, :listener
|
104
119
|
|
105
|
-
|
120
|
+
# Remove in Puma 7?
|
121
|
+
def closed?
|
122
|
+
@to_io.closed?
|
123
|
+
end
|
106
124
|
|
107
125
|
# Test to see if io meets a bare minimum of functioning, @to_io needs to be
|
108
126
|
# used for MiniSSL::Socket
|
@@ -138,6 +156,7 @@ module Puma
|
|
138
156
|
|
139
157
|
def reset(fast_check=true)
|
140
158
|
@parser.reset
|
159
|
+
@io_buffer.reset
|
141
160
|
@read_header = true
|
142
161
|
@read_proxy = !!@expect_proxy_proto
|
143
162
|
@env = @proto_env.dup
|
@@ -148,6 +167,7 @@ module Puma
|
|
148
167
|
@body_remain = 0
|
149
168
|
@peerip = nil if @remote_addr_header
|
150
169
|
@in_last_chunk = false
|
170
|
+
@http_content_length_limit_exceeded = false
|
151
171
|
|
152
172
|
if @buffer
|
153
173
|
return false unless try_to_parse_proxy_protocol
|
@@ -207,6 +227,17 @@ module Puma
|
|
207
227
|
end
|
208
228
|
|
209
229
|
def try_to_finish
|
230
|
+
if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
|
231
|
+
@http_content_length_limit_exceeded = true
|
232
|
+
end
|
233
|
+
|
234
|
+
if @http_content_length_limit_exceeded
|
235
|
+
@buffer = nil
|
236
|
+
@body = EmptyBody
|
237
|
+
set_ready
|
238
|
+
return true
|
239
|
+
end
|
240
|
+
|
210
241
|
return read_body if in_data_phase
|
211
242
|
|
212
243
|
begin
|
@@ -236,6 +267,10 @@ module Puma
|
|
236
267
|
|
237
268
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
238
269
|
|
270
|
+
if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
|
271
|
+
@http_content_length_limit_exceeded = true
|
272
|
+
end
|
273
|
+
|
239
274
|
if @parser.finished?
|
240
275
|
return setup_body
|
241
276
|
elsif @parsed_bytes >= MAX_HEADER
|
@@ -273,7 +308,7 @@ module Puma
|
|
273
308
|
return @peerip if @peerip
|
274
309
|
|
275
310
|
if @remote_addr_header
|
276
|
-
hdr = (@env[@remote_addr_header] ||
|
311
|
+
hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
|
277
312
|
@peerip = hdr
|
278
313
|
return hdr
|
279
314
|
end
|
@@ -281,6 +316,16 @@ module Puma
|
|
281
316
|
@peerip ||= @io.peeraddr.last
|
282
317
|
end
|
283
318
|
|
319
|
+
def peer_family
|
320
|
+
return @peer_family if @peer_family
|
321
|
+
|
322
|
+
@peer_family ||= begin
|
323
|
+
@io.local_address.afamily
|
324
|
+
rescue
|
325
|
+
Socket::AF_INET
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
284
329
|
# Returns true if the persistent connection can be closed immediately
|
285
330
|
# without waiting for the configured idle/shutdown timeout.
|
286
331
|
# @version 5.0.0
|
@@ -304,7 +349,7 @@ module Puma
|
|
304
349
|
private
|
305
350
|
|
306
351
|
def setup_body
|
307
|
-
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :
|
352
|
+
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
308
353
|
|
309
354
|
if @env[HTTP_EXPECT] == CONTINUE
|
310
355
|
# TODO allow a hook here to check the headers before
|
@@ -347,8 +392,8 @@ module Puma
|
|
347
392
|
cl = @env[CONTENT_LENGTH]
|
348
393
|
|
349
394
|
if cl
|
350
|
-
# cannot contain characters that are not \d
|
351
|
-
if cl
|
395
|
+
# cannot contain characters that are not \d, or be empty
|
396
|
+
if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
|
352
397
|
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
353
398
|
end
|
354
399
|
else
|
@@ -401,7 +446,7 @@ module Puma
|
|
401
446
|
end
|
402
447
|
|
403
448
|
begin
|
404
|
-
chunk = @io.read_nonblock(want)
|
449
|
+
chunk = @io.read_nonblock(want, @read_buffer)
|
405
450
|
rescue IO::WaitReadable
|
406
451
|
return false
|
407
452
|
rescue SystemCallError, IOError
|
@@ -433,7 +478,7 @@ module Puma
|
|
433
478
|
def read_chunked_body
|
434
479
|
while true
|
435
480
|
begin
|
436
|
-
chunk = @io.read_nonblock(4096)
|
481
|
+
chunk = @io.read_nonblock(4096, @read_buffer)
|
437
482
|
rescue IO::WaitReadable
|
438
483
|
return false
|
439
484
|
rescue SystemCallError, IOError
|
@@ -459,6 +504,7 @@ module Puma
|
|
459
504
|
@chunked_body = true
|
460
505
|
@partial_part_left = 0
|
461
506
|
@prev_chunk = ""
|
507
|
+
@excess_cr = 0
|
462
508
|
|
463
509
|
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
464
510
|
@body.unlink
|
@@ -509,11 +555,11 @@ module Puma
|
|
509
555
|
|
510
556
|
while !io.eof?
|
511
557
|
line = io.gets
|
512
|
-
if line.end_with?(
|
558
|
+
if line.end_with?(CHUNK_VALID_ENDING)
|
513
559
|
# Puma doesn't process chunk extensions, but should parse if they're
|
514
560
|
# present, which is the reason for the semicolon regex
|
515
561
|
chunk_hex = line.strip[/\A[^;]+/]
|
516
|
-
if chunk_hex
|
562
|
+
if CHUNK_SIZE_INVALID.match? chunk_hex
|
517
563
|
raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
|
518
564
|
end
|
519
565
|
len = chunk_hex.to_i(16)
|
@@ -521,19 +567,39 @@ module Puma
|
|
521
567
|
@in_last_chunk = true
|
522
568
|
@body.rewind
|
523
569
|
rest = io.read
|
524
|
-
|
525
|
-
if rest.bytesize < last_crlf_size
|
570
|
+
if rest.bytesize < CHUNK_VALID_ENDING_SIZE
|
526
571
|
@buffer = nil
|
527
|
-
@partial_part_left =
|
572
|
+
@partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
|
528
573
|
return false
|
529
574
|
else
|
530
|
-
|
575
|
+
# if the next character is a CRLF, set buffer to everything after that CRLF
|
576
|
+
start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
|
577
|
+
CHUNK_VALID_ENDING_SIZE
|
578
|
+
else # we have started a trailer section, which we do not support. skip it!
|
579
|
+
rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
|
580
|
+
end
|
581
|
+
|
582
|
+
@buffer = rest[start_of_rest..-1]
|
531
583
|
@buffer = nil if @buffer.empty?
|
532
584
|
set_ready
|
533
585
|
return true
|
534
586
|
end
|
535
587
|
end
|
536
588
|
|
589
|
+
# Track the excess as a function of the size of the
|
590
|
+
# header vs the size of the actual data. Excess can
|
591
|
+
# go negative (and is expected to) when the body is
|
592
|
+
# significant.
|
593
|
+
# The additional of chunk_hex.size and 2 compensates
|
594
|
+
# for a client sending 1 byte in a chunked body over
|
595
|
+
# a long period of time, making sure that that client
|
596
|
+
# isn't accidentally eventually punished.
|
597
|
+
@excess_cr += (line.size - len - chunk_hex.size - 2)
|
598
|
+
|
599
|
+
if @excess_cr >= MAX_CHUNK_EXCESS
|
600
|
+
raise HttpParserError, "Maximum chunk excess detected"
|
601
|
+
end
|
602
|
+
|
537
603
|
len += 2
|
538
604
|
|
539
605
|
part = io.read(len)
|
@@ -561,6 +627,10 @@ module Puma
|
|
561
627
|
@partial_part_left = len - part.size
|
562
628
|
end
|
563
629
|
else
|
630
|
+
if @prev_chunk.size + chunk.size >= MAX_CHUNK_HEADER_SIZE
|
631
|
+
raise HttpParserError, "maximum size of chunk header exceeded"
|
632
|
+
end
|
633
|
+
|
564
634
|
@prev_chunk = line
|
565
635
|
return false
|
566
636
|
end
|
@@ -576,10 +646,14 @@ module Puma
|
|
576
646
|
|
577
647
|
def set_ready
|
578
648
|
if @body_read_start
|
579
|
-
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :
|
649
|
+
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
|
580
650
|
end
|
581
651
|
@requests_served += 1
|
582
652
|
@ready = true
|
583
653
|
end
|
654
|
+
|
655
|
+
def above_http_content_limit(value)
|
656
|
+
@http_content_length_limit&.< value
|
657
|
+
end
|
584
658
|
end
|
585
659
|
end
|