puma 7.2.1 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/puma/binder.rb CHANGED
@@ -90,7 +90,7 @@ module Puma
90
90
  # @version 5.0.0
91
91
  #
92
92
  def create_activated_fds(env_hash)
93
- @log_writer.debug "ENV['LISTEN_FDS'] #{@env['LISTEN_FDS'].inspect} env_hash['LISTEN_PID'] #{env_hash['LISTEN_PID'].inspect}"
93
+ @log_writer.debug { "ENV['LISTEN_FDS'] #{@env['LISTEN_FDS'].inspect} env_hash['LISTEN_PID'] #{env_hash['LISTEN_PID'].inspect}" }
94
94
  return [] unless env_hash['LISTEN_FDS'] && env_hash['LISTEN_PID'].to_i == $$
95
95
  env_hash['LISTEN_FDS'].to_i.times do |index|
96
96
  sock = TCPServer.for_fd(socket_activation_fd(index))
@@ -102,7 +102,7 @@ module Puma
102
102
  [:tcp, addr, port]
103
103
  end
104
104
  @activated_sockets[key] = sock
105
- @log_writer.debug "Registered #{key.join ':'} for activation from LISTEN_FDS"
105
+ @log_writer.debug { "Registered #{key.join ':'} for activation from LISTEN_FDS" }
106
106
  end
107
107
  ["LISTEN_FDS", "LISTEN_PID"] # Signal to remove these keys from ENV
108
108
  end
data/lib/puma/cli.rb CHANGED
@@ -148,7 +148,7 @@ module Puma
148
148
 
149
149
  o.on "-p", "--port PORT", "Define the TCP port to bind to",
150
150
  "Use -b for more advanced options" do |arg|
151
- user_config.bind "tcp://#{Configuration::DEFAULTS[:tcp_host]}:#{arg}"
151
+ user_config.port arg, Configuration.default_tcp_host
152
152
  end
153
153
 
154
154
  o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg|
data/lib/puma/client.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'detect'
4
4
  require_relative 'io_buffer'
5
+ require_relative 'client_env'
5
6
  require 'tempfile'
6
7
 
7
8
  if Puma::IS_JRUBY
@@ -20,8 +21,8 @@ module Puma
20
21
  #———————————————————————— DO NOT USE — this class is for internal use only ———
21
22
 
22
23
 
23
- # An instance of this class represents a unique request from a client.
24
- # For example, this could be a web request from a browser or from CURL.
24
+ # An instance of this class wraps a connection/socket.
25
+ # For example, this could be an http request from a browser or from CURL.
25
26
  #
26
27
  # An instance of `Puma::Client` can be used as if it were an IO object
27
28
  # by the reactor. The reactor is expected to call `#to_io`
@@ -29,12 +30,18 @@ module Puma
29
30
  # `IO::try_convert` (which may call `#to_io`) when a new socket is
30
31
  # registered.
31
32
  #
32
- # Instances of this class are responsible for knowing if
33
- # the header and body are fully buffered via the `try_to_finish` method.
33
+ # Instances of this class are responsible for knowing if the request line,
34
+ # headers and body are fully buffered and verified via the `try_to_finish` method.
35
+ # All verification of each request is done in the `Client` object.
34
36
  # They can be used to "time out" a response via the `timeout_at` reader.
35
37
  #
38
+ # Most of the code for env processing and verification is contained
39
+ # in `Puma::ClientEnv`, which is included.
40
+ #
36
41
  class Client # :nodoc:
37
42
 
43
+ include ClientEnv
44
+
38
45
  # this tests all values but the last, which must be chunked
39
46
  ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
40
47
 
@@ -65,7 +72,13 @@ module Puma
65
72
  # no body share this one object since it has no state.
66
73
  EmptyBody = NullIO.new
67
74
 
68
- include Puma::Const
75
+ attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
76
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded,
77
+ :requests_served, :error_status_code
78
+
79
+ attr_writer :peerip, :http_content_length_limit, :supported_http_methods
80
+
81
+ attr_accessor :remote_addr_header, :listener, :env_set_http_version
69
82
 
