puma 3.12.6 → 4.3.5

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +122 -2
  3. data/README.md +76 -48
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  7. data/docs/images/puma-connection-flow.png +0 -0
  8. data/docs/images/puma-general-arch.png +0 -0
  9. data/docs/plugins.md +20 -10
  10. data/docs/restart.md +4 -2
  11. data/docs/systemd.md +27 -9
  12. data/docs/tcp_mode.md +96 -0
  13. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  14. data/ext/puma_http11/extconf.rb +13 -0
  15. data/ext/puma_http11/http11_parser.c +37 -62
  16. data/ext/puma_http11/http11_parser.java.rl +21 -37
  17. data/ext/puma_http11/http11_parser_common.rl +3 -3
  18. data/ext/puma_http11/mini_ssl.c +78 -8
  19. data/ext/puma_http11/org/jruby/puma/Http11.java +106 -114
  20. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
  21. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  22. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -4
  23. data/ext/puma_http11/puma_http11.c +2 -0
  24. data/lib/puma.rb +8 -0
  25. data/lib/puma/accept_nonblock.rb +7 -1
  26. data/lib/puma/app/status.rb +35 -29
  27. data/lib/puma/binder.rb +38 -60
  28. data/lib/puma/cli.rb +4 -0
  29. data/lib/puma/client.rb +229 -207
  30. data/lib/puma/cluster.rb +53 -30
  31. data/lib/puma/configuration.rb +4 -3
  32. data/lib/puma/const.rb +22 -18
  33. data/lib/puma/control_cli.rb +30 -5
  34. data/lib/puma/dsl.rb +299 -75
  35. data/lib/puma/events.rb +4 -1
  36. data/lib/puma/io_buffer.rb +1 -6
  37. data/lib/puma/launcher.rb +95 -53
  38. data/lib/puma/minissl.rb +35 -17
  39. data/lib/puma/minissl/context_builder.rb +76 -0
  40. data/lib/puma/plugin.rb +5 -2
  41. data/lib/puma/plugin/tmp_restart.rb +2 -0
  42. data/lib/puma/rack/builder.rb +2 -0
  43. data/lib/puma/rack/urlmap.rb +2 -0
  44. data/lib/puma/rack_default.rb +2 -0
  45. data/lib/puma/reactor.rb +110 -57
  46. data/lib/puma/runner.rb +11 -3
  47. data/lib/puma/server.rb +59 -48
  48. data/lib/puma/single.rb +3 -3
  49. data/lib/puma/thread_pool.rb +15 -33
  50. data/lib/puma/util.rb +1 -6
  51. data/lib/rack/handler/puma.rb +3 -3
  52. data/tools/docker/Dockerfile +16 -0
  53. data/tools/jungle/init.d/puma +6 -6
  54. data/tools/trickletest.rb +0 -1
  55. metadata +21 -8
  56. data/lib/puma/compat.rb +0 -14
  57. data/lib/puma/convenient.rb +0 -25
  58. data/lib/puma/daemon_ext.rb +0 -33
  59. data/lib/puma/delegation.rb +0 -13
  60. data/lib/puma/java_io_buffer.rb +0 -47
  61. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
@@ -93,7 +93,10 @@ module Puma
93
93
  # parsing exception.
94
94
  #
95
95
  def parse_error(server, env, error)
96
- @stderr.puts "#{Time.now}: HTTP parse error, malformed request (#{env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR]}): #{error.inspect}\n---\n"
96
+ @stderr.puts "#{Time.now}: HTTP parse error, malformed request " \
97
+ "(#{env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR]}#{env[REQUEST_PATH]}): " \
98
+ "#{error.inspect}" \
99
+ "\n---\n"
97
100
  end
98
101
 
99
102
  # An SSL error has occurred.
@@ -1,9 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'puma/detect'
4
-
5
- if Puma.jruby?
6
- require 'puma/java_io_buffer'
7
- else
8
- require 'puma/puma_http11'
9
- end
4
+ require 'puma/puma_http11'
@@ -2,12 +2,9 @@
2
2
 
3
3
  require 'puma/events'
4
4
  require 'puma/detect'
5
-
6
5
  require 'puma/cluster'
7
6
  require 'puma/single'
8
-
9
7
  require 'puma/const'
10
-
11
8
  require 'puma/binder'
12
9
 
13
10
  module Puma
@@ -63,6 +60,9 @@ module Puma
63
60
  @options = @config.options
64
61
  @config.clamp
65
62
 
63
+ @events.formatter = Events::PidFormatter.new if clustered?
64
+ @events.formatter = options[:log_formatter] if @options[:log_formatter]
65
+
66
66
  generate_restart_data
67
67
 
