puma 5.5.2 → 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 +336 -3
- data/README.md +61 -16
- data/bin/puma-wild +1 -1
- data/docs/architecture.md +4 -4
- data/docs/compile_options.md +34 -0
- data/docs/fork_worker.md +1 -3
- data/docs/nginx.md +1 -1
- data/docs/signals.md +1 -0
- data/docs/systemd.md +1 -2
- data/docs/testing_benchmarks_local_files.md +150 -0
- data/docs/testing_test_rackup_ci_files.md +36 -0
- data/ext/puma_http11/extconf.rb +28 -14
- 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 +135 -23
- 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 +188 -102
- data/ext/puma_http11/puma_http11.c +18 -10
- data/lib/puma/app/status.rb +7 -4
- data/lib/puma/binder.rb +62 -51
- data/lib/puma/cli.rb +19 -20
- data/lib/puma/client.rb +108 -26
- data/lib/puma/cluster/worker.rb +23 -16
- data/lib/puma/cluster/worker_handle.rb +8 -1
- data/lib/puma/cluster.rb +62 -41
- data/lib/puma/commonlogger.rb +21 -14
- data/lib/puma/configuration.rb +76 -55
- data/lib/puma/const.rb +133 -97
- data/lib/puma/control_cli.rb +21 -18
- data/lib/puma/detect.rb +12 -2
- data/lib/puma/dsl.rb +270 -55
- 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/launcher/bundle_pruner.rb +104 -0
- data/lib/puma/launcher.rb +114 -175
- data/lib/puma/log_writer.rb +147 -0
- data/lib/puma/minissl/context_builder.rb +30 -16
- data/lib/puma/minissl.rb +126 -17
- data/lib/puma/null_io.rb +5 -0
- data/lib/puma/plugin/systemd.rb +90 -0
- data/lib/puma/plugin/tmp_restart.rb +1 -1
- data/lib/puma/plugin.rb +1 -1
- data/lib/puma/rack/builder.rb +6 -6
- data/lib/puma/rack_default.rb +19 -4
- data/lib/puma/reactor.rb +19 -10
- data/lib/puma/request.rb +365 -161
- data/lib/puma/runner.rb +55 -22
- data/lib/puma/sd_notify.rb +149 -0
- data/lib/puma/server.rb +91 -94
- data/lib/puma/single.rb +13 -11
- data/lib/puma/state_file.rb +39 -7
- data/lib/puma/thread_pool.rb +25 -21
- data/lib/puma/util.rb +12 -14
- data/lib/puma.rb +12 -11
- data/lib/rack/handler/puma.rb +113 -86
- data/tools/Dockerfile +1 -1
- 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,9 @@ 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
|
24
|
+
@conf = conf
|
33
25
|
@listeners = []
|
34
26
|
@inherited_fds = {}
|
35
27
|
@activated_sockets = {}
|
@@ -37,7 +29,7 @@ module Puma
|
|
37
29
|
|
38
30
|
@proto_env = {
|
39
31
|
"rack.version".freeze => RACK_VERSION,
|
40
|
-
"rack.errors".freeze =>
|
32
|
+
"rack.errors".freeze => log_writer.stderr,
|
41
33
|
"rack.multithread".freeze => conf.options[:max_threads] > 1,
|
42
34
|
"rack.multiprocess".freeze => conf.options[:workers] >= 1,
|
43
35
|
"rack.run_once".freeze => false,
|
@@ -50,14 +42,12 @@ module Puma
|
|
50
42
|
# infer properly.
|
51
43
|
|
52
44
|
"QUERY_STRING".freeze => "",
|
53
|
-
SERVER_PROTOCOL => HTTP_11,
|
54
45
|
SERVER_SOFTWARE => PUMA_SERVER_STRING,
|
55
46
|
GATEWAY_INTERFACE => CGI_VER
|
56
47
|
}
|
57
48
|
|
58
49
|
@envs = {}
|
59
50
|
@ios = []
|
60
|
-
localhost_authority
|
61
51
|
end
|
62
52
|
|
63
53
|
attr_reader :ios
|
@@ -79,7 +69,7 @@ module Puma
|
|
79
69
|
# @!attribute [r] connected_ports
|
80
70
|
# @version 5.0.0
|
81
71
|
def connected_ports
|
82
|
-
ios.map { |io| io.addr[1] }.uniq
|
72
|
+
t = ios.map { |io| io.addr[1] }; t.uniq!; t
|
83
73
|
end
|
84
74
|
|
85
75
|
# @version 5.0.0
|
@@ -97,7 +87,7 @@ module Puma
|
|
97
87
|
# @version 5.0.0
|
98
88
|
#
|
99
89
|
def create_activated_fds(env_hash)
|
100
|
-
@
|
90
|
+
@log_writer.debug "ENV['LISTEN_FDS'] #{ENV['LISTEN_FDS'].inspect} env_hash['LISTEN_PID'] #{env_hash['LISTEN_PID'].inspect}"
|
101
91
|
return [] unless env_hash['LISTEN_FDS'] && env_hash['LISTEN_PID'].to_i == $$
|
102
92
|
env_hash['LISTEN_FDS'].to_i.times do |index|
|
103
93
|
sock = TCPServer.for_fd(socket_activation_fd(index))
|
@@ -105,11 +95,11 @@ module Puma
|
|
105
95
|
[:unix, Socket.unpack_sockaddr_un(sock.getsockname)]
|
106
96
|
rescue ArgumentError # Try to parse as a port/ip
|
107
97
|
port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
|
108
|
-
addr = "[#{addr}]" if addr
|
98
|
+
addr = "[#{addr}]" if addr&.include? ':'
|
109
99
|
[:tcp, addr, port]
|
110
100
|
end
|
111
101
|
@activated_sockets[key] = sock
|
112
|
-
@
|
102
|
+
@log_writer.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
|
113
103
|
end
|
114
104
|
["LISTEN_FDS", "LISTEN_PID"] # Signal to remove these keys from ENV
|
115
105
|
end
|
@@ -151,29 +141,30 @@ module Puma
|
|
151
141
|
end
|
152
142
|
end
|
153
143
|
|
154
|
-
def parse(binds,
|
144
|
+
def parse(binds, log_writer = nil, log_msg = 'Listening')
|
145
|
+
log_writer ||= @log_writer
|
155
146
|
binds.each do |str|
|
156
147
|
uri = URI.parse str
|
157
148
|
case uri.scheme
|
158
149
|
when "tcp"
|
159
150
|
if fd = @inherited_fds.delete(str)
|
160
151
|
io = inherit_tcp_listener uri.host, uri.port, fd
|
161
|
-
|
152
|
+
log_writer.log "* Inherited #{str}"
|
162
153
|
elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
|
163
154
|
io = inherit_tcp_listener uri.host, uri.port, sock
|
164
|
-
|
155
|
+
log_writer.log "* Activated #{str}"
|
165
156
|
else
|
166
157
|
ios_len = @ios.length
|
167
158
|
params = Util.parse_query uri.query
|
168
159
|
|
169
|
-
|
170
|
-
|
160
|
+
low_latency = params.key?('low_latency') && params['low_latency'] != 'false'
|
161
|
+
backlog = params.fetch('backlog', 1024).to_i
|
171
162
|
|
172
|
-
io = add_tcp_listener uri.host, uri.port,
|
163
|
+
io = add_tcp_listener uri.host, uri.port, low_latency, backlog
|
173
164
|
|
174
165
|
@ios[ios_len..-1].each do |i|
|
175
166
|
addr = loc_addr_str i
|
176
|
-
|
167
|
+
log_writer.log "* #{log_msg} on http://#{addr}"
|
177
168
|
end
|
178
169
|
end
|
179
170
|
|
@@ -188,14 +179,14 @@ module Puma
|
|
188
179
|
end
|
189
180
|
|
190
181
|
if fd = @inherited_fds.delete(str)
|
191
|
-
@unix_paths << path unless abstract
|
182
|
+
@unix_paths << path unless abstract || File.exist?(path)
|
192
183
|
io = inherit_unix_listener path, fd
|
193
|
-
|
184
|
+
log_writer.log "* Inherited #{str}"
|
194
185
|
elsif sock = @activated_sockets.delete([ :unix, path ]) ||
|
195
186
|
@activated_sockets.delete([ :unix, File.realdirpath(path) ])
|
196
187
|
@unix_paths << path unless abstract || File.exist?(path)
|
197
188
|
io = inherit_unix_listener path, sock
|
198
|
-
|
189
|
+
log_writer.log "* Activated #{str}"
|
199
190
|
else
|
200
191
|
umask = nil
|
201
192
|
mode = nil
|
@@ -219,11 +210,12 @@ module Puma
|
|
219
210
|
|
220
211
|
@unix_paths << path unless abstract || File.exist?(path)
|
221
212
|
io = add_unix_listener path, umask, mode, backlog
|
222
|
-
|
213
|
+
log_writer.log "* #{log_msg} on #{str}"
|
223
214
|
end
|
224
215
|
|
225
216
|
@listeners << [str, io]
|
226
217
|
when "ssl"
|
218
|
+
cert_key = %w[cert key]
|
227
219
|
|
228
220
|
raise "Puma compiled without SSL support" unless HAS_SSL
|
229
221
|
|
@@ -232,36 +224,51 @@ module Puma
|
|
232
224
|
# If key and certs are not defined and localhost gem is required.
|
233
225
|
# localhost gem will be used for self signed
|
234
226
|
# Load localhost authority if not loaded.
|
235
|
-
|
227
|
+
# Ruby 3 `values_at` accepts an array, earlier do not
|
228
|
+
if params.values_at(*cert_key).all? { |v| v.to_s.empty? }
|
229
|
+
ctx = localhost_authority && localhost_authority_context
|
230
|
+
end
|
236
231
|
|
237
|
-
ctx ||=
|
232
|
+
ctx ||=
|
233
|
+
begin
|
234
|
+
# Extract cert_pem and key_pem from options[:store] if present
|
235
|
+
cert_key.each do |v|
|
236
|
+
if params[v]&.start_with?('store:')
|
237
|
+
index = Integer(params.delete(v).split('store:').last)
|
238
|
+
params["#{v}_pem"] = @conf.options[:store][index]
|
239
|
+
end
|
240
|
+
end
|
241
|
+
MiniSSL::ContextBuilder.new(params, @log_writer).context
|
242
|
+
end
|
238
243
|
|
239
244
|
if fd = @inherited_fds.delete(str)
|
240
|
-
|
245
|
+
log_writer.log "* Inherited #{str}"
|
241
246
|
io = inherit_ssl_listener fd, ctx
|
242
247
|
elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
|
243
248
|
io = inherit_ssl_listener sock, ctx
|
244
|
-
|
249
|
+
log_writer.log "* Activated #{str}"
|
245
250
|
else
|
246
251
|
ios_len = @ios.length
|
247
|
-
|
252
|
+
backlog = params.fetch('backlog', 1024).to_i
|
253
|
+
low_latency = params['low_latency'] != 'false'
|
254
|
+
io = add_ssl_listener uri.host, uri.port, ctx, low_latency, backlog
|
248
255
|
|
249
256
|
@ios[ios_len..-1].each do |i|
|
250
257
|
addr = loc_addr_str i
|
251
|
-
|
258
|
+
log_writer.log "* #{log_msg} on ssl://#{addr}?#{uri.query}"
|
252
259
|
end
|
253
260
|
end
|
254
261
|
|
255
262
|
@listeners << [str, io] if io
|
256
263
|
else
|
257
|
-
|
264
|
+
log_writer.error "Invalid URI: #{str}"
|
258
265
|
end
|
259
266
|
end
|
260
267
|
|
261
268
|
# If we inherited fds but didn't use them (because of a
|
262
269
|
# configuration change), then be sure to close them.
|
263
270
|
@inherited_fds.each do |str, fd|
|
264
|
-
|
271
|
+
log_writer.log "* Closing unused inherited connection: #{str}"
|
265
272
|
|
266
273
|
begin
|
267
274
|
IO.for_fd(fd).close
|
@@ -281,7 +288,7 @@ module Puma
|
|
281
288
|
fds = @ios.map(&:to_i)
|
282
289
|
@activated_sockets.each do |key, sock|
|
283
290
|
next if fds.include? sock.to_i
|
284
|
-
|
291
|
+
log_writer.log "* Closing unused activated socket: #{key.first}://#{key[1..-1].join ':'}"
|
285
292
|
begin
|
286
293
|
sock.close
|
287
294
|
rescue SystemCallError
|
@@ -305,7 +312,7 @@ module Puma
|
|
305
312
|
local_certificates_path = File.expand_path("~/.localhost")
|
306
313
|
[File.join(local_certificates_path, "localhost.key"), File.join(local_certificates_path, "localhost.crt")]
|
307
314
|
end
|
308
|
-
MiniSSL::ContextBuilder.new({ "key" => key_path, "cert" => crt_path }, @
|
315
|
+
MiniSSL::ContextBuilder.new({ "key" => key_path, "cert" => crt_path }, @log_writer).context
|
309
316
|
end
|
310
317
|
|
311
318
|
# Tell the server to listen on host +host+, port +port+.
|
@@ -443,11 +450,14 @@ module Puma
|
|
443
450
|
|
444
451
|
def close_listeners
|
445
452
|
@listeners.each do |l, io|
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
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
|
451
461
|
end
|
452
462
|
end
|
453
463
|
|
@@ -468,9 +478,10 @@ module Puma
|
|
468
478
|
|
469
479
|
# @!attribute [r] loopback_addresses
|
470
480
|
def loopback_addresses
|
471
|
-
Socket.ip_address_list.select do |addrinfo|
|
481
|
+
t = Socket.ip_address_list.select do |addrinfo|
|
472
482
|
addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
|
473
|
-
end
|
483
|
+
end
|
484
|
+
t.map! { |addrinfo| addrinfo.ip_address }; t.uniq!; t
|
474
485
|
end
|
475
486
|
|
476
487
|
def loc_addr_str(io)
|
data/lib/puma/cli.rb
CHANGED
@@ -3,36 +3,31 @@
|
|
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
|
14
|
-
# The CLI exports
|
15
|
-
# apps to pick it up. An app
|
16
|
-
#
|
17
|
-
#
|
14
|
+
# The CLI exports a Puma::Configuration instance here to allow
|
15
|
+
# apps to pick it up. An app must load this object conditionally
|
16
|
+
# because it is not set if the app is launched via any mechanism
|
17
|
+
# other than the CLI class.
|
18
18
|
attr_accessor :cli_config
|
19
19
|
end
|
20
20
|
|
21
21
|
# Handles invoke a Puma::Server in a command line style.
|
22
22
|
#
|
23
23
|
class CLI
|
24
|
-
KEYS_NOT_TO_PERSIST_IN_STATE = Launcher::KEYS_NOT_TO_PERSIST_IN_STATE
|
25
|
-
|
26
24
|
# Create a new CLI object using +argv+ as the command line
|
27
25
|
# arguments.
|
28
26
|
#
|
29
|
-
|
30
|
-
# this object will report status on.
|
31
|
-
#
|
32
|
-
def initialize(argv, events=Events.stdio)
|
27
|
+
def initialize(argv, log_writer = LogWriter.stdio, events = Events.new)
|
33
28
|
@debug = false
|
34
29
|
@argv = argv.dup
|
35
|
-
|
30
|
+
@log_writer = log_writer
|
36
31
|
@events = events
|
37
32
|
|
38
33
|
@conf = nil
|
@@ -68,7 +63,7 @@ module Puma
|
|
68
63
|
end
|
69
64
|
end
|
70
65
|
|
71
|
-
@launcher = Puma::Launcher.new(@conf, :events => @events, :argv => argv)
|
66
|
+
@launcher = Puma::Launcher.new(@conf, :log_writer => @log_writer, :events => @events, :argv => argv)
|
72
67
|
end
|
73
68
|
|
74
69
|
attr_reader :launcher
|
@@ -82,7 +77,7 @@ module Puma
|
|
82
77
|
|
83
78
|
private
|
84
79
|
def unsupported(str)
|
85
|
-
@
|
80
|
+
@log_writer.error(str)
|
86
81
|
raise UnsupportedOption
|
87
82
|
end
|
88
83
|
|
@@ -98,7 +93,7 @@ module Puma
|
|
98
93
|
#
|
99
94
|
|
100
95
|
def setup_options
|
101
|
-
@conf = Configuration.new do |user_config, file_config|
|
96
|
+
@conf = Configuration.new({}, {events: @events}) do |user_config, file_config|
|
102
97
|
@parser = OptionParser.new do |o|
|
103
98
|
o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
|
104
99
|
user_config.bind arg
|
@@ -151,7 +146,7 @@ module Puma
|
|
151
146
|
|
152
147
|
o.on "-p", "--port PORT", "Define the TCP port to bind to",
|
153
148
|
"Use -b for more advanced options" do |arg|
|
154
|
-
user_config.bind "tcp://#{Configuration::
|
149
|
+
user_config.bind "tcp://#{Configuration::DEFAULTS[:tcp_host]}:#{arg}"
|
155
150
|
end
|
156
151
|
|
157
152
|
o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg|
|
@@ -184,6 +179,10 @@ module Puma
|
|
184
179
|
user_config.restart_command cmd
|
185
180
|
end
|
186
181
|
|
182
|
+
o.on "-s", "--silent", "Do not log prompt messages other than errors" do
|
183
|
+
@log_writer = LogWriter.new(NullIO.new, $stderr)
|
184
|
+
end
|
185
|
+
|
187
186
|
o.on "-S", "--state PATH", "Where to store the state details" do |arg|
|
188
187
|
user_config.state_path arg
|
189
188
|
end
|
data/lib/puma/client.rb
CHANGED
@@ -8,7 +8,8 @@ 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
14
|
require 'forwardable'
|
14
15
|
|
@@ -23,6 +24,11 @@ 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
33
|
# For example, this could be a web request from a browser or from CURL.
|
28
34
|
#
|
@@ -35,7 +41,21 @@ module Puma
|
|
35
41
|
# Instances of this class are responsible for knowing if
|
36
42
|
# the header and body are fully buffered via the `try_to_finish` method.
|
37
43
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
38
|
-
|
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
|
+
|
39
59
|
# The object used for a request with no body. All requests with
|
40
60
|
# no body share this one object since it has no state.
|
41
61
|
EmptyBody = NullIO.new
|
@@ -46,12 +66,9 @@ module Puma
|
|
46
66
|
def initialize(io, env=nil)
|
47
67
|
@io = io
|
48
68
|
@to_io = io.to_io
|
69
|
+
@io_buffer = IOBuffer.new
|
49
70
|
@proto_env = env
|
50
|
-
|
51
|
-
@env = nil
|
52
|
-
else
|
53
|
-
@env = env.dup
|
54
|
-
end
|
71
|
+
@env = env&.dup
|
55
72
|
|
56
73
|
@parser = HttpParser.new
|
57
74
|
@parsed_bytes = 0
|
@@ -69,7 +86,11 @@ module Puma
|
|
69
86
|
@requests_served = 0
|
70
87
|
@hijacked = false
|
71
88
|
|
89
|
+
@http_content_length_limit = nil
|
90
|
+
@http_content_length_limit_exceeded = false
|
91
|
+
|
72
92
|
@peerip = nil
|
93
|
+
@peer_family = nil
|
73
94
|
@listener = nil
|
74
95
|
@remote_addr_header = nil
|
75
96
|
@expect_proxy_proto = false
|
@@ -77,12 +98,15 @@ module Puma
|
|
77
98
|
@body_remain = 0
|
78
99
|
|
79
100
|
@in_last_chunk = false
|
101
|
+
|
102
|
+
# need unfrozen ASCII-8BIT, +'' is UTF-8
|
103
|
+
@read_buffer = String.new # rubocop: disable Performance/UnfreezeString
|
80
104
|
end
|
81
105
|
|
82
106
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
83
|
-
:tempfile
|
107
|
+
:tempfile, :io_buffer, :http_content_length_limit_exceeded
|
84
108
|
|
85
|
-
attr_writer :peerip
|
109
|
+
attr_writer :peerip, :http_content_length_limit
|
86
110
|
|
87
111
|
attr_accessor :remote_addr_header, :listener
|
88
112
|
|
@@ -122,6 +146,7 @@ module Puma
|
|
122
146
|
|
123
147
|
def reset(fast_check=true)
|
124
148
|
@parser.reset
|
149
|
+
@io_buffer.reset
|
125
150
|
@read_header = true
|
126
151
|
@read_proxy = !!@expect_proxy_proto
|
127
152
|
@env = @proto_env.dup
|
@@ -132,6 +157,7 @@ module Puma
|
|
132
157
|
@body_remain = 0
|
133
158
|
@peerip = nil if @remote_addr_header
|
134
159
|
@in_last_chunk = false
|
160
|
+
@http_content_length_limit_exceeded = false
|
135
161
|
|
136
162
|
if @buffer
|
137
163
|
return false unless try_to_parse_proxy_protocol
|
@@ -161,7 +187,7 @@ module Puma
|
|
161
187
|
def close
|
162
188
|
begin
|
163
189
|
@io.close
|
164
|
-
rescue IOError
|
190
|
+
rescue IOError, Errno::EBADF
|
165
191
|
Puma::Util.purge_interrupt_queue
|
166
192
|
end
|
167
193
|
end
|
@@ -191,6 +217,17 @@ module Puma
|
|
191
217
|
end
|
192
218
|
|
193
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
|
223
|
+
|
224
|
+
if @http_content_length_limit_exceeded
|
225
|
+
@buffer = nil
|
226
|
+
@body = EmptyBody
|
227
|
+
set_ready
|
228
|
+
return true
|
229
|
+
end
|
230
|
+
|
194
231
|
return read_body if in_data_phase
|
195
232
|
|
196
233
|
begin
|
@@ -220,6 +257,10 @@ module Puma
|
|
220
257
|
|
221
258
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
222
259
|
|
260
|
+
if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
|
261
|
+
@http_content_length_limit_exceeded = true
|
262
|
+
end
|
263
|
+
|
223
264
|
if @parser.finished?
|
224
265
|
return setup_body
|
225
266
|
elsif @parsed_bytes >= MAX_HEADER
|
@@ -257,7 +298,7 @@ module Puma
|
|
257
298
|
return @peerip if @peerip
|
258
299
|
|
259
300
|
if @remote_addr_header
|
260
|
-
hdr = (@env[@remote_addr_header] ||
|
301
|
+
hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
|
261
302
|
@peerip = hdr
|
262
303
|
return hdr
|
263
304
|
end
|
@@ -265,6 +306,16 @@ module Puma
|
|
265
306
|
@peerip ||= @io.peeraddr.last
|
266
307
|
end
|
267
308
|
|
309
|
+
def peer_family
|
310
|
+
return @peer_family if @peer_family
|
311
|
+
|
312
|
+
@peer_family ||= begin
|
313
|
+
@io.local_address.afamily
|
314
|
+
rescue
|
315
|
+
Socket::AF_INET
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
268
319
|
# Returns true if the persistent connection can be closed immediately
|
269
320
|
# without waiting for the configured idle/shutdown timeout.
|
270
321
|
# @version 5.0.0
|
@@ -288,7 +339,7 @@ module Puma
|
|
288
339
|
private
|
289
340
|
|
290
341
|
def setup_body
|
291
|
-
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :
|
342
|
+
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
292
343
|
|
293
344
|
if @env[HTTP_EXPECT] == CONTINUE
|
294
345
|
# TODO allow a hook here to check the headers before
|
@@ -302,16 +353,27 @@ module Puma
|
|
302
353
|
body = @parser.body
|
303
354
|
|
304
355
|
te = @env[TRANSFER_ENCODING2]
|
305
|
-
|
306
356
|
if te
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
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}'"
|
312
369
|
end
|
313
|
-
elsif
|
314
|
-
|
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}'"
|
315
377
|
end
|
316
378
|
end
|
317
379
|
|
@@ -319,7 +381,12 @@ module Puma
|
|
319
381
|
|
320
382
|
cl = @env[CONTENT_LENGTH]
|
321
383
|
|
322
|
-
|
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
|
323
390
|
@buffer = body.empty? ? nil : body
|
324
391
|
@body = EmptyBody
|
325
392
|
set_ready
|
@@ -369,7 +436,7 @@ module Puma
|
|
369
436
|
end
|
370
437
|
|
371
438
|
begin
|
372
|
-
chunk = @io.read_nonblock(want)
|
439
|
+
chunk = @io.read_nonblock(want, @read_buffer)
|
373
440
|
rescue IO::WaitReadable
|
374
441
|
return false
|
375
442
|
rescue SystemCallError, IOError
|
@@ -401,7 +468,7 @@ module Puma
|
|
401
468
|
def read_chunked_body
|
402
469
|
while true
|
403
470
|
begin
|
404
|
-
chunk = @io.read_nonblock(4096)
|
471
|
+
chunk = @io.read_nonblock(4096, @read_buffer)
|
405
472
|
rescue IO::WaitReadable
|
406
473
|
return false
|
407
474
|
rescue SystemCallError, IOError
|
@@ -478,7 +545,13 @@ module Puma
|
|
478
545
|
while !io.eof?
|
479
546
|
line = io.gets
|
480
547
|
if line.end_with?("\r\n")
|
481
|
-
|
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)
|
482
555
|
if len == 0
|
483
556
|
@in_last_chunk = true
|
484
557
|
@body.rewind
|
@@ -509,7 +582,12 @@ module Puma
|
|
509
582
|
|
510
583
|
case
|
511
584
|
when got == len
|
512
|
-
|
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
|
513
591
|
when got <= len - 2
|
514
592
|
write_chunk(part)
|
515
593
|
@partial_part_left = len - part.size
|
@@ -533,10 +611,14 @@ module Puma
|
|
533
611
|
|
534
612
|
def set_ready
|
535
613
|
if @body_read_start
|
536
|
-
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :
|
614
|
+
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
|
537
615
|
end
|
538
616
|
@requests_served += 1
|
539
617
|
@ready = true
|
540
618
|
end
|
619
|
+
|
620
|
+
def above_http_content_limit(value)
|
621
|
+
@http_content_length_limit&.< value
|
622
|
+
end
|
541
623
|
end
|
542
624
|
end
|