70
83
  def initialize(io, env=nil)
71
84
  @io = io
@@ -91,7 +104,8 @@ module Puma
91
104
  @hijacked = false
92
105
 
93
106
  @http_content_length_limit = nil
94
- @http_content_length_limit_exceeded = false
107
+ @http_content_length_limit_exceeded = nil
108
+ @error_status_code = nil
95
109
 
96
110
  @peerip = nil
97
111
  @peer_family = nil
@@ -107,14 +121,6 @@ module Puma
107
121
  @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
108
122
  end
109
123
 
110
- attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
111
- :tempfile, :io_buffer, :http_content_length_limit_exceeded,
112
- :requests_served
113
-
114
- attr_writer :peerip, :http_content_length_limit
115
-
116
- attr_accessor :remote_addr_header, :listener
117
-
118
124
  # Remove in Puma 7?
119
125
  def closed?
120
126
  @to_io.closed?
@@ -164,7 +170,8 @@ module Puma
164
170
  @body_remain = 0
165
171
  @peerip = nil if @remote_addr_header
166
172
  @in_last_chunk = false
167
- @http_content_length_limit_exceeded = false
173
+ @http_content_length_limit_exceeded = nil
174
+ @error_status_code = nil
168
175
  end
169
176
 
170
177
  # only used with back-to-back requests contained in the buffer
@@ -174,12 +181,7 @@ module Puma
174
181
 
175
182
  @parsed_bytes = parser_execute
176
183
 
177
- if @parser.finished?
178
- return setup_body
179
- elsif @parsed_bytes >= MAX_HEADER
180
- raise HttpParserError,
181
- "HEADER is longer than allowed, aborting client early."
182
- end
184
+ @parser.finished? ? process_env_body : false
183
185
  end
184
186
  end
185
187
 
@@ -247,17 +249,6 @@ module Puma
247
249
  end
248
250
 
249
251
  def try_to_finish
250
- if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
251
- @http_content_length_limit_exceeded = true
252
- end
253
-
254
- if @http_content_length_limit_exceeded
255
- @buffer = nil
256
- @body = EmptyBody
257
- set_ready
258
- return true
259
- end
260
-
261
252
  return read_body if in_data_phase
262
253
 
263
254
  data = nil
@@ -288,18 +279,7 @@ module Puma
288
279
 
289
280
  @parsed_bytes = parser_execute
290
281
 
291
- if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
292
- @http_content_length_limit_exceeded = true
293
- end
294
-
295
- if @parser.finished?
296
- setup_body
297
- elsif @parsed_bytes >= MAX_HEADER
298
- raise HttpParserError,
299
- "HEADER is longer than allowed, aborting client early."
300
- else
301
- false
302
- end
282
+ @parser.finished? ? process_env_body : false
303
283
  end
304
284
 
305
285
  def eagerly_finish
@@ -319,7 +299,12 @@ module Puma
319
299
  # @return [Integer] bytes of buffer read by parser
320
300
  #
321
301
  def parser_execute
322
- @parser.execute(@env, @buffer, @parsed_bytes)
302
+ ret = @parser.execute(@env, @buffer, @parsed_bytes)
303
+
304
+ if @env[REQUEST_METHOD] && @supported_http_methods != :any && !@supported_http_methods.key?(@env[REQUEST_METHOD])
305
+ raise HttpParserError501, "#{@env[REQUEST_METHOD]} method is not supported"
306
+ end
307
+ ret
323
308
  rescue => e
324
309
  @env[HTTP_CONNECTION] = 'close'
325
310
  raise e unless HttpParserError === e && e.message.include?('non-SSL')
@@ -353,6 +338,15 @@ module Puma
353
338
  end
354
339
  end
355
340
 
341
+ # processes the `env` and the request body
342
+ def process_env_body
343
+ temp = setup_body
344
+ normalize_env
345
+ req_env_post_parse
346
+ raise HttpParserError if @error_status_code
347
+ temp
348
+ end
349
+
356
350
  def timeout!
