puma 5.6.4 → 6.3.1

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +275 -4
  3. data/LICENSE +0 -0
  4. data/README.md +60 -20
  5. data/bin/puma-wild +1 -1
  6. data/docs/architecture.md +0 -0
  7. data/docs/compile_options.md +34 -0
  8. data/docs/deployment.md +0 -0
  9. data/docs/fork_worker.md +1 -3
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/jungle/README.md +0 -0
  14. data/docs/jungle/rc.d/README.md +0 -0
  15. data/docs/jungle/rc.d/puma.conf +0 -0
  16. data/docs/kubernetes.md +0 -0
  17. data/docs/nginx.md +1 -1
  18. data/docs/plugins.md +0 -0
  19. data/docs/rails_dev_mode.md +0 -0
  20. data/docs/restart.md +0 -0
  21. data/docs/signals.md +0 -0
  22. data/docs/stats.md +0 -0
  23. data/docs/systemd.md +1 -2
  24. data/docs/testing_benchmarks_local_files.md +150 -0
  25. data/docs/testing_test_rackup_ci_files.md +36 -0
  26. data/ext/puma_http11/PumaHttp11Service.java +0 -0
  27. data/ext/puma_http11/ext_help.h +0 -0
  28. data/ext/puma_http11/extconf.rb +18 -10
  29. data/ext/puma_http11/http11_parser.c +1 -1
  30. data/ext/puma_http11/http11_parser.h +1 -1
  31. data/ext/puma_http11/http11_parser.java.rl +2 -2
  32. data/ext/puma_http11/http11_parser.rl +2 -2
  33. data/ext/puma_http11/http11_parser_common.rl +2 -2
  34. data/ext/puma_http11/mini_ssl.c +93 -26
  35. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
  36. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  37. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  38. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +166 -65
  39. data/ext/puma_http11/puma_http11.c +17 -9
  40. data/lib/puma/app/status.rb +7 -4
  41. data/lib/puma/binder.rb +49 -52
  42. data/lib/puma/cli.rb +12 -18
  43. data/lib/puma/client.rb +69 -23
  44. data/lib/puma/cluster/worker.rb +18 -11
  45. data/lib/puma/cluster/worker_handle.rb +4 -1
  46. data/lib/puma/cluster.rb +33 -30
  47. data/lib/puma/commonlogger.rb +21 -14
  48. data/lib/puma/configuration.rb +76 -58
  49. data/lib/puma/const.rb +129 -92
  50. data/lib/puma/control_cli.rb +21 -18
  51. data/lib/puma/detect.rb +4 -0
  52. data/lib/puma/dsl.rb +187 -49
  53. data/lib/puma/error_logger.rb +18 -9
  54. data/lib/puma/events.rb +6 -126
  55. data/lib/puma/io_buffer.rb +39 -4
  56. data/lib/puma/jruby_restart.rb +2 -1
  57. data/lib/puma/json_serialization.rb +0 -0
  58. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  59. data/lib/puma/launcher.rb +113 -175
  60. data/lib/puma/log_writer.rb +147 -0
  61. data/lib/puma/minissl/context_builder.rb +24 -12
  62. data/lib/puma/minissl.rb +108 -15
  63. data/lib/puma/null_io.rb +5 -0
  64. data/lib/puma/plugin/systemd.rb +90 -0
  65. data/lib/puma/plugin/tmp_restart.rb +1 -1
  66. data/lib/puma/plugin.rb +0 -0
  67. data/lib/puma/rack/builder.rb +6 -6
  68. data/lib/puma/rack/urlmap.rb +0 -0
  69. data/lib/puma/rack_default.rb +19 -4
  70. data/lib/puma/reactor.rb +19 -10
  71. data/lib/puma/request.rb +365 -166
  72. data/lib/puma/runner.rb +52 -20
  73. data/lib/puma/sd_notify.rb +149 -0
  74. data/lib/puma/server.rb +73 -73
  75. data/lib/puma/single.rb +13 -11
  76. data/lib/puma/state_file.rb +2 -4
  77. data/lib/puma/thread_pool.rb +23 -19
  78. data/lib/puma/util.rb +12 -14
  79. data/lib/puma.rb +12 -11
  80. data/lib/rack/handler/puma.rb +113 -86
  81. data/tools/Dockerfile +0 -0
  82. data/tools/trickletest.rb +0 -0
  83. metadata +10 -5
  84. data/lib/puma/queue_close.rb +0 -26
  85. 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,8 @@ 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
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 => events.stderr,
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
- @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}"
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
- @events.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
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, logger, log_msg = 'Listening')
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
- logger.log "* Inherited #{str}"
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
- logger.log "* Activated #{str}"
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
- opt = params.key?('low_latency') && params['low_latency'] != 'false'
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, opt, backlog
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
- logger.log "* #{log_msg} on http://#{addr}"
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
- logger.log "* Inherited #{str}"
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
- logger.log "* Activated #{str}"
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
- logger.log "* #{log_msg} on #{str}"
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
- if params.values_at('cert', 'key').all? { |v| v.to_s.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? }
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
- ['cert', 'key'].each do |v|
244
- if params[v] && params[v].start_with?('store:')
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, @events).context
241
+ MiniSSL::ContextBuilder.new(params, @log_writer).context
250
242
  end
