puma 5.6.4 → 6.4.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|