357
351
  write_error(408) if in_data_phase
358
352
  raise ConnectionError
@@ -369,12 +363,12 @@ module Puma
369
363
  return @peerip if @peerip
370
364
 
371
365
  if @remote_addr_header
372
- hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
366
+ hdr = (@env[@remote_addr_header] || socket_peerip).split(/[\s,]/).first
373
367
  @peerip = hdr
374
368
  return hdr
375
369
  end
376
370
 
377
- @peerip ||= @io.peeraddr.last
371
+ @peerip ||= socket_peerip
378
372
  end
379
373
 
380
374
  def peer_family
@@ -409,19 +403,36 @@ module Puma
409
403
 
410
404
  private
411
405
 
406
+ IPV4_MAPPED_IPV6_PREFIX = "::ffff:"
407
+ private_constant :IPV4_MAPPED_IPV6_PREFIX
408
+
409
+ def socket_peerip
410
+ unmap_ipv6(@io.peeraddr.last)
411
+ end
412
+
413
+ # Converts IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) back to
414
+ # their IPv4 form. These addresses appear when IPv4 clients connect to
415
+ # a dual-stack IPv6 socket.
416
+ def unmap_ipv6(addr)
417
+ addr.delete_prefix(IPV4_MAPPED_IPV6_PREFIX)
418
+ end
419
+
420
+ # Checks the request `Transfer-Encoding` and/or `Content-Length` to see if
421
+ # they are valid. Raises errors if not, otherwise reads the body.
422
+ # @return [Boolean] true if the body can be completely read, false otherwise
423
+ #
412
424
  def setup_body
413
425
  @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
414
426
 
415
427
  if @env[HTTP_EXPECT] == CONTINUE
416
- # TODO allow a hook here to check the headers before
417
- # going forward
428
+ # TODO allow a hook here to check the headers before going forward
418
429
  @io << HTTP_11_100
419
430
  @io.flush
420
431
  end
421
432
 
422
433
  @read_header = false
423
434
 
424
- body = @parser.body
435
+ parser_body = @parser.body
425
436
 
426
437
  te = @env[TRANSFER_ENCODING2]
427
438
  if te
@@ -438,10 +449,10 @@ module Puma
438
449
  raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
439
450
  end
440
451
  @env.delete TRANSFER_ENCODING2
441
- return setup_chunked_body body
452
+ return setup_chunked_body parser_body
442
453
  elsif te_lwr == CHUNKED
443
454
  @env.delete TRANSFER_ENCODING2
444
- return setup_chunked_body body
455
+ return setup_chunked_body parser_body
445
456
  elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
446
457
  raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
447
458
  else
@@ -456,10 +467,12 @@ module Puma
456
467
  if cl
457
468
  # cannot contain characters that are not \d, or be empty
458
469
  if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
470
+ @error_status_code = 400
471
+ @env[HTTP_CONNECTION] = 'close'
459
472
  raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
460
473
  end
461
474
  else
462
- @buffer = body.empty? ? nil : body
475
+ @buffer = parser_body.empty? ? nil : parser_body
463
476
  @body = EmptyBody
464
477
  set_ready
465
478
  return true
@@ -467,23 +480,25 @@ module Puma
467
480
 
468
481
  content_length = cl.to_i
469
482
 
470
- remain = content_length - body.bytesize
483
+ raise_above_http_content_limit if @http_content_length_limit&.< content_length
484
+
485
+ remain = content_length - parser_body.bytesize
471
486
 
472
487
  if remain <= 0
473
- # Part of the body is a pipelined request OR garbage. We'll deal with that later.
488
+ # Part of the parser_body is a pipelined request OR garbage. We'll deal with that later.
474
489
  if content_length == 0
475
490
  @body = EmptyBody
476
- if body.empty?
491
+ if parser_body.empty?
477
492
  @buffer = nil
478
493
  else
479
- @buffer = body
494
+ @buffer = parser_body
480
495
  end