68
68
  if clustered? && !Process.respond_to?(:fork)
@@ -81,7 +81,6 @@ module Puma
81
81
  set_rack_environment
82
82
 
83
83
  if clustered?
84
- @events.formatter = Events::PidFormatter.new
85
84
  @options[:logger] = @events
86
85
 
87
86
  @runner = Cluster.new(self, @events)
@@ -124,19 +123,6 @@ module Puma
124
123
  File.unlink(path) if path && File.exist?(path)
125
124
  end
126
125
 
127
- # If configured, write the pid of the current process out
128
- # to a file.
129
- def write_pid
130
- path = @options[:pidfile]
131
- return unless path
132
-
133
- File.open(path, 'w') { |f| f.puts Process.pid }
134
- cur = Process.pid
135
- at_exit do
136
- delete_pidfile if cur == Process.pid
137
- end
138
- end
139
-
140
126
  # Begin async shutdown of the server
141
127
  def halt
142
128
  @status = :halt
@@ -198,6 +184,7 @@ module Puma
198
184
  when :exit
199
185
  # nothing
200
186
  end
187
+ @binder.close_unix_paths
201
188
  end
202
189
 
203
190
  # Return which tcp port the launcher is using, if it's using TCP
@@ -214,8 +201,25 @@ module Puma
214
201
  end
215
202
  end
216
203
 
204
+ def close_binder_listeners
205
+ @binder.close_listeners
206
+ end
207
+
217
208
  private
218
209
 
210
+ # If configured, write the pid of the current process out
211
+ # to a file.
212
+ def write_pid
213
+ path = @options[:pidfile]
214
+ return unless path
215
+
216
+ File.open(path, 'w') { |f| f.puts Process.pid }
217
+ cur = Process.pid
218
+ at_exit do
219
+ delete_pidfile if cur == Process.pid
220
+ end
221
+ end
222
+
219
223
  def reload_worker_directory
220
224
  @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
221
225
  end
@@ -235,48 +239,71 @@ module Puma
235
239
  Dir.chdir(@restart_dir)
236
240
  Kernel.exec(*argv)
237
241
  else
238
- redirects = {:close_others => true}
239
- @binder.listeners.each_with_index do |(l, io), i|
240
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
241
- redirects[io.to_i] = io.to_i
242
- end
243
-
244
242
  argv = restart_args
245
243
  Dir.chdir(@restart_dir)
246
- argv += [redirects] if RUBY_VERSION >= '1.9'
244
+ argv += [@binder.redirects_for_restart]
247
245
  Kernel.exec(*argv)
248
246
  end
249
247
  end
250
248
 
251
- def prune_bundler
252
- return unless defined?(Bundler)
253
- puma = Bundler.rubygems.loaded_specs("puma")
254
- dirs = puma.require_paths.map { |x| File.join(puma.full_gem_path, x) }
249
+ def dependencies_and_files_to_require_after_prune
250
+ puma = spec_for_gem("puma")
251
+
252
+ deps = puma.runtime_dependencies.map do |d|
253
+ "#{d.name}:#{spec_for_gem(d.name).version}"
254
+ end
255
+
256
+ [deps, require_paths_for_gem(puma) + extra_runtime_deps_directories]
257
+ end
258
+
259
+ def extra_runtime_deps_directories
260
+ Array(@options[:extra_runtime_dependencies]).map do |d_name|
261
+ if (spec = spec_for_gem(d_name))
262
+ require_paths_for_gem(spec)
263
+ else
264
+ log "* Could not load extra dependency: #{d_name}"
265
+ nil
266
+ end
267
+ end.flatten.compact
268
+ end
269
+
270
+ def puma_wild_location
271
+ puma = spec_for_gem("puma")
272
+ dirs = require_paths_for_gem(puma)
255
273
  puma_lib_dir = dirs.detect { |x| File.exist? File.join(x, '../bin/puma-wild') }
274
+ File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
275
+ end
256
276
 
257
- unless puma_lib_dir
277
+ def prune_bundler
278
+ return unless defined?(Bundler)
279
+ require_rubygems_min_version!(Gem::Version.new("2.2"), "prune_bundler")
280
+ unless puma_wild_location
258
281
  log "! Unable to prune Bundler environment, continuing"
259
282
  return
260
283
  end
261
284
 
262
- deps = puma.runtime_dependencies.map do |d|
263
- spec = Bundler.rubygems.loaded_specs(d.name)
264
- "#{d.name}:#{spec.version.to_s}"
265
- end
285
+ deps, dirs = dependencies_and_files_to_require_after_prune
266
286
 
267
287
  log '* Pruning Bundler environment'
268
288
  home = ENV['GEM_HOME']
