puma 5.6.4 → 6.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +372 -6
  3. data/LICENSE +0 -0
  4. data/README.md +79 -29
  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 +12 -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 +1 -0
  21. data/docs/signals.md +0 -0
  22. data/docs/stats.md +0 -0
  23. data/docs/systemd.md +3 -6
  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 +22 -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 +153 -27
  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 +167 -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 +51 -54
  42. data/lib/puma/cli.rb +16 -18
  43. data/lib/puma/client.rb +100 -26
  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 +102 -40
  47. data/lib/puma/commonlogger.rb +21 -14
  48. data/lib/puma/configuration.rb +77 -59
  49. data/lib/puma/const.rb +129 -92
  50. data/lib/puma/control_cli.rb +33 -23
  51. data/lib/puma/detect.rb +7 -4
  52. data/lib/puma/dsl.rb +251 -53
  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 +26 -12
  62. data/lib/puma/minissl.rb +113 -15
  63. data/lib/puma/null_io.rb +21 -2
  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 +1 -1
  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 +56 -20
  73. data/lib/puma/sd_notify.rb +149 -0
  74. data/lib/puma/server.rb +137 -87
  75. data/lib/puma/single.rb +13 -11
  76. data/lib/puma/state_file.rb +4 -6
  77. data/lib/puma/thread_pool.rb +57 -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 +2 -2
  82. data/tools/trickletest.rb +0 -0
  83. metadata +11 -6
  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+.
@@ -337,7 +330,7 @@ module Puma
337
330
  return
338
331
  end
339
332
 
340
- host = host[1..-2] if host and host[0..0] == '['
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[0..0] == '['
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
- 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
@@ -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::DefaultTCPHost}:#{arg}"
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
- @events = Events.new NullIO.new, $stderr
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
- require 'puma/detect'
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 = "\r\n".freeze
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
- if !env
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
- def_delegators :@io, :closed?
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] || LOCALHOST_IP).split(/[\s,]/).first
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, :millisecond)
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 =~ CONTENT_LENGTH_VALUE_INVALID
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?("\r\n")
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 =~ CHUNK_SIZE_INVALID
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
- last_crlf_size = "\r\n".bytesize
525
- if rest.bytesize < last_crlf_size
570
+ if rest.bytesize < CHUNK_VALID_ENDING_SIZE
526
571
  @buffer = nil
527
- @partial_part_left = last_crlf_size - rest.bytesize
572
+ @partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
528
573
  return false
529
574
  else
530
- @buffer = rest[last_crlf_size..-1]
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, :millisecond) - @body_read_start
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