481
496
  elsif remain == 0
482
- @body = StringIO.new body
497
+ @body = StringIO.new parser_body
483
498
  @buffer = nil
484
499
  else
485
- @body = StringIO.new(body[0,content_length])
486
- @buffer = body[content_length..-1]
500
+ @body = StringIO.new(parser_body[0,content_length])
501
+ @buffer = parser_body[content_length..-1]
487
502
  end
488
503
  set_ready
489
504
  return true
@@ -495,12 +510,12 @@ module Puma
495
510
  @body.binmode
496
511
  @tempfile = @body
497
512
  else
498
- # The body[0,0] trick is to get an empty string in the same
499
- # encoding as body.
500
- @body = StringIO.new body[0,0]
513
+ # The parser_body[0,0] trick is to get an empty string in the same
514
+ # encoding as parser_body.
515
+ @body = StringIO.new parser_body[0,0]
501
516
  end
502
517
 
503
- @body.write body
518
+ @body.write parser_body
504
519
 
505
520
  @body_remain = remain
506
521
 
@@ -593,6 +608,10 @@ module Puma
593
608
 
594
609
  # @version 5.0.0
595
610
  def write_chunk(str)
611
+ if @http_content_length_limit&.< @chunked_content_length + str.bytesize
612
+ raise_above_http_content_limit
613
+ end
614
+
596
615
  @chunked_content_length += @body.write(str)
597
616
  end
598
617
 
@@ -725,8 +744,13 @@ module Puma
725
744
  @ready = true
726
745
  end
727
746
 
728
- def above_http_content_limit(value)
729
- @http_content_length_limit&.< value
747
+ def raise_above_http_content_limit
748
+ @http_content_length_limit_exceeded = true
749
+ @buffer = nil
750
+ @body = EmptyBody
751
+ @error_status_code = 413
752
+ @env[HTTP_CONNECTION] = 'close'
753
+ raise HttpParserError, "Payload Too Large"
730
754
  end
731
755
  end
732
756
  end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+