269
289
  Bundler.with_clean_env do
270
290
  ENV['GEM_HOME'] = home
271
291
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
272
- wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
273
- args = [Gem.ruby, wild, '-I', dirs.join(':'), deps.join(',')] + @original_argv
292
+ args = [Gem.ruby, puma_wild_location, '-I', dirs.join(':'), deps.join(',')] + @original_argv
274
293
  # Ruby 2.0+ defaults to true which breaks socket activation
275
- args += [{:close_others => false}] if RUBY_VERSION >= '2.0'
294
+ args += [{:close_others => false}]
276
295
  Kernel.exec(*args)
277
296
  end
278
297
  end
279
298
 
299
+ def spec_for_gem(gem_name)
300
+ Bundler.rubygems.loaded_specs(gem_name)
301
+ end
302
+
303
+ def require_paths_for_gem(gem_spec)
304
+ gem_spec.full_require_paths
305
+ end
306
+
280
307
  def log(str)
281
308
  @events.log str
282
309
  end
@@ -296,6 +323,21 @@ module Puma
296
323
  log "- Goodbye!"
297
324
  end
298
325
 
326
+ def log_thread_status
327
+ Thread.list.each do |thread|
328
+ log "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}"
329
+ logstr = "Thread: TID-#{thread.object_id.to_s(36)}"
330
+ logstr += " #{thread.name}" if thread.respond_to?(:name)
331
+ log logstr
332
+
333
+ if thread.backtrace
334
+ log thread.backtrace.join("\n")
335
+ else
336
+ log "<no backtrace available>"
337
+ end
338
+ end
339
+ end
340
+
299
341
  def set_process_title
300
342
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
301
343
  end
@@ -319,16 +361,6 @@ module Puma
319
361
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
320
362
  end
321
363
 
322
- def close_binder_listeners
323
- @binder.listeners.each do |l, io|
324
- io.close
325
- uri = URI.parse(l)
326
- next unless uri.scheme == 'unix'
327
- File.unlink("#{uri.host}#{uri.path}")
328
- end
329
- end
330
-
331
-
332
364
  def generate_restart_data
333
365
  if dir = @options[:directory]
334
366
  @restart_dir = dir
@@ -397,7 +429,7 @@ module Puma
397
429
  Signal.trap "SIGTERM" do
398
430
  graceful_stop
399
431
 
400
- raise SignalException, "SIGTERM"
432
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
401
433
  end
402
434
  rescue Exception
403
435
  log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
@@ -405,12 +437,6 @@ module Puma
405
437
 
406
438
  begin
407
439
  Signal.trap "SIGINT" do
408
- if Puma.jruby?
409
- @status = :exit
410
- graceful_stop
411
- exit
412
- end
413
-
414
440
  stop
415
441
  end
416
442
  rescue Exception
@@ -428,6 +454,22 @@ module Puma
428
454
  rescue Exception
429
455
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
430
456
  end
457
+
458
+ begin
459
+ Signal.trap "SIGINFO" do
460
+ log_thread_status
461
+ end
462
+ rescue Exception
463
+ # Not going to log this one, as SIGINFO is *BSD only and would be pretty annoying
464
+ # to see this constantly on Linux.
465
+ end
466
+ end
467
+
468
+ def require_rubygems_min_version!(min_version, feature)
469
+ return if min_version <= Gem::Version.new(Gem::VERSION)
470
+
471
+ raise "#{feature} is not supported on your version of RubyGems. " \
472
+ "You must have RubyGems #{min_version}+ to use this feature."
431
473
  end
432
474
  end
433
475
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  begin
4
4
  require 'io/wait'
5
- rescue LoadError
5
+ rescue LoadError
6
6
  end
7
7
 
8
8
  module Puma
@@ -54,22 +54,21 @@ module Puma
54
54
  output = engine_read_all
55
55
  return output if output
56
56
 
57
- begin
58
- data = @socket.read_nonblock(size, exception: false)
59
- if data == :wait_readable || data == :wait_writable
60
- if @socket.to_io.respond_to?(data)
61
- @socket.to_io.__send__(data)
62
- elsif data == :wait_readable
63
- IO.select([@socket.to_io])
64
- else
65
- IO.select(nil, [@socket.to_io])
66
- end
67
- elsif !data
68
- return nil
69
- else
70
- break
71
- end
72
- end while true
57
+ data = @socket.read_nonblock(size, exception: false)
58
+ if data == :wait_readable || data == :wait_writable
59
+ # It would make more sense to let @socket.read_nonblock raise
60
+ # EAGAIN if necessary but it seems like it'll misbehave on Windows.
61
+ # I don't have a Windows machine to debug this so I can't explain
62
+ # exactly whats happening in that OS. Please let me know if you
63
+ # find out!
64
+ #
65
+ # In the meantime, we can emulate the correct behavior by
66
+ # capturing :wait_readable & :wait_writable and raising EAGAIN
67
+ # ourselves.
68
+ raise IO::EAGAINWaitReadable
69
+ elsif data.nil?
70
+ return nil
71
+ end
73
72
 