251
243
 
252
244
  if fd = @inherited_fds.delete(str)
253
- logger.log "* Inherited #{str}"
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
- logger.log "* Activated #{str}"
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
- io = add_ssl_listener uri.host, uri.port, ctx, optimize_for_latency = true, backlog
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
- logger.log "* #{log_msg} on ssl://#{addr}?#{uri.query}"
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
- logger.error "Invalid URI: #{str}"
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
- logger.log "* Closing unused inherited connection: #{str}"
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
- 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 ':'}"
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 }, @events).context
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+.
@@ -457,11 +450,14 @@ module Puma
457
450
 
458
451
  def close_listeners
459
452
  @listeners.each do |l, io|
460
- io.close unless io.closed?
461
- uri = URI.parse l
462
- next unless uri.scheme == 'unix'
463
- unix_path = "#{uri.host}#{uri.path}"
464
- 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
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.map { |addrinfo| addrinfo.ip_address }.uniq
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
- 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
@@ -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
- # +stdout+ and +stderr+ can be set to IO-like objects which
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
- @events.error(str)
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
@@ -152,7 +146,7 @@ module Puma
152
146
 
153
147
  o.on "-p", "--port PORT", "Define the TCP port to bind to",
154
148
  "Use -b for more advanced options" do |arg|
155
- user_config.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}"
149
+ user_config.bind "tcp://#{Configuration::DEFAULTS[:tcp_host]}:#{arg}"
156
150
  end
157
151
 
158
152
  o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg|
@@ -186,7 +180,7 @@ module Puma
186
180
  end
187
181
 
188
182
  o.on "-s", "--silent", "Do not log prompt messages other than errors" do
189
- @events = Events.new NullIO.new, $stderr
183
+ @log_writer = LogWriter.new(NullIO.new, $stderr)
190
184
  end
191
185
 
192
186
  o.on "-S", "--state PATH", "Where to store the state details" do |arg|
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
 
@@ -25,6 +26,9 @@ module Puma
25
26
 
26
27
  class HttpParserError501 < IOError; end
27
28
 
29
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
30
+
31
+
28
32
  # An instance of this class represents a unique request from a client.
29
33
  # For example, this could be a web request from a browser or from CURL.
30
34
  #
@@ -38,14 +42,15 @@ module Puma
38
42
  # the header and body are fully buffered via the `try_to_finish` method.
39
43
  # They can be used to "time out" a response via the `timeout_at` reader.
40
44
  #
41
- class Client
45
+ class Client # :nodoc:
42
46
 
43
47
  # this tests all values but the last, which must be chunked
44
48
  ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
45
49
 
46
50
  # chunked body validation
47
51
  CHUNK_SIZE_INVALID = /[^\h]/.freeze
48
- CHUNK_VALID_ENDING = "\r\n".freeze
52
+ CHUNK_VALID_ENDING = Const::LINE_END
53
+ CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
49
54
 
50
55
  # Content-Length header value validation
51
56
  CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
@@ -62,12 +67,9 @@ module Puma
62
67
  def initialize(io, env=nil)
63
68
  @io = io
64
69
  @to_io = io.to_io
70
+ @io_buffer = IOBuffer.new
65
71
  @proto_env = env
66
- if !env
67
- @env = nil
68
- else
69
- @env = env.dup
70
- end
72
+ @env = env&.dup
71
73
 
72
74
  @parser = HttpParser.new
73
75
  @parsed_bytes = 0
@@ -85,7 +87,11 @@ module Puma
85
87
  @requests_served = 0
86
88
  @hijacked = false
87
89
 
90
+ @http_content_length_limit = nil
91
+ @http_content_length_limit_exceeded = false
92
+
88
93
  @peerip = nil
94
+ @peer_family = nil
89
95
  @listener = nil
90
96
  @remote_addr_header = nil
91
97
  @expect_proxy_proto = false
@@ -93,12 +99,15 @@ module Puma
93
99
  @body_remain = 0
94
100
 
95
101
  @in_last_chunk = false
102
+
103
+ # need unfrozen ASCII-8BIT, +'' is UTF-8
104
+ @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
96
105
  end
97
106
 
98
107
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
99
- :tempfile
108
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded
100
109
 
101
- attr_writer :peerip
110
+ attr_writer :peerip, :http_content_length_limit
102
111
 
103
112
  attr_accessor :remote_addr_header, :listener
104
113
 
@@ -138,6 +147,7 @@ module Puma
138
147
 
139
148
  def reset(fast_check=true)
140
149
  @parser.reset
150
+ @io_buffer.reset
141
151
  @read_header = true
142
152
  @read_proxy = !!@expect_proxy_proto
143
153
  @env = @proto_env.dup
@@ -148,6 +158,7 @@ module Puma
148
158
  @body_remain = 0
149
159
  @peerip = nil if @remote_addr_header
150
160
  @in_last_chunk = false
161
+ @http_content_length_limit_exceeded = false
151
162
 
152
163
  if @buffer
