puma 3.12.0 → 4.3.8

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +164 -0
  3. data/README.md +76 -48
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/plugins.md +20 -10
  7. data/docs/restart.md +4 -2
  8. data/docs/systemd.md +27 -9
  9. data/docs/tcp_mode.md +96 -0
  10. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  11. data/ext/puma_http11/extconf.rb +13 -0
  12. data/ext/puma_http11/http11_parser.c +40 -63
  13. data/ext/puma_http11/http11_parser.java.rl +21 -37
  14. data/ext/puma_http11/http11_parser.rl +3 -1
  15. data/ext/puma_http11/http11_parser_common.rl +3 -3
  16. data/ext/puma_http11/mini_ssl.c +86 -4
  17. data/ext/puma_http11/org/jruby/puma/Http11.java +106 -114
  18. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
  19. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  20. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -4
  21. data/ext/puma_http11/puma_http11.c +3 -0
  22. data/lib/puma.rb +8 -0
  23. data/lib/puma/accept_nonblock.rb +7 -1
  24. data/lib/puma/app/status.rb +37 -29
  25. data/lib/puma/binder.rb +47 -68
  26. data/lib/puma/cli.rb +6 -0
  27. data/lib/puma/client.rb +244 -199
  28. data/lib/puma/cluster.rb +55 -30
  29. data/lib/puma/commonlogger.rb +2 -0
  30. data/lib/puma/configuration.rb +6 -3
  31. data/lib/puma/const.rb +32 -18
  32. data/lib/puma/control_cli.rb +41 -14
  33. data/lib/puma/detect.rb +2 -0
  34. data/lib/puma/dsl.rb +311 -77
  35. data/lib/puma/events.rb +6 -1
  36. data/lib/puma/io_buffer.rb +3 -6
  37. data/lib/puma/jruby_restart.rb +2 -0
  38. data/lib/puma/launcher.rb +99 -55
  39. data/lib/puma/minissl.rb +37 -17
  40. data/lib/puma/minissl/context_builder.rb +76 -0
  41. data/lib/puma/null_io.rb +2 -0
  42. data/lib/puma/plugin.rb +7 -2
  43. data/lib/puma/plugin/tmp_restart.rb +2 -0
  44. data/lib/puma/rack/builder.rb +4 -1
  45. data/lib/puma/rack/urlmap.rb +2 -0
  46. data/lib/puma/rack_default.rb +2 -0
  47. data/lib/puma/reactor.rb +112 -57
  48. data/lib/puma/runner.rb +13 -3
  49. data/lib/puma/server.rb +119 -48
  50. data/lib/puma/single.rb +5 -3
  51. data/lib/puma/state_file.rb +2 -0
  52. data/lib/puma/tcp_logger.rb +2 -0
  53. data/lib/puma/thread_pool.rb +17 -33
  54. data/lib/puma/util.rb +2 -6
  55. data/lib/rack/handler/puma.rb +6 -3
  56. data/tools/docker/Dockerfile +16 -0
  57. data/tools/jungle/init.d/puma +6 -6
  58. data/tools/trickletest.rb +0 -1
  59. metadata +26 -14
  60. data/lib/puma/compat.rb +0 -14
  61. data/lib/puma/convenient.rb +0 -23
  62. data/lib/puma/daemon_ext.rb +0 -31
  63. data/lib/puma/delegation.rb +0 -11
  64. data/lib/puma/java_io_buffer.rb +0 -45
  65. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
data/lib/puma/events.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/const'
2
4
  require "puma/null_io"
3
5
  require 'stringio'
@@ -91,7 +93,10 @@ module Puma
91
93
  # parsing exception.
92
94
  #
93
95
  def parse_error(server, env, error)
94
- @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"
95
100
  end
96
101
 
97
102
  # An SSL error has occurred.
@@ -1,7 +1,4 @@
1
- require 'puma/detect'
1
+ # frozen_string_literal: true
2
2
 
