puma 5.0.4 → 5.1.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.

@@ -34,6 +34,34 @@ module Puma
34
34
  class DSL
35
35
  include ConfigDefault
36
36
 
37
+ # convenience method so logic can be used in CI
38
+ # @see ssl_bind
39
+ #
40
+ def self.ssl_bind_str(host, port, opts)
41
+ verify = opts.fetch(:verify_mode, 'none').to_s
42
+
43
+ tls_str =
44
+ if opts[:no_tlsv1_1] then '&no_tlsv1_1=true'
45
+ elsif opts[:no_tlsv1] then '&no_tlsv1=true'
46
+ else ''
47
+ end
48
+
49
+ ca_additions = "&ca=#{opts[:ca]}" if ['peer', 'force_peer'].include?(verify)
50
+
51
+ if defined?(JRUBY_VERSION)
52
+ ssl_cipher_list = opts[:ssl_cipher_list] ?
53
+ "&ssl_cipher_list=#{opts[:ssl_cipher_list]}" : nil
54
+ keystore_additions = "keystore=#{opts[:keystore]}&keystore-pass=#{opts[:keystore_pass]}"
55
+ "ssl://#{host}:#{port}?#{keystore_additions}#{ssl_cipher_list}" \
56
+ "&verify_mode=#{verify}#{tls_str}#{ca_additions}"
57
+ else
58
+ ssl_cipher_filter = opts[:ssl_cipher_filter] ?
59
+ "&ssl_cipher_filter=#{opts[:ssl_cipher_filter]}" : nil
60
+ "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}" \
61
+ "#{ssl_cipher_filter}&verify_mode=#{verify}#{tls_str}#{ca_additions}"
62
+ end
63
+ end
64
+
37
65
  def initialize(options, config)
38
66
  @config = config
39
67
  @options = options
@@ -191,6 +219,32 @@ module Puma
191
219
  @options[:binds] = []
192
220
  end
193
221
 
222
+ # Bind to (systemd) activated sockets, regardless of configured binds.
223
+ #
224
+ # Systemd can present sockets as file descriptors that are already opened.
225
+ # By default Puma will use these but only if it was explicitly told to bind
226
+ # to the socket. If not, it will close the activated sockets. This means
227
+ # all configuration is duplicated.
228
+ #
229
+ # Binds can contain additional configuration, but only SSL config is really
230
+ # relevant since the unix and TCP socket options are ignored.
231
+ #
232
+ # This means there is a lot of duplicated configuration for no additional
233
+ # value in most setups. This method tells the launcher to bind to all
234
+ # activated sockets, regardless of existing bind.
235
+ #
236
+ # To clear configured binds, the value only can be passed. This will clear
237
+ # out any binds that may have been configured.
238
+ #
239
+ # @example Use any systemd activated sockets as well as configured binds
240
+ # bind_to_activated_sockets
241
+ #
242
+ # @example Only bind to systemd activated sockets, ignoring other binds
243
+ # bind_to_activated_sockets 'only'
244
+ def bind_to_activated_sockets(bind=true)
245
+ @options[:bind_to_activated_sockets] = bind
246
+ end
247
+
194
248
  # Define the TCP port to bind to. Use +bind+ for more advanced options.
195
249
  #
196
250
  # @example
@@ -376,28 +430,15 @@ module Puma
376
430
  # ssl_cipher_filter: cipher_filter, # optional
377
431
  # verify_mode: verify_mode, # default 'none'
378
432
  # }
379
- # @example For JRuby additional keys are required: keystore & keystore_pass.
433
+ # @example For JRuby, two keys are required: keystore & keystore_pass.
380
434
  # ssl_bind '127.0.0.1', '9292', {
381
- # cert: path_to_cert,
382
- # key: path_to_key,
383
- # ssl_cipher_filter: cipher_filter, # optional
384
- # verify_mode: verify_mode, # default 'none'
385
435
  # keystore: path_to_keystore,
386
- # keystore_pass: password
436
+ # keystore_pass: password,
437
+ # ssl_cipher_list: cipher_list, # optional
438
+ # verify_mode: verify_mode # default 'none'
387
439
  # }
388
440
  def ssl_bind(host, port, opts)