153
164
  return false unless try_to_parse_proxy_protocol
@@ -207,6 +218,17 @@ module Puma
207
218
  end
208
219
 
209
220
  def try_to_finish
221
+ if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
222
+ @http_content_length_limit_exceeded = true
223
+ end
224
+
225
+ if @http_content_length_limit_exceeded
226
+ @buffer = nil
227
+ @body = EmptyBody
228
+ set_ready
229
+ return true
230
+ end
231
+
210
232
  return read_body if in_data_phase
211
233
 
212
234
  begin
@@ -236,6 +258,10 @@ module Puma
236
258
 
237
259
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
238
260
 
261
+ if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
262
+ @http_content_length_limit_exceeded = true
263
+ end
264
+
239
265
  if @parser.finished?
240
266
  return setup_body
241
267
  elsif @parsed_bytes >= MAX_HEADER
@@ -273,7 +299,7 @@ module Puma
273
299
  return @peerip if @peerip
274
300
 
275
301
  if @remote_addr_header
276
- hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
302
+ hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
277
303
  @peerip = hdr
278
304
  return hdr
279
305
  end
@@ -281,6 +307,16 @@ module Puma
281
307
  @peerip ||= @io.peeraddr.last
282
308
  end
283
309
 
310
+ def peer_family
311
+ return @peer_family if @peer_family
312
+
313
+ @peer_family ||= begin
314
+ @io.local_address.afamily
315
+ rescue
316
+ Socket::AF_INET
317
+ end
318
+ end
319
+
284
320
  # Returns true if the persistent connection can be closed immediately
285
321
  # without waiting for the configured idle/shutdown timeout.
286
322
  # @version 5.0.0
@@ -304,7 +340,7 @@ module Puma
304
340
  private
305
341
 
306
342
  def setup_body
307
- @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
343
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
308
344
 
309
345
  if @env[HTTP_EXPECT] == CONTINUE
310
346
  # TODO allow a hook here to check the headers before
@@ -347,8 +383,8 @@ module Puma
347
383
  cl = @env[CONTENT_LENGTH]
348
384
 
349
385
  if cl
350
- # cannot contain characters that are not \d
351
- if cl =~ CONTENT_LENGTH_VALUE_INVALID
386
+ # cannot contain characters that are not \d, or be empty
387
+ if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
352
388
  raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
353
389
  end
354
390
  else
@@ -401,7 +437,7 @@ module Puma
401
437
  end
402
438
 
403
439
  begin
404
- chunk = @io.read_nonblock(want)
440
+ chunk = @io.read_nonblock(want, @read_buffer)
405
441
  rescue IO::WaitReadable
406
442
  return false
407
443
  rescue SystemCallError, IOError
@@ -433,7 +469,7 @@ module Puma
433
469
  def read_chunked_body
434
470
  while true
435
471
  begin
436
- chunk = @io.read_nonblock(4096)
472
+ chunk = @io.read_nonblock(4096, @read_buffer)
437
473
  rescue IO::WaitReadable
438
474
  return false
439
475
  rescue SystemCallError, IOError
@@ -509,11 +545,11 @@ module Puma
509
545
 
510
546
  while !io.eof?
511
547
  line = io.gets
512
- if line.end_with?("\r\n")
548
+ if line.end_with?(CHUNK_VALID_ENDING)
513
549
  # Puma doesn't process chunk extensions, but should parse if they're
514
550
  # present, which is the reason for the semicolon regex
515
551
  chunk_hex = line.strip[/\A[^;]+/]
516
- if chunk_hex =~ CHUNK_SIZE_INVALID
552
+ if CHUNK_SIZE_INVALID.match? chunk_hex
517
553
  raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
518
554
  end
519
555
  len = chunk_hex.to_i(16)
@@ -521,13 +557,19 @@ module Puma
521
557
  @in_last_chunk = true
522
558
  @body.rewind
523
559
  rest = io.read
524
- last_crlf_size = "\r\n".bytesize
525
- if rest.bytesize < last_crlf_size
560
+ if rest.bytesize < CHUNK_VALID_ENDING_SIZE
526
561
  @buffer = nil
527
- @partial_part_left = last_crlf_size - rest.bytesize
562
+ @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
528
563
  return false
529
564
  else
530
- @buffer = rest[last_crlf_size..-1]
565
+ # if the next character is a CRLF, set buffer to everything after that CRLF
566
+ start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
567
+ CHUNK_VALID_ENDING_SIZE
568
+ else # we have started a trailer section, which we do not support. skip it!
569
+ rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
570
+ end
571
+
572
+ @buffer = rest[start_of_rest..-1]
531
573
  @buffer = nil if @buffer.empty?
532
574
  set_ready
533
575
  return true
@@ -576,10 +618,14 @@ module Puma
576
618
 
577
619
  def set_ready
578
620
  if @body_read_start
579
- @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
621
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
580
622
  end
581
623
  @requests_served += 1
582
624
  @ready = true
583
625
  end
626
+
627
+ def above_http_content_limit(value)
628
+ @http_content_length_limit&.< value
629
+ end
584
630
  end
585
631
  end