3
- if Puma.jruby?
4
- require 'puma/java_io_buffer'
5
- else
6
- require 'puma/puma_http11'
7
- end
3
+ require 'puma/detect'
4
+ require 'puma/puma_http11'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ffi'
2
4
 
3
5
  module Puma
data/lib/puma/launcher.rb CHANGED
@@ -1,11 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/events'
2
4
  require 'puma/detect'
3
-
4
5
  require 'puma/cluster'
5
6
  require 'puma/single'
6
-
7
7
  require 'puma/const'
8
-
9
8
  require 'puma/binder'
10
9
 
11
10
  module Puma
@@ -61,10 +60,13 @@ module Puma
61
60
  @options = @config.options
62
61
  @config.clamp
63
62
 
63
+ @events.formatter = Events::PidFormatter.new if clustered?
64
+ @events.formatter = options[:log_formatter] if @options[:log_formatter]
65
+
64
66
  generate_restart_data
65
67
 
66
- if clustered? && (Puma.jruby? || Puma.windows?)
67
- unsupported 'worker mode not supported on JRuby or Windows'
68
+ if clustered? && !Process.respond_to?(:fork)
69
+ unsupported "worker mode not supported on #{RUBY_ENGINE} on this platform"
68
70
  end
69
71
 
70
72
  if @options[:daemon] && Puma.windows?
@@ -79,7 +81,6 @@ module Puma
79
81
  set_rack_environment
80
82
 
81
83
  if clustered?
82
- @events.formatter = Events::PidFormatter.new
83
84
  @options[:logger] = @events
84
85
 
85
86
  @runner = Cluster.new(self, @events)
@@ -122,19 +123,6 @@ module Puma
122
123
  File.unlink(path) if path && File.exist?(path)
123
124
  end
124
125
 
125
- # If configured, write the pid of the current process out
126
- # to a file.
127
- def write_pid
128
- path = @options[:pidfile]
129
- return unless path
130
-
131
- File.open(path, 'w') { |f| f.puts Process.pid }
132
- cur = Process.pid
133
- at_exit do
134
- delete_pidfile if cur == Process.pid
135
- end
136
- end
137
-
138
126
  # Begin async shutdown of the server
139
127
  def halt
140
128
  @status = :halt
@@ -196,6 +184,7 @@ module Puma
196
184
  when :exit
197
185
  # nothing
198
186
  end
187
+ @binder.close_unix_paths
199
188
  end
200
189
 
201
190
  # Return which tcp port the launcher is using, if it's using TCP
@@ -212,8 +201,25 @@ module Puma
212
201
  end
213
202
  end
214
203
 
204
+ def close_binder_listeners
205
+ @binder.close_listeners
206
+ end
207
+
215
208
  private
216
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
+
217
223
  def reload_worker_directory
218
224
  @runner.reload_worker_directory if @runner.respond_to?(:reload_worker_directory)
219
225
  end
@@ -233,48 +239,71 @@ module Puma
233
239
  Dir.chdir(@restart_dir)
234
240
  Kernel.exec(*argv)
235
241
  else
236
- redirects = {:close_others => true}
237
- @binder.listeners.each_with_index do |(l, io), i|
238
- ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
239
- redirects[io.to_i] = io.to_i
240
- end
241
-
242
242
  argv = restart_args
243
243
  Dir.chdir(@restart_dir)
244
- argv += [redirects] if RUBY_VERSION >= '1.9'
244
+ argv += [@binder.redirects_for_restart]
245
245
  Kernel.exec(*argv)
246
246
  end
247
247
  end
248
248
 
249
- def prune_bundler
250
- return unless defined?(Bundler)
251
- puma = Bundler.rubygems.loaded_specs("puma")
252
- 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)
253
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
254
276
 
255
- 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
256
281
  log "! Unable to prune Bundler environment, continuing"
257
282
  return
258
283
  end
259
284
 