5
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
6
+
7
+
8
+ # This module is included in `Client`. It contains code to process the `env`
9
+ # before it is passed to the app.
10
+ #
11
+ module ClientEnv # :nodoc:
12
+
13
+ include Puma::Const
14
+
15
+ # Given a Hash +env+ for the request read from +client+, add
16
+ # and fixup keys to comply with Rack's env guidelines.
17
+ # @param env [Hash] see Puma::Client#env, from request
18
+ # @param client [Puma::Client] only needed for Client#peerip
19
+ #
20
+ def normalize_env
21
+ if host = @env[HTTP_HOST]
22
+ # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
23
+ if colon = host.rindex("]:") # IPV6 with port
24
+ @env[SERVER_NAME] = host[0, colon+1]
25
+ @env[SERVER_PORT] = host[colon+2, host.bytesize]
26
+ elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
27
+ @env[SERVER_NAME] = host[0, colon]
28
+ @env[SERVER_PORT] = host[colon+1, host.bytesize]
29
+ else
30
+ @env[SERVER_NAME] = host
31
+ @env[SERVER_PORT] = default_server_port
32
+ end
33
+ else
34
+ @env[SERVER_NAME] = LOCALHOST
35
+ @env[SERVER_PORT] = default_server_port
36
+ end
37
+
38
+ unless @env[REQUEST_PATH]
39
+ # it might be a dumbass full host request header
40
+ uri = begin
41
+ URI.parse(@env[REQUEST_URI])
42
+ rescue URI::InvalidURIError
43
+ raise Puma::HttpParserError
44
+ end
45
+ @env[REQUEST_PATH] = uri.path
46
+
47
+ # A nil env value will cause a LintError (and fatal errors elsewhere),
48
+ # so only set the env value if there actually is a value.
49
+ @env[QUERY_STRING] = uri.query if uri.query
50
+ end
51
+
52
+ @env[PATH_INFO] = @env[REQUEST_PATH].to_s # #to_s in case it's nil
53
+
54
+ # From https://www.ietf.org/rfc/rfc3875 :
55
+ # "Script authors should be aware that the REMOTE_ADDR and
56
+ # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
57
+ # may not identify the ultimate source of the request.
58
+ # They identify the client for the immediate request to the
59
+ # server; that client may be a proxy, gateway, or other
60
+ # intermediary acting on behalf of the actual source client."
61
+ #
62
+
63
+ unless @env.key?(REMOTE_ADDR)
64
+ begin
65
+ addr = peerip
66
+ rescue Errno::ENOTCONN
67
+ # Client disconnects can result in an inability to get the
68
+ # peeraddr from the socket; default to unspec.
69
+ if peer_family == Socket::AF_INET6
70
+ addr = UNSPECIFIED_IPV6
71
+ else
72
+ addr = UNSPECIFIED_IPV4
73
+ end
74
+ end
75
+
76
+ # Set unix socket addrs to localhost
77
+ if addr.empty?
78
+ addr = peer_family == Socket::AF_INET6 ? LOCALHOST_IPV6 : LOCALHOST_IPV4
79
+ end
80
+
81
+ @env[REMOTE_ADDR] = addr
82
+ end
83
+
84
+ # The legacy HTTP_VERSION header can be sent as a client header.
85
+ # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
86
+ @env[HTTP_VERSION] = @env[SERVER_PROTOCOL] if @env_set_http_version
87
+
88
+ @env[PUMA_SOCKET] = @io
89
+
90
+ if @env[HTTPS_KEY] && @io.peercert
91
+ @env[PUMA_PEERCERT] = @io.peercert
92
+ end
93
+
94
+ @env[HIJACK_P] = true
95
+ @env[HIJACK] = method(:full_hijack).to_proc
96
+
97
+ @env[RACK_INPUT] = @body || EmptyBody
98
+ @env[RACK_URL_SCHEME] ||= default_server_port == PORT_443 ? HTTPS : HTTP
99
+ end
100
+
101
+ # Fixup any headers with `,` in the name to have `_` now. We emit
102
+ # headers with `,` in them during the parse phase to avoid ambiguity
103
+ # with the `-` to `_` conversion for critical headers. But here for
104
+ # compatibility, we'll convert them back. This code is written to
105
+ # avoid allocation in the common case (ie there are no headers
106
+ # with `,` in their names), that's why it has the extra conditionals.
107
+ #
108
+ # @note If a normalized version of a `,` header already exists, we ignore
109
+ # the `,` version. This prevents clobbering headers managed by proxies
110
+ # but not by clients (Like X-Forwarded-For).
111
+ #
112
+ # @param env [Hash] see Puma::Client#env, from request, modifies in place
113
+ # @version 5.0.3
114
+ #
115
+ def req_env_post_parse
116
+ to_delete = nil
117
+ to_add = nil
118
+
119
+ @env.each do |k,v|
120
+ if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
121
+ if to_delete
122
+ to_delete << k
123
+ else
124
+ to_delete = [k]
125
+ end
126
+
127
+ new_k = k.tr(",", "_")
128
+ if @env.key?(new_k)
129
+ next
130
+ end
131
+
132
+ unless to_add
133
+ to_add = {}
134
+ end
135
+
136
+ to_add[new_k] = v
137
+ end
138
+ end
139
+
140
+ if to_delete # rubocop:disable Style/SafeNavigation
141
+ to_delete.each { |k| env.delete(k) }
142
+ end
143
+
144
+ if to_add
145
+ @env.merge! to_add
146
+ end
147
+
148
+ # A rack extension. If the app writes #call'ables to this
149
+ # array, we will invoke them when the request is done.
150
+ #
151
+ env[RACK_AFTER_REPLY] ||= []
152
+ env[RACK_RESPONSE_FINISHED] ||= []
153
+ end
154
+
155
+ HTTP_ON_VALUES = { "on" => true, HTTPS => true }
156
+ private_constant :HTTP_ON_VALUES
157
+
158
+ # @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
159
+ #
160
+ def default_server_port
161
+ if HTTP_ON_VALUES[@env[HTTPS_KEY]] ||
162
+ @env[HTTP_X_FORWARDED_PROTO]&.start_with?(HTTPS) ||
163
+ @env[HTTP_X_FORWARDED_SCHEME] == HTTPS ||
164
+ @env[HTTP_X_FORWARDED_SSL] == "on"
165
+ PORT_443
166
+ else
167
+ PORT_80
168
+ end
169
+ end
170
+ end
171
+ end
data/lib/puma/cluster.rb CHANGED
@@ -87,7 +87,7 @@ module Puma
87
87
  end
