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.

Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +336 -3
  3. data/README.md +61 -16
  4. data/bin/puma-wild +1 -1
  5. data/docs/architecture.md +4 -4
  6. data/docs/compile_options.md +34 -0
  7. data/docs/fork_worker.md +1 -3
  8. data/docs/nginx.md +1 -1
  9. data/docs/signals.md +1 -0
  10. data/docs/systemd.md +1 -2
  11. data/docs/testing_benchmarks_local_files.md +150 -0
  12. data/docs/testing_test_rackup_ci_files.md +36 -0
  13. data/ext/puma_http11/extconf.rb +28 -14
  14. data/ext/puma_http11/http11_parser.c +1 -1
  15. data/ext/puma_http11/http11_parser.h +1 -1
  16. data/ext/puma_http11/http11_parser.java.rl +2 -2
  17. data/ext/puma_http11/http11_parser.rl +2 -2
  18. data/ext/puma_http11/http11_parser_common.rl +2 -2
  19. data/ext/puma_http11/mini_ssl.c +135 -23
  20. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  21. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  22. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +188 -102
  23. data/ext/puma_http11/puma_http11.c +18 -10
  24. data/lib/puma/app/status.rb +7 -4
  25. data/lib/puma/binder.rb +62 -51
  26. data/lib/puma/cli.rb +19 -20
  27. data/lib/puma/client.rb +108 -26
  28. data/lib/puma/cluster/worker.rb +23 -16
  29. data/lib/puma/cluster/worker_handle.rb +8 -1
  30. data/lib/puma/cluster.rb +62 -41
  31. data/lib/puma/commonlogger.rb +21 -14
  32. data/lib/puma/configuration.rb +76 -55
  33. data/lib/puma/const.rb +133 -97
  34. data/lib/puma/control_cli.rb +21 -18
  35. data/lib/puma/detect.rb +12 -2
  36. data/lib/puma/dsl.rb +270 -55
  37. data/lib/puma/error_logger.rb +18 -9
  38. data/lib/puma/events.rb +6 -126
  39. data/lib/puma/io_buffer.rb +39 -4
  40. data/lib/puma/jruby_restart.rb +2 -1
  41. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  42. data/lib/puma/launcher.rb +114 -175
  43. data/lib/puma/log_writer.rb +147 -0
  44. data/lib/puma/minissl/context_builder.rb +30 -16
  45. data/lib/puma/minissl.rb +126 -17
  46. data/lib/puma/null_io.rb +5 -0
  47. data/lib/puma/plugin/systemd.rb +90 -0
  48. data/lib/puma/plugin/tmp_restart.rb +1 -1
  49. data/lib/puma/plugin.rb +1 -1
  50. data/lib/puma/rack/builder.rb +6 -6
  51. data/lib/puma/rack_default.rb +19 -4
  52. data/lib/puma/reactor.rb +19 -10
  53. data/lib/puma/request.rb +365 -161
  54. data/lib/puma/runner.rb +55 -22
  55. data/lib/puma/sd_notify.rb +149 -0
  56. data/lib/puma/server.rb +91 -94
  57. data/lib/puma/single.rb +13 -11
  58. data/lib/puma/state_file.rb +39 -7
  59. data/lib/puma/thread_pool.rb +25 -21
  60. data/lib/puma/util.rb +12 -14
  61. data/lib/puma.rb +12 -11
  62. data/lib/rack/handler/puma.rb +113 -86
  63. data/tools/Dockerfile +1 -1
  64. metadata +11 -6
  65. data/lib/puma/queue_close.rb +0 -26
  66. 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
- require 'puma/const'
7
- require 'puma/util'
8
- require 'puma/configuration'
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
- require 'puma/minissl'
14
- require 'puma/minissl/context_builder'
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(events, conf = Configuration.new)
32
- @events = events
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 => events.stderr,
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
- @events.debug "ENV['LISTEN_FDS'] #{ENV['LISTEN_FDS'].inspect} env_hash['LISTEN_PID'] #{env_hash['LISTEN_PID'].inspect}"
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
- @events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
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, logger, log_msg = 'Listening')
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
- logger.log "* Inherited #{str}"
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
- logger.log "* Activated #{str}"
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
- opt = params.key?('low_latency') && params['low_latency'] != 'false'
170
- bak = params.fetch('backlog', 1024).to_i
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, opt, bak
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
- logger.log "* #{log_msg} on http://#{addr}"
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
- logger.log "* Inherited #{str}"
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
- logger.log "* Activated #{str}"
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
- logger.log "* #{log_msg} on #{str}"
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
- ctx = localhost_authority && localhost_authority_context if params.empty?
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 ||= MiniSSL::ContextBuilder.new(params, @events).context
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
- logger.log "* Inherited #{str}"
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
- logger.log "* Activated #{str}"
249
+ log_writer.log "* Activated #{str}"
245
250
  else
246
251
  ios_len = @ios.length
247
- io = add_ssl_listener uri.host, uri.port, ctx
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
- logger.log "* #{log_msg} on ssl://#{addr}?#{uri.query}"
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
- logger.error "Invalid URI: #{str}"
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
- logger.log "* Closing unused inherited connection: #{str}"
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
- logger.log "* Closing unused activated socket: #{key.first}://#{key[1..-1].join ':'}"
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 }, @events).context
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
- io.close unless io.closed?
447
- uri = URI.parse l
448
- next unless uri.scheme == 'unix'
449
- unix_path = "#{uri.host}#{uri.path}"
450
- File.unlink unix_path if @unix_paths.include?(unix_path) && File.exist?(unix_path)
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.map { |addrinfo| addrinfo.ip_address }.uniq
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
- require 'puma'
7
- require 'puma/configuration'
8
- require 'puma/launcher'
9
- require 'puma/const'
10
- require 'puma/events'
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 its Puma::Configuration object here to allow
15
- # apps to pick it up. An app needs to use it conditionally though
16
- # since it is not set if the app is launched via another
17
- # mechanism than the CLI class.
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
- # +stdout+ and +stderr+ can be set to IO-like objects which
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
- @events.error(str)
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::DefaultTCPHost}:#{arg}"
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
- require 'puma/detect'
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
- class Client
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
- if !env
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] || LOCALHOST_IP).split(/[\s,]/).first
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, :millisecond)
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
- if te.include?(",")
308
- te.split(",").each do |part|
309
- if CHUNKED.casecmp(part.strip) == 0
310
- return setup_chunked_body(body)
311
- end
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 CHUNKED.casecmp(te) == 0
314
- return setup_chunked_body(body)
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
- unless cl
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
- len = line.strip.to_i(16)
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
- write_chunk(part[0..-3]) # to skip the ending \r\n
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, :millisecond) - @body_read_start
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