389
- verify = opts.fetch(:verify_mode, 'none').to_s
390
- no_tlsv1 = opts.fetch(:no_tlsv1, 'false')
391
- no_tlsv1_1 = opts.fetch(:no_tlsv1_1, 'false')
392
- ca_additions = "&ca=#{opts[:ca]}" if ['peer', 'force_peer'].include?(verify)
393
-
394
- if defined?(JRUBY_VERSION)
395
- keystore_additions = "keystore=#{opts[:keystore]}&keystore-pass=#{opts[:keystore_pass]}"
396
- bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&#{keystore_additions}&verify_mode=#{verify}&no_tlsv1=#{no_tlsv1}&no_tlsv1_1=#{no_tlsv1_1}#{ca_additions}"
397
- else
398
- ssl_cipher_filter = "&ssl_cipher_filter=#{opts[:ssl_cipher_filter]}" if opts[:ssl_cipher_filter]
399
- bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}#{ssl_cipher_filter}&verify_mode=#{verify}&no_tlsv1=#{no_tlsv1}&no_tlsv1_1=#{no_tlsv1_1}#{ca_additions}"
400
- end
441
+ bind self.class.ssl_bind_str(host, port, opts)
401
442
  end
402
443
 
403
444
  # Use +path+ as the file to store the server info state. This is
@@ -561,7 +602,7 @@ module Puma
561
602
  end
562
603
 
563
604
  # Preload the application before starting the workers; this conflicts with
564
- # phased restart feature. This is off by default.
605
+ # phased restart feature. On by default if your app uses more than 1 worker.
565
606
  #
566
607
  # @note Cluster mode only.
567
608
  # @example
@@ -810,5 +851,12 @@ module Puma
810
851
  def nakayoshi_fork(enabled=true)
811
852
  @options[:nakayoshi_fork] = enabled
812
853
  end
854
+
855
+ # The number of requests to attempt inline before sending a client back to
856
+ # the reactor to be subject to normal ordering.
857
+ #
858
+ def max_fast_inline(num_of_requests)
859
+ @options[:max_fast_inline] = Float(num_of_requests)
860
+ end
813
861
  end
814
862
  end
@@ -137,10 +137,26 @@ module Puma
137
137
  register(:on_booted, &block)
138
138
  end
139
139
 
140
+ def on_restart(&block)
141
+ register(:on_restart, &block)
142
+ end
143
+
144
+ def on_stopped(&block)
145
+ register(:on_stopped, &block)
146
+ end
147
+
140
148
  def fire_on_booted!
141
149
  fire(:on_booted)
142
150
  end
143
151
 
152
+ def fire_on_restart!
153
+ fire(:on_restart)
154
+ end
155
+
156
+ def fire_on_stopped!
157
+ fire(:on_stopped)
158
+ end
159
+
144
160
  DEFAULT = new(STDOUT, STDERR)
145
161
 
146
162
  # Returns an Events object which writes its status to 2 StringIO
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+ require 'stringio'
3
+
4
+ module Puma
5
+
6
+ # Puma deliberately avoids the use of the json gem and instead performs JSON
7
+ # serialization without any external dependencies. In a puma cluster, loading
8
+ # any gem into the puma master process means that operators cannot use a
9
+ # phased restart to upgrade their application if the new version of that
10
+ # application uses a different version of that gem. The json gem in
11
+ # particular is additionally problematic because it leverages native
12
+ # extensions. If the puma master process relies on a gem with native
13
+ # extensions and operators remove gems from disk related to old releases,
14
+ # subsequent phased restarts can fail.
15
+ #
16
+ # The implementation of JSON serialization in this module is not designed to
17
+ # be particularly full-featured or fast. It just has to handle the few places
18
+ # where Puma relies on JSON serialization internally.
19
+
20
+ module JSON
21
+ QUOTE = /"/
22
+ BACKSLASH = /\\/
23
+ CONTROL_CHAR_TO_ESCAPE = /[\x00-\x1F]/ # As required by ECMA-404
24
+ CHAR_TO_ESCAPE = Regexp.union QUOTE, BACKSLASH, CONTROL_CHAR_TO_ESCAPE
25
+
26
+ class SerializationError < StandardError; end
27
+
28
+ class << self
29
+ def generate(value)
30
+ StringIO.open do |io|
31
+ serialize_value io, value
32
+ io.string
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def serialize_value(output, value)
39
+ case value
40
+ when Hash
41
+ output << '{'
42
+ value.each_with_index do |(k, v), index|
43
+ output << ',' if index != 0
44
+ serialize_object_key output, k
45
+ output << ':'
46
+ serialize_value output, v
47
+ end
48
+ output << '}'
49
+ when Array
50
+ output << '['
51
+ value.each_with_index do |member, index|
52
+ output << ',' if index != 0
53
+ serialize_value output, member
54
+ end
55
+ output << ']'
56
+ when Integer, Float
57
+ output << value.to_s
58
+ when String
59
+ serialize_string output, value
60
+ when true
61
+ output << 'true'
62
+ when false
63
+ output << 'false'
64
+ when nil
65
+ output << 'null'
66
+ else
67
+ raise SerializationError, "Unexpected value of type #{value.class}"
68
+ end
69
+ end
70
+
71
+ def serialize_string(output, value)
72
+ output << '"'
73
+ output << value.gsub(CHAR_TO_ESCAPE) do |character|
74
+ case character
75
+ when BACKSLASH
76
+ '\\\\'
77
+ when QUOTE
78
+ '\\"'
79
+ when CONTROL_CHAR_TO_ESCAPE
80
+ '\u%.4X' % character.ord
81
+ end
82
+ end
83
+ output << '"'
84
+ end
85
+
86
+ def serialize_object_key(output, value)
87
+ case value
88
+ when Symbol, String
89
+ serialize_string output, value.to_s
90
+ else
91
+ raise SerializationError, "Could not serialize object of type #{value.class} as object key"
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -58,6 +58,13 @@ module Puma
58
58
 