88
88
 
89
89
  debug "Spawned worker: #{pid}"
90
- @workers << WorkerHandle.new(idx, pid, @phase, @options)
90
+ @workers.insert(idx, WorkerHandle.new(idx, pid, @phase, @options))
91
91
  end
92
92
 
93
93
  if @options[:fork_worker] && all_workers_in_phase?
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'socket'
4
+ require 'uri'
5
+
3
6
  require_relative 'plugin'
4
7
  require_relative 'const'
5
8
  require_relative 'dsl'
@@ -131,14 +134,16 @@ module Puma
131
134
 
132
135
  DEFAULTS = {
133
136
  auto_trim_time: 30,
134
- binds: ['tcp://0.0.0.0:9292'.freeze],
135
- fiber_per_request: !!ENV.fetch("PUMA_FIBER_PER_REQUEST", false),
137
+ binds: ['tcp://[::]:9292'.freeze],
136
138
  debug: false,
137
- enable_keep_alives: true,
138
139
  early_hints: nil,
140
+ enable_keep_alives: true,
139
141
  environment: 'development'.freeze,
142
+ fiber_per_request: !!ENV.fetch("PUMA_FIBER_PER_REQUEST", false),
140
143
  # Number of seconds to wait until we get the first data for the request.
141
144
  first_data_timeout: 30,
145
+ force_shutdown_after: -1,
146
+ http_content_length_limit: nil,
142
147
  # Number of seconds to wait until the next request before shutting down.
143
148
  idle_timeout: nil,
144
149
  io_selector_backend: :auto,
@@ -147,6 +152,7 @@ module Puma
147
152
  # Limits how many requests a keep alive connection can make.
148
153
  # The connection will be closed after it reaches `max_keep_alive`
149
154
  # requests.
155
+ max_io_threads: 0,
150
156
  max_keep_alive: 999,
151
157
  max_threads: Puma.mri? ? 5 : 16,
152
158
  min_threads: 0,
@@ -161,10 +167,10 @@ module Puma
161
167
  raise_exception_on_sigterm: true,
162
168
  reaping_time: 1,
163
169
  remote_address: :socket,
164
- silence_single_worker_warning: false,
165
170
  silence_fork_callback_warning: false,
171
+ silence_single_worker_warning: false,
166
172
  tag: File.basename(Dir.getwd),
167
- tcp_host: '0.0.0.0'.freeze,
173
+ tcp_host: '::'.freeze,
168
174
  tcp_port: 9292,
169
175
  wait_for_less_busy_worker: 0.005,
170
176
  worker_boot_timeout: 60,
@@ -173,11 +179,10 @@ module Puma
173
179
  worker_shutdown_timeout: 30,
174
180
  worker_timeout: 60,
175
181
  workers: 0,
176
- http_content_length_limit: nil
177
182
  }
178
183
 
179
184
  def initialize(user_options={}, default_options = {}, env = ENV, &block)
180
- default_options = self.puma_default_options(env).merge(default_options)
185
+ default_options = self.puma_default_options(env).merge(events: Events.new).merge(default_options)
181
186
 
182
187
  @_options = UserFileDefaultOptions.new(user_options, default_options)
183
188
  @plugins = PluginLoader.new
@@ -230,6 +235,8 @@ module Puma
230
235
 
231
236
  def puma_default_options(env = ENV)
232
237
  defaults = DEFAULTS.dup