74
73
  @engine.inject(data)
75
74
  output = engine_read_all
@@ -177,6 +176,12 @@ module Puma
177
176
 
178
177
  class Context
179
178
  attr_accessor :verify_mode
179
+ attr_reader :no_tlsv1, :no_tlsv1_1
180
+
181
+ def initialize
182
+ @no_tlsv1 = false
183
+ @no_tlsv1_1 = false
184
+ end
180
185
 
181
186
  if defined?(JRUBY_VERSION)
182
187
  # jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair
@@ -220,6 +225,19 @@ module Puma
220
225
  raise "Cert not configured" unless @cert
221
226
  end
222
227
  end
228
+
229
+ # disables TLSv1
230
+ def no_tlsv1=(tlsv1)
231
+ raise ArgumentError, "Invalid value of no_tlsv1" unless ['true', 'false', true, false].include?(tlsv1)
232
+ @no_tlsv1 = tlsv1
233
+ end
234
+
235
+ # disables TLSv1 and TLSv1.1. Overrides `#no_tlsv1=`
236
+ def no_tlsv1_1=(tlsv1_1)
237
+ raise ArgumentError, "Invalid value of no_tlsv1" unless ['true', 'false', true, false].include?(tlsv1_1)
238
+ @no_tlsv1_1 = tlsv1_1
239
+ end
240
+
223
241
  end
224
242
 
225
243
  VERIFY_NONE = 0
@@ -0,0 +1,76 @@
1
+ module Puma
2
+ module MiniSSL
3
+ class ContextBuilder
4
+ def initialize(params, events)
5
+ require 'puma/minissl'
6
+ MiniSSL.check
7
+
8
+ @params = params
9
+ @events = events
10
+ end
11
+
12
+ def context
13
+ ctx = MiniSSL::Context.new
14
+
15
+ if defined?(JRUBY_VERSION)
16
+ unless params['keystore']
17
+ events.error "Please specify the Java keystore via 'keystore='"
18
+ end
19
+
20
+ ctx.keystore = params['keystore']
21
+
22
+ unless params['keystore-pass']
23
+ events.error "Please specify the Java keystore password via 'keystore-pass='"
24
+ end
25
+
26
+ ctx.keystore_pass = params['keystore-pass']
27
+ ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
28
+ else
29
+ unless params['key']
30
+ events.error "Please specify the SSL key via 'key='"
31
+ end
32
+
33
+ ctx.key = params['key']
34
+
35
+ unless params['cert']
36
+ events.error "Please specify the SSL cert via 'cert='"
37
+ end
38
+
39
+ ctx.cert = params['cert']
40
+
41
+ if ['peer', 'force_peer'].include?(params['verify_mode'])
42
+ unless params['ca']
43
+ events.error "Please specify the SSL ca via 'ca='"
44
+ end
45
+ end
46
+
47
+ ctx.ca = params['ca'] if params['ca']
48
+ ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
49
+ end
50
+
51
+ ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
52
+ ctx.no_tlsv1_1 = true if params['no_tlsv1_1'] == 'true'
53
+
54
+ if params['verify_mode']
55
+ ctx.verify_mode = case params['verify_mode']
56
+ when "peer"
57
+ MiniSSL::VERIFY_PEER
58
+ when "force_peer"
59
+ MiniSSL::VERIFY_PEER | MiniSSL::VERIFY_FAIL_IF_NO_PEER_CERT
60
+ when "none"
61
+ MiniSSL::VERIFY_NONE
62
+ else
63
+ events.error "Please specify a valid verify_mode="
64
+ MiniSSL::VERIFY_NONE
65
+ end
66
+ end
67
+
68
+ ctx
69
+ end
70
+
71
+ private
72
+
73
+ attr_reader :params, :events
74
+ end
75
+ end
76
+ end
@@ -62,8 +62,11 @@ module Puma
62
62
  end
63
63
 
64
64
  def fire_background
65
- @background.each do |b|
66
- Thread.new(&b)
65
+ @background.each_with_index do |b, i|
66
+ Thread.new do
67
+ Puma.set_thread_name "plugin background #{i}"
68
+ b.call
69
+ end
67
70
  end
68
71
  end
69
72
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/plugin'
2
4
 
3
5
  Puma::Plugin.create do