59
59
  @config.load
60
60
 
61
+ if @config.options[:bind_to_activated_sockets]
62
+ @config.options[:binds] = @binder.synthesize_binds_from_activated_fs(
63
+ @config.options[:binds],
64
+ @config.options[:bind_to_activated_sockets] == 'only'
65
+ )
66
+ end
67
+
61
68
  @options = @config.options
62
69
  @config.clamp
63
70
 
@@ -87,6 +94,8 @@ module Puma
87
94
  Puma.stats_object = @runner
88
95
 
89
96
  @status = :run
97
+
98
+ log_config if ENV['PUMA_LOG_CONFIG']
90
99
  end
91
100
 
92
101
  attr_reader :binder, :events, :config, :options, :restart_dir
@@ -130,6 +139,7 @@ module Puma
130
139
 
131
140
  # Begin async shutdown of the server gracefully
132
141
  def stop
142
+ @events.fire_on_stopped!
133
143
  @status = :stop
134
144
  @runner.stop
135
145
  end
@@ -168,6 +178,7 @@ module Puma
168
178
 
169
179
  setup_signals
170
180
  set_process_title
181
+ integrate_with_systemd
171
182
  @runner.run
172
183
 
173
184
  case @status
@@ -229,11 +240,10 @@ module Puma
229
240
  def write_pid
230
241
  path = @options[:pidfile]
231
242
  return unless path
232
-
233
- File.open(path, 'w') { |f| f.puts Process.pid }
234
- cur = Process.pid
243
+ cur_pid = Process.pid
244
+ File.write path, cur_pid, mode: 'wb:UTF-8'
235
245
  at_exit do
236
- delete_pidfile if cur == Process.pid
246
+ delete_pidfile if cur_pid == Process.pid
237
247
  end
238
248
  end
239
249
 
@@ -242,6 +252,7 @@ module Puma
242
252
  end
243
253
 
244
254
  def restart!
255
+ @events.fire_on_restart!
245
256
  @config.run_hooks :on_restart, self, @events
246
257
 
247
258
  if Puma.jruby?
@@ -316,6 +327,30 @@ module Puma
316
327
  end
317
328
  end
318
329
 
330
+ #
331
+ # Puma's systemd integration allows Puma to inform systemd:
332
+ # 1. when it has successfully started
333
+ # 2. when it is starting shutdown
334
+ # 3. periodically for a liveness check with a watchdog thread
335
+ #
336
+
337
+ def integrate_with_systemd
338
+ return unless ENV["NOTIFY_SOCKET"]
339
+
340
+ begin
341
+ require 'puma/systemd'
342
+ rescue LoadError
343
+ log "Systemd integration failed. It looks like you're trying to use systemd notify but don't have sd_notify gem installed"
344
+ return
345
+ end
346
+
347
+ log "* Enabling systemd notification integration"
348
+
349
+ systemd = Systemd.new(@events)
350
+ systemd.hook_events
351
+ systemd.start_watchdog
352
+ end
353
+
319
354
  def spec_for_gem(gem_name)
320
355
  Bundler.rubygems.loaded_specs(gem_name)
321
356
  end
@@ -338,6 +373,7 @@ module Puma
338
373
  end
339
374
 
340
375
  def graceful_stop
376
+ @events.fire_on_stopped!
341
377
  @runner.stop_blocked
342
378
  log "=== puma shutdown: #{Time.now} ==="