260
- deps = puma.runtime_dependencies.map do |d|
261
- spec = Bundler.rubygems.loaded_specs(d.name)
262
- "#{d.name}:#{spec.version.to_s}"
263
- end
285
+ deps, dirs = dependencies_and_files_to_require_after_prune
264
286
 
265
287
  log '* Pruning Bundler environment'
266
288
  home = ENV['GEM_HOME']
267
289
  Bundler.with_clean_env do
268
290
  ENV['GEM_HOME'] = home
269
291
  ENV['PUMA_BUNDLER_PRUNED'] = '1'
270
- wild = File.expand_path(File.join(puma_lib_dir, "../bin/puma-wild"))
271
- 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
272
293
  # Ruby 2.0+ defaults to true which breaks socket activation
273
- args += [{:close_others => false}] if RUBY_VERSION >= '2.0'
294
+ args += [{:close_others => false}]
274
295
  Kernel.exec(*args)
275
296
  end
276
297
  end
277
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
+
278
307
  def log(str)
279
308
  @events.log str
280
309
  end
@@ -294,6 +323,21 @@ module Puma
294
323
  log "- Goodbye!"
295
324
  end
296
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
+
297
341
  def set_process_title
298
342
  Process.respond_to?(:setproctitle) ? Process.setproctitle(title) : $0 = title
299
343
  end
@@ -317,16 +361,6 @@ module Puma
317
361
  @options[:prune_bundler] && clustered? && !@options[:preload_app]
318
362
  end
319
363
 
320
- def close_binder_listeners
321
- @binder.listeners.each do |l, io|
322
- io.close
323
- uri = URI.parse(l)
324
- next unless uri.scheme == 'unix'
325
- File.unlink("#{uri.host}#{uri.path}")
326
- end
327
- end
328
-
329
-
330
364
  def generate_restart_data
331
365
  if dir = @options[:directory]
332
366
  @restart_dir = dir
@@ -395,7 +429,7 @@ module Puma
395
429
  Signal.trap "SIGTERM" do
396
430
  graceful_stop
397
431
 
398
- raise SignalException, "SIGTERM"
432
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
399
433
  end
400
434
  rescue Exception
401
435
  log "*** SIGTERM not implemented, signal based gracefully stopping unavailable!"
@@ -403,12 +437,6 @@ module Puma
403
437
 
404
438
  begin
405
439
  Signal.trap "SIGINT" do
406
- if Puma.jruby?
407
- @status = :exit
408
- graceful_stop
409
- exit
410
- end
411
-
412
440
  stop
413
441
  end
414
442
  rescue Exception
@@ -426,6 +454,22 @@ module Puma
426
454
  rescue Exception
427
455
  log "*** SIGHUP not implemented, signal based logs reopening unavailable!"
428
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."
429
473
  end
430
474
  end
431
475
  end
data/lib/puma/minissl.rb CHANGED
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require 'io/wait'
3
- rescue LoadError
5
+ rescue LoadError
4
6
  end
5
7
 
6
8
  module Puma
@@ -52,22 +54,21 @@ module Puma
52
54
  output = engine_read_all
53
55
  return output if output
54
56
 
55
- begin
56
- data = @socket.read_nonblock(size, exception: false)
57
- if data == :wait_readable || data == :wait_writable
58
- if @socket.to_io.respond_to?(data)
59
- @socket.to_io.__send__(data)
60
- elsif data == :wait_readable
61
- IO.select([@socket.to_io])
62
- else
63
- IO.select(nil, [@socket.to_io])
64
- end
65
- elsif !data
66
- return nil
67
- else
68
- break
69
- end
70
- 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
71
72
 
72
73
  @engine.inject(data)
73
74
  output = engine_read_all
@@ -175,6 +176,12 @@ module Puma
175
176
 
176
177
  class Context
177
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
178
185
 
179
186
  if defined?(JRUBY_VERSION)
180
187
  # jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair
@@ -218,6 +225,19 @@ module Puma
218
225
  raise "Cert not configured" unless @cert
219
226
  end
220
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
+
221
241
  end
222
242
 
223
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