238
+ defaults[:tcp_host] = self.class.default_tcp_host
239
+ defaults[:binds] = [self.class.default_tcp_bind]
233
240
  puma_options_from_env(env).each { |k,v| defaults[k] = v if v }
234
241
  defaults
235
242
  end
@@ -277,8 +284,10 @@ module Puma
277
284
  # This also calls load if it hasn't been called yet.
278
285
  def clamp
279
286
  load unless @loaded
287
+ run_mode_hooks
280
288
  set_conditional_default_options
281
289
  @_options.finalize_values
290
+ rewrite_unavailable_ipv6_binds!
282
291
  @clamped = true
283
292
  warn_hooks
284
293
  options
@@ -335,7 +344,7 @@ module Puma
335
344
  # @param arg [Launcher, Int] `:before_restart` passes Launcher
336
345
  #
337
346
  def run_hooks(key, arg, log_writer, hook_data = nil)
338
- log_writer.debug "Running #{key} hooks"
347
+ log_writer.debug { "Running #{key} hooks" }
339
348
 
340
349
  options.all_of(key).each do |hook_options|
341
350
  begin
@@ -357,6 +366,23 @@ module Puma
357
366
  options.final_options
358
367
  end
359
368
 
369
+ def self.default_tcp_host
370
+ ipv6_interface_available? ? Const::UNSPECIFIED_IPV6 : Const::UNSPECIFIED_IPV4
371
+ end
372
+
373
+ def self.default_tcp_bind(port = DEFAULTS[:tcp_port])
374
+ URI::Generic.build(scheme: 'tcp', host: default_tcp_host, port: Integer(port)).to_s
375
+ end
376
+
377
+ def self.ipv6_interface_available?
378
+ Socket.getifaddrs.any? do |ifaddr|
379
+ addr = ifaddr.addr
380
+ addr&.ipv6? && !addr&.ipv6_loopback?
381
+ end
382
+ rescue StandardError
383
+ false
384
+ end
385
+
360
386
  def self.temp_path
361
387
  require 'tmpdir'
362
388
 
@@ -372,6 +398,31 @@ module Puma
372
398
 
373
399
  private
374
400
 
401
+ def rewrite_unavailable_ipv6_binds!
402
+ return if self.class.ipv6_interface_available?
403
+
404
+ tried_ipv6_bind = false
405
+ bind_schemes = ['tcp', 'ssl']
406
+
407
+ @_options[:binds] = Array(@_options[:binds]).map do |bind|
408
+ uri = URI.parse(bind)
409
+ next bind unless bind_schemes.include?(uri.scheme)
410
+
411
+ host = uri.host&.delete_prefix('[')&.delete_suffix(']')
412
+ next bind unless host&.include?(':')
413
+
414
+ tried_ipv6_bind = true
415
+ next bind unless host == Const::UNSPECIFIED_IPV6
416
+
417
+ uri.host = Const::UNSPECIFIED_IPV4
418
+ uri.to_s
419
+ rescue URI::InvalidURIError
420
+ bind
421
+ end
422
+
423
+ warn "WARNING: IPv6 bind requested but no non-loopback IPv6 interface was detected" if tried_ipv6_bind
424
+ end
425
+
375
426
  def require_processor_counter
376
427
  require 'concurrent/utility/processor_counter'
377
428
  rescue LoadError
@@ -433,6 +484,17 @@ module Puma
433
484
  rack_app
434
485
  end
435
486
 
487
+ def run_mode_hooks
488
+ workers_before = @_options[:workers]
489
+ key = workers_before > 0 ? :cluster : :single
490
+
491
+ @_options.all_of(key).each(&:call)
492
+
493
+ unless @_options[:workers] == workers_before
494
+ raise "cannot change the number of workers inside a #{key} configuration hook"
495
+ end
496
+ end
497
+
436
498
  def set_conditional_default_options
437
499
  @_options.default_options[:preload_app] = !@_options[:prune_bundler] &&
438
500
  (@_options[:workers] > 1) && Puma.forkable?