343
379
  log "- Goodbye!"
@@ -493,5 +529,14 @@ module Puma
493
529
  Bundler.with_unbundled_env { yield }
494
530
  end
495
531
  end
532
+
533
+ def log_config
534
+ log "Configuration:"
535
+
536
+ @config.final_options
537
+ .each { |config_key, value| log "- #{config_key}: #{value}" }
538
+
539
+ log "\n"
540
+ end
496
541
  end
497
542
  end
@@ -38,11 +38,11 @@ module Puma
38
38
  end
39
39
  end
40
40
 
41
- # Add a new IO object to monitor.
41
+ # Add a new client to monitor.
42
42
  # The object must respond to #timeout and #timeout_at.
43
43
  # Returns false if the reactor is already shut down.
44
- def add(io)
45
- @input << io
44
+ def add(client)
45
+ @input << client
46
46
  @selector.wakeup
47
47
  true
48
48
  rescue ClosedQueueError
@@ -92,17 +92,19 @@ module Puma
92
92
  end
93
93
 
94
94
  # Start monitoring the object.
95
- def register(io)
96
- @selector.register(io, :r).value = io
97
- @timeouts << io
95
+ def register(client)
96
+ @selector.register(client.to_io, :r).value = client
97
+ @timeouts << client
98
+ rescue ArgumentError
99
+ # unreadable clients raise error when processed by NIO
98
100
  end
99
101
 
100
102
  # 'Wake up' a monitored object by calling the provided block.
101
103
  # Stop monitoring the object if the block returns `true`.
102
- def wakeup!(io)
103
- if @block.call(io)
104
- @selector.deregister(io)
105
- @timeouts.delete(io)
104
+ def wakeup!(client)
105
+ if @block.call client
106
+ @selector.deregister client.to_io
107
+ @timeouts.delete client
106
108
  end
107
109
  end
108
110
  end
@@ -282,13 +282,20 @@ module Puma
282
282
  end
283
283
  # private :normalize_env
284
284
 
285
+ # @param header_key [#to_s]
286
+ # @return [Boolean]
287
+ #
288
+ def illegal_header_key?(header_key)
289
+ !!(ILLEGAL_HEADER_KEY_REGEX =~ header_key.to_s)
290
+ end
291
+
285
292
  # @param header_value [#to_s]
286
293
  # @return [Boolean]
287
294
  #
288
- def possible_header_injection?(header_value)
289
- !!(HTTP_INJECTION_REGEX =~ header_value.to_s)
295
+ def illegal_header_value?(header_value)
296
+ !!(ILLEGAL_HEADER_VALUE_REGEX =~ header_value.to_s)
290
297
  end
291
- private :possible_header_injection?
298
+ private :illegal_header_key?, :illegal_header_value?
292
299
 
293
300
  # Fixup any headers with `,` in the name to have `_` now. We emit
294
301
  # headers with `,` in them during the parse phase to avoid ambiguity
@@ -334,9 +341,11 @@ module Puma
334
341
  def str_early_hints(headers)
335
342
  eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
336
343
  headers.each_pair do |k, vs|
344
+ next if illegal_header_key?(k)
345
+
337
346
  if vs.respond_to?(:to_s) && !vs.to_s.empty?
338
347
  vs.to_s.split(NEWLINE).each do |v|
339
- next if possible_header_injection?(v)
348
+ next if illegal_header_value?(v)
340
349
  eh_str << "#{k}: #{v}\r\n"
341
350
  end
342
351
  else
@@ -399,9 +408,11 @@ module Puma
399
408
  res_info[:response_hijack] = nil
400
409
 
401
410
  headers.each do |k, vs|
411
+ next if illegal_header_key?(k)
412
+
402
413
  case k.downcase
403
414
  when CONTENT_LENGTH2
404
- next if possible_header_injection?(vs)
415
+ next if illegal_header_value?(vs)
405
416
  res_info[:content_length] = vs
406
417
  next
407
418
  when TRANSFER_ENCODING
@@ -410,11 +421,13 @@ module Puma
410
421
  when HIJACK
411
422
  res_info[:response_hijack] = vs
412
423
  next
424
+ when BANNED_HEADER_KEY
425
+ next
413
426
  end
414
427
 
415
428
  if vs.respond_to?(:to_s) && !vs.to_s.empty?
416
429
  vs.to_s.split(NEWLINE).each do |v|
417
- next if possible_header_injection?(v)
430
+ next if illegal_header_value?(v)
418
431
  lines.append k, colon, v, line_ending
419
432
  end
420
433
  else