├── examples ├── CA │ ├── serial │ ├── newcerts │ │ ├── cert_1.pem │ │ └── cert_2.pem │ ├── cacert.pem │ └── private │ │ └── cakeypair.pem ├── puma │ ├── keystore.jks │ ├── client-certs │ │ ├── keystore.jks │ │ ├── server.p12 │ │ ├── client.crt │ │ ├── server.crt │ │ ├── ca.crt │ │ ├── client_expired.crt │ │ ├── client_unknown.crt │ │ ├── unknown_ca.crt │ │ ├── ca.key │ │ ├── server.key │ │ ├── client.key │ │ ├── client_expired.key │ │ ├── unknown_ca.key │ │ ├── client_unknown.key │ │ └── generate.rb │ ├── csr_puma.pem │ ├── puma_keypair.pem │ └── cert_puma.pem ├── qc_config.rb └── plugins │ └── redis_stop_puma.rb ├── test ├── config │ ├── plugin.rb │ ├── settings.rb │ ├── suppress_exception.rb │ ├── simple.rb │ ├── custom_log_formatter.rb │ ├── control_no_token.rb │ ├── app.rb │ ├── with_integer_convert.rb │ ├── state_file_testing_config.rb │ ├── ssl_config.rb │ └── ab_rs.rb ├── shell │ ├── t1_conf.rb │ ├── t3_conf.rb │ ├── t2_conf.rb │ ├── run.rb │ ├── t1.rb │ ├── t2.rb │ └── t3.rb ├── rackup │ ├── hello.ru │ ├── lobster.ru │ ├── hello-bind.ru │ ├── hello-env.ru │ └── sleep.ru ├── helpers │ └── apps.rb ├── test_tcp_rack.rb ├── test_unix_socket.rb ├── test_http10.rb ├── test_iobuffer.rb ├── test_minissl.rb ├── test_null_io.rb ├── test_tcp_logger.rb ├── test_app_status.rb ├── test_rack_server.rb ├── test_web_server.rb ├── test_binder.rb ├── test_pumactl.rb ├── helper.rb ├── test_events.rb ├── test_thread_pool.rb ├── test_rack_handler.rb ├── test_http11.rb └── test_config.rb ├── .gitattributes ├── lib ├── puma │ ├── io_buffer.rb │ ├── rack_default.rb │ ├── detect.rb │ ├── delegation.rb │ ├── convenient.rb │ ├── state_file.rb │ ├── accept_nonblock.rb │ ├── null_io.rb │ ├── daemon_ext.rb │ ├── plugin │ │ └── tmp_restart.rb │ ├── tcp_logger.rb │ ├── jruby_restart.rb │ ├── app │ │ └── status.rb │ ├── plugin.rb │ ├── rack │ │ └── urlmap.rb │ ├── single.rb │ ├── util.rb │ ├── commonlogger.rb │ ├── events.rb │ └── runner.rb ├── puma.rb └── rack │ └── handler │ └── puma.rb ├── docs ├── images │ ├── puma-general-arch.png │ ├── puma-connection-flow.png │ └── puma-connection-flow-no-reactor.png ├── plugins.md ├── architecture.md ├── nginx.md ├── restart.md ├── signals.md └── deployment.md ├── bin ├── puma ├── pumactl └── puma-wild ├── tools ├── jungle │ ├── rc.d │ │ ├── puma.conf │ │ ├── puma │ │ └── README.md │ ├── README.md │ ├── init.d │ │ ├── run-puma │ │ └── README.md │ └── upstart │ │ ├── puma-manager.conf │ │ ├── README.md │ │ └── puma.conf └── trickletest.rb ├── win_gem_test ├── Rakefile_wintest ├── package_gem.rb └── puma.ps1 ├── .github └── issue_template.md ├── .gitignore ├── Gemfile ├── Release.md ├── ext └── puma_http11 │ ├── PumaHttp11Service.java │ ├── ext_help.h │ ├── extconf.rb │ ├── http11_parser.h │ ├── http11_parser_common.rl │ ├── org │ └── jruby │ │ └── puma │ │ └── IOBuffer.java │ ├── io_buffer.c │ ├── http11_parser.rl │ └── http11_parser.java.rl ├── appveyor.yml ├── .rubocop.yml ├── puma.gemspec ├── LICENSE ├── .rubocop_todo.yml ├── .travis.yml ├── Rakefile └── CODE_OF_CONDUCT.md /examples/CA/serial: -------------------------------------------------------------------------------- 1 | 0003 -------------------------------------------------------------------------------- /test/config/plugin.rb: -------------------------------------------------------------------------------- 1 | plugin :tmp_restart 2 | -------------------------------------------------------------------------------- /test/config/settings.rb: -------------------------------------------------------------------------------- 1 | port 3000 2 | threads 3, 5 3 | -------------------------------------------------------------------------------- /test/config/suppress_exception.rb: -------------------------------------------------------------------------------- 1 | raise_exception_on_sigterm false 2 | -------------------------------------------------------------------------------- /test/config/simple.rb: -------------------------------------------------------------------------------- 1 | pidfile "/tmp/jruby.pid" 2 | switch_user "daemon", "daemon" 3 | -------------------------------------------------------------------------------- /test/shell/t1_conf.rb: -------------------------------------------------------------------------------- 1 | log_requests 2 | stdout_redirect "t1-stdout" 3 | pidfile "t1-pid" 4 | -------------------------------------------------------------------------------- /test/rackup/hello.ru: -------------------------------------------------------------------------------- 1 | run lambda { |env| [200, {"Content-Type" => "text/plain"}, ["Hello World"]] } 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text eol=lf 3 | *.png binary 4 | -------------------------------------------------------------------------------- /examples/puma/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/puma/master/examples/puma/keystore.jks -------------------------------------------------------------------------------- /test/rackup/lobster.ru: -------------------------------------------------------------------------------- 1 | require 'rack/lobster' 2 | 3 | use Rack::ShowExceptions 4 | run Rack::Lobster.new 5 | -------------------------------------------------------------------------------- /lib/puma/io_buffer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'puma/detect' 4 | require 'puma/puma_http11' 5 | -------------------------------------------------------------------------------- /docs/images/puma-general-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/puma/master/docs/images/puma-general-arch.png -------------------------------------------------------------------------------- /docs/images/puma-connection-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/puma/master/docs/images/puma-connection-flow.png -------------------------------------------------------------------------------- /examples/puma/client-certs/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/puma/master/examples/puma/client-certs/keystore.jks -------------------------------------------------------------------------------- /examples/puma/client-certs/server.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/puma/master/examples/puma/client-certs/server.p12 -------------------------------------------------------------------------------- /test/config/custom_log_formatter.rb: -------------------------------------------------------------------------------- 1 | log_formatter do |str| 2 | "[#{Process.pid}] [#{Socket.gethostname}] #{Time.now}: #{str}" 3 | end 4 | -------------------------------------------------------------------------------- /test/rackup/hello-bind.ru: -------------------------------------------------------------------------------- 1 | #\ -O bind=tcp://127.0.0.1:9292 2 | run lambda { |env| [200, {"Content-Type" => "text/plain"}, ["Hello World"]] } 3 | -------------------------------------------------------------------------------- /docs/images/puma-connection-flow-no-reactor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattbrictson/puma/master/docs/images/puma-connection-flow-no-reactor.png -------------------------------------------------------------------------------- /test/rackup/hello-env.ru: -------------------------------------------------------------------------------- 1 | ENV["RAND"] ||= rand.to_s 2 | run lambda { |env| [200, {"Content-Type" => "text/plain"}, ["Hello RAND #{ENV["RAND"]}"]] } 3 | -------------------------------------------------------------------------------- /test/config/control_no_token.rb: -------------------------------------------------------------------------------- 1 | activate_control_app 'unix:///tmp/pumactl.sock', { no_token: true } 2 | 3 | app do |env| 4 | [200, {}, ["embedded app"]] 5 | end 6 | -------------------------------------------------------------------------------- /test/shell/t3_conf.rb: -------------------------------------------------------------------------------- 1 | pidfile "t3-pid" 2 | workers 3 3 | on_worker_boot do |index| 4 | File.open("t3-worker-#{index}-pid", "w") { |f| f.puts Process.pid } 5 | end 6 | -------------------------------------------------------------------------------- /bin/puma: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Copyright (c) 2011 Evan Phoenix 4 | # 5 | 6 | require 'puma/cli' 7 | 8 | cli = Puma::CLI.new ARGV 9 | 10 | cli.run 11 | -------------------------------------------------------------------------------- /test/config/app.rb: -------------------------------------------------------------------------------- 1 | port ENV['PORT'] if ENV['PORT'] 2 | 3 | app do |env| 4 | [200, {}, ["embedded app"]] 5 | end 6 | 7 | lowlevel_error_handler do |err| 8 | [200, {}, ["error page"]] 9 | end 10 | -------------------------------------------------------------------------------- /test/shell/t2_conf.rb: -------------------------------------------------------------------------------- 1 | log_requests 2 | stdout_redirect "t2-stdout" 3 | pidfile "t2-pid" 4 | bind "tcp://0.0.0.0:10103" 5 | rackup File.expand_path('../rackup/hello.ru', File.dirname(__FILE__)) 6 | daemonize 7 | -------------------------------------------------------------------------------- /lib/puma/rack_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rack/handler/puma' 4 | 5 | module Rack::Handler 6 | def self.default(options = {}) 7 | Rack::Handler::Puma 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /bin/pumactl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'puma/control_cli' 4 | 5 | cli = Puma::ControlCLI.new ARGV.dup 6 | 7 | begin 8 | cli.run 9 | rescue => e 10 | STDERR.puts e.message 11 | exit 1 12 | end 13 | -------------------------------------------------------------------------------- /tools/jungle/rc.d/puma.conf: -------------------------------------------------------------------------------- 1 | { 2 | "servers" : [ 3 | { 4 | "dir": "/path/to/rails/project", 5 | "user": "deploy-user", 6 | "ruby_version": "ruby.version", 7 | "ruby_env": "rbenv" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/config/with_integer_convert.rb: -------------------------------------------------------------------------------- 1 | persistent_timeout "6" 2 | first_data_timeout "3" 3 | 4 | workers "2" 5 | threads "4", "8" 6 | 7 | worker_timeout "90" 8 | worker_boot_timeout "120" 9 | worker_shutdown_timeout "150" 10 | -------------------------------------------------------------------------------- /test/shell/run.rb: -------------------------------------------------------------------------------- 1 | results = %w[t1 t2 t3].map do |test| 2 | system("ruby -rrubygems test/shell/#{test}.rb ") # > /dev/null 2>&1 3 | end 4 | 5 | if results.any? { |r| r != true } 6 | exit 1 7 | else 8 | exit 0 9 | end 10 | -------------------------------------------------------------------------------- /win_gem_test/Rakefile_wintest: -------------------------------------------------------------------------------- 1 | # rake -f Rakefile_wintest -N -R norakelib 2 | 3 | require "rake/testtask" 4 | 5 | Rake::TestTask.new(:win_test) do |t| 6 | t.libs << "test" 7 | t.warning = false 8 | t.options = '--verbose' 9 | end 10 | 11 | task :default => [:win_test] 12 | -------------------------------------------------------------------------------- /lib/puma/detect.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Puma 4 | IS_JRUBY = defined?(JRUBY_VERSION) 5 | 6 | def self.jruby? 7 | IS_JRUBY 8 | end 9 | 10 | IS_WINDOWS = RUBY_PLATFORM =~ /mswin|ming|cygwin/ 11 | 12 | def self.windows? 13 | IS_WINDOWS 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/puma/delegation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Puma 4 | module Delegation 5 | def forward(what, who) 6 | module_eval <<-CODE 7 | def #{what}(*args, &block) 8 | #{who}.#{what}(*args, &block) 9 | end 10 | CODE 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/rackup/sleep.ru: -------------------------------------------------------------------------------- 1 | # call with "GET /sleep HTTP/1.1\r\n\r\n", where is the number of 2 | # seconds to sleep 3 | # same as TestApps::SLEEP 4 | 5 | run lambda { |env| 6 | dly = (env['REQUEST_PATH'][/\/sleep(\d+)/,1] || '0').to_i 7 | sleep dly 8 | [200, {"Content-Type" => "text/plain"}, ["Slept #{dly}"]] 9 | } 10 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | 3 | 1) ... 4 | 5 | 2) ... 6 | 7 | 3) ... 8 | 9 | ### Expected behavior 10 | 11 | Tell us what should happen ... 12 | 13 | ### Actual behavior 14 | 15 | Tell us what happens instead ... 16 | 17 | ### System configuration 18 | 19 | **Ruby version**: 20 | **Rails version**: 21 | **Puma version**: 22 | -------------------------------------------------------------------------------- /test/config/state_file_testing_config.rb: -------------------------------------------------------------------------------- 1 | pidfile "t3-pid" 2 | workers 3 3 | on_worker_boot do |index| 4 | File.open("t3-worker-#{index}-pid", "w") { |f| f.puts Process.pid } 5 | end 6 | 7 | before_fork { 1 } 8 | on_worker_shutdown { 1 } 9 | on_worker_boot { 1 } 10 | on_worker_fork { 1 } 11 | on_restart { 1 } 12 | after_worker_boot { 1 } 13 | lowlevel_error_handler { 1 } 14 | -------------------------------------------------------------------------------- /test/helpers/apps.rb: -------------------------------------------------------------------------------- 1 | module TestApps 2 | 3 | # call with "GET /sleep HTTP/1.1\r\n\r\n", where is the number of 4 | # seconds to sleep 5 | # same as rackup/sleep.ru 6 | SLEEP = -> (env) do 7 | dly = (env['REQUEST_PATH'][/\/sleep(\d+)/,1] || '0').to_i 8 | sleep dly 9 | [200, {"Content-Type" => "text/plain"}, ["Slept #{dly}"]] 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /examples/qc_config.rb: -------------------------------------------------------------------------------- 1 | full_hostname = `hostname`.strip 2 | domainname = full_hostname.split('.')[1..-1].join('.') 3 | hostname = full_hostname.split('.')[0] 4 | 5 | CA[:hostname] = hostname 6 | CA[:domainname] = domainname 7 | CA[:CA_dir] = File.join Dir.pwd, "CA" 8 | CA[:password] = 'puma' 9 | 10 | CERTS << { 11 | :type => 'server', 12 | :hostname => 'puma' 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | scratch/ 2 | *.bundle 3 | *.log 4 | *.o 5 | *.so 6 | *.jar 7 | *.rbc 8 | doc 9 | log 10 | pkg 11 | tmp 12 | t/ 13 | .rbx/ 14 | Gemfile.lock 15 | .idea/ 16 | /test/test_puma.state 17 | /test/test_server.sock 18 | /test/test_control.sock 19 | 20 | # windows local build artifacts 21 | /win_gem_test/shared/ 22 | /win_gem_test/packages/ 23 | /win_gem_test/test_logs/ 24 | /Rakefile_wintest 25 | *.gem 26 | /lib/puma/puma_http11.rb 27 | -------------------------------------------------------------------------------- /test/shell/t1.rb: -------------------------------------------------------------------------------- 1 | system "ruby -rrubygems -Ilib bin/puma -p 10102 -C test/shell/t1_conf.rb test/rackup/hello.ru &" 2 | sleep 5 3 | system "curl http://localhost:10102/" 4 | 5 | system "kill `cat t1-pid`" 6 | 7 | sleep 1 8 | 9 | log = File.read("t1-stdout") 10 | 11 | File.unlink "t1-stdout" if File.file? "t1-stdout" 12 | File.unlink "t1-pid" if File.file? "t1-pid" 13 | 14 | if log =~ %r!GET / HTTP/1\.1! 15 | exit 0 16 | else 17 | exit 1 18 | end 19 | -------------------------------------------------------------------------------- /test/config/ssl_config.rb: -------------------------------------------------------------------------------- 1 | key = File.expand_path "../../examples/puma/puma_keypair.pem", __FILE__ 2 | cert = File.expand_path "../../examples/puma/cert_puma.pem", __FILE__ 3 | ca = File.expand_path "../../examples/puma/client-certs/ca.crt", __FILE__ 4 | 5 | ssl_bind "0.0.0.0", 9292, :cert => cert, :key => key, :verify_mode => "peer", :ca => ca 6 | 7 | app do |env| 8 | [200, {}, ["embedded app"]] 9 | end 10 | 11 | lowlevel_error_handler do |err| 12 | [200, {}, ["error page"]] 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rdoc" 6 | gem "rake-compiler" 7 | 8 | gem "nio4r", "~> 2.0" 9 | gem "rack", "< 3.0" 10 | gem "minitest", "~> 5.11" 11 | gem "minitest-retry" 12 | gem "minitest-proveit" 13 | 14 | gem "jruby-openssl", :platform => "jruby" 15 | 16 | gem "rubocop", "~> 0.58.0" 17 | 18 | if %w(2.2.7 2.2.8 2.2.9 2.2.10 2.3.4 2.4.1).include? RUBY_VERSION 19 | gem "stopgap_13632", "~> 1.0", :platforms => ["mri", "mingw", "x64_mingw"] 20 | end 21 | 22 | gem 'm' 23 | -------------------------------------------------------------------------------- /lib/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Standard libraries 4 | require 'socket' 5 | require 'tempfile' 6 | require 'time' 7 | require 'etc' 8 | require 'uri' 9 | require 'stringio' 10 | 11 | require 'thread' 12 | 13 | module Puma 14 | autoload :Const, 'puma/const' 15 | autoload :Server, 'puma/server' 16 | autoload :Launcher, 'puma/launcher' 17 | 18 | def self.stats_object=(val) 19 | @get_stats = val 20 | end 21 | 22 | def self.stats 23 | @get_stats.stats 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/config/ab_rs.rb: -------------------------------------------------------------------------------- 1 | url = ARGV.shift 2 | count = (ARGV.shift || 1000).to_i 3 | 4 | STDOUT.sync = true 5 | 6 | 1.upto(5) do |i| 7 | print "#{i}: " 8 | str = `ab -n #{count} -c #{i} #{url} 2>/dev/null` 9 | 10 | rs = /Requests per second:\s+([\d.]+)\s/.match(str) 11 | puts rs[1] 12 | end 13 | 14 | puts "Keep Alive:" 15 | 16 | 1.upto(5) do |i| 17 | print "#{i}: " 18 | str = `ab -n #{count} -k -c #{i} #{url} 2>/dev/null` 19 | 20 | rs = /Requests per second:\s+([\d.]+)\s/.match(str) 21 | puts rs[1] 22 | end 23 | -------------------------------------------------------------------------------- /Release.md: -------------------------------------------------------------------------------- 1 | ## Before Release 2 | 3 | - Make sure tests pass and your last local commit matches master. 4 | - Run tests with latest jruby 5 | - Update the version in `const.rb`. 6 | - Make sure there is a history entry in `History.md`. 7 | - On minor version updates i.e. from 3.10.x to 3.11.x update the "codename" in `const.rb`. 8 | 9 | # Release process 10 | 11 | Using "3.7.1" as a version example. 12 | 13 | 1. `bundle exec rake release` 14 | 2. Switch to latest JRuby version 15 | 3. `rake java gem` 16 | 4. `gem push pkg/puma-3.7.1-java.gem` 17 | -------------------------------------------------------------------------------- /lib/puma/convenient.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'puma/launcher' 4 | require 'puma/configuration' 5 | 6 | module Puma 7 | def self.run(opts={}) 8 | cfg = Puma::Configuration.new do |user_config| 9 | if port = opts[:port] 10 | user_config.port port 11 | end 12 | 13 | user_config.quiet 14 | 15 | yield c 16 | end 17 | 18 | cfg.clamp 19 | 20 | events = Puma::Events.null 21 | 22 | launcher = Puma::Launcher.new cfg, :events => events 23 | launcher.run 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /bin/puma-wild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Copyright (c) 2014 Evan Phoenix 4 | # 5 | 6 | require 'rubygems' 7 | 8 | gems = ARGV.shift 9 | 10 | inc = "" 11 | 12 | if gems == "-I" 13 | inc = ARGV.shift 14 | $LOAD_PATH.concat inc.split(":") 15 | gems = ARGV.shift 16 | end 17 | 18 | gems.split(",").each do |s| 19 | name, ver = s.split(":",2) 20 | gem name, ver 21 | end 22 | 23 | module Puma; end 24 | 25 | Puma.const_set("WILD_ARGS", ["-I", inc, gems]) 26 | 27 | require 'puma/cli' 28 | 29 | cli = Puma::CLI.new ARGV 30 | 31 | cli.run 32 | -------------------------------------------------------------------------------- /test/shell/t2.rb: -------------------------------------------------------------------------------- 1 | system "ruby -rrubygems -Ilib bin/pumactl -F test/shell/t2_conf.rb start" 2 | sleep 5 3 | system "curl http://localhost:10103/" 4 | 5 | out=`ruby -rrubygems -Ilib bin/pumactl -F test/shell/t2_conf.rb status` 6 | 7 | system "ruby -rrubygems -Ilib bin/pumactl -F test/shell/t2_conf.rb stop" 8 | 9 | sleep 1 10 | 11 | log = File.read("t2-stdout") 12 | 13 | File.unlink "t2-stdout" if File.file? "t2-stdout" 14 | 15 | if log =~ %r(GET / HTTP/1\.1) && !File.file?("t2-pid") && out == "Puma is started\n" 16 | exit 0 17 | else 18 | exit 1 19 | end 20 | -------------------------------------------------------------------------------- /ext/puma_http11/PumaHttp11Service.java: -------------------------------------------------------------------------------- 1 | package puma; 2 | 3 | import java.io.IOException; 4 | 5 | import org.jruby.Ruby; 6 | import org.jruby.runtime.load.BasicLibraryService; 7 | 8 | import org.jruby.puma.Http11; 9 | import org.jruby.puma.IOBuffer; 10 | import org.jruby.puma.MiniSSL; 11 | 12 | public class PumaHttp11Service implements BasicLibraryService { 13 | public boolean basicLoad(final Ruby runtime) throws IOException { 14 | Http11.createHttp11(runtime); 15 | IOBuffer.createIOBuffer(runtime); 16 | MiniSSL.createMiniSSL(runtime); 17 | return true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ext/puma_http11/ext_help.h: -------------------------------------------------------------------------------- 1 | #ifndef ext_help_h 2 | #define ext_help_h 3 | 4 | #define RAISE_NOT_NULL(T) if(T == NULL) rb_raise(rb_eArgError, "%s", "NULL found for " # T " when shouldn't be."); 5 | #define DATA_GET(from,type,name) Data_Get_Struct(from,type,name); RAISE_NOT_NULL(name); 6 | #define REQUIRE_TYPE(V, T) if(TYPE(V) != T) rb_raise(rb_eTypeError, "%s", "Wrong argument type for " # V " required " # T); 7 | #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) 8 | 9 | #ifdef DEBUG 10 | #define TRACE() fprintf(stderr, "> %s:%d:%s\n", __FILE__, __LINE__, __FUNCTION__) 11 | #else 12 | #define TRACE() 13 | #endif 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /lib/puma/state_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | 5 | module Puma 6 | class StateFile 7 | def initialize 8 | @options = {} 9 | end 10 | 11 | def save(path) 12 | File.write path, YAML.dump(@options) 13 | end 14 | 15 | def load(path) 16 | @options = YAML.load File.read(path) 17 | end 18 | 19 | FIELDS = %w!control_url control_auth_token pid! 20 | 21 | FIELDS.each do |f| 22 | define_method f do 23 | @options[f] 24 | end 25 | 26 | define_method "#{f}=" do |v| 27 | @options[f] = v 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/puma/accept_nonblock.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'openssl' 4 | 5 | module OpenSSL 6 | module SSL 7 | class SSLServer 8 | unless public_method_defined? :accept_nonblock 9 | def accept_nonblock 10 | sock = @svr.accept_nonblock 11 | 12 | begin 13 | ssl = OpenSSL::SSL::SSLSocket.new(sock, @ctx) 14 | ssl.sync_close = true 15 | ssl.accept if @start_immediately 16 | ssl 17 | rescue SSLError => ex 18 | sock.close 19 | raise ex 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /tools/jungle/README.md: -------------------------------------------------------------------------------- 1 | # Puma as a service 2 | 3 | ## Upstart 4 | 5 | See `/tools/jungle/upstart` for Ubuntu's upstart scripts. 6 | 7 | ## Systemd 8 | 9 | See [/docs/systemd](https://github.com/puma/puma/blob/master/docs/systemd.md). 10 | 11 | ## Init.d 12 | 13 | Deprecatation Warning : `init.d` was replaced by `systemd` since Debian 8 and Ubuntu 16.04, you should look into [/docs/systemd](https://github.com/puma/puma/blob/master/docs/systemd.md) unless you are on an older OS. 14 | 15 | See `/tools/jungle/init.d` for tools to use with init.d and start-stop-daemon. 16 | 17 | ## rc.d 18 | 19 | See `/tools/jungle/rc.d` for FreeBSD's rc.d scripts 20 | -------------------------------------------------------------------------------- /examples/puma/csr_puma.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBhzCB8QIBADBIMQswCQYDVQQGEwJVUzEOMAwGA1UECgwFbG9jYWwxDTALBgNV 3 | BAsMBGFlcm8xCzAJBgNVBAsMAkNBMQ0wCwYDVQQDDARwdW1hMIGfMA0GCSqGSIb3 4 | DQEBAQUAA4GNADCBiQKBgQCvF80yn6D+kqGwMSQHcpHUwCRt+c39Qoy99fCWdenP 5 | thfUscecy62Ij8+rKYCnoE9y766a5baowdDKqq3IBOZn2Ove3zfueGbHAbWehFop 6 | G2xySf0UPjdmWk+DRDlCeFLig6xfAnOKWo+N0MViso3dNK8gYzb6FWqlWgZgAcMp 7 | swIDAQABoAAwDQYJKoZIhvcNAQEEBQADgYEAmRsmIQ0pF9iPOO7V1NeHxrVpFz1B 8 | CZK0yAIGlCWqzpFO/OILN1hJfFnsFl7hZWipoARk15fN1sSXQF3Xb7/sc/8qVhyz 9 | oY38uu/8CE9CTdUutniLzP/4sUomXjslKNVV0qKtmfsFkj2tHtWjJkGAyZUcoKeG 10 | hDJxQlIHhZa7Xvw= 11 | -----END CERTIFICATE REQUEST----- 12 | -------------------------------------------------------------------------------- /win_gem_test/package_gem.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'rubygems/package' 5 | 6 | spec = Gem::Specification.load("./puma.gemspec") 7 | 8 | spec.files.concat ['Rakefile_wintest', 'lib/puma/puma_http11.rb'] 9 | spec.files.concat Dir['lib/**/*.so'] 10 | spec.test_files = Dir['{examples,test}/**/*.*'] 11 | 12 | # below lines are required and not gem specific 13 | spec.platform = ARGV[0] 14 | spec.required_ruby_version = [">= #{ARGV[1]}", "< #{ARGV[2]}"] 15 | spec.extensions = [] 16 | if spec.respond_to?(:metadata=) 17 | spec.metadata.delete("msys2_mingw_dependencies") 18 | spec.metadata['commit'] = ENV['commit_info'] 19 | end 20 | 21 | Gem::Package.build(spec) 22 | -------------------------------------------------------------------------------- /tools/jungle/init.d/run-puma: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # on system boot, and root have no rbenv installed, 4 | # after start-stop-daemon switched to current user, we have to init rbenv 5 | if [ -d "$HOME/.rbenv/bin" ]; then 6 | PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH" 7 | eval "$(rbenv init -)" 8 | elif [ -d "/usr/local/rbenv/bin" ]; then 9 | PATH="/usr/local/rbenv/bin:/usr/local/rbenv/shims:$PATH" 10 | eval "$(rbenv init -)" 11 | elif [ -f /usr/local/rvm/scripts/rvm ]; then 12 | source /etc/profile.d/rvm.sh 13 | elif [ -f "$HOME/.rvm/scripts/rvm" ]; then 14 | source "$HOME/.rvm/scripts/rvm" 15 | fi 16 | 17 | app=$1; config=$2; log=$3; 18 | cd $app && exec bundle exec puma -C $config >> $log 2>&1 19 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | install: 2 | # download shared script files 3 | - ps: >- 4 | if ( !(Test-Path -Path ./shared -PathType Container) ) { 5 | $uri = 'https://ci.appveyor.com/api/projects/MSP-Greg/av-gem-build-test/artifacts/shared.7z' 6 | $7z = 'C:\Program Files\7-Zip\7z.exe' 7 | $fn = "$env:TEMP\shared.7z" 8 | (New-Object System.Net.WebClient).DownloadFile($uri, $fn) 9 | &$7z x $fn -owin_gem_test 1> $null 10 | Remove-Item -LiteralPath $fn -Force 11 | Write-Host "Downloaded shared files" -ForegroundColor Yellow 12 | } 13 | 14 | build_script: 15 | - ps: .\win_gem_test\puma.ps1 $env:gem_bits 16 | 17 | environment: 18 | matrix: 19 | - gem_bits: 64 20 | - gem_bits: 32 21 | -------------------------------------------------------------------------------- /test/test_tcp_rack.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | class TestTCPRack < Minitest::Test 4 | 5 | def setup 6 | @port = UniquePort.call 7 | @host = "127.0.0.1" 8 | 9 | @events = Puma::Events.new STDOUT, STDERR 10 | @server = Puma::Server.new nil, @events 11 | end 12 | 13 | def teardown 14 | @server.stop(true) 15 | end 16 | 17 | def test_passes_the_socket 18 | @server.tcp_mode! 19 | 20 | body = "We sell hats for a discount!\n" 21 | 22 | @server.app = proc do |env, socket| 23 | socket << body 24 | socket.close 25 | end 26 | 27 | @server.add_tcp_listener @host, @port 28 | @server.run 29 | 30 | sock = TCPSocket.new @host, @port 31 | 32 | assert_equal body, sock.read 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /ext/puma_http11/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | dir_config("puma_http11") 4 | 5 | unless ENV["DISABLE_SSL"] 6 | dir_config("openssl") 7 | 8 | if %w'crypto libeay32'.find {|crypto| have_library(crypto, 'BIO_read')} and 9 | %w'ssl ssleay32'.find {|ssl| have_library(ssl, 'SSL_CTX_new')} 10 | 11 | have_header "openssl/bio.h" 12 | 13 | # below is yes for 1.0.2 & later 14 | have_func "DTLS_method" , "openssl/ssl.h" 15 | 16 | # below are yes for 1.1.0 & later, may need to check func rather than macro 17 | # with versions after 1.1.1 18 | have_func "TLS_server_method" , "openssl/ssl.h" 19 | have_macro "SSL_CTX_set_min_proto_version", "openssl/ssl.h" 20 | end 21 | end 22 | 23 | create_makefile("puma/puma_http11") 24 | -------------------------------------------------------------------------------- /lib/puma/null_io.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Puma 4 | # Provides an IO-like object that always appears to contain no data. 5 | # Used as the value for rack.input when the request has no body. 6 | # 7 | class NullIO 8 | def gets 9 | nil 10 | end 11 | 12 | def each 13 | end 14 | 15 | # Mimics IO#read with no data. 16 | # 17 | def read(count = nil, _buffer = nil) 18 | (count && count > 0) ? nil : "" 19 | end 20 | 21 | def rewind 22 | end 23 | 24 | def close 25 | end 26 | 27 | def size 28 | 0 29 | end 30 | 31 | def eof? 32 | true 33 | end 34 | 35 | def sync=(v) 36 | end 37 | 38 | def puts(*ary) 39 | end 40 | 41 | def write(*ary) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/shell/t3.rb: -------------------------------------------------------------------------------- 1 | system "ruby -rrubygems -Ilib bin/puma -p 10102 -C test/shell/t3_conf.rb test/rackup/hello.ru &" 2 | sleep 5 3 | 4 | worker_pid_was_present = File.file? "t3-worker-2-pid" 5 | 6 | system "kill `cat t3-worker-2-pid`" # kill off a worker 7 | 8 | sleep 2 9 | 10 | worker_index_within_number_of_workers = !File.file?("t3-worker-3-pid") 11 | 12 | system "kill `cat t3-pid`" 13 | 14 | File.unlink "t3-pid" if File.file? "t3-pid" 15 | File.unlink "t3-worker-0-pid" if File.file? "t3-worker-0-pid" 16 | File.unlink "t3-worker-1-pid" if File.file? "t3-worker-1-pid" 17 | File.unlink "t3-worker-2-pid" if File.file? "t3-worker-2-pid" 18 | File.unlink "t3-worker-3-pid" if File.file? "t3-worker-3-pid" 19 | 20 | if worker_pid_was_present and worker_index_within_number_of_workers 21 | exit 0 22 | else 23 | exit 1 24 | end 25 | -------------------------------------------------------------------------------- /test/test_unix_socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helper" 4 | 5 | class TestPumaUnixSocket < Minitest::Test 6 | 7 | App = lambda { |env| [200, {}, ["Works"]] } 8 | 9 | Path = "test/puma.sock" 10 | 11 | def setup 12 | return unless UNIX_SKT_EXIST 13 | @server = Puma::Server.new App 14 | @server.add_unix_listener Path 15 | @server.run 16 | end 17 | 18 | def teardown 19 | return unless UNIX_SKT_EXIST 20 | @server.stop(true) 21 | end 22 | 23 | def test_server 24 | skip UNIX_SKT_MSG unless UNIX_SKT_EXIST 25 | sock = UNIXSocket.new Path 26 | 27 | sock << "GET / HTTP/1.0\r\nHost: blah.com\r\n\r\n" 28 | 29 | expected = "HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nWorks" 30 | 31 | assert_equal expected, sock.read(expected.size) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/puma/daemon_ext.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Process 4 | 5 | # This overrides the default version because it is broken if it 6 | # exists. 7 | 8 | if respond_to? :daemon 9 | class << self 10 | remove_method :daemon 11 | end 12 | end 13 | 14 | def self.daemon(nochdir=false, noclose=false) 15 | exit if fork # Parent exits, child continues. 16 | 17 | Process.setsid # Become session leader. 18 | 19 | exit if fork # Zap session leader. See [1]. 20 | 21 | Dir.chdir "/" unless nochdir # Release old working directory. 22 | 23 | if !noclose 24 | STDIN.reopen File.open("/dev/null", "r") 25 | 26 | null_out = File.open "/dev/null", "w" 27 | STDOUT.reopen null_out 28 | STDERR.reopen null_out 29 | end 30 | 31 | 0 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/puma/plugin/tmp_restart.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'puma/plugin' 4 | 5 | Puma::Plugin.create do 6 | def start(launcher) 7 | path = File.join("tmp", "restart.txt") 8 | 9 | orig = nil 10 | 11 | # If we can't write to the path, then just don't bother with this plugin 12 | begin 13 | File.write(path, "") unless File.exist?(path) 14 | orig = File.stat(path).mtime 15 | rescue SystemCallError 16 | return 17 | end 18 | 19 | in_background do 20 | while true 21 | sleep 2 22 | 23 | begin 24 | mtime = File.stat(path).mtime 25 | rescue SystemCallError 26 | # If the file has disappeared, assume that means don't restart 27 | else 28 | if mtime > orig 29 | launcher.restart 30 | break 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /examples/puma/puma_keypair.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQCvF80yn6D+kqGwMSQHcpHUwCRt+c39Qoy99fCWdenPthfUscec 3 | y62Ij8+rKYCnoE9y766a5baowdDKqq3IBOZn2Ove3zfueGbHAbWehFopG2xySf0U 4 | PjdmWk+DRDlCeFLig6xfAnOKWo+N0MViso3dNK8gYzb6FWqlWgZgAcMpswIDAQAB 5 | AoGAHv/UyZivdULas4oPue3T2dnm2T239ZXZuywW21ym96pij7ql/6Gj6KClgMVJ 6 | TOQ6DLxYqn3vF/OwlqEfQWF0tTUYY+xNbEDE1YsbrS5/FSzbaEYYOHzRl/vMmnsf 7 | aNgYaSjOIecin7L71Wzq0piMIxg8BLb6IVECBku9EQNzxuECQQDZsbRgg1XZGj+r 8 | XAu/qXTNKQ/r7k+iPN5bXON6ApBomG+4Q7VVITL3tkGzLOphRZ37Q28FrN4B4gtC 9 | Xb9il5lDAkEAzecTSopPi2VdcME4WWmwn1rbTp/jJNt4dGZLsNfj9RejVDd32i/L 10 | P7wCpoPDaaVcoF2HgvCs39qatyVg6ecu0QJBALN4q+q9nDMGTuNpWU5D2EWjyrqJ 11 | mCF66R6NcASQxJlWwxQ4zfBHFIvgOD4Nk5VqHZqet5MIN2d6AipOu4/+x50CQHDp 12 | jf+rd1GHBcXGf8MwnUXWCjvEnEhi/lw+mLVivsRx8QRG4rfIy9monX949Flj8DaU 13 | 87IPj422kG9s1QeP2nECQQCkg+RUcoQm7SiM8OXuXNeHQlvQNp65geFRxzKAXxT/ 14 | +1Mbtwnd3AXXZBekFDDpE9U3ZQjahoe7oc1oUBuw5hXL 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/test_http10.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | require "puma/puma_http11" 4 | 5 | class Http10ParserTest < Minitest::Test 6 | def test_parse_simple 7 | parser = Puma::HttpParser.new 8 | req = {} 9 | http = "GET / HTTP/1.0\r\n\r\n" 10 | nread = parser.execute(req, http, 0) 11 | 12 | assert nread == http.length, "Failed to parse the full HTTP request" 13 | assert parser.finished?, "Parser didn't finish" 14 | assert !parser.error?, "Parser had error" 15 | assert nread == parser.nread, "Number read returned from execute does not match" 16 | 17 | assert_equal '/', req['REQUEST_PATH'] 18 | assert_equal 'HTTP/1.0', req['HTTP_VERSION'] 19 | assert_equal '/', req['REQUEST_URI'] 20 | assert_equal 'GET', req['REQUEST_METHOD'] 21 | assert_nil req['FRAGMENT'] 22 | assert_nil req['QUERY_STRING'] 23 | 24 | parser.reset 25 | assert parser.nread == 0, "Number read after reset should be 0" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/test_iobuffer.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | require "puma/io_buffer" 4 | 5 | class TestIOBuffer < Minitest::Test 6 | attr_accessor :iobuf 7 | def setup 8 | self.iobuf = Puma::IOBuffer.new 9 | end 10 | 11 | def test_initial_size 12 | assert_equal 0, iobuf.used 13 | assert iobuf.capacity > 0 14 | end 15 | 16 | def test_append_op 17 | iobuf << "abc" 18 | assert_equal "abc", iobuf.to_s 19 | iobuf << "123" 20 | assert_equal "abc123", iobuf.to_s 21 | assert_equal 6, iobuf.used 22 | end 23 | 24 | def test_append 25 | expected = "mary had a little lamb" 26 | iobuf.append("mary", " ", "had ", "a little", " lamb") 27 | assert_equal expected, iobuf.to_s 28 | assert_equal expected.length, iobuf.used 29 | end 30 | 31 | def test_reset 32 | iobuf << "content" 33 | assert_equal "content", iobuf.to_s 34 | iobuf.reset 35 | assert_equal 0, iobuf.used 36 | assert_equal "", iobuf.to_s 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /lib/puma/tcp_logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Puma 4 | class TCPLogger 5 | def initialize(logger, app, quiet=false) 6 | @logger = logger 7 | @app = app 8 | @quiet = quiet 9 | end 10 | 11 | FORMAT = "%s - %s" 12 | 13 | def log(who, str) 14 | now = Time.now.strftime("%d/%b/%Y %H:%M:%S") 15 | 16 | log_str = "#{now} - #{who} - #{str}" 17 | 18 | case @logger 19 | when IO 20 | @logger.puts log_str 21 | when Events 22 | @logger.log log_str 23 | end 24 | end 25 | 26 | def call(env, socket) 27 | who = env[Const::REMOTE_ADDR] 28 | log who, "connected" unless @quiet 29 | 30 | env['log'] = lambda { |str| log(who, str) } 31 | 32 | begin 33 | @app.call env, socket 34 | rescue Object => e 35 | log who, "exception: #{e.message} (#{e.class})" 36 | else 37 | log who, "disconnected" unless @quiet 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/test_minissl.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | require "puma/minissl" 4 | 5 | class TestMiniSSL < Minitest::Test 6 | 7 | if Puma.jruby? 8 | def test_raises_with_invalid_keystore_file 9 | ctx = Puma::MiniSSL::Context.new 10 | 11 | exception = assert_raises(ArgumentError) { ctx.keystore = "/no/such/keystore" } 12 | assert_equal("No such keystore file '/no/such/keystore'", exception.message) 13 | end 14 | else 15 | def test_raises_with_invalid_key_file 16 | ctx = Puma::MiniSSL::Context.new 17 | 18 | exception = assert_raises(ArgumentError) { ctx.key = "/no/such/key" } 19 | assert_equal("No such key file '/no/such/key'", exception.message) 20 | end 21 | 22 | def test_raises_with_invalid_cert_file 23 | ctx = Puma::MiniSSL::Context.new 24 | 25 | exception = assert_raises(ArgumentError) { ctx.cert = "/no/such/cert" } 26 | assert_equal("No such cert file '/no/such/cert'", exception.message) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /tools/jungle/upstart/puma-manager.conf: -------------------------------------------------------------------------------- 1 | # /etc/init/puma-manager.conf - manage a set of Pumas 2 | 3 | # This example config should work with Ubuntu 12.04+. It 4 | # allows you to manage multiple Puma instances with 5 | # Upstart, Ubuntu's native service management tool. 6 | # 7 | # See puma.conf for how to manage a single Puma instance. 8 | # 9 | # Use "stop puma-manager" to stop all Puma instances. 10 | # Use "start puma-manager" to start all instances. 11 | # Use "restart puma-manager" to restart all instances. 12 | # Crazy, right? 13 | # 14 | 15 | description "Manages the set of puma processes" 16 | 17 | # This starts upon bootup and stops on shutdown 18 | start on runlevel [2345] 19 | stop on runlevel [06] 20 | 21 | # Set this to the number of Puma processes you want 22 | # to run on this machine 23 | env PUMA_CONF="/etc/puma.conf" 24 | 25 | pre-start script 26 | for i in `cat $PUMA_CONF`; do 27 | app=`echo $i | cut -d , -f 1` 28 | logger -t "puma-manager" "Starting $app" 29 | start puma app=$app 30 | done 31 | end script 32 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisabledByDefault: true 3 | TargetRubyVersion: 2.2 4 | DisplayCopNames: true 5 | StyleGuideCopsOnly: false 6 | Exclude: 7 | - 'tmp/**/*' 8 | - 'vendor/**/*' 9 | - 'Rakefile' 10 | 11 | Layout/SpaceAfterColon: 12 | Enabled: true 13 | 14 | Layout/SpaceAroundKeyword: 15 | Enabled: true 16 | 17 | Layout/SpaceBeforeBlockBraces: 18 | EnforcedStyleForEmptyBraces: no_space 19 | Enabled: true 20 | 21 | Layout/SpaceBeforeFirstArg: 22 | Enabled: true 23 | 24 | Layout/SpaceInsideParens: 25 | Enabled: true 26 | 27 | Layout/Tab: 28 | Enabled: true 29 | 30 | Layout/TrailingBlankLines: 31 | Enabled: true 32 | 33 | Layout/TrailingWhitespace: 34 | Enabled: true 35 | 36 | Lint/Debugger: 37 | Enabled: true 38 | 39 | Naming/MethodName: 40 | Enabled: true 41 | EnforcedStyle: snake_case 42 | Exclude: 43 | - 'test/**/**' 44 | 45 | Naming/VariableName: 46 | Enabled: true 47 | 48 | Style/MethodDefParentheses: 49 | Enabled: true 50 | 51 | Style/TrailingCommaInArguments: 52 | Enabled: true 53 | 54 | Performance: 55 | Enabled: true 56 | -------------------------------------------------------------------------------- /tools/trickletest.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'stringio' 3 | 4 | def do_test(st, chunk) 5 | s = TCPSocket.new('127.0.0.1',ARGV[0].to_i); 6 | req = StringIO.new(st) 7 | nout = 0 8 | randstop = rand(st.length / 10) 9 | STDERR.puts "stopping after: #{randstop}" 10 | 11 | begin 12 | while data = req.read(chunk) 13 | nout += s.write(data) 14 | s.flush 15 | sleep 0.1 16 | if nout > randstop 17 | STDERR.puts "BANG! after #{nout} bytes." 18 | break 19 | end 20 | end 21 | rescue Object => e 22 | STDERR.puts "ERROR: #{e}" 23 | ensure 24 | s.close 25 | end 26 | end 27 | 28 | content = "-" * (1024 * 240) 29 | st = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\nContent-Length: #{content.length}\r\n\r\n#{content}" 30 | 31 | puts "length: #{content.length}" 32 | 33 | threads = [] 34 | ARGV[1].to_i.times do 35 | t = Thread.new do 36 | size = 100 37 | puts ">>>> #{size} sized chunks" 38 | do_test(st, size) 39 | end 40 | 41 | t.abort_on_exception = true 42 | threads << t 43 | end 44 | 45 | threads.each {|t| t.join} 46 | -------------------------------------------------------------------------------- /examples/puma/cert_puma.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC/jCCAeagAwIBAgIBAjANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJVUzEO 3 | MAwGA1UECgwFbG9jYWwxDTALBgNVBAsMBGFlcm8xCzAJBgNVBAMMAkNBMB4XDTEy 4 | MDExNDAwMjcyN1oXDTEzMDExMzAwMjcyN1owSDELMAkGA1UEBhMCVVMxDjAMBgNV 5 | BAoMBWxvY2FsMQ0wCwYDVQQLDARhZXJvMQswCQYDVQQLDAJDQTENMAsGA1UEAwwE 6 | cHVtYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEArxfNMp+g/pKhsDEkB3KR 7 | 1MAkbfnN/UKMvfXwlnXpz7YX1LHHnMutiI/PqymAp6BPcu+umuW2qMHQyqqtyATm 8 | Z9jr3t837nhmxwG1noRaKRtsckn9FD43ZlpPg0Q5QnhS4oOsXwJzilqPjdDFYrKN 9 | 3TSvIGM2+hVqpVoGYAHDKbMCAwEAAaOBhTCBgjAMBgNVHRMBAf8EAjAAMDEGCWCG 10 | SAGG+EIBDQQkFiJSdWJ5L09wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0G 11 | A1UdDgQWBBTyDyJlmYBDwfWdRj6lWGvoY43k9DALBgNVHQ8EBAMCBaAwEwYDVR0l 12 | BAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEFBQADggEBAIbBVfoVCG8RyVesPW+q 13 | 5i0wAMbHZ1fwv1RKp17c68DYDs0YYPi0bA0ss8AgpU6thWmskxPiFaE6D5x8iv9f 14 | zkcHxgr1Mrbx6RLx9tLUVehSmRv3aiVO4k9Mp6vf+rJK1AYeaGBmvoqTBLwy7Jrt 15 | ytKMdqMJj5jKWkWgEGgTnjzbcOClmCQab9isigIzTxMyC/LjeKZe8pPeVX6OM8bY 16 | y8XGZp9B7uwdPzqt/g25IzTC0KsQwq8cB0raAtZzIyTNv42zcUjmQNVazAozCTcq 17 | MsEtK2z7TYBC3udTsdyS2qVqCpsk7IMOBGrw8vk4SNhO+coiDObW2K/HNvhl0tZC 18 | oQI= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /examples/CA/newcerts/cert_1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC/jCCAeagAwIBAgIBATANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJVUzEO 3 | MAwGA1UECgwFbG9jYWwxDTALBgNVBAsMBGFlcm8xCzAJBgNVBAMMAkNBMB4XDTEy 4 | MDExNDAwMTcyN1oXDTEzMDExMzAwMTcyN1owSDELMAkGA1UEBhMCVVMxDjAMBgNV 5 | BAoMBWxvY2FsMQ0wCwYDVQQLDARhZXJvMQswCQYDVQQLDAJDQTENMAsGA1UEAwwE 6 | cHVtYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxkKjXHIYV4CVFB4YZuVk 7 | sXqdb7X9+igKPSkxFZoyjwW+AGiN27OwTvETLQiPMaB1WiJtDF9NEmlwYXl4dHWz 8 | 5QPVJtQ5ud7FdCJWBUc+K6LS4xwixwis/FNZVfVcyxkR+cm7mVwxwrxle1lJasoJ 9 | ouqtVePdt+j+9hcJfLIaZD8CAwEAAaOBhTCBgjAMBgNVHRMBAf8EAjAAMDEGCWCG 10 | SAGG+EIBDQQkFiJSdWJ5L09wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0G 11 | A1UdDgQWBBQDrltCvbaqNl/TvFNb/NEIEnbJ2jALBgNVHQ8EBAMCBaAwEwYDVR0l 12 | BAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEFBQADggEBAF8xrbIbIz7YW6ydjM6g 13 | gesu8T6q5RcJo9k2CWhd4RgJdfVJSZbCtAAMoRQTErX9Ng6afdlMWD1KnCfbP0IJ 14 | dq+Umh1BMfzhpYR35NPO/RZPR7Et0OMmmWCbwJBzgI6z8R8qiSuR/to6C7BjiWzo 15 | rp7S2fenkB6DfzZvHvIDojQ0OpnD2oYBOn/UyAma4I7XzXWe9IIUMARjS5CYZsv9 16 | HBU3B+e5F9ANi3lRc7x5jIAqVt292HaH+c1UCn0/r/73cW1Z/iNYA9PgS2QKdmYq 17 | b3oQRFk0wM5stNVsrn7xGftmOETv8pD6r4P8jyw7Ib+ypr10WrFOm7uOscPS4QE2 18 | Mf0= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /examples/CA/newcerts/cert_2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC/jCCAeagAwIBAgIBAjANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJVUzEO 3 | MAwGA1UECgwFbG9jYWwxDTALBgNVBAsMBGFlcm8xCzAJBgNVBAMMAkNBMB4XDTEy 4 | MDExNDAwMjcyN1oXDTEzMDExMzAwMjcyN1owSDELMAkGA1UEBhMCVVMxDjAMBgNV 5 | BAoMBWxvY2FsMQ0wCwYDVQQLDARhZXJvMQswCQYDVQQLDAJDQTENMAsGA1UEAwwE 6 | cHVtYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEArxfNMp+g/pKhsDEkB3KR 7 | 1MAkbfnN/UKMvfXwlnXpz7YX1LHHnMutiI/PqymAp6BPcu+umuW2qMHQyqqtyATm 8 | Z9jr3t837nhmxwG1noRaKRtsckn9FD43ZlpPg0Q5QnhS4oOsXwJzilqPjdDFYrKN 9 | 3TSvIGM2+hVqpVoGYAHDKbMCAwEAAaOBhTCBgjAMBgNVHRMBAf8EAjAAMDEGCWCG 10 | SAGG+EIBDQQkFiJSdWJ5L09wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0G 11 | A1UdDgQWBBTyDyJlmYBDwfWdRj6lWGvoY43k9DALBgNVHQ8EBAMCBaAwEwYDVR0l 12 | BAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQEFBQADggEBAIbBVfoVCG8RyVesPW+q 13 | 5i0wAMbHZ1fwv1RKp17c68DYDs0YYPi0bA0ss8AgpU6thWmskxPiFaE6D5x8iv9f 14 | zkcHxgr1Mrbx6RLx9tLUVehSmRv3aiVO4k9Mp6vf+rJK1AYeaGBmvoqTBLwy7Jrt 15 | ytKMdqMJj5jKWkWgEGgTnjzbcOClmCQab9isigIzTxMyC/LjeKZe8pPeVX6OM8bY 16 | y8XGZp9B7uwdPzqt/g25IzTC0KsQwq8cB0raAtZzIyTNv42zcUjmQNVazAozCTcq 17 | MsEtK2z7TYBC3udTsdyS2qVqCpsk7IMOBGrw8vk4SNhO+coiDObW2K/HNvhl0tZC 18 | oQI= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /examples/puma/client-certs/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDAzCCAeugAwIBAgIBAzANBgkqhkiG9w0BAQUFADA4MRMwEQYKCZImiZPyLGQB 3 | GRYDbmV0MRQwEgYKCZImiZPyLGQBGRYEcHVtYTELMAkGA1UEAwwCY2EwIBcNMTQw 4 | MjAyMjAwODI3WhgPMjExNTAxMDkyMDA4MjdaMDwxEzARBgoJkiaJk/IsZAEZFgNu 5 | ZXQxFDASBgoJkiaJk/IsZAEZFgRwdW1hMQ8wDQYDVQQDDAZjbGllbnQwggEiMA0G 6 | CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGnFKNrNj9pvHIK+iUf1UoDJ0O7hpj 7 | jn7QWd65xnHWN9YF9RfBVRxg7HLcPls6GL4c+e5KQP1W0o4gSzwbUc3a/LyqkTEA 8 | dligEjXTkQY6tCn/51CuClynreQ98wdgQrayzobKhWMALG7IRLraprZmiQJpxWOF 9 | evd7WkF32AwSklZMEdWcLdI36swTV0UzuR9IDUnIh5GGPbikF/6hQ1E1+rL/sZkh 10 | czYNEniJGk2pD3MqJguTvYTF24k1KEOV5koSuAPnyl4E/dX9g3AIHWo8OhqVs/4P 11 | hrX6++qmrsVz9LIvPw3+SMAE3QU49J7uAANRQMBlxWhlbIpeFi8zNig7AgMBAAGj 12 | EjAQMA4GA1UdDwEB/wQEAwIEsDANBgkqhkiG9w0BAQUFAAOCAQEAtxbX3CfQgMwa 13 | CWYTjupyTC+KDajbkLLsNXw49PeTIj0FOjAdKA1zyEZcrxtaU+flJr8QHdI8HyZH 14 | hpofnOTSBg5k9y4Qz8gjI1Nsh0H8WU/d7F//2l2fUDOhVAb6JtTAKnMpU4snb0GD 15 | bxcO6QxfNh50Qdb7KoJH7baJ3aAnsRrLVGqQ7jH20iMu163j/pYw4dDskFMr65Le 16 | bMB3NeQ5pHwtYf2J5EliKCtH+Df/BTIl9u1vviZs84gA0Odai/YaMZWCqFiWqIax 17 | lkMHNSDWh2G++qMn9erLjRtYDAbIt3VhMncUpEBx3lBEIaVg7qyfpWQ4EkkkylH6 18 | WRv06vukVg== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /examples/puma/client-certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDBjCCAe6gAwIBAgIBAjANBgkqhkiG9w0BAQUFADA4MRMwEQYKCZImiZPyLGQB 3 | GRYDbmV0MRQwEgYKCZImiZPyLGQBGRYEcHVtYTELMAkGA1UEAwwCY2EwIBcNMTQw 4 | MjAyMjAwODI3WhgPMjExNTAxMDkyMDA4MjdaMD8xEzARBgoJkiaJk/IsZAEZFgNu 5 | ZXQxFDASBgoJkiaJk/IsZAEZFgRwdW1hMRIwEAYDVQQDDAkxMjcuMC4wLjEwggEi 6 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDK2ioejidqPJzhGgYh9Nc/CD8g 7 | n/vFqchULvmG796R01Rx0Xk5v7OgWs4GMhvJ8o4soCxTmACyPStdemDlocdzZf2d 8 | yfv1alVVfBBwqSsiekiB7IiSvpyg5t3h8XqWJcKtP00tPEYmAkVuMbVSxQPrsEi5 9 | 47kxu7zyiV0RavaZbODgxkupSjEr0DHa1h7pkip53ekz/rnoceVcvSnCdOahUVj6 10 | ZwMkOQtay/b6746ttbfQh1ygbqTbV/lcV9erldlDkqKG0gQ6gxaBcbIiom1p+ohu 11 | CcoTDGZu431KOU6ZygbGxaIEZY9Zbyg9Dp+o6Zyyd7UTY/0JcCWUq7O/XaN5AgMB 12 | AAGjEjAQMA4GA1UdDwEB/wQEAwIEsDANBgkqhkiG9w0BAQUFAAOCAQEAIfMqanJJ 13 | aVD6XuS3aj0I31L4RiPSfKhkPiuO+lqBGzZhUEKwnEqVWLosFF1SK8Inbu1c1uyP 14 | zRb0tB4nSO01L8Oc5kTfuN9lr3nNaWDpGksa/S5e9WndQk95XF3FLt7FJii8wWnM 15 | 9xGW27lurskbpuZc1M7IkD5W90y2fF19qB8fY8B2RGovPJEsDKSZ7pwSozijGR4Q 16 | 2iIY4Lk9/vYxEYMRixE2+exYiKTNfaPt+CgxHxXksn0LvbYYQTxUmDgvSxXdrnCc 17 | 4Kb1BbxOmB8XF17aJuRdUJxDxlnQK5LpoUWGfW7jFPbfX4d3nzpxjPaxvr3peRQV 18 | DNtRoD9mFvocbQ== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /examples/puma/client-certs/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDEDCCAfigAwIBAgIBATANBgkqhkiG9w0BAQUFADA4MRMwEQYKCZImiZPyLGQB 3 | GRYDbmV0MRQwEgYKCZImiZPyLGQBGRYEcHVtYTELMAkGA1UEAwwCY2EwIBcNMTQw 4 | MjAyMjAwODI3WhgPMjExNTAxMDkyMDA4MjdaMDgxEzARBgoJkiaJk/IsZAEZFgNu 5 | ZXQxFDASBgoJkiaJk/IsZAEZFgRwdW1hMQswCQYDVQQDDAJjYTCCASIwDQYJKoZI 6 | hvcNAQEBBQADggEPADCCAQoCggEBAN0u8/heGbkoFDDsx6uidE6DKPvjIPnZPFzR 7 | CMkzrNgIeq/hfAItIJAO0m8YZivkUWeE3ut4ibSL+OVTvLRWDL/L736LILUxrD2f 8 | joKHHLSVIUWl3H0VjYDE2RCiVkvxP4sAo7EYecZesTtb7W7DdAjHztFZIl+wT+ri 9 | MlxDRmYxwsOPQtL0/wJZF80uTpC29V47NY9ITd/A+1xMblPAuQKO3vqZ4Yq07mO/ 10 | KKSbepo07v7jMhNOSHf8VBFlTzzG5AHmxZUW0qjCkJBV8N1MiT9cIk81ZuSqOZu3 11 | A+aDAlOYPJe2WVpGskCme9HkJaHTeP87tQUsLqRsLgq/AXh5R58CAwEAAaMjMCEw 12 | DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQAD 13 | ggEBAKiGZ57rSAlL9slQ0FklVjpe+YrfZvmaXTRPl+9YhikoVI91u1r/qA9PrXKn 14 | cL6u66SU6kJwI5572uT1TpKO7jQLXJZV0LO17WuF3P7y44QnNb53Em2GYi8DD/gq 15 | X0Y1u8QzIxo4uomWiE73fnao2I9eErKNi/xCySaX/SLQ/9tcEgUyeLlTtJZ3feVF 16 | 7K0llR+hSb0Wy/uWnP7qP59YsyCJl1H23j7IEVCTMsOQ4tyIK16+qRA+aVLtE9f5 17 | orsrOWWGJOdAn1nCJweKqhG1vd3GKGRW3Rf/iugCbvgJy0NFLfTpeJ4fJosC3A/K 18 | 6K+pe9hNsi2kBPwC67QeVjnbqd4= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/test_null_io.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helper" 4 | 5 | require "puma/null_io" 6 | 7 | class TestNullIO < Minitest::Test 8 | parallelize_me! 9 | 10 | attr_accessor :nio 11 | 12 | def setup 13 | self.nio = Puma::NullIO.new 14 | end 15 | 16 | def test_eof_returns_true 17 | assert nio.eof? 18 | end 19 | 20 | def test_gets_returns_nil 21 | assert_nil nio.gets 22 | end 23 | 24 | def test_each_never_yields 25 | nio.instance_variable_set(:@foo, :baz) 26 | nio.each { @foo = :bar } 27 | assert_equal :baz, nio.instance_variable_get(:@foo) 28 | end 29 | 30 | def test_read_with_no_arguments 31 | assert_equal "", nio.read 32 | end 33 | 34 | def test_read_with_nil_length 35 | assert_equal "", nio.read(nil) 36 | end 37 | 38 | def test_read_with_zero_length 39 | assert_equal "", nio.read(0) 40 | end 41 | 42 | def test_read_with_positive_integer_length 43 | assert_nil nio.read(1) 44 | end 45 | 46 | def test_read_with_length_and_buffer 47 | buf = "" 48 | assert_nil nio.read(1, buf) 49 | assert_equal "", buf 50 | end 51 | 52 | def test_size 53 | assert_equal 0, nio.size 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /examples/puma/client-certs/client_expired.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDCTCCAfGgAwIBAgIBBDANBgkqhkiG9w0BAQUFADA4MRMwEQYKCZImiZPyLGQB 3 | GRYDbmV0MRQwEgYKCZImiZPyLGQBGRYEcHVtYTELMAkGA1UEAwwCY2EwHhcNMTQw 4 | MjAyMjAwODI3WhcNMTQwODA0MDgwODI3WjBEMRMwEQYKCZImiZPyLGQBGRYDbmV0 5 | MRQwEgYKCZImiZPyLGQBGRYEcHVtYTEXMBUGA1UEAwwOY2xpZW50LWV4cGlyZWQw 6 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZQSVfI4KNwus/+9gE9Rww 7 | +cehxzw80fNi4tmruSApitTIk1u1r1rYVexkBkVTtl6Fg/aNAAdsI4aATanyGj0m 8 | yRqEMxYMt8RtzAYHY6ZEJBm4WUAa44W7WNG2ZA/e0bCDq4Sn+hlPJw0e4iQimJqi 9 | 8+iitgyTdicTKDR+9kTS3W/33PZqSwqqnN55m9n9A5FIKwd8fbPsO8k6xIhFS2sL 10 | KZ2TkAYLNXu2vFGJR7b37U8mYcHObB1p7U7WYJ2JCf21WZOC4iI25Xk7MFSUYPqb 11 | W/iV+41EcslbHwAZHEjqeNynKNlnZokVrviOFeFrHqXbVKp43027L3RZr/JXfxMl 12 | AgMBAAGjEjAQMA4GA1UdDwEB/wQEAwIEsDANBgkqhkiG9w0BAQUFAAOCAQEAdCLR 13 | jmHeQDrtl9w0cr8Vls+clhoWSDIEj2NC7PRUbDS5T0kAnF/N64n9RJFPS+4bpZaT 14 | c9v3DXzdaTTp7moUrwVc3EKVLV5EJcm+TcuUhbL2ZnRgFHggVaoePShBHkDJGLz9 15 | lR30KJnKsyFKEDEyD4rYtYvg98858EtkuxKLsD8efQ/9V8WDLAJJWTsJweEbEpIq 16 | GqblQnBeNrLZ7yS32NAM9jnB9wPsMXPZnAAV/o/U6TTwIO9ChApWX+qer1/mIoc7 17 | 90/XhxEVw6EcXfGPnsLJ85n9FNGbWnLFRxvFAYcD0z6KQYxVHDiUAMSKqAkpENYO 18 | k3gVOw5YNxNpPmUrjw== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /test/test_tcp_logger.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | require "puma/tcp_logger" 4 | 5 | class TestTCPLogger < Minitest::Test 6 | 7 | def setup 8 | @events = Puma::Events.new STDOUT, STDERR 9 | @server = Puma::Server.new nil, @events 10 | 11 | @server.app = proc { |env, socket|} 12 | @server.tcp_mode! 13 | 14 | @socket = nil 15 | end 16 | 17 | def test_events 18 | # in lib/puma/launcher.rb:85 19 | # Puma::Events is default tcp_logger for cluster mode 20 | logger = Puma::Events.new(STDOUT, STDERR) 21 | logger.instance_variable_set(:@stdout, $stdout) # ensure capture_process_io has access to the loggers output 22 | out, err = capture_subprocess_io do 23 | Puma::TCPLogger.new(logger, @server.app).call({}, @socket) 24 | end 25 | assert_match(/connected/, out) 26 | assert_equal('', err) 27 | end 28 | 29 | def test_io 30 | # in lib/puma/configuration.rb:184 31 | # STDOUT is default tcp_logger for single mode 32 | logger = STDOUT 33 | out, err = capture_subprocess_io do 34 | Puma::TCPLogger.new(logger, @server.app).call({}, @socket) 35 | end 36 | assert_match(/connected/, out) 37 | assert_equal('', err) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /examples/puma/client-certs/client_unknown.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDEzCCAfugAwIBAgIBAjANBgkqhkiG9w0BAQUFADBAMRMwEQYKCZImiZPyLGQB 3 | GRYDbmV0MRQwEgYKCZImiZPyLGQBGRYEcHVtYTETMBEGA1UEAwwKY2EtdW5rbm93 4 | bjAgFw0xNDAyMDIyMDA4MjdaGA8yMTE1MDEwOTIwMDgyN1owRDETMBEGCgmSJomT 5 | 8ixkARkWA25ldDEUMBIGCgmSJomT8ixkARkWBHB1bWExFzAVBgNVBAMMDmNsaWVu 6 | dC11bmtub3duMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ewf7nWJ 7 | WcHI3NB+gjz5jWnpNwnU47hjIWq+S41iIymN7FsNdssfzeGb3ZElcQAfofvNf75F 8 | 6mE7YEpbKRU7t/Nptvx+Rd1YGOS2N/PWdj3IcJgvQ2guiU0aYkdB7lC1vlI0QzHT 9 | dte9pGK/ZPp/mvRZGwi9WmwlNhBwOzvdyRuLyi63dmZ8vgyZrfbmGhZYdhCZ77Uv 10 | i+VYqYv5X30I2gQkV6YQMj/AF5Fmt9a4TNGfIjXb6FKmNhlsDMduovrfQMh2umMK 11 | YYQ4A+Vi2yMAZkKeD5cogGLS8wmg76n7miPaKLVU9xTEf55IO+HjIBIqz6VG8qbg 12 | iBV4Lr6BkkaKTQIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCBLAwDQYJKoZIhvcNAQEF 13 | BQADggEBAGGaA7fZLOqxx1wIIay0Ewni3ljzR+RAlpTHAh4x+NilcaQ2ils+JoGH 14 | /DCdX2iD5nevGVm1DANBhfAuFxXGGBjoOLqtg/sO7Rk51IV9WjDVB2rGeH3hoTCk 15 | Qi6Bazdlcvvs3SyFEKcJm2zXizR7O9I+tDv++F6bbaHSBWB6tB9g93pZuMR+smvR 16 | Ll2+/jRGPe1Pif1UFs5DR8QshpvxrIwCmO1vznLhDeA5Pde6CtahGJvi1Y25L1h0 17 | 9l0LjMxxqgVh8h4A5AR8VufCcDiaT8lzCkz4G4jQYFhrJXmBn8Em6NZfdP/LmM9I 18 | 0zEB2Y3lp32ng+WMyaqNh6nfpxEfBoY= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /examples/puma/client-certs/unknown_ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDIDCCAgigAwIBAgIBATANBgkqhkiG9w0BAQUFADBAMRMwEQYKCZImiZPyLGQB 3 | GRYDbmV0MRQwEgYKCZImiZPyLGQBGRYEcHVtYTETMBEGA1UEAwwKY2EtdW5rbm93 4 | bjAgFw0xNDAyMDIyMDA4MjdaGA8yMTE1MDEwOTIwMDgyN1owQDETMBEGCgmSJomT 5 | 8ixkARkWA25ldDEUMBIGCgmSJomT8ixkARkWBHB1bWExEzARBgNVBAMMCmNhLXVu 6 | a25vd24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDlvCM8F2q9vE7d 7 | PMgao53q0U3MIJgNMOw4eZGDfxZrbGM3x5Zws3/7jzngE8BFc/Y+RhC73T/dN6E9 8 | ZT2jfwlRRSUxx8Pq2OUMA9Pb8fj8TAoLGwC6txKaqy/UqKqhVGjQ3FUS3cXBzR85 9 | PGN9mhIB72+ftcWzw0KSNb+pYG8tg+1p6Nb+UlSrjS9/Z0KM8zKnteMG75qhtKnC 10 | rtD6RBiqp98c5r/JJ+LANODaCjtVj5SJTVd/MyshvrNlfYPlMgt+/tU8qSlKzwMa 11 | HcN7KA+oT0blOojaUNJMjgqwCI8QeTP1/DEDfvJvTtzPkaz/ctrmbHzQvLS8Lh6f 12 | KVv32cg9AgMBAAGjIzAhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG 13 | MA0GCSqGSIb3DQEBBQUAA4IBAQCpp/LR62GvtZVrscMVKMfHtlotU67g1nP+FESE 14 | 7nJ5Av4rhaxRFHkF1YdQINyB6mL4fHzDu1g4aLdZmTRjOZcYw6Y2xrJZ/X1lIg29 15 | 7X4s5AlyHJUstWJnk/FrycPBJqZ75b5SJOayaMiAW+fEsQM2wETISkLitQyVlU3V 16 | CtITVjcvgrnsFmnN/qi75EnxxkohZFZGtC2f/NZufYmbpB2FHMt9hhddG7nMawGK 17 | dnpbEAiDiQO757Td3vSfAQN6ahopwe2YbrgirrwMQpScoy5pKdbrhMXTLCuwXZmj 18 | KR6n2WyS0IzminNy1M4FeB4Pq82VH4rFPwl+t6PWjSHaF87V 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /puma.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | version = File.read(File.expand_path("../lib/puma/const.rb", __FILE__))[/VERSION = "(\d+\.\d+\.\d+)"/, 1] || raise 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "puma" 7 | s.version = version 8 | s.authors = ["Evan Phoenix"] 9 | s.description = "Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications. Puma is intended for use in both development and production environments. It's great for highly concurrent Ruby implementations such as Rubinius and JRuby as well as as providing process worker support to support CRuby well." 10 | s.summary = "Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications" 11 | s.email = ["evan@phx.io"] 12 | s.executables = ["puma", "pumactl"] 13 | s.extensions = ["ext/puma_http11/extconf.rb"] 14 | s.add_runtime_dependency "nio4r", "~> 2.0" 15 | s.metadata["msys2_mingw_dependencies"] = "openssl" 16 | s.files = `git ls-files -- bin docs ext lib tools`.split("\n") + 17 | %w[History.md LICENSE README.md] 18 | s.homepage = "http://puma.io" 19 | s.metadata["changelog_uri"] = "https://github.com/puma/puma/blob/master/History.md" 20 | s.license = "BSD-3-Clause" 21 | s.required_ruby_version = Gem::Requirement.new(">= 2.2") 22 | end 23 | -------------------------------------------------------------------------------- /examples/CA/cacert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDxzCCAq+gAwIBAgIBADANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJVUzEO 3 | MAwGA1UECgwFbG9jYWwxDTALBgNVBAsMBGFlcm8xCzAJBgNVBAMMAkNBMB4XDTEy 4 | MDExNDAwMTcyN1oXDTE3MDExMjAwMTcyN1owOTELMAkGA1UEBhMCVVMxDjAMBgNV 5 | BAoMBWxvY2FsMQ0wCwYDVQQLDARhZXJvMQswCQYDVQQDDAJDQTCCASIwDQYJKoZI 6 | hvcNAQEBBQADggEPADCCAQoCggEBAN8bteVsrQxfKHzYuwP1vjH2r6qavPK/agCK 7 | bbPXZmWfUUGjL4ZT4jmnz/B6QNBBKTE/zWcuLXvyRR2FUCi8c5itUvraJVIuBPT/ 8 | lvAZfbyIMpdHG1RPwA6jgTTXm7hnfZc0lCzsFRLk106XrjKeIkZOWffnVLNS2dyH 9 | X51/yZAS8wFyfx58gabC3hvzJLWw/fDSB/qQsOjp5XCCrP30ILPads/P2dEFNZ1M 10 | bjGNErVjmEWEorbUsh6Gu3OyElicVf9hgHspFYNwl1rc5IX7Z5eQM9Yd/Lm1mlvU 11 | iM839ZPn2UOtS9EDdeeZImTSALSUoFJjMdt8+synSDUuGPczUzECAwEAAaOB2TCB 12 | 1jAPBgNVHRMBAf8EBTADAQH/MDEGCWCGSAGG+EIBDQQkFiJSdWJ5L09wZW5TU0wg 13 | R2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBReztszntEK4mwESl/gPjc8 14 | VKU2ljAOBgNVHQ8BAf8EBAMCAQYwYQYDVR0jBFowWIAUXs7bM57RCuJsBEpf4D43 15 | PFSlNpahPaQ7MDkxCzAJBgNVBAYTAlVTMQ4wDAYDVQQKDAVsb2NhbDENMAsGA1UE 16 | CwwEYWVybzELMAkGA1UEAwwCQ0GCAQAwDQYJKoZIhvcNAQEFBQADggEBABC6pRY+ 17 | c+MKGG6hWv9FKTW5drw/9bfKxl+dVcKPP5YWuoAMtStkCVnDleQ7K2oN4o7kwr7Q 18 | cU3mmYJZjqRu43JBebzupBGKqe/mNWGN0EuCMT7khFEXbO3bwpcL0fhCO7+RZccx 19 | GF/LKglLgQSE+/SKOHlHdJZlS3EgPghrtoSiptx9ytXzkgCoEKypbAEmcArWvzzF 20 | 81ZYjkLAwCrrB/qNAKnI0AKXMCiqnZu+8a16p5z+HGCjpTLB3NQ3YlyFF0jbr/ow 21 | R1Fb07t0uO2o22nuua+iK8lKqWLE6eQUIu/YB6DMEgjk+D6of+WQ+38GC35QyPKA 22 | 9nQ8kMf2RkiGN6M= 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | ## Plugins 2 | 3 | Puma 3.0 added support for plugins that can augment configuration and service operations. 4 | 5 | 2 canonical plugins to look to aid in development of further plugins: 6 | 7 | * [tmp\_restart](https://github.com/puma/puma/blob/master/lib/puma/plugin/tmp_restart.rb): Restarts the server if the file `tmp/restart.txt` is touched 8 | * [heroku](https://github.com/puma/puma-heroku/blob/master/lib/puma/plugin/heroku.rb): Packages up the default configuration used by puma on Heroku 9 | 10 | Plugins are activated in a puma configuration file (such as `config/puma.rb'`) by adding `plugin "name"`, such as `plugin "heroku"`. 11 | 12 | Plugins are activated based simply on path requirements so, activating the `heroku` plugin will simply be doing `require "puma/plugin/heroku"`. This allows gems to provide multiple plugins (as well as unrelated gems to provide puma plugins). 13 | 14 | The `tmp_restart` plugin is bundled with puma, so it can always be used. 15 | 16 | To use the `heroku` plugin, add `puma-heroku` to your Gemfile or install it. 17 | 18 | ### API 19 | 20 | At present, there are 2 hooks that plugins can use: `start` and `config`. 21 | 22 | `start` runs when the server has started and allows the plugin to start other functionality to augment puma. 23 | 24 | `config` runs when the server is being configured and is passed a `Puma::DSL` object that can be used to add additional configuration. 25 | 26 | Any public methods in `Puma::Plugin` are the public API that any plugin may use. 27 | 28 | In the future, more hooks and APIs will be added. 29 | -------------------------------------------------------------------------------- /examples/plugins/redis_stop_puma.rb: -------------------------------------------------------------------------------- 1 | require 'puma/plugin' 2 | require 'redis' 3 | 4 | # How to stop Puma on Heroku 5 | # - You can't use normal methods because the dyno is not accessible 6 | # - There's no file system, no way to send signals 7 | # but ... 8 | # - You can use Redis or Memcache; any network distributed key-value 9 | # store 10 | 11 | # 1. Add this plugin to your 'lib' directory 12 | # 2. In the `puma.rb` config file add the following lines 13 | # === Plugins === 14 | # require './lib/puma/plugin/redis_stop_puma' 15 | # plugin 'redis_stop_puma' 16 | # 3. Now, when you set the redis key "puma::restart::web.1", your web.1 dyno 17 | # will restart 18 | # 4. Sniffing the Heroku logs for R14 errors is application (and configuration) 19 | # specific. I use the Logentries service, watch for the pattern and the call 20 | # a webhook back into my app to set the Redis key. YMMV 21 | 22 | # You can test this locally by setting the DYNO environment variable when 23 | # when starting puma, e.g. `DYNO=pants.1 puma` 24 | 25 | Puma::Plugin.create do 26 | def start(launcher) 27 | 28 | hostname = ENV['DYNO'] 29 | return unless hostname 30 | 31 | redis = Redis.new(url: ENV.fetch('REDIS_URL', nil)) 32 | return unless redis.ping == 'PONG' 33 | 34 | in_background do 35 | while true 36 | sleep 2 37 | if message = redis.get("puma::restart::#{hostname}") 38 | redis.del("puma::restart::#{hostname}") 39 | $stderr.puts message 40 | launcher.stop 41 | break 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Some code copyright (c) 2005, Zed Shaw 2 | Copyright (c) 2011, Evan Phoenix 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name of the Evan Phoenix nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | inherit_from: "./.rubocop.yml" 2 | 3 | # 29 offenses 4 | Layout/SpaceAroundOperators: 5 | Enabled: true 6 | 7 | # 21 offenses 8 | Layout/SpaceInsideBlockBraces: 9 | Enabled: true 10 | 11 | # 16 offenses 12 | Layout/SpaceAroundEqualsInParameterDefault: 13 | Enabled: true 14 | EnforcedStyle: no_space 15 | 16 | # 15 offenses 17 | Layout/SpaceInsideHashLiteralBraces: 18 | Enabled: true 19 | EnforcedStyle: no_space 20 | 21 | # 8 offenses 22 | Layout/EmptyLines: 23 | Enabled: true 24 | 25 | # 4 offenses 26 | Layout/EmptyLinesAroundClassBody: 27 | Enabled: true 28 | Exclude: 29 | - 'test/**/*' 30 | 31 | # 6 offenses 32 | Layout/EmptyLinesAroundMethodBody: 33 | Enabled: true 34 | 35 | # 5 offenses 36 | Layout/EmptyLinesAroundModuleBody: 37 | Enabled: true 38 | 39 | # 5 offenses 40 | Layout/IndentationWidth: 41 | Enabled: true 42 | 43 | # 3 offenses 44 | Layout/AccessModifierIndentation: 45 | EnforcedStyle: indent 46 | 47 | # 2 offenses 48 | Style/WhileUntilModifier: 49 | Enabled: true 50 | 51 | # 1 offense 52 | Style/TernaryParentheses: 53 | Enabled: true 54 | 55 | # >200 offenses for 80 56 | # 58 offenses for 100 57 | # 18 offenses for 120 58 | Metrics/LineLength: 59 | Max: 120 60 | AllowHeredoc: true 61 | AllowURI: true 62 | URISchemes: 63 | - http 64 | - https 65 | IgnoreCopDirectives: false 66 | IgnoredPatterns: [] 67 | 68 | # 1 offense 69 | Metrics/ParameterLists: 70 | Max: 5 71 | 72 | # 1 offense 73 | Performance/RedundantMatch: 74 | Enabled: true 75 | 76 | # 1 offense 77 | Performance/RedundantBlockCall: 78 | Enabled: true 79 | 80 | # 1 offense 81 | Performance/StringReplacement: 82 | Enabled: true -------------------------------------------------------------------------------- /ext/puma_http11/http11_parser.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2005 Zed A. Shaw 3 | * You can redistribute it and/or modify it under the same terms as Ruby. 4 | * License 3-clause BSD 5 | */ 6 | 7 | #ifndef http11_parser_h 8 | #define http11_parser_h 9 | 10 | #define RSTRING_NOT_MODIFIED 1 11 | #include "ruby.h" 12 | 13 | #include 14 | 15 | #if defined(_WIN32) 16 | #include 17 | #endif 18 | 19 | #define BUFFER_LEN 1024 20 | 21 | struct puma_parser; 22 | 23 | typedef void (*element_cb)(struct puma_parser* hp, 24 | const char *at, size_t length); 25 | 26 | typedef void (*field_cb)(struct puma_parser* hp, 27 | const char *field, size_t flen, 28 | const char *value, size_t vlen); 29 | 30 | typedef struct puma_parser { 31 | int cs; 32 | size_t body_start; 33 | int content_len; 34 | size_t nread; 35 | size_t mark; 36 | size_t field_start; 37 | size_t field_len; 38 | size_t query_start; 39 | 40 | VALUE request; 41 | VALUE body; 42 | 43 | field_cb http_field; 44 | element_cb request_method; 45 | element_cb request_uri; 46 | element_cb fragment; 47 | element_cb request_path; 48 | element_cb query_string; 49 | element_cb http_version; 50 | element_cb header_done; 51 | 52 | char buf[BUFFER_LEN]; 53 | 54 | } puma_parser; 55 | 56 | int puma_parser_init(puma_parser *parser); 57 | int puma_parser_finish(puma_parser *parser); 58 | size_t puma_parser_execute(puma_parser *parser, const char *data, 59 | size_t len, size_t off); 60 | int puma_parser_has_error(puma_parser *parser); 61 | int puma_parser_is_finished(puma_parser *parser); 62 | 63 | #define puma_parser_nread(parser) (parser)->nread 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /examples/puma/client-certs/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA3S7z+F4ZuSgUMOzHq6J0ToMo++Mg+dk8XNEIyTOs2Ah6r+F8 3 | Ai0gkA7SbxhmK+RRZ4Te63iJtIv45VO8tFYMv8vvfosgtTGsPZ+OgocctJUhRaXc 4 | fRWNgMTZEKJWS/E/iwCjsRh5xl6xO1vtbsN0CMfO0VkiX7BP6uIyXENGZjHCw49C 5 | 0vT/AlkXzS5OkLb1Xjs1j0hN38D7XExuU8C5Ao7e+pnhirTuY78opJt6mjTu/uMy 6 | E05Id/xUEWVPPMbkAebFlRbSqMKQkFXw3UyJP1wiTzVm5Ko5m7cD5oMCU5g8l7ZZ 7 | WkayQKZ70eQlodN4/zu1BSwupGwuCr8BeHlHnwIDAQABAoIBABjXyj1OTHNYhhQM 8 | tEyZ3Zhn8PWByFVnyfje3a7DqBlHsogIuoYADZVApPAnfGpXpbEL4oHuMwFda2JO 9 | qnZS5/Gu9UJwXAceAiuVvUr55AaAbZFGFOLTxeX9tifBJBI5kZqKQtiEWEEop510 10 | MNHtEB5gWuF2sn6u7fsC1wc34zNdE/4hR1njsppIlJ1GP0ICQUI/YKybipF6+pI7 11 | +vg7bF5DU93/uUoRY83NjREeAswFE67OOHi592YIyr5TLu06NDqJ2EBneXyO63Q7 12 | pAakX84SI7k8p8t9XBW6D56pT23JYcrDXwayH1P+SAA4FdDhPlZlGPWjXqQc0biA 13 | l1HlfiECgYEA8bwVsukWSGE+XdRnilVATP4zTS7ZpcYzUufS9+pa5w0sVIkDScgL 14 | 3YrB0rY7BS/kImOg+xrFz7ILCoIyjDCEVtly0Hc1aZw9SWu545lfFWcd7s7nN5nQ 15 | iM9jGxoAWu/VT2GKIfhxMK89CzLD1DGgqsiyYxmPMyplekupUEkkiHECgYEA6jxl 16 | uNddzzfKZKHEWS1Tax0hgchOaVMyML35ySz5kgw1tSO8G64eKtrSCq7wcSvb4uc3 17 | hz1yl5Ydxqa+1qX5Qi+UcoRhZZHqGTsbid1aiQJKltk4ImtlptBbX+NTvEDIDblQ 18 | fzse/a+upesutwaTchlXtuPG2F53UZeQ831GWQ8CgYAqKdtDDILVdxiwtwakS0Be 19 | 7Yu3L6/IyWxUTpkuotLeMB8GU6ueJ+Vh6/zoqt5ahkLteKEwizfrhSuF1rXIXAIJ 20 | P/5VvCU12YmbD84pk6vRCN5gs/gCa7LC2iF4La3YLrLvGJ1GVZYwnrAwDte3YDyc 21 | 7UqoHGIs031FuoK6vTdBEQKBgHG5bz3mOqKgGMDxFY6ihgzMcPc9FGzousaVhhAZ 22 | qPYyvWS7+9mImRb/dNlBBHY98B1jWz9rIxbcCIrpbGB05ucuiKltAoi45mrnmsA9 23 | 23YHycUho7J6aDkskiClE4OkBD09iwqq3qoWwPnHjL/KDo5oJYEjZ+inPNE9gF/n 24 | o98bAoGBAMLZ7BYOXU1svCwuEz9RdAyXsrOX+Z9DW9i6WMVlfk9K1IxwpXYCvOjO 25 | J+wJuQtuNbwKqNPw1DUEp/25cDVoekRAxaKgYGlJFib8vEGtbQ7GQK9bDA11/sIz 26 | PQSfc92Y4+qpQ9WzhsZXip49itzBFgmN7/4eaohpvyHCFkVCZVpf 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/puma/client-certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAytoqHo4najyc4RoGIfTXPwg/IJ/7xanIVC75hu/ekdNUcdF5 3 | Ob+zoFrOBjIbyfKOLKAsU5gAsj0rXXpg5aHHc2X9ncn79WpVVXwQcKkrInpIgeyI 4 | kr6coObd4fF6liXCrT9NLTxGJgJFbjG1UsUD67BIueO5Mbu88oldEWr2mWzg4MZL 5 | qUoxK9Ax2tYe6ZIqed3pM/656HHlXL0pwnTmoVFY+mcDJDkLWsv2+u+OrbW30Idc 6 | oG6k21f5XFfXq5XZQ5KihtIEOoMWgXGyIqJtafqIbgnKEwxmbuN9SjlOmcoGxsWi 7 | BGWPWW8oPQ6fqOmcsne1E2P9CXAllKuzv12jeQIDAQABAoIBABBL4JBd2TrGrc/D 8 | uHRn6BbvQasMTzy8/BQPRgqaIKZUdPdD3dpO1U5vnReQVP0vWE6re4QntP6cvWwg 9 | FcK88XoK2oofnPdFWJ+qfOOgI4/8hPCzIPGxEII4qeCp9rAzTmV+rWOR8QzCp/NH 10 | WQrSOxNnMSCF8+3T6EUP1gM9NZxzpycvoa3Xk4QduPdSN9+ifLOohvRwq0PsP8z5 11 | 6tPIxEyHTmUjJhDh0en2fXFs6ncxvzQJ+p8R3cSaACDDmAR3uuKgpinC7zLKRyIB 12 | rqThVMOO+yxMYNNZ+JIJbuaAAAQy1znPfPsy8syFEvOBhZXJNLEpTqr7n4kHvxpW 13 | MI8ukTUCgYEA6zCSMEb9lH/z/qgJPYbZcUuT/M9/EJdiDlazfYrzAHsmwL/FRXjc 14 | vAJCeayy3A/oMBWJ+tQrRCPb0e/LU2kqLJRWENKN7uTAHGntDJOtM94ZWDLcAySv 15 | zo6usr7BhLmP8ySVojjbWoWI4+SHONYcxsk1v5O7f0ZbzMoDoQPPcl8CgYEA3M0Y 16 | l8mDcPlm90r0/CKq5egpzWvb6dvz5Sly83bJIK1CnjyZUbmQZSO2fp9fFFffZ3SG 17 | tbgDJ5xQ5Ie+H2mTCsCqkIRqi8tCnbHCXcN40N3SXxcS4e4UcMhVCAHrGODqHrAb 18 | if8uTxwozxZtYklaZwhszdtY0lWRG2BzILfOKScCgYBOjyvVqnDboJ3cyz5C6f9J 19 | 48fr41d7MEXVqkpMPhSLbZd1PNllKkj5F/wibnhUH5AcN6WePi6xlRTBHEsbcn5e 20 | 47GX7uzwBkLReuRulgl90MtAdcSd3CxJX8mk9Sjo757QxcChrkI/C2m9TcGJT6PP 21 | Fri4ZF111wek8TmjGAW8GwKBgDhuuvBgcpW3SJe/sqmWerNUCQsVnBlDPCy/0T9k 22 | hrcxUSt8NXtrv/n5jLUEKpracqDQaXWcWEIRc6NVBkSlCQ3gfDd/gHPGOXpwakro 23 | oMJRT2k6TnssDFFfAkyPoPS012GMhR1Z+Q4DFnMHOmG6eb6HqrdabnMjp3ilyAb+ 24 | s1RVAoGAQCGfhL3j9ShiTlpbOcL6CdERk4Jzw7mD4g6gVvyKLJWwACl5Y7YgVcfU 25 | Bsm9c3GM2OkAAHDlYd8oBvaWArI5eN93zLgD4uU/Bm08SKpQqOOghqrFQy3B9Ngr 26 | eEgVYYvmHikJfcUzOYfotRdH4APGt8EAL2007oyox7Yucv5pzNA= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/puma/client-certs/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAxpxSjazY/abxyCvolH9VKAydDu4aY45+0FneucZx1jfWBfUX 3 | wVUcYOxy3D5bOhi+HPnuSkD9VtKOIEs8G1HN2vy8qpExAHZYoBI105EGOrQp/+dQ 4 | rgpcp63kPfMHYEK2ss6GyoVjACxuyES62qa2ZokCacVjhXr3e1pBd9gMEpJWTBHV 5 | nC3SN+rME1dFM7kfSA1JyIeRhj24pBf+oUNRNfqy/7GZIXM2DRJ4iRpNqQ9zKiYL 6 | k72ExduJNShDleZKErgD58peBP3V/YNwCB1qPDoalbP+D4a1+vvqpq7Fc/SyLz8N 7 | /kjABN0FOPSe7gADUUDAZcVoZWyKXhYvMzYoOwIDAQABAoIBAA7C+aPMEAiyStAs 8 | 60l2OVcTsOy2J8H0ilpkA5jdNgLM/ZxNvilBcS2HBXZ3MAKeairvLJXaRLoaRjQC 9 | Q4JoTxuSo1cuGW1GXonvMI77/XGJiIGbqLR20rIny4oLMSYnbzrU/NG6nkQaCVXb 10 | PeQYdgAi+MnxwNbf79r8N1d3+FW85vjczo2aobWnJpir8U+xp5pe4xpqP8nddP7v 11 | tdfIku8HBt76pu4ZfZynO6z9C+ZKS1s7YmBkGNuE46kwl9dhdmZGawAHxNYNAAJS 12 | FPLHR11f3syjtPUUm3MAr5BlCFd6vgWYJFrdKv8/uH++WAiVvApnG/FjPh90i42p 13 | muGHJbECgYEA+eiHPNzlCwYpPzY+n/AfBQ48G7sYeHk/yEDvhkJOnwAQEQMMw1s9 14 | AGHFTaKa2rb6fruZp8qCXiuzhWq2x7e5+W2Buj+VU15fI1JCJW1s2GdlGOAclvEW 15 | HvhcmlwtmSOWgTv1bbVSgQZB9hjbVK+81yQ9hn6AxJUT9k1IU3HALGkCgYEAy3Ow 16 | DBX97AVn+1h4I/7cTqzjjLCaBn4UVcy0s3hsbvW/b+aYb8a3F4wJ7mWXvFk9Lb4h 17 | uMfka6DYHszma1Bp7BEr63QIAhf936PXAljgtBBCron+9Y6DD83mz/9sX/MTo/pE 18 | 2J/qHOqYwoboDoscgtVXZxNM3UX70RJ6RBKAKwMCgYEAxpX+kWC/KWl18WM7lICN 19 | Rckv/qFIKsO+6XSgYcHjE/pKyhnwVHT2Ho2S6cRi5ZYtq/OLgIgt3INBnq1UHZRj 20 | 1k8snUHVeXAujbTaFz/DFJvk/EVqso9Vkrqta4QAQAbFnGB3APzrWNgOJm9OKxeT 21 | KisEMRHpZU1JlZmH9bcYjLECgYAPb1tvz0tQWKim3PNgZ7l3Do7E4bENxQrt53Xe 22 | F8jCMkqvxqLR+BVz59/pAjQcyfhmPAJ67k9aCv3aeFkS0yr2Ced3GXpyDjfoe5mY 23 | R/3kK0ejzjxVjNZMoKZeKVajgOGAk0Ad3yP3xaSJPYrlb5BeLKlQ3Jn8P473MZut 24 | BmpK2QKBgQCr7aaFL5Ypv21kBKVI478jk7v/6PcYOztkFbOsje5A4SlkhlaE5u0h 25 | iK0jON8MnAieLeP5QvyXy5n6wL/6THUSxpm3ZJRXpNgKHqENJrZBh3HpmrtzXjxF 26 | WLMGl20yrsNUE8WR8wAJ5ECxwUPGZazDY9CD6C0Vm0LUWYdgZQnjnA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/puma/client-certs/client_expired.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA2UElXyOCjcLrP/vYBPUcMPnHocc8PNHzYuLZq7kgKYrUyJNb 3 | ta9a2FXsZAZFU7ZehYP2jQAHbCOGgE2p8ho9JskahDMWDLfEbcwGB2OmRCQZuFlA 4 | GuOFu1jRtmQP3tGwg6uEp/oZTycNHuIkIpiaovPoorYMk3YnEyg0fvZE0t1v99z2 5 | aksKqpzeeZvZ/QORSCsHfH2z7DvJOsSIRUtrCymdk5AGCzV7trxRiUe29+1PJmHB 6 | zmwdae1O1mCdiQn9tVmTguIiNuV5OzBUlGD6m1v4lfuNRHLJWx8AGRxI6njcpyjZ 7 | Z2aJFa74jhXhax6l21SqeN9Nuy90Wa/yV38TJQIDAQABAoIBAQDOhvSc7aflVZ/H 8 | koT3qX8kO78AVuM3uiqSHa7pZTJi63x+ND9hhxJoR75SE/gBrYNLj3ho79cegOMS 9 | w0HEShdJ8LFJbTsP2f5cljBBBAUCEAN3UTj0lsgBolyx84t2uYYAlaOk/8bhjPEX 10 | I8lQLhwKvq2vSDrKT+6zcmv9KeWhQWoQavq+QAkTVO9gAqmlkdMEjZntT8hau/qC 11 | jkgW7MG/U/CkbALcrbhAWBtMSvUDSKHrrva7XPaMq5nDvX0Wj6PZhY9KaaweR8ZR 12 | xfrgzbFfRSKdbT5dD7IcwQhjV51hev6+q8pIFgTiFimeNq4TvKgH5MMwixBnVM+3 13 | djBTB4+dAoGBAO5BYdbpEuVDlwMfHo//R/BJEGJn9dwc3ZpBmJ6vQGmLGjf/oXBr 14 | 9tDf/yZKDLwVAgnRdVkllMxpEWrFjD3OpnukbvzTijBi0AQAljRSwNlMTsLwAifi 15 | EBXvENFG/7iJKssCQBD6rkeNir3VRlMa5khI9jHahZ0B53RtQCYYDzZ/AoGBAOlv 16 | W005wD3g9K2P5BIo+qXB43ZFFsAFOnhu7jUyTciu/95iJ+zw/AGHI/JhNy+jSHnw 17 | ZCARLy1c2CImAshadYuWDqR5okR+xHHj3Lgf9ig7lbSf+skn3R4y6fTlxxNdbbU7 18 | dbZbiMm5CyUHTR6957BQaS7mfQZJG0OP5G9fl0xbAoGAOSNa+HRbALqN68S5yqTZ 19 | Nsn+8OqnrssJZiYXGO9Ejks61XUr3U83GO6vPRqDJVQQchRWhTObFM6Zy7ZmpKf7 20 | iylrKJz+xg3cfyk43IGAGFzRgrSWf8QaQXhc2yOgzjuvFJKMlMXZp/VM8avFOsb3 21 | tRwyVtBmPLopLOXKfZhFhbcCgYA/etbbU18h9LDVGhItlhNDTEys9vDO2x0hbxk8 22 | QifA8UYHla3B027Ug4mU+jblr4OgFW1FAydPMLZd4vRSw7a/dNkahTFJayfEyPBW 23 | 6eoo2rtFWVP7q+mHstTIkkvmyjtxU3AZXR7/rGCJe0jPmVkOK2/PH0LUmMDfSJwY 24 | ZWhhjQKBgDCB823bmF6+7J0mtNFFKvRMz6k0wKz7Qe6+AkwmyR3v9IBpL4UMFgIq 25 | xdRR7iGhlRHaWVZyzG3WQ1ZgLmVUsfmk9OrD5PfhKaElKvaRr8e+MHOesQ6AgWW2 26 | YXr6vgr6tykVtjG4/v98r05+9q10HH0xOhbuBz+1P7IyLfTCWxbE 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/puma/client-certs/unknown_ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA5bwjPBdqvbxO3TzIGqOd6tFNzCCYDTDsOHmRg38Wa2xjN8eW 3 | cLN/+4854BPARXP2PkYQu90/3TehPWU9o38JUUUlMcfD6tjlDAPT2/H4/EwKCxsA 4 | urcSmqsv1KiqoVRo0NxVEt3Fwc0fOTxjfZoSAe9vn7XFs8NCkjW/qWBvLYPtaejW 5 | /lJUq40vf2dCjPMyp7XjBu+aobSpwq7Q+kQYqqffHOa/ySfiwDTg2go7VY+UiU1X 6 | fzMrIb6zZX2D5TILfv7VPKkpSs8DGh3DeygPqE9G5TqI2lDSTI4KsAiPEHkz9fwx 7 | A37yb07cz5Gs/3La5mx80Ly0vC4enylb99nIPQIDAQABAoIBAQCbxT6K30HkFsvO 8 | nQj9bxWDg5nhn/QZdaOmA2AULlbwTdTUnIM4Na3Az3OpqRrEvQUpYm60Qyergq3U 9 | qFHsCxYxQdYfc9k24wwjYnEDgIWX5KMmto9/CuUVdJ+A7UCNFWPgwpT4ruEJMGFM 10 | eNLo9k/heg1Q2HqOEgaQhttHKHkZ/UJaR6XXeucBfJtXSIWf42omeDRNhlwsQ+LY 11 | WbTv3XmiFbu1Bhkk67xlpyuGEL1g9Auz9P2z+2Q2LV66kNhfInIFtUYFeNpkjgUJ 12 | TtDRU7UBOm+YPMjDfjVUzPbzvCVAtxG4t0ZSJJcQF3N4+HfpoL1c33CCqYwkh1KI 13 | xJi1CgjtAoGBAPR3u8OovMmSJ6tdee0WyTahYmK6VOtmSm4IJf1t/wUvf/u6X/Q6 14 | U06TxUAiAs8rMwvvtgPeLYxtaEKO0PSD0rHNL1MHnBwYAmLvAFyCc5tuuyb3ZIyg 15 | 1oAz/hW5bYgAL32nmDrlwq0W+KU478SRWWYZA2raO3Ha08I1YVbgkSHPAoGBAPCS 16 | fIexjEPxeyZJe4+iKQcmWW42HA9rWIpu/9FDZxvn+PpWhBwMzlxjTlQS2V3nMSHp 17 | Jtzyj+Y2R1SO8OQoqZ6s7G+cv8Ni+FqOidcUPUH6aDc2A0ihb0FANp4FQsrv7riP 18 | W12mWfniTxZri+nKAwpXjEjko8yi05go3y0dTjQzAoGAJq+n6/+Q2Ikjc+/X8pfv 19 | gZCqZBs+gv3t+1mYwXEdsTFiHHDS7HAqbL3fshVvwl8AtfvaHuSS6q0JmbbGBFu0 20 | BOUGfyouHxgBkKxnrzwJlWhBf5oYtFRjfWg85i0w0xvMaCMUaQWg+AkxkdvfvYiO 21 | 0CRXMRqV25+YcRxHahshfGsCgYEAus9Vql1B6YS8N4f6ThgDOg0ahw23jnWyJJV7 22 | SznG+JGS8np6TfnXyUBIE9srNdMQgR+20P3+piriCxSQlOvKg3AOjcEv2/6fklp7 23 | SSvrQa+8e5sSw7SwWwANKXo2WrYkLucLcNZ7qiKFfYh39kyrPb2sLvJ1C7QpEVAz 24 | tam7D6cCgYBdqh+SlwryiX351eh+tLOlysAPJ1cb3JotnZY1WYKfR9r5PlJsisut 25 | dcjOOaz5/Uo/UlVKOjOxxUuB8FIIJGPvx6lo0hq8ornS0CdqhDspUx4aAD0iZ6+y 26 | iccYnG0CLW3HpS4B1B7a8ktXW59m+tT9Fl+usOmwdPNIzcdAMqff+A== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/puma/client-certs/client_unknown.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA0Ewf7nWJWcHI3NB+gjz5jWnpNwnU47hjIWq+S41iIymN7FsN 3 | dssfzeGb3ZElcQAfofvNf75F6mE7YEpbKRU7t/Nptvx+Rd1YGOS2N/PWdj3IcJgv 4 | Q2guiU0aYkdB7lC1vlI0QzHTdte9pGK/ZPp/mvRZGwi9WmwlNhBwOzvdyRuLyi63 5 | dmZ8vgyZrfbmGhZYdhCZ77Uvi+VYqYv5X30I2gQkV6YQMj/AF5Fmt9a4TNGfIjXb 6 | 6FKmNhlsDMduovrfQMh2umMKYYQ4A+Vi2yMAZkKeD5cogGLS8wmg76n7miPaKLVU 7 | 9xTEf55IO+HjIBIqz6VG8qbgiBV4Lr6BkkaKTQIDAQABAoIBAC41pSPWqWjjJ7ds 8 | /ZPRCR/JLjbKlJMMVdmU/7BtJidc0aJstLj06RJYiaaGy8Kc32elH/rF8GbFuVFs 9 | TXr4ve3aL0qsCytepmunWZFiI+LJZA0uhdWzaBeHpmHFIyhGeXtGa1e41wvXYrf0 10 | PDefpu1uZdIshy1nLn4m+W76ogI6FrcyUQ+XRN6f5x6af70Kfi414qf9NLOvajnl 11 | JCx90WpdwBp1jC8totKDp9kvILCymFnGBl93NUl8F3Tz7zLkh2SIhaKxQIHTc9g0 12 | LPNkd1Q5tIl/rG3lZ6vw2/UlCVB1NdLsItlJVIJhu6ChIrSXeoDg0GNlJFp7op6W 13 | jlJDoRUCgYEA67Nuzrv/lkVljRFDVw97DTiCbLL7SDXDH6Jf43+Bz6prGLqd4Nfd 14 | LTO7AdLGd5UTWB1Oj2U94pFXNFkucGv65ck6QAPZ4UrCk9lyjx/dN7d6CxUBDeu2 15 | 4zPeHy/mAgkVmKSzEy5L2ERwoQqK8A/g6HtuThB87gtKUpnG+QSfz6sCgYEA4jyE 16 | kPRudqne0VXWL/Mhnrls2PNo4MDIOxUp+KcFLGY/44RIEoit3WLRDXULAgT3U20Z 17 | lY7nQJyU+/CaEH6rIpADp6VRPA0XJo7HCuMGEO7bk8AAXkXTplVs+XbsD0221AKl 18 | GRpS0CtcvHllHiwG8iL+zHAJzL4wbNY36L97decCgYB0UuHk9bN2Hlm3/UUWunUo 19 | WTNFIjARuzbJbgGU7WDLdHfWhINWbDKkFFu+0p9QdSpO2mfjLTwVjVVUaI8avK/e 20 | qCkvXrcxEQxmm3KGYFt1HAAHaB5VGHfyOa7uBV2ms4UNCHu4g6i620warnFTeQKu 21 | ufv+WvTNJpVPnsUsMLQOcQKBgQDcfgDxydjTPDIWseLjrsGIkc29EFaaHinIM5NJ 22 | bXbEVA9WbflUXvOc/g8jX3xQBokKPR2fPryxoyos9c0h4GJoeBWn0Z5/uX5jrOne 23 | +W5TGIjW0l1JhCKITV+9LqNZMvPKY52G/rnRe0GRy3q60kwet+6/Tz6t1nsZyBqL 24 | c/we5wKBgQDc0rE459diZTpxKzumgUGlutKWhqPDGO+NwMa8xaaPvn/k6bg5avx2 25 | 8by3BSWhc/YEK7qtVcO1sDr7m9dHtqxrk8+2CC+ZI6wfc359xB9uImrbs9Jqz3VZ 26 | +Ji2VOirgm/oZNzhpi2l7yG2atXg0PqLMkS/ft6gWyAjzy/Q8WDSjQ== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tools/jungle/rc.d/puma: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | 4 | # PROVIDE: puma 5 | 6 | . /etc/rc.subr 7 | 8 | name="puma" 9 | start_cmd="puma_start" 10 | stop_cmd="puma_stop" 11 | restart_cmd="puma_restart" 12 | rcvar=puma_enable 13 | required_files=/usr/local/etc/puma.conf 14 | 15 | puma_start() 16 | { 17 | server_count=$(/usr/local/bin/jq ".servers[] .ruby_env" /usr/local/etc/puma.conf | wc -l) 18 | i=0 19 | while [ "$i" -lt "$server_count" ]; do 20 | rb_env=$(/usr/local/bin/jq -r ".servers[$i].ruby_env" /usr/local/etc/puma.conf) 21 | dir=$(/usr/local/bin/jq -r ".servers[$i].dir" /usr/local/etc/puma.conf) 22 | user=$(/usr/local/bin/jq -r ".servers[$i].user" /usr/local/etc/puma.conf) 23 | rb_ver=$(/usr/local/bin/jq -r ".servers[$i].ruby_version" /usr/local/etc/puma.conf) 24 | case $rb_env in 25 | "rbenv") 26 | su - $user -c "cd $dir && rbenv shell $rb_ver && bundle exec puma -C $dir/config/puma.rb -d" 27 | ;; 28 | *) 29 | ;; 30 | esac 31 | i=$(( i + 1 )) 32 | done 33 | } 34 | 35 | puma_stop() 36 | { 37 | pkill ruby 38 | } 39 | 40 | puma_restart() 41 | { 42 | server_count=$(/usr/local/bin/jq ".servers[] .ruby_env" /usr/local/etc/puma.conf | wc -l) 43 | i=0 44 | while [ "$i" -lt "$server_count" ]; do 45 | rb_env=$(/usr/local/bin/jq -r ".servers[$i].ruby_env" /usr/local/etc/puma.conf) 46 | dir=$(/usr/local/bin/jq -r ".servers[$i].dir" /usr/local/etc/puma.conf) 47 | user=$(/usr/local/bin/jq -r ".servers[$i].user" /usr/local/etc/puma.conf) 48 | rb_ver=$(/usr/local/bin/jq -r ".servers[$i].ruby_version" /usr/local/etc/puma.conf) 49 | case $rb_env in 50 | "rbenv") 51 | su - $user -c "cd $dir && pkill ruby && rbenv shell $ruby_version && bundle exec puma -C $dir/config/puma.rb -d" 52 | ;; 53 | *) 54 | ;; 55 | esac 56 | i=$(( i + 1 )) 57 | done 58 | } 59 | 60 | load_rc_config $name 61 | run_rc_command "$1" 62 | -------------------------------------------------------------------------------- /examples/CA/private/cakeypair.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,8FE374A296255ED1 4 | 5 | g6YSW6TUA/9dSscxCWPm11bG6DedWJ6fanU6V7O2n9WbGOLE0ogz877D/5gPr94+ 6 | WJHnCb0O4gyKQA307XA9nq+HAPTyJFKroEz1CPXVrITV8AO+vJ/PUc1y1LQ1ymMk 7 | fcvI3ZNdbDBr7OL7luYch7qoVULJ4kwJTU7WT9XzINiSnS3Ccqh6ZEPFyKIcxP2s 8 | 11WkpxdDJ911nCXVUoa9Hd5tQk7mHZuf7XL01up08SDobx/imaU9VN8QQG6AFE55 9 | jVtfv7MxP+9gHmHQxuhYuDMnu5GIwuJPFHvI7Gi9jcwvee/GhcKBnKdpFc92fJJ8 10 | +TIqR2D21EHDBoep1fMGgbPOl+9z1hdE78Sj6tHwjeRF93mhJWyYNQWQ5ViKLnoF 11 | j11idWOXwkOCFttRBMd74QG6GyxTvs8FNDOXmm361Muk94a4fbKRJvKvYZlBnYKu 12 | fOmJNFf2zEVVHjBCbvM4swAT09cWLxRMRTiFb5y7QAEmtFO4WLavlmnNCdMq/uC4 13 | CpFqGtoiaCimunjTfvkBaJngSfTYSrd4cStnx/c0XK++dni+bLXUHOyMxvihl5vn 14 | SiFlzWTmoWf1gxNZgOSKY432R6T1CQXfnAd3x/FCJjfPqFt+RAFXjlVFNA0FZyVE 15 | sCxhVx1eZsr7aMJ5H9RehUr6b9swUEm4UGX5H3/GG7GNCZU+fA+Wfi9cl1zqJFey 16 | Ho5UjjmRgdV1qapioqCd+Ce/mG0LxRPt/hYdA6G5h4zheRc3KZ7YbIwWRwlkm2w5 17 | is4ToZKwheycaaQnUfOdHUTtZ4Kv0kRof+LMcDUDTrsydWF4T4xGxGD7/CVJkH1G 18 | 5OTVsfv6Tw7kEMYaXYBQPs0u3GSxY3CZ+k5wATr9PBBYcArSkt5WNQYCJfO/MnWF 19 | z/31hp/ziCIoesgo6uZMO4Dr5Pka54nc4O4KOblvUUMX07WkYGrc4nxBGvhQ5Jl4 20 | A8dJBPCK3OlsVCnHYrDQ0cemhLOYPuiyKTtCUIs2nHuiM4RwoCRJgsVBUnKK+tTx 21 | AkM9uQvYsrZ/DoBooBdXJQy3uiHH86zEskiy72H8Wgcu8GbLt2JgCyhXkwDzrIRf 22 | hnAN4FS2VNOt5dDTVHBWG1vIxxlM2+LrYpY/QqihNgotZ+C4VWHkoDwbF478JgxM 23 | 5Yk+0X9kGvLQbZCJFXdAKAyr/AzRH+Hx1cDvSi7gypf8qOEZwD1rq7f0qw8jnqfG 24 | 3QIFoN1/+xTAV8lTlGhvbQYz1XHVBH9l7TSQDLIrnwHTIv+PdZbTveGftCCnLdDo 25 | wBLBnw4mKVCtnHrEgXMQF62yuwueQ8zhdh8jf3osYV/COlRZwQQGgZtnQCeeyDIh 26 | 8GJR9b4uv22QDNv7J2vcqTEWJdnpAZvIBFGuCBCAgev+URLGW2ELXfWQwNgc5+yP 27 | nGRXo+IwD1uhvEqtuin+cAn/sJhOa66g0ZcV/3AcrdQhbicn12YM71cMvA/XRKf5 28 | rpo8bAEwDqyoFoywH4IHM3HNV45rS+brskz6tZC5ELondCPVmUqgVu7ELHlJfPXx 29 | RbzbMPJEGr8WjWUiTDhrD2vWgoJ6NRKkDAUYm6KQb8Sbajd2JAAlYntLz5jKqNqN 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /tools/jungle/rc.d/README.md: -------------------------------------------------------------------------------- 1 | # Puma as a service using rc.d 2 | 3 | Manage multilpe Puma servers as services on one box using FreeBSD's rc.d service. 4 | 5 | ## Dependencies 6 | 7 | * `jq` - a command-line json parser is needed to parse the json in the config file 8 | 9 | ## Installation 10 | 11 | # Copy the puma script to the rc.d directory (make sure everyone has read/execute perms) 12 | sudo cp puma /usr/local/etc/rc.d/ 13 | 14 | # Create an empty configuration file 15 | sudo touch /usr/local/etc/puma.conf 16 | 17 | # Enable the puma service 18 | sudo echo 'puma_enable="YES"' >> /etc/rc.conf 19 | 20 | ## Managing the jungle 21 | 22 | Puma apps are referenced in /usr/local/etc/puma.conf by default. 23 | 24 | Start the jungle running: 25 | 26 | `service puma start` 27 | 28 | This script will run at boot time. 29 | 30 | 31 | You can also stop the jungle (stops ALL puma instances) by running: 32 | 33 | `service puma stop` 34 | 35 | 36 | To restart the jungle: 37 | 38 | `service puma restart` 39 | 40 | ## Conventions 41 | 42 | * The script expects: 43 | * a config file to exist under `config/puma.rb` in your app. E.g.: `/home/apps/my-app/config/puma.rb`. 44 | 45 | You can always change those defaults by editing the scripts. 46 | 47 | ## Here's what a minimal app's config file should have 48 | 49 | ``` 50 | { 51 | "servers" : [ 52 | { 53 | "dir": "/path/to/rails/project", 54 | "user": "deploy-user", 55 | "ruby_version": "ruby.version", 56 | "ruby_env": "rbenv" 57 | } 58 | ] 59 | } 60 | ``` 61 | 62 | ## Before starting... 63 | 64 | You need to customise `puma.conf` to: 65 | 66 | * Set the right user your app should be running on unless you want root to execute it! 67 | * Set the directory of the app 68 | * Set the ruby version to execute 69 | * Set the ruby environment (currently set to rbenv, since that is the only ruby environment currently supported) 70 | * Add additional server instances following the scheme in the example 71 | 72 | ## Notes: 73 | 74 | Only rbenv is currently supported. 75 | -------------------------------------------------------------------------------- /ext/puma_http11/http11_parser_common.rl: -------------------------------------------------------------------------------- 1 | %%{ 2 | 3 | machine puma_parser_common; 4 | 5 | #### HTTP PROTOCOL GRAMMAR 6 | # line endings 7 | CRLF = "\r\n"; 8 | 9 | # character types 10 | CTL = (cntrl | 127); 11 | safe = ("$" | "-" | "_" | "."); 12 | extra = ("!" | "*" | "'" | "(" | ")" | ","); 13 | reserved = (";" | "/" | "?" | ":" | "@" | "&" | "=" | "+"); 14 | unsafe = (CTL | " " | "\"" | "#" | "%" | "<" | ">"); 15 | national = any -- (alpha | digit | reserved | extra | safe | unsafe); 16 | unreserved = (alpha | digit | safe | extra | national); 17 | escape = ("%" xdigit xdigit); 18 | uchar = (unreserved | escape | "%"); 19 | pchar = (uchar | ":" | "@" | "&" | "=" | "+"); 20 | tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t"); 21 | 22 | # elements 23 | token = (ascii -- (CTL | tspecials)); 24 | 25 | # URI schemes and absolute paths 26 | scheme = ( alpha | digit | "+" | "-" | "." )* ; 27 | absolute_uri = (scheme ":" (uchar | reserved )*); 28 | 29 | path = ( pchar+ ( "/" pchar* )* ) ; 30 | query = ( uchar | reserved )* %query_string ; 31 | param = ( pchar | "/" )* ; 32 | params = ( param ( ";" param )* ) ; 33 | rel_path = ( path? %request_path (";" params)? ) ("?" %start_query query)?; 34 | absolute_path = ( "/"+ rel_path ); 35 | 36 | Request_URI = ( "*" | absolute_uri | absolute_path ) >mark %request_uri; 37 | Fragment = ( uchar | reserved )* >mark %fragment; 38 | Method = ( upper | digit | safe ){1,20} >mark %request_method; 39 | 40 | http_number = ( digit+ "." digit+ ) ; 41 | HTTP_Version = ( "HTTP/" http_number ) >mark %http_version ; 42 | Request_Line = ( Method " " Request_URI ("#" Fragment){0,1} " " HTTP_Version CRLF ) ; 43 | 44 | field_name = ( token -- ":" )+ >start_field $snake_upcase_field %write_field; 45 | 46 | field_value = any* >start_value %write_value; 47 | 48 | message_header = field_name ":" " "* field_value :> CRLF; 49 | 50 | Request = Request_Line ( message_header )* ( CRLF @done ); 51 | 52 | main := Request; 53 | 54 | }%% 55 | -------------------------------------------------------------------------------- /lib/puma/jruby_restart.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ffi' 4 | 5 | module Puma 6 | module JRubyRestart 7 | extend FFI::Library 8 | ffi_lib 'c' 9 | 10 | attach_function :execlp, [:string, :varargs], :int 11 | attach_function :chdir, [:string], :int 12 | attach_function :fork, [], :int 13 | attach_function :exit, [:int], :void 14 | attach_function :setsid, [], :int 15 | 16 | def self.chdir_exec(dir, argv) 17 | chdir(dir) 18 | cmd = argv.first 19 | argv = ([:string] * argv.size).zip(argv).flatten 20 | argv << :string 21 | argv << nil 22 | execlp(cmd, *argv) 23 | raise SystemCallError.new(FFI.errno) 24 | end 25 | 26 | PermKey = 'PUMA_DAEMON_PERM' 27 | RestartKey = 'PUMA_DAEMON_RESTART' 28 | 29 | # Called to tell things "Your now always in daemon mode, 30 | # don't try to reenter it." 31 | # 32 | def self.perm_daemonize 33 | ENV[PermKey] = "1" 34 | end 35 | 36 | def self.daemon? 37 | ENV.key?(PermKey) || ENV.key?(RestartKey) 38 | end 39 | 40 | def self.daemon_init 41 | return true if ENV.key?(PermKey) 42 | 43 | return false unless ENV.key? RestartKey 44 | 45 | master = ENV[RestartKey] 46 | 47 | # In case the master disappears early 48 | begin 49 | Process.kill "SIGUSR2", master.to_i 50 | rescue SystemCallError => e 51 | end 52 | 53 | ENV[RestartKey] = "" 54 | 55 | setsid 56 | 57 | null = File.open "/dev/null", "w+" 58 | STDIN.reopen null 59 | STDOUT.reopen null 60 | STDERR.reopen null 61 | 62 | true 63 | end 64 | 65 | def self.daemon_start(dir, argv) 66 | ENV[RestartKey] = Process.pid.to_s 67 | 68 | if k = ENV['PUMA_JRUBY_DAEMON_OPTS'] 69 | ENV['JRUBY_OPTS'] = k 70 | end 71 | 72 | cmd = argv.first 73 | argv = ([:string] * argv.size).zip(argv).flatten 74 | argv << :string 75 | argv << nil 76 | 77 | chdir(dir) 78 | ret = fork 79 | return ret if ret != 0 80 | execlp(cmd, *argv) 81 | raise SystemCallError.new(FFI.errno) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /tools/jungle/upstart/README.md: -------------------------------------------------------------------------------- 1 | # Puma as a service using Upstart 2 | 3 | Manage multiple Puma servers as services on the same box using Ubuntu upstart. 4 | 5 | ## Installation 6 | 7 | # Copy the scripts to services directory 8 | sudo cp puma.conf puma-manager.conf /etc/init 9 | 10 | # Create an empty configuration file 11 | sudo touch /etc/puma.conf 12 | 13 | ## Managing the jungle 14 | 15 | Puma apps are referenced in /etc/puma.conf by default. Add each app's path as a new line, e.g.: 16 | 17 | ``` 18 | /home/apps/my-cool-ruby-app 19 | /home/apps/another-app/current 20 | ``` 21 | 22 | Start the jungle running: 23 | 24 | `sudo start puma-manager` 25 | 26 | This script will run at boot time. 27 | 28 | Start a single puma like this: 29 | 30 | `sudo start puma app=/path/to/app` 31 | 32 | ## Logs 33 | 34 | Everything is logged by upstart, defaulting to `/var/log/upstart`. 35 | 36 | Each puma instance is named after its directory, so for an app called `/home/apps/my-app` the log file would be `/var/log/upstart/puma-_home_apps_my-app.log`. 37 | 38 | ## Conventions 39 | 40 | * The script expects: 41 | * a config file to exist under `config/puma.rb` in your app. E.g.: `/home/apps/my-app/config/puma.rb`. 42 | * a temporary folder to put the PID, socket and state files to exist called `tmp/puma`. E.g.: `/home/apps/my-app/tmp/puma`. Puma will take care of the files for you. 43 | 44 | You can always change those defaults by editing the scripts. 45 | 46 | ## Here's what a minimal app's config file should have 47 | 48 | ``` 49 | pidfile "/path/to/app/tmp/puma/pid" 50 | state_path "/path/to/app/tmp/puma/state" 51 | activate_control_app 52 | ``` 53 | 54 | ## Before starting... 55 | 56 | You need to customise `puma.conf` to: 57 | 58 | * Set the right user your app should be running on unless you want root to execute it! 59 | * Look for `setuid apps` and `setgid apps`, uncomment those lines and replace `apps` to whatever your deployment user is. 60 | * Replace `apps` on the paths (or set the right paths to your user's home) everywhere else. 61 | * Uncomment the source lines for `rbenv` or `rvm` support unless you use a system wide installation of Ruby. 62 | -------------------------------------------------------------------------------- /test/test_app_status.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helper" 4 | 5 | require "puma/app/status" 6 | require "rack" 7 | 8 | class TestAppStatus < Minitest::Test 9 | parallelize_me! 10 | 11 | class FakeServer 12 | def initialize 13 | @status = :running 14 | @backlog = 0 15 | @running = 0 16 | end 17 | 18 | attr_reader :status 19 | attr_accessor :backlog, :running 20 | 21 | def stop 22 | @status = :stop 23 | end 24 | 25 | def halt 26 | @status = :halt 27 | end 28 | 29 | def stats 30 | "{}" 31 | end 32 | end 33 | 34 | def setup 35 | @server = FakeServer.new 36 | @app = Puma::App::Status.new(@server) 37 | @app.auth_token = nil 38 | end 39 | 40 | def lint(uri) 41 | app = Rack::Lint.new @app 42 | mock_env = Rack::MockRequest.env_for uri 43 | app.call mock_env 44 | end 45 | 46 | def test_bad_token 47 | @app.auth_token = "abcdef" 48 | 49 | status, _, _ = lint('/whatever') 50 | 51 | assert_equal 403, status 52 | end 53 | 54 | def test_good_token 55 | @app.auth_token = "abcdef" 56 | 57 | status, _, _ = lint('/whatever?token=abcdef') 58 | 59 | assert_equal 404, status 60 | end 61 | 62 | def test_unsupported 63 | status, _, _ = lint('/not-real') 64 | 65 | assert_equal 404, status 66 | end 67 | 68 | def test_stop 69 | status, _ , app = lint('/stop') 70 | 71 | assert_equal :stop, @server.status 72 | assert_equal 200, status 73 | assert_equal ['{ "status": "ok" }'], app.enum_for.to_a 74 | end 75 | 76 | def test_halt 77 | status, _ , app = lint('/halt') 78 | 79 | assert_equal :halt, @server.status 80 | assert_equal 200, status 81 | assert_equal ['{ "status": "ok" }'], app.enum_for.to_a 82 | end 83 | 84 | def test_stats 85 | @server.backlog = 1 86 | @server.running = 9 87 | status, _ , app = lint('/stats') 88 | 89 | assert_equal 200, status 90 | assert_equal ['{}'], app.enum_for.to_a 91 | end 92 | 93 | def test_alternate_location 94 | status, _ , _ = lint('__alternatE_location_/stats') 95 | assert_equal 200, status 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | env: OS="Xenial 16.04 OpenSSL 1.0.2" 3 | language: ruby 4 | cache: bundler 5 | 6 | before_install: 7 | # rubygems 2.7.8 and greater include bundler, leave 2.6.0 untouched 8 | - | 9 | r_eng="$(ruby -e 'STDOUT.write RUBY_ENGINE')"; 10 | rv="$(ruby -e 'STDOUT.write RUBY_VERSION')"; 11 | if [ "$r_eng" == "ruby" ]; then 12 | if [ "$rv" \< "2.3" ]; then gem update --system 2.7.9 --no-document 13 | elif [ "$rv" \< "2.6" ]; then gem update --system --no-document --conservative 14 | fi 15 | fi 16 | if [ "$TRAVIS_OS_NAME" == "osx" ]; then 17 | brew update 18 | brew install ragel 19 | fi 20 | - ruby -v && gem --version && bundle version 21 | 22 | before_script: 23 | - if [ "$jit" == "yes" ]; then export RUBYOPT=--jit ; fi ; echo RUBYOPT is $RUBYOPT 24 | - bundle exec rake compile 25 | 26 | script: 27 | - bundle exec rake 28 | 29 | rvm: 30 | - 2.2.10 31 | - 2.3.8 32 | - 2.4.6 33 | - 2.5.5 34 | - 2.6.3 35 | - ruby-head 36 | 37 | matrix: 38 | fast_finish: true 39 | include: 40 | - rvm: 2.2.10 41 | dist: trusty 42 | env: OS="Trusty 14.04 OpenSSL 1.0.1" 43 | - rvm: 2.6.3 44 | dist: bionic 45 | env: OS="Bionic 18.04 OpenSSL 1.1.1" 46 | - rvm: ruby-head 47 | env: jit=yes 48 | - rvm: 2.4.6 49 | os: osx 50 | osx_image: xcode11 51 | env: OS="osx xcode11" 52 | - rvm: 2.5.5 53 | os: osx 54 | osx_image: xcode11 55 | env: OS="osx xcode11" 56 | - rvm: jruby-9.2.8.0 57 | env: JRUBY_OPTS="--debug" JAVA_OPTS="--add-opens java.base/sun.nio.ch=org.jruby.dist --add-opens java.base/java.io=org.jruby.dist --add-opens java.base/java.util.zip=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.security.cert=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED" 58 | - rvm: jruby-head 59 | dist: bionic 60 | env: OS="Bionic 18.04" 61 | 62 | allow_failures: 63 | - rvm: ruby-head 64 | - rvm: ruby-head 65 | env: jit=yes 66 | - rvm: jruby-9.2.8.0 67 | - rvm: jruby-head 68 | dist: bionic 69 | env: OS="Bionic 18.04" 70 | 71 | env: 72 | global: 73 | - TESTOPTS="-v" 74 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | ## Overview 4 | 5 | ![http://bit.ly/2iJuFky](images/puma-general-arch.png) 6 | 7 | Puma is a threaded web server, processing requests across a TCP or UNIX socket. 8 | 9 | Workers accept connections from the socket and a thread in the worker's thread pool processes the client's request. 10 | 11 | Clustered mode is shown/discussed here. Single mode is analogous to having a single worker process. 12 | 13 | ## Connection pipeline 14 | 15 | ![http://bit.ly/2zwzhEK](images/puma-connection-flow.png) 16 | 17 | * Upon startup, Puma listens on a TCP or UNIX socket. 18 | * The backlog of this socket is configured (with a default of 1024), determining how many established but unaccepted connections can exist concurrently. 19 | * This socket backlog is distinct from the "backlog" of work as reported by the control server stats. The latter is the number of connections in that worker's "todo" set waiting for a worker thread. 20 | * By default, a single, separate thread is used to receive HTTP requests across the socket. 21 | * When at least one worker thread is available for work, a connection is accepted and placed in this request buffer 22 | * This thread waits for entire HTTP requests to be received over the connection 23 | * The time spent waiting for the HTTP request body to be received is exposed to the Rack app as `env['puma.request_body_wait']` (milliseconds) 24 | * Once received, the connection is pushed into the "todo" set 25 | * Worker threads pop work off the "todo" set for processing 26 | * The thread processes the request via the rack application (which generates the HTTP response) 27 | * The thread writes the response to the connection 28 | * Finally, the thread become available to process another connection in the "todo" set 29 | 30 | ### Disabling `queue_requests` 31 | 32 | ![http://bit.ly/2zxCJ1Z](images/puma-connection-flow-no-reactor.png) 33 | 34 | The `queue_requests` option is `true` by default, enabling the separate thread used to buffer requests as described above. 35 | 36 | If set to `false`, this buffer will not be used for connections while waiting for the request to arrive. 37 | In this mode, when a connection is accepted, it is added to the "todo" queue immediately, and a worker will synchronously do any waiting necessary to read the HTTP request from the socket. 38 | -------------------------------------------------------------------------------- /ext/puma_http11/org/jruby/puma/IOBuffer.java: -------------------------------------------------------------------------------- 1 | package org.jruby.puma; 2 | 3 | import org.jruby.*; 4 | import org.jruby.anno.JRubyMethod; 5 | import org.jruby.runtime.ObjectAllocator; 6 | import org.jruby.runtime.ThreadContext; 7 | import org.jruby.runtime.builtin.IRubyObject; 8 | import org.jruby.util.ByteList; 9 | 10 | /** 11 | * @author kares 12 | */ 13 | public class IOBuffer extends RubyObject { 14 | 15 | private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() { 16 | public IRubyObject allocate(Ruby runtime, RubyClass klass) { 17 | return new IOBuffer(runtime, klass); 18 | } 19 | }; 20 | 21 | public static void createIOBuffer(Ruby runtime) { 22 | RubyModule mPuma = runtime.defineModule("Puma"); 23 | RubyClass cIOBuffer = mPuma.defineClassUnder("IOBuffer", runtime.getObject(), ALLOCATOR); 24 | cIOBuffer.defineAnnotatedMethods(IOBuffer.class); 25 | } 26 | 27 | private static final int DEFAULT_SIZE = 4096; 28 | 29 | final ByteList buffer = new ByteList(DEFAULT_SIZE); 30 | 31 | IOBuffer(Ruby runtime, RubyClass klass) { 32 | super(runtime, klass); 33 | } 34 | 35 | @JRubyMethod 36 | public RubyInteger used(ThreadContext context) { 37 | return context.runtime.newFixnum(buffer.getRealSize()); 38 | } 39 | 40 | @JRubyMethod 41 | public RubyInteger capacity(ThreadContext context) { 42 | return context.runtime.newFixnum(buffer.unsafeBytes().length); 43 | } 44 | 45 | @JRubyMethod 46 | public IRubyObject reset() { 47 | buffer.setRealSize(0); 48 | return this; 49 | } 50 | 51 | @JRubyMethod(name = { "to_s", "to_str" }) 52 | public RubyString to_s(ThreadContext context) { 53 | return RubyString.newStringShared(context.runtime, buffer.unsafeBytes(), 0, buffer.getRealSize()); 54 | } 55 | 56 | @JRubyMethod(name = "<<") 57 | public IRubyObject add(IRubyObject str) { 58 | addImpl(str.convertToString()); 59 | return this; 60 | } 61 | 62 | @JRubyMethod(rest = true) 63 | public IRubyObject append(IRubyObject[] strs) { 64 | for (IRubyObject str : strs) addImpl(str.convertToString()); 65 | return this; 66 | } 67 | 68 | private void addImpl(RubyString str) { 69 | buffer.append(str.getByteList()); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /tools/jungle/upstart/puma.conf: -------------------------------------------------------------------------------- 1 | # /etc/init/puma.conf - Puma config 2 | 3 | # This example config should work with Ubuntu 12.04+. It 4 | # allows you to manage multiple Puma instances with 5 | # Upstart, Ubuntu's native service management tool. 6 | # 7 | # See puma-manager.conf for how to manage all Puma instances at once. 8 | # 9 | # Save this config as /etc/init/puma.conf then manage puma with: 10 | # sudo start puma app=PATH_TO_APP 11 | # sudo stop puma app=PATH_TO_APP 12 | # sudo status puma app=PATH_TO_APP 13 | # 14 | # or use the service command: 15 | # sudo service puma {start,stop,restart,status} 16 | # 17 | 18 | description "Puma Background Worker" 19 | 20 | # no "start on", we don't want to automatically start 21 | stop on (stopping puma-manager or runlevel [06]) 22 | 23 | # change apps to match your deployment user if you want to use this as a less privileged user (recommended!) 24 | setuid apps 25 | setgid apps 26 | 27 | respawn 28 | respawn limit 3 30 29 | 30 | instance ${app} 31 | 32 | script 33 | # this script runs in /bin/sh by default 34 | # respawn as bash so we can source in rbenv/rvm 35 | # quoted heredoc to tell /bin/sh not to interpret 36 | # variables 37 | 38 | # source ENV variables manually as Upstart doesn't, eg: 39 | #. /etc/environment 40 | 41 | exec /bin/bash <<'EOT' 42 | # set HOME to the setuid user's home, there doesn't seem to be a better, portable way 43 | export HOME="$(eval echo ~$(id -un))" 44 | 45 | if [ -d "/usr/local/rbenv/bin" ]; then 46 | export PATH="/usr/local/rbenv/bin:/usr/local/rbenv/shims:$PATH" 47 | elif [ -d "$HOME/.rbenv/bin" ]; then 48 | export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH" 49 | elif [ -f /etc/profile.d/rvm.sh ]; then 50 | source /etc/profile.d/rvm.sh 51 | elif [ -f /usr/local/rvm/scripts/rvm ]; then 52 | source /etc/profile.d/rvm.sh 53 | elif [ -f "$HOME/.rvm/scripts/rvm" ]; then 54 | source "$HOME/.rvm/scripts/rvm" 55 | elif [ -f /usr/local/share/chruby/chruby.sh ]; then 56 | source /usr/local/share/chruby/chruby.sh 57 | if [ -f /usr/local/share/chruby/auto.sh ]; then 58 | source /usr/local/share/chruby/auto.sh 59 | fi 60 | # if you aren't using auto, set your version here 61 | # chruby 2.0.0 62 | fi 63 | 64 | cd $app 65 | logger -t puma "Starting server: $app" 66 | 67 | exec bundle exec puma -C config/puma.rb 68 | EOT 69 | end script 70 | -------------------------------------------------------------------------------- /lib/puma/app/status.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | 5 | module Puma 6 | module App 7 | # Check out {#call}'s source code to see what actions this web application 8 | # can respond to. 9 | class Status 10 | def initialize(cli) 11 | @cli = cli 12 | @auth_token = nil 13 | end 14 | OK_STATUS = '{ "status": "ok" }'.freeze 15 | 16 | attr_accessor :auth_token 17 | 18 | def authenticate(env) 19 | return true unless @auth_token 20 | env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}") 21 | end 22 | 23 | def rack_response(status, body, content_type='application/json') 24 | headers = { 25 | 'Content-Type' => content_type, 26 | 'Content-Length' => body.bytesize.to_s 27 | } 28 | 29 | [status, headers, [body]] 30 | end 31 | 32 | def call(env) 33 | unless authenticate(env) 34 | return rack_response(403, 'Invalid auth token', 'text/plain') 35 | end 36 | 37 | case env['PATH_INFO'] 38 | when /\/stop$/ 39 | @cli.stop 40 | return rack_response(200, OK_STATUS) 41 | 42 | when /\/halt$/ 43 | @cli.halt 44 | return rack_response(200, OK_STATUS) 45 | 46 | when /\/restart$/ 47 | @cli.restart 48 | return rack_response(200, OK_STATUS) 49 | 50 | when /\/phased-restart$/ 51 | if !@cli.phased_restart 52 | return rack_response(404, '{ "error": "phased restart not available" }') 53 | else 54 | return rack_response(200, OK_STATUS) 55 | end 56 | 57 | when /\/reload-worker-directory$/ 58 | if !@cli.send(:reload_worker_directory) 59 | return rack_response(404, '{ "error": "reload_worker_directory not available" }') 60 | else 61 | return rack_response(200, OK_STATUS) 62 | end 63 | 64 | when /\/gc$/ 65 | GC.start 66 | return rack_response(200, OK_STATUS) 67 | 68 | when /\/gc-stats$/ 69 | return rack_response(200, GC.stat.to_json) 70 | 71 | when /\/stats$/ 72 | return rack_response(200, @cli.stats) 73 | else 74 | rack_response 404, "Unsupported action", 'text/plain' 75 | end 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /tools/jungle/init.d/README.md: -------------------------------------------------------------------------------- 1 | # Puma daemon service 2 | 3 | Deprecatation Warning : `init.d` was replaced by `systemd` since Debian 8 and Ubuntu 16.04, you should look into [/docs/systemd](https://github.com/puma/puma/blob/master/docs/systemd.md) unless you are on an older OS. 4 | 5 | Init script to manage multiple Puma servers on the same box using start-stop-daemon. 6 | 7 | ## Installation 8 | 9 | # Copy the init script to services directory 10 | sudo cp puma /etc/init.d 11 | sudo chmod +x /etc/init.d/puma 12 | 13 | # Make it start at boot time. 14 | sudo update-rc.d -f puma defaults 15 | 16 | # Copy the Puma runner to an accessible location 17 | sudo cp run-puma /usr/local/bin 18 | sudo chmod +x /usr/local/bin/run-puma 19 | 20 | # Create an empty configuration file 21 | sudo touch /etc/puma.conf 22 | 23 | ## Managing the jungle 24 | 25 | Puma apps are held in /etc/puma.conf by default. It's mainly a CSV file and every line represents one app. Here's the syntax: 26 | 27 | app-path,user,config-file-path,log-file-path,environment-variables 28 | 29 | You can add an instance by editing the file or running the following command: 30 | 31 | sudo /etc/init.d/puma add /path/to/app user /path/to/app/config/puma.rb /path/to/app/log/puma.log 32 | 33 | The config and log paths, as well as the environment variables, are optional parameters and default to: 34 | 35 | * config: /path/to/app/*config/puma.rb* 36 | * log: /path/to/app/*log/puma.log* 37 | * environment: (empty) 38 | 39 | Multiple environment variables need to be separated by a semicolon, e.g. 40 | 41 | FOO=1;BAR=2 42 | 43 | To remove an app, simply delete the line from the config file or run: 44 | 45 | sudo /etc/init.d/puma remove /path/to/app 46 | 47 | The command will make sure the Puma instance stops before removing it from the jungle. 48 | 49 | ## Assumptions 50 | 51 | * The script expects a temporary folder named /path/to/app/*tmp/puma* to exist. Create it if it's not there by default. 52 | The pid and state files should live there and must be called: *tmp/puma/pid* and *tmp/puma/state*. 53 | You can change those if you want but you'll have to adapt the script for it to work. 54 | 55 | * Here's what a minimal app's config file should have: 56 | 57 | ``` 58 | pidfile "/path/to/app/tmp/puma/pid" 59 | state_path "/path/to/app/tmp/puma/state" 60 | activate_control_app 61 | ``` 62 | -------------------------------------------------------------------------------- /win_gem_test/puma.ps1: -------------------------------------------------------------------------------- 1 | # PowerShell script for building & testing SQLite3-Ruby fat binary gem 2 | # Code by MSP-Greg, see https://github.com/MSP-Greg/av-gem-build-test 3 | 4 | # load utility functions, pass 64 or 32 5 | . $PSScriptRoot\shared\appveyor_setup.ps1 $args[0] 6 | if ($LastExitCode) { exit } 7 | 8 | # above is required code 9 | #———————————————————————————————————————————————————————————————— above for all repos 10 | 11 | Make-Const gem_name 'puma' 12 | Make-Const repo_name 'puma' 13 | Make-Const url_repo 'https://github.com/puma/puma.git' 14 | 15 | #———————————————————————————————————————————————————————————————— lowest ruby version 16 | Make-Const ruby_vers_low 22 17 | # null = don't compile; false = compile, ignore test (allow failure); 18 | # true = compile & test 19 | Make-Const trunk $false ; Make-Const trunk_x64 $false 20 | Make-Const trunk_JIT $null ; Make-Const trunk_x64_JIT $null 21 | 22 | #———————————————————————————————————————————————————————————————— make info 23 | Make-Const dest_so 'lib\puma' 24 | Make-Const exts @( 25 | @{ 'conf' = 'ext/puma_http11/extconf.rb' ; 'so' = 'puma_http11' } 26 | ) 27 | Make-Const write_so_require $true 28 | 29 | #———————————————————————————————————————————————————————————————— Pre-Compile 30 | # runs before compiling starts on every ruby version 31 | function Pre-Compile { 32 | # load the correct OpenSSL version in the build system 33 | Check-OpenSSL 34 | Write-Host Compiling With $env:SSL_VERS 35 | } 36 | 37 | #———————————————————————————————————————————————————————————————— Pre-Gem-Install 38 | function Pre-Gem-Install { 39 | if ($ruby -lt '23') { 40 | gem install -N --no-user-install nio4r:2.3.1 41 | } else { 42 | gem install -N --no-user-install nio4r 43 | } 44 | } 45 | 46 | #———————————————————————————————————————————————————————————————— Run-Tests 47 | function Run-Tests { 48 | # call with comma separated list of gems to install or update 49 | Update-Gems minitest, minitest-retry, minitest-proveit, rack, rake 50 | $env:CI = 1 51 | rake -f Rakefile_wintest -N -R norakelib | Set-Content -Path $log_name -PassThru -Encoding UTF8 52 | # add info after test results 53 | $(ruby -ropenssl -e "STDOUT.puts $/ + OpenSSL::OPENSSL_LIBRARY_VERSION") | 54 | Add-Content -Path $log_name -PassThru -Encoding UTF8 55 | minitest # collects test results 56 | } 57 | 58 | #———————————————————————————————————————————————————————————————— below for all repos 59 | # below is required code 60 | Make-Const dir_gem $(Convert-Path $PSScriptRoot\..) 61 | Make-Const dir_ps $PSScriptRoot 62 | 63 | Push-Location $PSScriptRoot 64 | .\shared\make.ps1 65 | .\shared\test.ps1 66 | Pop-Location 67 | exit $ttl_errors_fails + $exit_code 68 | -------------------------------------------------------------------------------- /docs/nginx.md: -------------------------------------------------------------------------------- 1 | # Nginx configuration example file 2 | 3 | This is a very common setup using an upstream. It was adapted from some Capistrano recipe I found on the Internet a while ago. 4 | 5 | ``` 6 | upstream myapp { 7 | server unix:///myapp/tmp/puma.sock; 8 | } 9 | 10 | server { 11 | listen 80; 12 | server_name myapp.com; 13 | 14 | # ~2 seconds is often enough for most folks to parse HTML/CSS and 15 | # retrieve needed images/icons/frames, connections are cheap in 16 | # nginx so increasing this is generally safe... 17 | keepalive_timeout 5; 18 | 19 | # path for static files 20 | root /myapp/public; 21 | access_log /myapp/log/nginx.access.log; 22 | error_log /myapp/log/nginx.error.log info; 23 | 24 | # this rewrites all the requests to the maintenance.html 25 | # page if it exists in the doc root. This is for capistrano's 26 | # disable web task 27 | if (-f $document_root/maintenance.html) { 28 | rewrite ^(.*)$ /maintenance.html last; 29 | break; 30 | } 31 | 32 | location / { 33 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 34 | proxy_set_header Host $http_host; 35 | 36 | # If the file exists as a static file serve it directly without 37 | # running all the other rewrite tests on it 38 | if (-f $request_filename) { 39 | break; 40 | } 41 | 42 | # check for index.html for directory index 43 | # if it's there on the filesystem then rewrite 44 | # the url to add /index.html to the end of it 45 | # and then break to send it to the next config rules. 46 | if (-f $request_filename/index.html) { 47 | rewrite (.*) $1/index.html break; 48 | } 49 | 50 | # this is the meat of the rack page caching config 51 | # it adds .html to the end of the url and then checks 52 | # the filesystem for that file. If it exists, then we 53 | # rewrite the url to have explicit .html on the end 54 | # and then send it on its way to the next config rule. 55 | # if there is no file on the fs then it sets all the 56 | # necessary headers and proxies to our upstream pumas 57 | if (-f $request_filename.html) { 58 | rewrite (.*) $1.html break; 59 | } 60 | 61 | if (!-f $request_filename) { 62 | proxy_pass http://myapp; 63 | break; 64 | } 65 | } 66 | 67 | # Now this supposedly should work as it gets the filenames with querystrings that Rails provides. 68 | # BUT there's a chance it could break the ajax calls. 69 | location ~* \.(ico|css|gif|jpe?g|png|js)(\?[0-9]+)?$ { 70 | expires max; 71 | break; 72 | } 73 | 74 | # Error pages 75 | # error_page 500 502 503 504 /500.html; 76 | location = /500.html { 77 | root /myapp/current/public; 78 | } 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "rake/testtask" 3 | require "rake/extensiontask" 4 | require "rake/javaextensiontask" 5 | require "rubocop/rake_task" 6 | require_relative 'lib/puma/detect' 7 | require 'rubygems/package_task' 8 | require 'bundler/gem_tasks' 9 | 10 | gemspec = Gem::Specification.load("puma.gemspec") 11 | Gem::PackageTask.new(gemspec).define 12 | 13 | # Add rubocop task 14 | RuboCop::RakeTask.new 15 | 16 | # generate extension code using Ragel (C and Java) 17 | desc "Generate extension code (C and Java) using Ragel" 18 | task :ragel 19 | 20 | file 'ext/puma_http11/http11_parser.c' => ['ext/puma_http11/http11_parser.rl'] do |t| 21 | begin 22 | sh "ragel #{t.prerequisites.last} -C -G2 -I ext/puma_http11 -o #{t.name}" 23 | rescue 24 | fail "Could not build wrapper using Ragel (it failed or not installed?)" 25 | end 26 | end 27 | task :ragel => ['ext/puma_http11/http11_parser.c'] 28 | 29 | file 'ext/puma_http11/org/jruby/puma/Http11Parser.java' => ['ext/puma_http11/http11_parser.java.rl'] do |t| 30 | begin 31 | sh "ragel #{t.prerequisites.last} -J -G2 -I ext/puma_http11 -o #{t.name}" 32 | rescue 33 | fail "Could not build wrapper using Ragel (it failed or not installed?)" 34 | end 35 | end 36 | task :ragel => ['ext/puma_http11/org/jruby/puma/Http11Parser.java'] 37 | 38 | if !Puma.jruby? 39 | # compile extensions using rake-compiler 40 | # C (MRI, Rubinius) 41 | Rake::ExtensionTask.new("puma_http11", gemspec) do |ext| 42 | # place extension inside namespace 43 | ext.lib_dir = "lib/puma" 44 | 45 | CLEAN.include "lib/puma/{1.8,1.9}" 46 | CLEAN.include "lib/puma/puma_http11.rb" 47 | end 48 | else 49 | # Java (JRuby) 50 | Rake::JavaExtensionTask.new("puma_http11", gemspec) do |ext| 51 | ext.lib_dir = "lib/puma" 52 | end 53 | end 54 | 55 | # the following is a fat-binary stub that will be used when 56 | # require 'puma/puma_http11' and will use either 1.8 or 1.9 version depending 57 | # on RUBY_VERSION 58 | file "lib/puma/puma_http11.rb" do |t| 59 | File.open(t.name, "w") do |f| 60 | f.puts "RUBY_VERSION =~ /(\d+.\d+)/" 61 | f.puts 'require "puma/#{$1}/puma_http11"' 62 | end 63 | end 64 | 65 | Rake::TestTask.new(:test) 66 | 67 | # tests require extension be compiled, but depend on the platform 68 | if Puma.jruby? 69 | task :test => [:java] 70 | else 71 | task :test => [:compile] 72 | end 73 | 74 | namespace :test do 75 | desc "Run the integration tests" 76 | 77 | task :integration do 78 | sh "ruby test/shell/run.rb" 79 | end 80 | 81 | desc "Run all tests" 82 | if (Puma.jruby? && ENV['TRAVIS']) || Puma.windows? 83 | task :all => :test 84 | else 85 | task :all => [:test, "test:integration"] 86 | end 87 | end 88 | 89 | task :default => [:rubocop, "test:all"] 90 | -------------------------------------------------------------------------------- /lib/puma/plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Puma 4 | class UnknownPlugin < RuntimeError; end 5 | 6 | class PluginLoader 7 | def initialize 8 | @instances = [] 9 | end 10 | 11 | def create(name) 12 | if cls = Plugins.find(name) 13 | plugin = cls.new(Plugin) 14 | @instances << plugin 15 | return plugin 16 | end 17 | 18 | raise UnknownPlugin, "File failed to register properly named plugin" 19 | end 20 | 21 | def fire_starts(launcher) 22 | @instances.each do |i| 23 | if i.respond_to? :start 24 | i.start(launcher) 25 | end 26 | end 27 | end 28 | end 29 | 30 | class PluginRegistry 31 | def initialize 32 | @plugins = {} 33 | @background = [] 34 | end 35 | 36 | def register(name, cls) 37 | @plugins[name] = cls 38 | end 39 | 40 | def find(name) 41 | name = name.to_s 42 | 43 | if cls = @plugins[name] 44 | return cls 45 | end 46 | 47 | begin 48 | require "puma/plugin/#{name}" 49 | rescue LoadError 50 | raise UnknownPlugin, "Unable to find plugin: #{name}" 51 | end 52 | 53 | if cls = @plugins[name] 54 | return cls 55 | end 56 | 57 | raise UnknownPlugin, "file failed to register a plugin" 58 | end 59 | 60 | def add_background(blk) 61 | @background << blk 62 | end 63 | 64 | def fire_background 65 | @background.each do |b| 66 | Thread.new(&b) 67 | end 68 | end 69 | end 70 | 71 | Plugins = PluginRegistry.new 72 | 73 | class Plugin 74 | # Matches 75 | # "C:/Ruby22/lib/ruby/gems/2.2.0/gems/puma-3.0.1/lib/puma/plugin/tmp_restart.rb:3:in `'" 76 | # AS 77 | # C:/Ruby22/lib/ruby/gems/2.2.0/gems/puma-3.0.1/lib/puma/plugin/tmp_restart.rb 78 | CALLER_FILE = / 79 | \A # start of string 80 | .+ # file path (one or more characters) 81 | (?= # stop previous match when 82 | :\d+ # a colon is followed by one or more digits 83 | :in # followed by a colon followed by in 84 | ) 85 | /x 86 | 87 | def self.extract_name(ary) 88 | path = ary.first[CALLER_FILE] 89 | 90 | m = %r!puma/plugin/([^/]*)\.rb$!.match(path) 91 | return m[1] 92 | end 93 | 94 | def self.create(&blk) 95 | name = extract_name(caller) 96 | 97 | cls = Class.new(self) 98 | 99 | cls.class_eval(&blk) 100 | 101 | Plugins.register name, cls 102 | end 103 | 104 | def initialize(loader) 105 | @loader = loader 106 | end 107 | 108 | def in_background(&blk) 109 | Plugins.add_background blk 110 | end 111 | 112 | def workers_supported? 113 | return false if Puma.jruby? || Puma.windows? 114 | true 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/test_rack_server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "helper" 3 | 4 | require "rack" 5 | 6 | class TestRackServer < Minitest::Test 7 | parallelize_me! 8 | 9 | class ErrorChecker 10 | def initialize(app) 11 | @app = app 12 | @exception = nil 13 | end 14 | 15 | attr_reader :exception, :env 16 | 17 | def call(env) 18 | begin 19 | @app.call(env) 20 | rescue Exception => e 21 | @exception = e 22 | [ 500, {}, ["Error detected"] ] 23 | end 24 | end 25 | end 26 | 27 | class ServerLint < Rack::Lint 28 | def call(env) 29 | check_env env 30 | 31 | @app.call(env) 32 | end 33 | end 34 | 35 | def setup 36 | @simple = lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } 37 | @server = Puma::Server.new @simple 38 | @server.add_tcp_listener "127.0.0.1", 0 39 | 40 | @stopped = false 41 | end 42 | 43 | def stop 44 | @server.stop(true) 45 | @stopped = true 46 | end 47 | 48 | def teardown 49 | @server.stop(true) unless @stopped 50 | end 51 | 52 | def test_lint 53 | @checker = ErrorChecker.new ServerLint.new(@simple) 54 | @server.app = @checker 55 | 56 | @server.run 57 | 58 | hit(["http://127.0.0.1:#{ @server.connected_port }/test"]) 59 | 60 | stop 61 | 62 | refute @checker.exception, "Checker raised exception" 63 | end 64 | 65 | def test_large_post_body 66 | @checker = ErrorChecker.new ServerLint.new(@simple) 67 | @server.app = @checker 68 | 69 | @server.run 70 | 71 | big = "x" * (1024 * 16) 72 | 73 | Net::HTTP.post_form URI.parse("http://127.0.0.1:#{ @server.connected_port }/test"), 74 | { "big" => big } 75 | 76 | stop 77 | 78 | refute @checker.exception, "Checker raised exception" 79 | end 80 | 81 | def test_path_info 82 | input = nil 83 | @server.app = lambda { |env| input = env; @simple.call(env) } 84 | @server.run 85 | 86 | hit(["http://127.0.0.1:#{ @server.connected_port }/test/a/b/c"]) 87 | 88 | stop 89 | 90 | assert_equal "/test/a/b/c", input['PATH_INFO'] 91 | end 92 | 93 | def test_after_reply 94 | closed = false 95 | 96 | @server.app = lambda do |env| 97 | env['rack.after_reply'] << lambda { closed = true } 98 | @simple.call(env) 99 | end 100 | 101 | @server.run 102 | 103 | hit(["http://127.0.0.1:#{ @server.connected_port }/test"]) 104 | 105 | stop 106 | 107 | assert_equal true, closed 108 | end 109 | 110 | def test_common_logger 111 | log = StringIO.new 112 | 113 | logger = Rack::CommonLogger.new(@simple, log) 114 | 115 | @server.app = logger 116 | 117 | @server.run 118 | 119 | hit(["http://127.0.0.1:#{ @server.connected_port }/test"]) 120 | 121 | stop 122 | 123 | assert_match %r!GET /test HTTP/1\.1!, log.string 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/puma/rack/urlmap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Puma::Rack 4 | # Rack::URLMap takes a hash mapping urls or paths to apps, and 5 | # dispatches accordingly. Support for HTTP/1.1 host names exists if 6 | # the URLs start with http:// or https://. 7 | # 8 | # URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part 9 | # relevant for dispatch is in the SCRIPT_NAME, and the rest in the 10 | # PATH_INFO. This should be taken care of when you need to 11 | # reconstruct the URL in order to create links. 12 | # 13 | # URLMap dispatches in such a way that the longest paths are tried 14 | # first, since they are most specific. 15 | 16 | class URLMap 17 | NEGATIVE_INFINITY = -1.0 / 0.0 18 | INFINITY = 1.0 / 0.0 19 | 20 | def initialize(map = {}) 21 | remap(map) 22 | end 23 | 24 | def remap(map) 25 | @mapping = map.map { |location, app| 26 | if location =~ %r{\Ahttps?://(.*?)(/.*)} 27 | host, location = $1, $2 28 | else 29 | host = nil 30 | end 31 | 32 | unless location[0] == ?/ 33 | raise ArgumentError, "paths need to start with /" 34 | end 35 | 36 | location = location.chomp('/') 37 | match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n') 38 | 39 | [host, location, match, app] 40 | }.sort_by do |(host, location, _, _)| 41 | [host ? -host.size : INFINITY, -location.size] 42 | end 43 | end 44 | 45 | def call(env) 46 | path = env['PATH_INFO'] 47 | script_name = env['SCRIPT_NAME'] 48 | http_host = env['HTTP_HOST'] 49 | server_name = env['SERVER_NAME'] 50 | server_port = env['SERVER_PORT'] 51 | 52 | is_same_server = casecmp?(http_host, server_name) || 53 | casecmp?(http_host, "#{server_name}:#{server_port}") 54 | 55 | @mapping.each do |host, location, match, app| 56 | unless casecmp?(http_host, host) \ 57 | || casecmp?(server_name, host) \ 58 | || (!host && is_same_server) 59 | next 60 | end 61 | 62 | next unless m = match.match(path.to_s) 63 | 64 | rest = m[1] 65 | next unless !rest || rest.empty? || rest[0] == ?/ 66 | 67 | env['SCRIPT_NAME'] = (script_name + location) 68 | env['PATH_INFO'] = rest 69 | 70 | return app.call(env) 71 | end 72 | 73 | [404, {'Content-Type' => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]] 74 | 75 | ensure 76 | env['PATH_INFO'] = path 77 | env['SCRIPT_NAME'] = script_name 78 | end 79 | 80 | private 81 | def casecmp?(v1, v2) 82 | # if both nil, or they're the same string 83 | return true if v1 == v2 84 | 85 | # if either are nil... (but they're not the same) 86 | return false if v1.nil? 87 | return false if v2.nil? 88 | 89 | # otherwise check they're not case-insensitive the same 90 | v1.casecmp(v2).zero? 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /examples/puma/client-certs/generate.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "puma" 3 | require "puma/minissl" 4 | 5 | case ARGV[0] 6 | 7 | when "s" 8 | 9 | app = proc {|env| 10 | p env['puma.peercert'] 11 | [200, {}, [ env['puma.peercert'] ]] 12 | } 13 | events = Puma::Events.new($stdout, $stderr) 14 | server = Puma::Server.new(app, events) 15 | 16 | context = Puma::MiniSSL::Context.new 17 | context.key = "certs/server.key" 18 | context.cert = "certs/server.crt" 19 | context.ca = "certs/ca.crt" 20 | #context.verify_mode = Puma::MiniSSL::VERIFY_NONE 21 | #context.verify_mode = Puma::MiniSSL::VERIFY_PEER 22 | context.verify_mode = Puma::MiniSSL::VERIFY_PEER | Puma::MiniSSL::VERIFY_FAIL_IF_NO_PEER_CERT 23 | 24 | server.add_ssl_listener("127.0.0.1", 4000, context) 25 | 26 | server.run 27 | sleep 28 | #server.stop(true) 29 | 30 | when "g" 31 | 32 | def issue_cert(dn, key, serial, not_before, not_after, extensions, issuer, issuer_key, digest) 33 | cert = OpenSSL::X509::Certificate.new 34 | issuer = cert unless issuer 35 | issuer_key = key unless issuer_key 36 | cert.version = 2 37 | cert.serial = serial 38 | cert.subject = dn 39 | cert.issuer = issuer.subject 40 | cert.public_key = key.public_key 41 | cert.not_before = not_before 42 | cert.not_after = not_after 43 | ef = OpenSSL::X509::ExtensionFactory.new 44 | ef.subject_certificate = cert 45 | ef.issuer_certificate = issuer 46 | extensions.each {|oid, value, critical| 47 | cert.add_extension(ef.create_extension(oid, value, critical)) 48 | } 49 | cert.sign(issuer_key, digest) 50 | cert 51 | end 52 | 53 | @ca_key = OpenSSL::PKey::RSA.generate(2048) 54 | @svr_key = OpenSSL::PKey::RSA.generate(2048) 55 | @cli_key = OpenSSL::PKey::RSA.generate(2048) 56 | @ca = OpenSSL::X509::Name.parse("/DC=net/DC=client-cbhq/CN=CA") 57 | @svr = OpenSSL::X509::Name.parse("/DC=net/DC=client-cbhq/CN=localhost") 58 | @cli = OpenSSL::X509::Name.parse("/DC=net/DC=client-cbhq/CN=localhost") 59 | now = Time.at(Time.now.to_i) 60 | ca_exts = [ 61 | ["basicConstraints","CA:TRUE",true], 62 | ["keyUsage","cRLSign,keyCertSign",true], 63 | ] 64 | ee_exts = [ 65 | #["keyUsage","keyEncipherment,digitalSignature",true], 66 | ["keyUsage","keyEncipherment,dataEncipherment,digitalSignature",true], 67 | ] 68 | @ca_cert = issue_cert(@ca, @ca_key, 1, now, now+3600_000, ca_exts, nil, nil, OpenSSL::Digest::SHA1.new) 69 | @svr_cert = issue_cert(@svr, @svr_key, 2, now, now+1800_000, ee_exts, @ca_cert, @ca_key, OpenSSL::Digest::SHA1.new) 70 | @cli_cert = issue_cert(@cli, @cli_key, 3, now, now+1800_000, ee_exts, @ca_cert, @ca_key, OpenSSL::Digest::SHA1.new) 71 | 72 | File.open("ca.crt","wb") {|f| f.print @ca_cert.to_pem } 73 | File.open("ca.key","wb") {|f| f.print @ca_key.to_pem } 74 | File.open("server.crt","wb") {|f| f.print @svr_cert.to_pem } 75 | File.open("server.key","wb") {|f| f.print @svr_key.to_pem } 76 | File.open("client1.crt","wb") {|f| f.print @cli_cert.to_pem } 77 | File.open("client1.key","wb") {|f| f.print @cli_key.to_pem } 78 | end 79 | -------------------------------------------------------------------------------- /docs/restart.md: -------------------------------------------------------------------------------- 1 | # Restarts 2 | 3 | To perform a restart, there are 3 builtin mechanisms: 4 | 5 | * Send the `puma` process the `SIGUSR2` signal (normal restart) 6 | * Send the `puma` process the `SIGUSR1` signal (restart in phases (a "rolling restart"), cluster mode only) 7 | * Use the status server and issue `/restart` 8 | 9 | No code is shared between the current and restarted process, so it should be safe to issue a restart any place where you would manually stop Puma and start it again. 10 | 11 | If the new process is unable to load, it will simply exit. You should therefore run Puma under a process monitor (see below) when using it in production. 12 | 13 | ### Normal vs Hot vs Phased Restart 14 | 15 | A hot restart means that no requests will be lost while deploying your new code, since the server socket is kept open between restarts. 16 | 17 | But beware, hot restart does not mean that the incoming requests won’t hang for multiple seconds while your new code has not fully deployed. If you need a zero downtime and zero hanging requests deploy, you must use phased restart. 18 | 19 | When you run pumactl phased-restart, Puma kills workers one-by-one, meaning that at least another worker is still available to serve requests, which lead to zero hanging requests (yay!). 20 | 21 | But again beware, upgrading an application sometimes involves upgrading the database schema. With phased restart, there may be a moment during the deployment where processes belonging to the previous version and processes belonging to the new version both exist at the same time. Any database schema upgrades you perform must therefore be backwards-compatible with the old application version. 22 | 23 | If you perform a lot of database migrations, you probably should not use phased restart and use a normal/hot restart instead (`pumactl restart`). That way, no code is shared while deploying (in that case, `preload_app!` might help for quicker deployment, see ["Clustered Mode" in the README](../README.md#clustered-mode)). 24 | 25 | **Note**: Hot and phased restarts are only available on MRI, not on JRuby. They are also unavailable on Windows servers. 26 | 27 | ### Release Directory 28 | 29 | If your symlink releases into a common working directory (i.e., `/current` from Capistrano), Puma won't pick up your new changes when running phased restarts without additional configuration. You should set your working directory within Puma's config to specify the directory it should use. This is a change from earlier versions of Puma (< 2.15) that would infer the directory for you. 30 | 31 | ```ruby 32 | # config/puma.rb 33 | 34 | directory '/var/www/current' 35 | ``` 36 | 37 | ### Cleanup Code 38 | 39 | Puma isn't able to understand all the resources that your app may use, so it provides a hook in the configuration file you pass to `-C` called `on_restart`. The block passed to `on_restart` will be called, unsurprisingly, just before Puma restarts itself. 40 | 41 | You should place code to close global log files, redis connections, etc. in this block so that their file descriptors don't leak into the restarted process. Failure to do so will result in slowly running out of descriptors and eventually obscure crashes as the server is restarted many times. 42 | -------------------------------------------------------------------------------- /test/test_web_server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (c) 2011 Evan Phoenix 3 | # Copyright (c) 2005 Zed A. Shaw 4 | 5 | require_relative "helper" 6 | 7 | require "puma/server" 8 | 9 | class TestHandler 10 | attr_reader :ran_test 11 | 12 | def call(env) 13 | @ran_test = true 14 | 15 | [200, {"Content-Type" => "text/plain"}, ["hello!"]] 16 | end 17 | end 18 | 19 | class WebServerTest < Minitest::Test 20 | VALID_REQUEST = "GET / HTTP/1.1\r\nHost: www.zedshaw.com\r\nContent-Type: text/plain\r\n\r\n" 21 | 22 | def setup 23 | @tester = TestHandler.new 24 | @server = Puma::Server.new @tester, Puma::Events.strings 25 | @server.add_tcp_listener "127.0.0.1", 0 26 | 27 | @server.run 28 | end 29 | 30 | def teardown 31 | @server.stop(true) 32 | end 33 | 34 | def test_simple_server 35 | hit(["http://127.0.0.1:#{@server.connected_port}/test"]) 36 | assert @tester.ran_test, "Handler didn't really run" 37 | end 38 | 39 | def test_trickle_attack 40 | socket = do_test(VALID_REQUEST, 3) 41 | assert_match "hello", socket.read 42 | socket.close 43 | end 44 | 45 | def test_close_client 46 | assert_raises IOError do 47 | do_test_raise(VALID_REQUEST, 10, 20) 48 | end 49 | end 50 | 51 | def test_bad_client 52 | socket = do_test("GET /test HTTP/BAD", 3) 53 | assert_match "Bad Request", socket.read 54 | socket.close 55 | end 56 | 57 | def test_header_is_too_long 58 | long = "GET /test HTTP/1.1\r\n" + ("X-Big: stuff\r\n" * 15000) + "\r\n" 59 | assert_raises Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EINVAL, IOError do 60 | do_test_raise(long, long.length/2, 10) 61 | end 62 | end 63 | 64 | # TODO: Why does this test take exactly 20 seconds? 65 | def test_file_streamed_request 66 | body = "a" * (Puma::Const::MAX_BODY * 2) 67 | long = "GET /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body 68 | socket = do_test(long, (Puma::Const::CHUNK_SIZE * 2) - 400) 69 | assert_match "hello", socket.read 70 | socket.close 71 | end 72 | 73 | private 74 | 75 | def do_test(string, chunk) 76 | # Do not use instance variables here, because it needs to be thread safe 77 | socket = TCPSocket.new("127.0.0.1", @server.connected_port); 78 | request = StringIO.new(string) 79 | chunks_out = 0 80 | 81 | while data = request.read(chunk) 82 | chunks_out += socket.write(data) 83 | socket.flush 84 | end 85 | socket 86 | end 87 | 88 | def do_test_raise(string, chunk, close_after = nil) 89 | # Do not use instance variables here, because it needs to be thread safe 90 | socket = TCPSocket.new("127.0.0.1", @server.connected_port); 91 | request = StringIO.new(string) 92 | chunks_out = 0 93 | 94 | while data = request.read(chunk) 95 | chunks_out += socket.write(data) 96 | socket.flush 97 | socket.close if close_after && chunks_out > close_after 98 | end 99 | 100 | socket.write(" ") # Some platforms only raise the exception on attempted write 101 | socket.flush 102 | socket 103 | ensure 104 | socket.close unless socket.closed? 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /test/test_binder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helper" 4 | 5 | require "puma/binder" 6 | require "puma/puma_http11" 7 | 8 | class TestBinderBase < Minitest::Test 9 | def setup 10 | @events = Puma::Events.null 11 | @binder = Puma::Binder.new(@events) 12 | @key = File.expand_path "../../examples/puma/puma_keypair.pem", __FILE__ 13 | @cert = File.expand_path "../../examples/puma/cert_puma.pem", __FILE__ 14 | end 15 | 16 | private 17 | 18 | def ssl_context_for_binder(binder) 19 | binder.instance_variable_get(:@ios)[0].instance_variable_get(:@ctx) 20 | end 21 | end 22 | 23 | class TestBinder < TestBinderBase 24 | def test_localhost_addresses_dont_alter_listeners_for_tcp_addresses 25 | @binder.parse(["tcp://localhost:10001"], @events) 26 | 27 | assert_equal [], @binder.listeners 28 | end 29 | end 30 | 31 | class TestBinderJRuby < TestBinderBase 32 | def setup 33 | super 34 | skip_unless :jruby 35 | end 36 | 37 | def test_binder_parses_jruby_ssl_options 38 | keystore = File.expand_path "../../examples/puma/keystore.jks", __FILE__ 39 | ssl_cipher_list = "TLS_DHE_RSA_WITH_DES_CBC_SHA,TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA" 40 | 41 | @binder.parse(["ssl://0.0.0.0:8080?keystore=#{keystore}&keystore-pass=&ssl_cipher_list=#{ssl_cipher_list}"], @events) 42 | 43 | assert_equal keystore, ssl_context_for_binder(@binder).keystore 44 | assert_equal ssl_cipher_list, ssl_context_for_binder(@binder).ssl_cipher_list 45 | end 46 | end 47 | 48 | class TestBinderMRI < TestBinderBase 49 | def setup 50 | super 51 | skip_on :jruby 52 | end 53 | 54 | def test_localhost_addresses_dont_alter_listeners_for_ssl_addresses 55 | @binder.parse(["ssl://localhost:10002?key=#{@key}&cert=#{@cert}"], @events) 56 | 57 | assert_equal [], @binder.listeners 58 | end 59 | 60 | def test_binder_parses_ssl_cipher_filter 61 | ssl_cipher_filter = "AES@STRENGTH" 62 | 63 | @binder.parse(["ssl://0.0.0.0?key=#{@key}&cert=#{@cert}&ssl_cipher_filter=#{ssl_cipher_filter}"], @events) 64 | 65 | assert_equal ssl_cipher_filter, ssl_context_for_binder(@binder).ssl_cipher_filter 66 | end 67 | 68 | def test_binder_parses_tlsv1_disabled 69 | @binder.parse(["ssl://0.0.0.0?key=#{@key}&cert=#{@cert}&no_tlsv1=true"], @events) 70 | 71 | assert ssl_context_for_binder(@binder).no_tlsv1 72 | end 73 | 74 | def test_binder_parses_tlsv1_enabled 75 | @binder.parse(["ssl://0.0.0.0?key=#{@key}&cert=#{@cert}&no_tlsv1=false"], @events) 76 | 77 | refute ssl_context_for_binder(@binder).no_tlsv1 78 | end 79 | 80 | def test_binder_parses_tlsv1_tlsv1_1_unspecified_defaults_to_enabled 81 | @binder.parse(["ssl://0.0.0.0?key=#{@key}&cert=#{@cert}"], @events) 82 | 83 | refute ssl_context_for_binder(@binder).no_tlsv1 84 | refute ssl_context_for_binder(@binder).no_tlsv1_1 85 | end 86 | 87 | def test_binder_parses_tlsv1_1_disabled 88 | @binder.parse(["ssl://0.0.0.0?key=#{@key}&cert=#{@cert}&no_tlsv1_1=true"], @events) 89 | 90 | assert ssl_context_for_binder(@binder).no_tlsv1_1 91 | end 92 | 93 | def test_binder_parses_tlsv1_1_enabled 94 | @binder.parse(["ssl://0.0.0.0?key=#{@key}&cert=#{@cert}&no_tlsv1_1=false"], @events) 95 | 96 | refute ssl_context_for_binder(@binder).no_tlsv1_1 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /docs/signals.md: -------------------------------------------------------------------------------- 1 | The [unix signal](http://en.wikipedia.org/wiki/Unix_signal) is a method of sending messages between [processes](http://en.wikipedia.org/wiki/Process_(computing)). When a signal is sent, the operating system interrupts the target process's normal flow of execution. There are standard signals that are used to stop a process but there are also custom signals that can be used for other purposes. This document is an attempt to list all supported signals that Puma will respond to. In general, signals need only be sent to the master process of a cluster. 2 | 3 | ## Sending Signals 4 | 5 | If you are new to signals it can be useful to see how they can be used. When a process is created in a *nix like operating system it will have a [PID - or process identifier](http://en.wikipedia.org/wiki/Process_identifier) that can be used to send signals to the process. For demonstration we will create an infinitely running process by tailing a file: 6 | 7 | ```sh 8 | $ echo "foo" >> my.log 9 | $ irb 10 | > pid = Process.spawn 'tail -f my.log' 11 | ``` 12 | 13 | From here we can see that the tail process is running by using the `ps` command: 14 | 15 | ```sh 16 | $ ps aux | grep tail 17 | schneems 87152 0.0 0.0 2432772 492 s032 S+ 12:46PM 0:00.00 tail -f my.log 18 | ``` 19 | 20 | You can send a signal in Ruby using the [Process module](http://www.ruby-doc.org/core-2.1.1/Process.html#kill-method): 21 | 22 | ``` 23 | $ irb 24 | > puts pid 25 | => 87152 26 | Process.detach(pid) # http://ruby-doc.org/core-2.1.1/Process.html#method-c-detach 27 | Process.kill("TERM", pid) 28 | ``` 29 | 30 | Now you will see via `ps` that there is no more `tail` process. Sometimes when referring to signals the `SIG` prefix will be used for instance `SIGTERM` is equivalent to sending `TERM` via `Process.kill`. 31 | 32 | ## Puma Signals 33 | 34 | Puma cluster responds to these signals: 35 | 36 | - `TTIN` increment the worker count by 1 37 | - `TTOU` decrement the worker count by 1 38 | - `TERM` send `TERM` to worker. Worker will attempt to finish then exit. 39 | - `USR2` restart workers. This also reloads puma configuration file, if there is one. 40 | - `USR1` restart workers in phases, a rolling restart. This will not reload configuration file. 41 | - `HUP` reopen log files defined in stdout_redirect configuration parameter. If there is no stdout_redirect option provided it will behave like `INT` 42 | - `INT` equivalent of sending Ctrl-C to cluster. Will attempt to finish then exit. 43 | - `CHLD` 44 | 45 | ## Callbacks order in case of different signals 46 | 47 | ### Start application 48 | 49 | ``` 50 | puma configuration file reloaded, if there is one 51 | * Pruning Bundler environment 52 | puma configuration file reloaded, if there is one 53 | 54 | before_fork 55 | on_worker_fork 56 | after_worker_fork 57 | 58 | Gemfile in context 59 | 60 | on_worker_boot 61 | 62 | Code of the app is loaded and running 63 | ``` 64 | 65 | ### Send USR2 66 | 67 | ``` 68 | on_worker_shutdown 69 | on_restart 70 | 71 | puma configuration file reloaded, if there is one 72 | 73 | before_fork 74 | on_worker_fork 75 | after_worker_fork 76 | 77 | Gemfile in context 78 | 79 | on_worker_boot 80 | 81 | Code of the app is loaded and running 82 | ``` 83 | 84 | ### Send USR1 85 | 86 | ``` 87 | on_worker_shutdown 88 | on_worker_fork 89 | after_worker_fork 90 | 91 | Gemfile in context 92 | 93 | on_worker_boot 94 | 95 | Code of the app is loaded and running 96 | ``` 97 | -------------------------------------------------------------------------------- /lib/puma/single.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'puma/runner' 4 | require 'puma/detect' 5 | require 'puma/plugin' 6 | 7 | module Puma 8 | # This class is instantiated by the `Puma::Launcher` and used 9 | # to boot and serve a Ruby application when no puma "workers" are needed 10 | # i.e. only using "threaded" mode. For example `$ puma -t 1:5` 11 | # 12 | # At the core of this class is running an instance of `Puma::Server` which 13 | # gets created via the `start_server` method from the `Puma::Runner` class 14 | # that this inherits from. 15 | class Single < Runner 16 | def stats 17 | b = @server.backlog || 0 18 | r = @server.running || 0 19 | t = @server.pool_capacity || 0 20 | m = @server.max_threads || 0 21 | %Q!{ "started_at": "#{@started_at.utc.iso8601}", "backlog": #{b}, "running": #{r}, "pool_capacity": #{t}, "max_threads": #{m} }! 22 | end 23 | 24 | def restart 25 | @server.begin_restart 26 | end 27 | 28 | def stop 29 | @server.stop(false) if @server 30 | end 31 | 32 | def halt 33 | @server.halt 34 | end 35 | 36 | def stop_blocked 37 | log "- Gracefully stopping, waiting for requests to finish" 38 | @control.stop(true) if @control 39 | @server.stop(true) if @server 40 | end 41 | 42 | def jruby_daemon? 43 | daemon? and Puma.jruby? 44 | end 45 | 46 | def jruby_daemon_start 47 | require 'puma/jruby_restart' 48 | JRubyRestart.daemon_start(@restart_dir, @launcher.restart_args) 49 | end 50 | 51 | def run 52 | already_daemon = false 53 | 54 | if jruby_daemon? 55 | require 'puma/jruby_restart' 56 | 57 | if JRubyRestart.daemon? 58 | # load and bind before redirecting IO so errors show up on stdout/stderr 59 | load_and_bind 60 | redirect_io 61 | end 62 | 63 | already_daemon = JRubyRestart.daemon_init 64 | end 65 | 66 | output_header "single" 67 | 68 | if jruby_daemon? 69 | if already_daemon 70 | JRubyRestart.perm_daemonize 71 | else 72 | pid = nil 73 | 74 | Signal.trap "SIGUSR2" do 75 | log "* Started new process #{pid} as daemon..." 76 | 77 | # Must use exit! so we don't unwind and run the ensures 78 | # that will be run by the new child (such as deleting the 79 | # pidfile) 80 | exit!(true) 81 | end 82 | 83 | Signal.trap "SIGCHLD" do 84 | log "! Error starting new process as daemon, exiting" 85 | exit 1 86 | end 87 | 88 | jruby_daemon_start 89 | sleep 90 | end 91 | else 92 | if daemon? 93 | log "* Daemonizing..." 94 | Process.daemon(true) 95 | redirect_io 96 | end 97 | 98 | load_and_bind 99 | end 100 | 101 | Plugins.fire_background 102 | 103 | @launcher.write_state 104 | 105 | start_control 106 | 107 | @server = server = start_server 108 | 109 | unless daemon? 110 | log "Use Ctrl-C to stop" 111 | redirect_io 112 | end 113 | 114 | @launcher.events.fire_on_booted! 115 | 116 | begin 117 | server.run.join 118 | rescue Interrupt 119 | # Swallow it 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/puma/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'uri/common' 4 | 5 | module Puma 6 | module Util 7 | module_function 8 | 9 | def pipe 10 | IO.pipe 11 | end 12 | 13 | # Unescapes a URI escaped string with +encoding+. +encoding+ will be the 14 | # target encoding of the string returned, and it defaults to UTF-8 15 | if defined?(::Encoding) 16 | def unescape(s, encoding = Encoding::UTF_8) 17 | URI.decode_www_form_component(s, encoding) 18 | end 19 | else 20 | def unescape(s, encoding = nil) 21 | URI.decode_www_form_component(s, encoding) 22 | end 23 | end 24 | module_function :unescape 25 | 26 | DEFAULT_SEP = /[&;] */n 27 | 28 | # Stolen from Mongrel, with some small modifications: 29 | # Parses a query string by breaking it up at the '&' 30 | # and ';' characters. You can also use this to parse 31 | # cookies by changing the characters used in the second 32 | # parameter (which defaults to '&;'). 33 | def parse_query(qs, d = nil, &unescaper) 34 | unescaper ||= method(:unescape) 35 | 36 | params = {} 37 | 38 | (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p| 39 | next if p.empty? 40 | k, v = p.split('=', 2).map(&unescaper) 41 | 42 | if cur = params[k] 43 | if cur.class == Array 44 | params[k] << v 45 | else 46 | params[k] = [cur, v] 47 | end 48 | else 49 | params[k] = v 50 | end 51 | end 52 | 53 | return params 54 | end 55 | 56 | # A case-insensitive Hash that preserves the original case of a 57 | # header when set. 58 | class HeaderHash < Hash 59 | def self.new(hash={}) 60 | HeaderHash === hash ? hash : super(hash) 61 | end 62 | 63 | def initialize(hash={}) 64 | super() 65 | @names = {} 66 | hash.each { |k, v| self[k] = v } 67 | end 68 | 69 | def each 70 | super do |k, v| 71 | yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v) 72 | end 73 | end 74 | 75 | def to_hash 76 | hash = {} 77 | each { |k,v| hash[k] = v } 78 | hash 79 | end 80 | 81 | def [](k) 82 | super(k) || super(@names[k.downcase]) 83 | end 84 | 85 | def []=(k, v) 86 | canonical = k.downcase 87 | delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary 88 | @names[k] = @names[canonical] = k 89 | super k, v 90 | end 91 | 92 | def delete(k) 93 | canonical = k.downcase 94 | result = super @names.delete(canonical) 95 | @names.delete_if { |name,| name.downcase == canonical } 96 | result 97 | end 98 | 99 | def include?(k) 100 | @names.include?(k) || @names.include?(k.downcase) 101 | end 102 | 103 | alias_method :has_key?, :include? 104 | alias_method :member?, :include? 105 | alias_method :key?, :include? 106 | 107 | def merge!(other) 108 | other.each { |k, v| self[k] = v } 109 | self 110 | end 111 | 112 | def merge(other) 113 | hash = dup 114 | hash.merge! other 115 | end 116 | 117 | def replace(other) 118 | clear 119 | other.each { |k, v| self[k] = v } 120 | self 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /test/test_pumactl.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | require 'puma/control_cli' 4 | 5 | class TestPumaControlCli < Minitest::Test 6 | def setup 7 | # use a pipe to get info across thread boundary 8 | @wait, @ready = IO.pipe 9 | end 10 | 11 | def wait_booted 12 | line = @wait.gets until line =~ /Listening on/ 13 | end 14 | 15 | def teardown 16 | @wait.close 17 | @ready.close 18 | end 19 | 20 | def find_open_port 21 | server = TCPServer.new("127.0.0.1", 0) 22 | server.addr[1] 23 | ensure 24 | server.close 25 | end 26 | 27 | def test_config_file 28 | control_cli = Puma::ControlCLI.new ["--config-file", "test/config/state_file_testing_config.rb", "halt"] 29 | assert_equal "t3-pid", control_cli.instance_variable_get("@pidfile") 30 | end 31 | 32 | def test_environment 33 | ENV.delete 'RACK_ENV' # remove from travis 34 | control_cli = Puma::ControlCLI.new ["halt"] 35 | assert_equal "development", control_cli.instance_variable_get("@environment") 36 | control_cli = Puma::ControlCLI.new ["-e", "test", "halt"] 37 | assert_equal "test", control_cli.instance_variable_get("@environment") 38 | end 39 | 40 | def test_config_file_exist 41 | ENV.delete 'RACK_ENV' # remove from travis 42 | port = 6001 43 | Dir.mktmpdir do |d| 44 | Dir.chdir(d) do 45 | FileUtils.mkdir("config") 46 | File.open("config/puma.rb", "w") { |f| f << "port #{port}" } 47 | control_cli = Puma::ControlCLI.new ["halt"] 48 | assert_equal "config/puma.rb", 49 | control_cli.instance_variable_get("@config_file") 50 | end 51 | end 52 | Dir.mktmpdir do |d| 53 | Dir.chdir(d) do 54 | FileUtils.mkdir_p("config/puma") 55 | File.open("config/puma/development.rb", "w") { |f| f << "port #{port}" } 56 | control_cli = Puma::ControlCLI.new ["halt"] 57 | assert_equal "config/puma/development.rb", 58 | control_cli.instance_variable_get("@config_file") 59 | end 60 | end 61 | end 62 | 63 | def test_control_no_token 64 | opts = [ 65 | "--config-file", "test/config/control_no_token.rb", 66 | "start" 67 | ] 68 | 69 | control_cli = Puma::ControlCLI.new opts, @ready, @ready 70 | assert_equal 'none', control_cli.instance_variable_get("@control_auth_token") 71 | end 72 | 73 | def test_control_url_and_status 74 | host = "127.0.0.1" 75 | port = find_open_port 76 | url = "tcp://#{host}:#{port}/" 77 | 78 | opts = [ 79 | "--control-url", url, 80 | "--control-token", "ctrl", 81 | "--config-file", "test/config/app.rb", 82 | ] 83 | 84 | control_cli = Puma::ControlCLI.new (opts + ["start"]), @ready, @ready 85 | t = Thread.new do 86 | Thread.current.abort_on_exception = true 87 | control_cli.run 88 | end 89 | 90 | wait_booted 91 | 92 | s = TCPSocket.new host, 9292 93 | s << "GET / HTTP/1.0\r\n\r\n" 94 | body = s.read 95 | assert_match "200 OK", body 96 | assert_match "embedded app", body 97 | 98 | status_cmd = Puma::ControlCLI.new(opts + ["status"]) 99 | out, _ = capture_subprocess_io do 100 | status_cmd.run 101 | end 102 | assert_match "Puma is started\n", out 103 | 104 | shutdown_cmd = Puma::ControlCLI.new(opts + ["halt"]) 105 | out, _ = capture_subprocess_io do 106 | shutdown_cmd.run 107 | end 108 | assert_match "Command halt sent success\n", out 109 | 110 | assert_kind_of Thread, t.join, "server didn't stop" 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at nate.berkopec@speedshop.co, 59 | richard.schneeman+no-recruiters@gmail.com, or evan@phx.io. All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an incident. 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | 76 | For answers to common questions about this code of conduct, see 77 | https://www.contributor-covenant.org/faq 78 | -------------------------------------------------------------------------------- /lib/puma/commonlogger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Puma 4 | # Rack::CommonLogger forwards every request to the given +app+, and 5 | # logs a line in the 6 | # {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common] 7 | # to the +logger+. 8 | # 9 | # If +logger+ is nil, CommonLogger will fall back +rack.errors+, which is 10 | # an instance of Rack::NullLogger. 11 | # 12 | # +logger+ can be any class, including the standard library Logger, and is 13 | # expected to have either +write+ or +<<+ method, which accepts the CommonLogger::FORMAT. 14 | # According to the SPEC, the error stream must also respond to +puts+ 15 | # (which takes a single argument that responds to +to_s+), and +flush+ 16 | # (which is called without arguments in order to make the error appear for 17 | # sure) 18 | class CommonLogger 19 | # Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common 20 | # 21 | # lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 - 22 | # 23 | # %{%s - %s [%s] "%s %s%s %s" %d %s\n} % 24 | FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n} 25 | 26 | HIJACK_FORMAT = %{%s - %s [%s] "%s %s%s %s" HIJACKED -1 %0.4f\n} 27 | 28 | CONTENT_LENGTH = 'Content-Length'.freeze 29 | PATH_INFO = 'PATH_INFO'.freeze 30 | QUERY_STRING = 'QUERY_STRING'.freeze 31 | REQUEST_METHOD = 'REQUEST_METHOD'.freeze 32 | 33 | def initialize(app, logger=nil) 34 | @app = app 35 | @logger = logger 36 | end 37 | 38 | def call(env) 39 | began_at = Time.now 40 | status, header, body = @app.call(env) 41 | header = Util::HeaderHash.new(header) 42 | 43 | # If we've been hijacked, then output a special line 44 | if env['rack.hijack_io'] 45 | log_hijacking(env, 'HIJACK', header, began_at) 46 | else 47 | ary = env['rack.after_reply'] 48 | ary << lambda { log(env, status, header, began_at) } 49 | end 50 | 51 | [status, header, body] 52 | end 53 | 54 | private 55 | 56 | def log_hijacking(env, status, header, began_at) 57 | now = Time.now 58 | 59 | msg = HIJACK_FORMAT % [ 60 | env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-", 61 | env["REMOTE_USER"] || "-", 62 | now.strftime("%d/%b/%Y %H:%M:%S"), 63 | env[REQUEST_METHOD], 64 | env[PATH_INFO], 65 | env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}", 66 | env["HTTP_VERSION"], 67 | now - began_at ] 68 | 69 | write(msg) 70 | end 71 | 72 | def log(env, status, header, began_at) 73 | now = Time.now 74 | length = extract_content_length(header) 75 | 76 | msg = FORMAT % [ 77 | env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-", 78 | env["REMOTE_USER"] || "-", 79 | now.strftime("%d/%b/%Y:%H:%M:%S %z"), 80 | env[REQUEST_METHOD], 81 | env[PATH_INFO], 82 | env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}", 83 | env["HTTP_VERSION"], 84 | status.to_s[0..3], 85 | length, 86 | now - began_at ] 87 | 88 | write(msg) 89 | end 90 | 91 | def write(msg) 92 | logger = @logger || env['rack.errors'] 93 | 94 | # Standard library logger doesn't support write but it supports << which actually 95 | # calls to write on the log device without formatting 96 | if logger.respond_to?(:write) 97 | logger.write(msg) 98 | else 99 | logger << msg 100 | end 101 | end 102 | 103 | def extract_content_length(headers) 104 | value = headers[CONTENT_LENGTH] or return '-' 105 | value.to_s == '0' ? '-' : value 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Copyright (c) 2011 Evan Phoenix 3 | # Copyright (c) 2005 Zed A. Shaw 4 | 5 | if %w(2.2.7 2.2.8 2.2.9 2.2.10 2.3.4 2.4.1).include? RUBY_VERSION 6 | begin 7 | require 'stopgap_13632' 8 | rescue LoadError 9 | puts "For test stability, you must install the stopgap_13632 gem." 10 | exit(1) 11 | end 12 | end 13 | 14 | require "net/http" 15 | require "timeout" 16 | require "minitest/autorun" 17 | require "minitest/pride" 18 | require "minitest/proveit" 19 | require_relative "helpers/apps" 20 | 21 | $LOAD_PATH << File.expand_path("../../lib", __FILE__) 22 | Thread.abort_on_exception = true 23 | 24 | require "puma" 25 | require "puma/events" 26 | require "puma/detect" 27 | 28 | # Either takes a string to do a get request against, or a tuple of [URI, HTTP] where 29 | # HTTP is some kind of Net::HTTP request object (POST, HEAD, etc.) 30 | def hit(uris) 31 | uris.map do |u| 32 | response = 33 | if u.kind_of? String 34 | Net::HTTP.get(URI.parse(u)) 35 | else 36 | url = URI.parse(u[0]) 37 | Net::HTTP.new(url.host, url.port).start {|h| h.request(u[1]) } 38 | end 39 | 40 | assert response, "Didn't get a response: #{u}" 41 | response 42 | end 43 | end 44 | 45 | module UniquePort 46 | @port = 3211 47 | @mutex = Mutex.new 48 | 49 | def self.call 50 | @mutex.synchronize { @port += 1 } 51 | end 52 | end 53 | 54 | module TimeoutEveryTestCase 55 | # our own subclass so we never confused different timeouts 56 | class TestTookTooLong < Timeout::Error 57 | end 58 | 59 | def run(*) 60 | ::Timeout.timeout(Puma.jruby? ? 120 : 60, TestTookTooLong) { super } 61 | end 62 | end 63 | 64 | if ENV['CI'] 65 | Minitest::Test.prepend TimeoutEveryTestCase 66 | 67 | require 'minitest/retry' 68 | Minitest::Retry.use! 69 | end 70 | 71 | module TestSkips 72 | 73 | # usage: skip NO_FORK_MSG unless HAS_FORK 74 | # windows >= 2.6 fork is not defined, < 2.6 fork raises NotImplementedError 75 | HAS_FORK = ::Process.respond_to? :fork 76 | NO_FORK_MSG = "Kernel.fork isn't available on the #{RUBY_PLATFORM} platform" 77 | 78 | # socket is required by puma 79 | # usage: skip UNIX_SKT_MSG unless UNIX_SKT_EXIST 80 | UNIX_SKT_EXIST = Object.const_defined? :UNIXSocket 81 | UNIX_SKT_MSG = "UnixSockets aren't available on the #{RUBY_PLATFORM} platform" 82 | 83 | # usage: skip_unless_signal_exist? :USR2 84 | def skip_unless_signal_exist?(sig, bt: caller) 85 | signal = sig.to_s 86 | unless Signal.list.key? signal 87 | skip "Signal #{signal} isn't available on the #{RUBY_PLATFORM} platform", bt 88 | end 89 | end 90 | 91 | # called with one or more params, like skip_on :jruby, :windows 92 | # optional suffix kwarg is appended to the skip message 93 | # optional suffix bt should generally not used 94 | def skip_on(*engs, suffix: '', bt: caller) 95 | skip_msg = false 96 | engs.each do |eng| 97 | skip_msg = case eng 98 | when :jruby then "Skipped on JRuby#{suffix}" if Puma.jruby? 99 | when :windows then "Skipped on Windows#{suffix}" if Puma.windows? 100 | when :appveyor then "Skipped on Appveyor#{suffix}" if ENV["APPVEYOR"] 101 | when :ci then "Skipped on ENV['CI']#{suffix}" if ENV["CI"] 102 | else false 103 | end 104 | skip skip_msg, bt if skip_msg 105 | end 106 | end 107 | 108 | # called with only one param 109 | def skip_unless(eng, bt: caller) 110 | skip_msg = case eng 111 | when :jruby then "Skip unless JRuby" unless Puma.jruby? 112 | when :windows then "Skip unless Windows" unless Puma.windows? 113 | else false 114 | end 115 | skip skip_msg, bt if skip_msg 116 | end 117 | end 118 | 119 | Minitest::Test.include TestSkips 120 | 121 | class Minitest::Test 122 | def self.run(reporter, options = {}) # :nodoc: 123 | prove_it! 124 | super 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /ext/puma_http11/io_buffer.c: -------------------------------------------------------------------------------- 1 | #define RSTRING_NOT_MODIFIED 1 2 | #include "ruby.h" 3 | 4 | #include 5 | 6 | struct buf_int { 7 | uint8_t* top; 8 | uint8_t* cur; 9 | 10 | size_t size; 11 | }; 12 | 13 | #define BUF_DEFAULT_SIZE 4096 14 | #define BUF_TOLERANCE 32 15 | 16 | static void buf_free(struct buf_int* internal) { 17 | xfree(internal->top); 18 | xfree(internal); 19 | } 20 | 21 | static VALUE buf_alloc(VALUE self) { 22 | VALUE buf; 23 | struct buf_int* internal; 24 | 25 | buf = Data_Make_Struct(self, struct buf_int, 0, buf_free, internal); 26 | 27 | internal->size = BUF_DEFAULT_SIZE; 28 | internal->top = ALLOC_N(uint8_t, BUF_DEFAULT_SIZE); 29 | internal->cur = internal->top; 30 | 31 | return buf; 32 | } 33 | 34 | static VALUE buf_append(VALUE self, VALUE str) { 35 | struct buf_int* b; 36 | size_t used, str_len, new_size; 37 | 38 | Data_Get_Struct(self, struct buf_int, b); 39 | 40 | used = b->cur - b->top; 41 | 42 | StringValue(str); 43 | str_len = RSTRING_LEN(str); 44 | 45 | new_size = used + str_len; 46 | 47 | if(new_size > b->size) { 48 | size_t n = b->size + (b->size / 2); 49 | uint8_t* top; 50 | uint8_t* old; 51 | 52 | new_size = (n > new_size ? n : new_size + BUF_TOLERANCE); 53 | 54 | top = ALLOC_N(uint8_t, new_size); 55 | old = b->top; 56 | memcpy(top, old, used); 57 | b->top = top; 58 | b->cur = top + used; 59 | b->size = new_size; 60 | xfree(old); 61 | } 62 | 63 | memcpy(b->cur, RSTRING_PTR(str), str_len); 64 | b->cur += str_len; 65 | 66 | return self; 67 | } 68 | 69 | static VALUE buf_append2(int argc, VALUE* argv, VALUE self) { 70 | struct buf_int* b; 71 | size_t used, new_size; 72 | int i; 73 | VALUE str; 74 | 75 | Data_Get_Struct(self, struct buf_int, b); 76 | 77 | used = b->cur - b->top; 78 | new_size = used; 79 | 80 | for(i = 0; i < argc; i++) { 81 | StringValue(argv[i]); 82 | 83 | str = argv[i]; 84 | 85 | new_size += RSTRING_LEN(str); 86 | } 87 | 88 | if(new_size > b->size) { 89 | size_t n = b->size + (b->size / 2); 90 | uint8_t* top; 91 | uint8_t* old; 92 | 93 | new_size = (n > new_size ? n : new_size + BUF_TOLERANCE); 94 | 95 | top = ALLOC_N(uint8_t, new_size); 96 | old = b->top; 97 | memcpy(top, old, used); 98 | b->top = top; 99 | b->cur = top + used; 100 | b->size = new_size; 101 | xfree(old); 102 | } 103 | 104 | for(i = 0; i < argc; i++) { 105 | long str_len; 106 | str = argv[i]; 107 | str_len = RSTRING_LEN(str); 108 | memcpy(b->cur, RSTRING_PTR(str), str_len); 109 | b->cur += str_len; 110 | } 111 | 112 | return self; 113 | } 114 | 115 | static VALUE buf_to_str(VALUE self) { 116 | struct buf_int* b; 117 | Data_Get_Struct(self, struct buf_int, b); 118 | 119 | return rb_str_new((const char*)(b->top), b->cur - b->top); 120 | } 121 | 122 | static VALUE buf_used(VALUE self) { 123 | struct buf_int* b; 124 | Data_Get_Struct(self, struct buf_int, b); 125 | 126 | return INT2FIX(b->cur - b->top); 127 | } 128 | 129 | static VALUE buf_capa(VALUE self) { 130 | struct buf_int* b; 131 | Data_Get_Struct(self, struct buf_int, b); 132 | 133 | return INT2FIX(b->size); 134 | } 135 | 136 | static VALUE buf_reset(VALUE self) { 137 | struct buf_int* b; 138 | Data_Get_Struct(self, struct buf_int, b); 139 | 140 | b->cur = b->top; 141 | return self; 142 | } 143 | 144 | void Init_io_buffer(VALUE puma) { 145 | VALUE buf = rb_define_class_under(puma, "IOBuffer", rb_cObject); 146 | 147 | rb_define_alloc_func(buf, buf_alloc); 148 | rb_define_method(buf, "<<", buf_append, 1); 149 | rb_define_method(buf, "append", buf_append2, -1); 150 | rb_define_method(buf, "to_str", buf_to_str, 0); 151 | rb_define_method(buf, "to_s", buf_to_str, 0); 152 | rb_define_method(buf, "used", buf_used, 0); 153 | rb_define_method(buf, "capacity", buf_capa, 0); 154 | rb_define_method(buf, "reset", buf_reset, 0); 155 | } 156 | -------------------------------------------------------------------------------- /lib/rack/handler/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rack/handler' 4 | 5 | module Rack 6 | module Handler 7 | module Puma 8 | DEFAULT_OPTIONS = { 9 | :Verbose => false, 10 | :Silent => false 11 | } 12 | 13 | def self.config(app, options = {}) 14 | require 'puma' 15 | require 'puma/configuration' 16 | require 'puma/events' 17 | require 'puma/launcher' 18 | 19 | default_options = DEFAULT_OPTIONS.dup 20 | 21 | # Libraries pass in values such as :Port and there is no way to determine 22 | # if it is a default provided by the library or a special value provided 23 | # by the user. A special key `user_supplied_options` can be passed. This 24 | # contains an array of all explicitly defined user options. We then 25 | # know that all other values are defaults 26 | if user_supplied_options = options.delete(:user_supplied_options) 27 | (options.keys - user_supplied_options).each do |k| 28 | default_options[k] = options.delete(k) 29 | end 30 | end 31 | 32 | conf = ::Puma::Configuration.new(options, default_options) do |user_config, file_config, default_config| 33 | user_config.quiet 34 | 35 | if options.delete(:Verbose) 36 | app = Rack::CommonLogger.new(app, STDOUT) 37 | end 38 | 39 | if options[:environment] 40 | user_config.environment options[:environment] 41 | end 42 | 43 | if options[:Threads] 44 | min, max = options.delete(:Threads).split(':', 2) 45 | user_config.threads min, max 46 | end 47 | 48 | if options[:Host] || options[:Port] 49 | host = options[:Host] || default_options[:Host] 50 | port = options[:Port] || default_options[:Port] 51 | self.set_host_port_to_config(host, port, user_config) 52 | end 53 | 54 | if default_options[:Host] 55 | file_config.set_default_host(default_options[:Host]) 56 | end 57 | self.set_host_port_to_config(default_options[:Host], default_options[:Port], default_config) 58 | 59 | user_config.app app 60 | end 61 | conf 62 | end 63 | 64 | def self.run(app, options = {}) 65 | conf = self.config(app, options) 66 | 67 | events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio 68 | 69 | launcher = ::Puma::Launcher.new(conf, :events => events) 70 | 71 | yield launcher if block_given? 72 | begin 73 | launcher.run 74 | rescue Interrupt 75 | puts "* Gracefully stopping, waiting for requests to finish" 76 | launcher.stop 77 | puts "* Goodbye!" 78 | end 79 | end 80 | 81 | def self.valid_options 82 | { 83 | "Host=HOST" => "Hostname to listen on (default: localhost)", 84 | "Port=PORT" => "Port to listen on (default: 8080)", 85 | "Threads=MIN:MAX" => "min:max threads to use (default 0:16)", 86 | "Verbose" => "Don't report each request (default: false)" 87 | } 88 | end 89 | 90 | def self.set_host_port_to_config(host, port, config) 91 | config.clear_binds! if host || port 92 | 93 | if host && (host[0,1] == '.' || host[0,1] == '/') 94 | config.bind "unix://#{host}" 95 | elsif host && host =~ /^ssl:\/\// 96 | uri = URI.parse(host) 97 | uri.port ||= port || ::Puma::Configuration::DefaultTCPPort 98 | config.bind uri.to_s 99 | else 100 | 101 | if host 102 | port ||= ::Puma::Configuration::DefaultTCPPort 103 | end 104 | 105 | if port 106 | host ||= ::Puma::Configuration::DefaultTCPHost 107 | config.port port, host 108 | end 109 | end 110 | end 111 | end 112 | 113 | register :puma, Puma 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /ext/puma_http11/http11_parser.rl: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2005 Zed A. Shaw 3 | * You can redistribute it and/or modify it under the same terms as Ruby. 4 | * License 3-clause BSD 5 | */ 6 | #include "http11_parser.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | /* 14 | * capitalizes all lower-case ASCII characters, 15 | * converts dashes to underscores. 16 | */ 17 | static void snake_upcase_char(char *c) 18 | { 19 | if (*c >= 'a' && *c <= 'z') 20 | *c &= ~0x20; 21 | else if (*c == '-') 22 | *c = '_'; 23 | } 24 | 25 | #define LEN(AT, FPC) (FPC - buffer - parser->AT) 26 | #define MARK(M,FPC) (parser->M = (FPC) - buffer) 27 | #define PTR_TO(F) (buffer + parser->F) 28 | 29 | /** Machine **/ 30 | 31 | %%{ 32 | 33 | machine puma_parser; 34 | 35 | action mark { MARK(mark, fpc); } 36 | 37 | 38 | action start_field { MARK(field_start, fpc); } 39 | action snake_upcase_field { snake_upcase_char((char *)fpc); } 40 | action write_field { 41 | parser->field_len = LEN(field_start, fpc); 42 | } 43 | 44 | action start_value { MARK(mark, fpc); } 45 | action write_value { 46 | parser->http_field(parser, PTR_TO(field_start), parser->field_len, PTR_TO(mark), LEN(mark, fpc)); 47 | } 48 | action request_method { 49 | parser->request_method(parser, PTR_TO(mark), LEN(mark, fpc)); 50 | } 51 | action request_uri { 52 | parser->request_uri(parser, PTR_TO(mark), LEN(mark, fpc)); 53 | } 54 | action fragment { 55 | parser->fragment(parser, PTR_TO(mark), LEN(mark, fpc)); 56 | } 57 | 58 | action start_query { MARK(query_start, fpc); } 59 | action query_string { 60 | parser->query_string(parser, PTR_TO(query_start), LEN(query_start, fpc)); 61 | } 62 | 63 | action http_version { 64 | parser->http_version(parser, PTR_TO(mark), LEN(mark, fpc)); 65 | } 66 | 67 | action request_path { 68 | parser->request_path(parser, PTR_TO(mark), LEN(mark,fpc)); 69 | } 70 | 71 | action done { 72 | parser->body_start = fpc - buffer + 1; 73 | parser->header_done(parser, fpc + 1, pe - fpc - 1); 74 | fbreak; 75 | } 76 | 77 | include puma_parser_common "http11_parser_common.rl"; 78 | 79 | }%% 80 | 81 | /** Data **/ 82 | %% write data; 83 | 84 | int puma_parser_init(puma_parser *parser) { 85 | int cs = 0; 86 | %% write init; 87 | parser->cs = cs; 88 | parser->body_start = 0; 89 | parser->content_len = 0; 90 | parser->mark = 0; 91 | parser->nread = 0; 92 | parser->field_len = 0; 93 | parser->field_start = 0; 94 | parser->request = Qnil; 95 | parser->body = Qnil; 96 | 97 | return 1; 98 | } 99 | 100 | 101 | /** exec **/ 102 | size_t puma_parser_execute(puma_parser *parser, const char *buffer, size_t len, size_t off) { 103 | const char *p, *pe; 104 | int cs = parser->cs; 105 | 106 | assert(off <= len && "offset past end of buffer"); 107 | 108 | p = buffer+off; 109 | pe = buffer+len; 110 | 111 | /* assert(*pe == '\0' && "pointer does not end on NUL"); */ 112 | assert((size_t) (pe - p) == len - off && "pointers aren't same distance"); 113 | 114 | %% write exec; 115 | 116 | if (!puma_parser_has_error(parser)) 117 | parser->cs = cs; 118 | parser->nread += p - (buffer + off); 119 | 120 | assert(p <= pe && "buffer overflow after parsing execute"); 121 | assert(parser->nread <= len && "nread longer than length"); 122 | assert(parser->body_start <= len && "body starts after buffer end"); 123 | assert(parser->mark < len && "mark is after buffer end"); 124 | assert(parser->field_len <= len && "field has length longer than whole buffer"); 125 | assert(parser->field_start < len && "field starts after buffer end"); 126 | 127 | return(parser->nread); 128 | } 129 | 130 | int puma_parser_finish(puma_parser *parser) 131 | { 132 | if (puma_parser_has_error(parser) ) { 133 | return -1; 134 | } else if (puma_parser_is_finished(parser) ) { 135 | return 1; 136 | } else { 137 | return 0; 138 | } 139 | } 140 | 141 | int puma_parser_has_error(puma_parser *parser) { 142 | return parser->cs == puma_parser_error; 143 | } 144 | 145 | int puma_parser_is_finished(puma_parser *parser) { 146 | return parser->cs >= puma_parser_first_final; 147 | } 148 | -------------------------------------------------------------------------------- /lib/puma/events.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'puma/const' 4 | require "puma/null_io" 5 | require 'stringio' 6 | 7 | module Puma 8 | # The default implement of an event sink object used by Server 9 | # for when certain kinds of events occur in the life of the server. 10 | # 11 | # The methods available are the events that the Server fires. 12 | # 13 | class Events 14 | class DefaultFormatter 15 | def call(str) 16 | str 17 | end 18 | end 19 | 20 | class PidFormatter 21 | def call(str) 22 | "[#{$$}] #{str}" 23 | end 24 | end 25 | 26 | include Const 27 | 28 | # Create an Events object that prints to +stdout+ and +stderr+. 29 | # 30 | def initialize(stdout, stderr) 31 | @formatter = DefaultFormatter.new 32 | @stdout = stdout.dup 33 | @stderr = stderr.dup 34 | 35 | @stdout.sync = true 36 | @stderr.sync = true 37 | 38 | @debug = ENV.key? 'PUMA_DEBUG' 39 | 40 | @hooks = Hash.new { |h,k| h[k] = [] } 41 | end 42 | 43 | attr_reader :stdout, :stderr 44 | attr_accessor :formatter 45 | 46 | # Fire callbacks for the named hook 47 | # 48 | def fire(hook, *args) 49 | @hooks[hook].each { |t| t.call(*args) } 50 | end 51 | 52 | # Register a callback for a given hook 53 | # 54 | def register(hook, obj=nil, &blk) 55 | if obj and blk 56 | raise "Specify either an object or a block, not both" 57 | end 58 | 59 | h = obj || blk 60 | 61 | @hooks[hook] << h 62 | 63 | h 64 | end 65 | 66 | # Write +str+ to +@stdout+ 67 | # 68 | def log(str) 69 | @stdout.puts format(str) 70 | end 71 | 72 | def write(str) 73 | @stdout.write format(str) 74 | end 75 | 76 | def debug(str) 77 | log("% #{str}") if @debug 78 | end 79 | 80 | # Write +str+ to +@stderr+ 81 | # 82 | def error(str) 83 | @stderr.puts format("ERROR: #{str}") 84 | exit 1 85 | end 86 | 87 | def format(str) 88 | formatter.call(str) 89 | end 90 | 91 | # An HTTP parse error has occurred. 92 | # +server+ is the Server object, +env+ the request, and +error+ a 93 | # parsing exception. 94 | # 95 | def parse_error(server, env, error) 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" 100 | end 101 | 102 | # An SSL error has occurred. 103 | # +server+ is the Server object, +peeraddr+ peer address, +peercert+ 104 | # any peer certificate (if present), and +error+ an exception object. 105 | # 106 | def ssl_error(server, peeraddr, peercert, error) 107 | subject = peercert ? peercert.subject : nil 108 | @stderr.puts "#{Time.now}: SSL error, peer: #{peeraddr}, peer cert: #{subject}, #{error.inspect}" 109 | end 110 | 111 | # An unknown error has occurred. 112 | # +server+ is the Server object, +error+ an exception object, 113 | # +kind+ some additional info, and +env+ the request. 114 | # 115 | def unknown_error(server, error, kind="Unknown", env=nil) 116 | if error.respond_to? :render 117 | error.render "#{Time.now}: #{kind} error", @stderr 118 | else 119 | if env 120 | string_block = [ "#{Time.now}: #{kind} error handling request { #{env['REQUEST_METHOD']} #{env['PATH_INFO']} }" ] 121 | string_block << error.inspect 122 | else 123 | string_block = [ "#{Time.now}: #{kind} error: #{error.inspect}" ] 124 | end 125 | string_block << error.backtrace 126 | @stderr.puts string_block.join("\n") 127 | end 128 | end 129 | 130 | def on_booted(&block) 131 | register(:on_booted, &block) 132 | end 133 | 134 | def fire_on_booted! 135 | fire(:on_booted) 136 | end 137 | 138 | DEFAULT = new(STDOUT, STDERR) 139 | 140 | # Returns an Events object which writes its status to 2 StringIO 141 | # objects. 142 | # 143 | def self.strings 144 | Events.new StringIO.new, StringIO.new 145 | end 146 | 147 | def self.stdio 148 | Events.new $stdout, $stderr 149 | end 150 | 151 | def self.null 152 | n = NullIO.new 153 | Events.new n, n 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /test/test_events.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | class TestEvents < Minitest::Test 4 | def test_null 5 | events = Puma::Events.null 6 | 7 | assert_instance_of Puma::NullIO, events.stdout 8 | assert_instance_of Puma::NullIO, events.stderr 9 | end 10 | 11 | def test_strings 12 | events = Puma::Events.strings 13 | 14 | assert_instance_of StringIO, events.stdout 15 | assert_instance_of StringIO, events.stderr 16 | end 17 | 18 | def test_stdio 19 | events = Puma::Events.stdio 20 | 21 | # events.stdout is a dup, so same file handle, different ruby object, but inspect should show the same file handle 22 | assert_equal STDOUT.inspect, events.stdout.inspect 23 | assert_equal STDERR.inspect, events.stderr.inspect 24 | end 25 | 26 | def test_stdio_respects_sync 27 | STDOUT.sync = false 28 | events = Puma::Events.stdio 29 | 30 | assert !STDOUT.sync 31 | assert events.stdout.sync 32 | end 33 | 34 | def test_register_callback_with_block 35 | res = false 36 | 37 | events = Puma::Events.null 38 | 39 | events.register(:exec) { res = true } 40 | 41 | events.fire(:exec) 42 | 43 | assert_equal true, res 44 | end 45 | 46 | def test_register_callback_with_object 47 | obj = Object.new 48 | 49 | def obj.res 50 | @res || false 51 | end 52 | 53 | def obj.call 54 | @res = true 55 | end 56 | 57 | events = Puma::Events.null 58 | 59 | events.register(:exec, obj) 60 | 61 | events.fire(:exec) 62 | 63 | assert_equal true, obj.res 64 | end 65 | 66 | def test_fire_callback_with_multiple_arguments 67 | res = [] 68 | 69 | events = Puma::Events.null 70 | 71 | events.register(:exec) { |*args| res.concat(args) } 72 | 73 | events.fire(:exec, :foo, :bar, :baz) 74 | 75 | assert_equal [:foo, :bar, :baz], res 76 | end 77 | 78 | def test_on_booted_callback 79 | res = false 80 | 81 | events = Puma::Events.null 82 | 83 | events.on_booted { res = true } 84 | 85 | events.fire_on_booted! 86 | 87 | assert res 88 | end 89 | 90 | def test_log_writes_to_stdout 91 | out, _ = capture_io do 92 | Puma::Events.stdio.log("ready") 93 | end 94 | 95 | assert_equal "ready\n", out 96 | end 97 | 98 | def test_write_writes_to_stdout 99 | out, _ = capture_io do 100 | Puma::Events.stdio.write("ready") 101 | end 102 | 103 | assert_equal "ready", out 104 | end 105 | 106 | def test_debug_writes_to_stdout_if_env_is_present 107 | original_debug, ENV["PUMA_DEBUG"] = ENV["PUMA_DEBUG"], "1" 108 | 109 | out, _ = capture_io do 110 | Puma::Events.stdio.debug("ready") 111 | end 112 | 113 | assert_equal "% ready\n", out 114 | ensure 115 | ENV["PUMA_DEBUG"] = original_debug 116 | end 117 | 118 | def test_debug_not_write_to_stdout_if_env_is_not_present 119 | out, _ = capture_io do 120 | Puma::Events.stdio.debug("ready") 121 | end 122 | 123 | assert_empty out 124 | end 125 | 126 | def test_error_writes_to_stderr_and_exits 127 | did_exit = false 128 | 129 | _, err = capture_io do 130 | Puma::Events.stdio.error("interrupted") 131 | end 132 | 133 | assert_equal "ERROR: interrupted", err 134 | rescue SystemExit 135 | did_exit = true 136 | ensure 137 | assert did_exit 138 | end 139 | 140 | def test_pid_formatter 141 | pid = Process.pid 142 | 143 | out, _ = capture_io do 144 | events = Puma::Events.stdio 145 | 146 | events.formatter = Puma::Events::PidFormatter.new 147 | 148 | events.write("ready") 149 | end 150 | 151 | assert_equal "[#{ pid }] ready", out 152 | end 153 | 154 | def test_custom_log_formatter 155 | custom_formatter = proc { |str| "-> #{ str }" } 156 | 157 | out, _ = capture_io do 158 | events = Puma::Events.stdio 159 | 160 | events.formatter = custom_formatter 161 | 162 | events.write("ready") 163 | end 164 | 165 | assert_equal "-> ready", out 166 | end 167 | 168 | def test_parse_error 169 | port = 0 170 | host = "127.0.0.1" 171 | app = proc { |env| [200, {"Content-Type" => "plain/text"}, ["hello\n"]] } 172 | events = Puma::Events.strings 173 | server = Puma::Server.new app, events 174 | 175 | server.add_tcp_listener host, port 176 | server.run 177 | 178 | sock = TCPSocket.new host, server.connected_port 179 | path = "/" 180 | params = "a"*1024*10 181 | 182 | sock << "GET #{path}?a=#{params} HTTP/1.1\r\nConnection: close\r\n\r\n" 183 | sock.read 184 | sleep 0.1 # important so that the previous data is sent as a packet 185 | assert_match %r!HTTP parse error, malformed request \(#{path}\)!, events.stderr.string 186 | server.stop(true) 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /ext/puma_http11/http11_parser.java.rl: -------------------------------------------------------------------------------- 1 | package org.jruby.puma; 2 | 3 | import org.jruby.util.ByteList; 4 | 5 | public class Http11Parser { 6 | 7 | /** Machine **/ 8 | 9 | %%{ 10 | 11 | machine puma_parser; 12 | 13 | action mark {parser.mark = fpc; } 14 | 15 | action start_field { parser.field_start = fpc; } 16 | action snake_upcase_field { /* FIXME stub */ } 17 | action write_field { 18 | parser.field_len = fpc-parser.field_start; 19 | } 20 | 21 | action start_value { parser.mark = fpc; } 22 | action write_value { 23 | if(parser.http_field != null) { 24 | parser.http_field.call(parser.data, parser.field_start, parser.field_len, parser.mark, fpc-parser.mark); 25 | } 26 | } 27 | action request_method { 28 | if(parser.request_method != null) 29 | parser.request_method.call(parser.data, parser.mark, fpc-parser.mark); 30 | } 31 | action request_uri { 32 | if(parser.request_uri != null) 33 | parser.request_uri.call(parser.data, parser.mark, fpc-parser.mark); 34 | } 35 | action fragment { 36 | if(parser.fragment != null) 37 | parser.fragment.call(parser.data, parser.mark, fpc-parser.mark); 38 | } 39 | 40 | action start_query {parser.query_start = fpc; } 41 | action query_string { 42 | if(parser.query_string != null) 43 | parser.query_string.call(parser.data, parser.query_start, fpc-parser.query_start); 44 | } 45 | 46 | action http_version { 47 | if(parser.http_version != null) 48 | parser.http_version.call(parser.data, parser.mark, fpc-parser.mark); 49 | } 50 | 51 | action request_path { 52 | if(parser.request_path != null) 53 | parser.request_path.call(parser.data, parser.mark, fpc-parser.mark); 54 | } 55 | 56 | action done { 57 | parser.body_start = fpc + 1; 58 | if(parser.header_done != null) 59 | parser.header_done.call(parser.data, fpc + 1, pe - fpc - 1); 60 | fbreak; 61 | } 62 | 63 | include puma_parser_common "http11_parser_common.rl"; 64 | 65 | }%% 66 | 67 | /** Data **/ 68 | %% write data; 69 | 70 | public static interface ElementCB { 71 | public void call(Object data, int at, int length); 72 | } 73 | 74 | public static interface FieldCB { 75 | public void call(Object data, int field, int flen, int value, int vlen); 76 | } 77 | 78 | public static class HttpParser { 79 | int cs; 80 | int body_start; 81 | int content_len; 82 | int nread; 83 | int mark; 84 | int field_start; 85 | int field_len; 86 | int query_start; 87 | 88 | Object data; 89 | ByteList buffer; 90 | 91 | public FieldCB http_field; 92 | public ElementCB request_method; 93 | public ElementCB request_uri; 94 | public ElementCB fragment; 95 | public ElementCB request_path; 96 | public ElementCB query_string; 97 | public ElementCB http_version; 98 | public ElementCB header_done; 99 | 100 | public void init() { 101 | cs = 0; 102 | 103 | %% write init; 104 | 105 | body_start = 0; 106 | content_len = 0; 107 | mark = 0; 108 | nread = 0; 109 | field_len = 0; 110 | field_start = 0; 111 | } 112 | } 113 | 114 | public final HttpParser parser = new HttpParser(); 115 | 116 | public int execute(ByteList buffer, int off) { 117 | int p, pe; 118 | int cs = parser.cs; 119 | int len = buffer.length(); 120 | assert off<=len : "offset past end of buffer"; 121 | 122 | p = off; 123 | pe = len; 124 | // get a copy of the bytes, since it may not start at 0 125 | // FIXME: figure out how to just use the bytes in-place 126 | byte[] data = buffer.bytes(); 127 | parser.buffer = buffer; 128 | 129 | %% write exec; 130 | 131 | parser.cs = cs; 132 | parser.nread += (p - off); 133 | 134 | assert p <= pe : "buffer overflow after parsing execute"; 135 | assert parser.nread <= len : "nread longer than length"; 136 | assert parser.body_start <= len : "body starts after buffer end"; 137 | assert parser.mark < len : "mark is after buffer end"; 138 | assert parser.field_len <= len : "field has length longer than whole buffer"; 139 | assert parser.field_start < len : "field starts after buffer end"; 140 | 141 | return parser.nread; 142 | } 143 | 144 | public int finish() { 145 | if(has_error()) { 146 | return -1; 147 | } else if(is_finished()) { 148 | return 1; 149 | } else { 150 | return 0; 151 | } 152 | } 153 | 154 | public boolean has_error() { 155 | return parser.cs == puma_parser_error; 156 | } 157 | 158 | public boolean is_finished() { 159 | return parser.cs == puma_parser_first_final; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/puma/runner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'puma/server' 4 | require 'puma/const' 5 | 6 | module Puma 7 | # Generic class that is used by `Puma::Cluster` and `Puma::Single` to 8 | # serve requests. This class spawns a new instance of `Puma::Server` via 9 | # a call to `start_server`. 10 | class Runner 11 | def initialize(cli, events) 12 | @launcher = cli 13 | @events = events 14 | @options = cli.options 15 | @app = nil 16 | @control = nil 17 | @started_at = Time.now 18 | end 19 | 20 | def daemon? 21 | @options[:daemon] 22 | end 23 | 24 | def development? 25 | @options[:environment] == "development" 26 | end 27 | 28 | def test? 29 | @options[:environment] == "test" 30 | end 31 | 32 | def log(str) 33 | @events.log str 34 | end 35 | 36 | def before_restart 37 | @control.stop(true) if @control 38 | end 39 | 40 | def error(str) 41 | @events.error str 42 | end 43 | 44 | def debug(str) 45 | @events.log "- #{str}" if @options[:debug] 46 | end 47 | 48 | def start_control 49 | str = @options[:control_url] 50 | return unless str 51 | 52 | require 'puma/app/status' 53 | 54 | uri = URI.parse str 55 | 56 | app = Puma::App::Status.new @launcher 57 | 58 | if token = @options[:control_auth_token] 59 | app.auth_token = token unless token.empty? || token == 'none' 60 | end 61 | 62 | control = Puma::Server.new app, @launcher.events 63 | control.min_threads = 0 64 | control.max_threads = 1 65 | 66 | case uri.scheme 67 | when "tcp" 68 | log "* Starting control server on #{str}" 69 | control.add_tcp_listener uri.host, uri.port 70 | when "unix" 71 | log "* Starting control server on #{str}" 72 | path = "#{uri.host}#{uri.path}" 73 | mask = @options[:control_url_umask] 74 | 75 | control.add_unix_listener path, mask 76 | else 77 | error "Invalid control URI: #{str}" 78 | end 79 | 80 | control.run 81 | @control = control 82 | end 83 | 84 | def ruby_engine 85 | if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" 86 | "ruby #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}" 87 | else 88 | if defined?(RUBY_ENGINE_VERSION) 89 | "#{RUBY_ENGINE} #{RUBY_ENGINE_VERSION} - ruby #{RUBY_VERSION}" 90 | else 91 | "#{RUBY_ENGINE} #{RUBY_VERSION}" 92 | end 93 | end 94 | end 95 | 96 | def output_header(mode) 97 | min_t = @options[:min_threads] 98 | max_t = @options[:max_threads] 99 | 100 | log "Puma starting in #{mode} mode..." 101 | log "* Version #{Puma::Const::PUMA_VERSION} (#{ruby_engine}), codename: #{Puma::Const::CODE_NAME}" 102 | log "* Min threads: #{min_t}, max threads: #{max_t}" 103 | log "* Environment: #{ENV['RACK_ENV']}" 104 | 105 | if @options[:mode] == :tcp 106 | log "* Mode: Lopez Express (tcp)" 107 | end 108 | end 109 | 110 | def redirected_io? 111 | @options[:redirect_stdout] || @options[:redirect_stderr] 112 | end 113 | 114 | def redirect_io 115 | stdout = @options[:redirect_stdout] 116 | stderr = @options[:redirect_stderr] 117 | append = @options[:redirect_append] 118 | 119 | if stdout 120 | unless Dir.exist?(File.dirname(stdout)) 121 | raise "Cannot redirect STDOUT to #{stdout}" 122 | end 123 | 124 | STDOUT.reopen stdout, (append ? "a" : "w") 125 | STDOUT.sync = true 126 | STDOUT.puts "=== puma startup: #{Time.now} ===" 127 | end 128 | 129 | if stderr 130 | unless Dir.exist?(File.dirname(stderr)) 131 | raise "Cannot redirect STDERR to #{stderr}" 132 | end 133 | 134 | STDERR.reopen stderr, (append ? "a" : "w") 135 | STDERR.sync = true 136 | STDERR.puts "=== puma startup: #{Time.now} ===" 137 | end 138 | end 139 | 140 | def load_and_bind 141 | unless @launcher.config.app_configured? 142 | error "No application configured, nothing to run" 143 | exit 1 144 | end 145 | 146 | # Load the app before we daemonize. 147 | begin 148 | @app = @launcher.config.app 149 | rescue Exception => e 150 | log "! Unable to load application: #{e.class}: #{e.message}" 151 | raise e 152 | end 153 | 154 | @launcher.binder.parse @options[:binds], self 155 | end 156 | 157 | def app 158 | @app ||= @launcher.config.app 159 | end 160 | 161 | def start_server 162 | min_t = @options[:min_threads] 163 | max_t = @options[:max_threads] 164 | 165 | server = Puma::Server.new app, @launcher.events, @options 166 | server.min_threads = min_t 167 | server.max_threads = max_t 168 | server.inherit_binder @launcher.binder 169 | 170 | if @options[:mode] == :tcp 171 | server.tcp_mode! 172 | end 173 | 174 | if @options[:early_hints] 175 | server.early_hints = true 176 | end 177 | 178 | unless development? || test? 179 | server.leak_stack_on_error = false 180 | end 181 | 182 | server 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment engineering for puma 2 | 3 | Puma is software that is expected to be run in a deployed environment eventually. 4 | You can certainly use it as your dev server only, but most people look to use 5 | it in their production deployments as well. 6 | 7 | To that end, this is meant to serve as a foundation of wisdom how to do that 8 | in a way that increases happiness and decreases downtime. 9 | 10 | ## Specifying puma 11 | 12 | Most people want to do this by putting `gem "puma"` into their Gemfile, so we'll 13 | go ahead and assume that. Go add it now... we'll wait. 14 | 15 | 16 | Welcome back! 17 | 18 | ## Single vs Cluster mode 19 | 20 | Puma was originally conceived as a thread-only webserver, but grew the ability to 21 | also use processes in version 2. 22 | 23 | Here are some rules of thumb: 24 | 25 | ### MRI 26 | 27 | * Use cluster mode and set the number of workers to 1.5x the number of cpu cores 28 | in the machine, minimum 2. 29 | * Set the number of threads to desired concurrent requests / number of workers. 30 | Puma defaults to 16 and that's a decent number. 31 | 32 | #### Migrating from Unicorn 33 | 34 | * If you're migrating from unicorn though, here are some settings to start with: 35 | * Set workers to half the number of unicorn workers you're using 36 | * Set threads to 2 37 | * Enjoy 50% memory savings 38 | * As you grow more confident in the thread safety of your app, you can tune the 39 | workers down and the threads up. 40 | 41 | #### Ubuntu / Systemd (Systemctl) Installation 42 | 43 | See [systemd.md](systemd.md) 44 | 45 | #### Worker utilization 46 | 47 | **How do you know if you've got enough (or too many workers)?** 48 | 49 | A good question. Due to MRI's GIL, only one thread can be executing Ruby code at a time. 50 | But since so many apps are waiting on IO from DBs, etc., they can utilize threads 51 | to make better use of the process. 52 | 53 | The rule of thumb is you never want processes that are pegged all the time. This 54 | means that there is more work to do than the process can get through. On the other 55 | hand, if you have processes that sit around doing nothing, then they're just eating 56 | up resources. 57 | 58 | Watch your CPU utilization over time and aim for about 70% on average. This means 59 | you've got capacity still but aren't starving threads. 60 | 61 | **Measuring utilization** 62 | 63 | Using a timestamp header from an upstream proxy server (eg. nginx or haproxy), it's 64 | possible to get an indication of how long requests have been waiting for a Puma 65 | thread to become available. 66 | 67 | * Have your upstream proxy set a header with the time it received the request: 68 | * nginx: `proxy_set_header X-Request-Start "${msec}";` 69 | * haproxy: `http-request set-header X-Request-Start "%t";` 70 | * In your Rack middleware, determine the amount of time elapsed since `X-Request-Start`. 71 | * To improve accuracy, you will want to subtract time spent waiting for slow clients: 72 | * `env['puma.request_body_wait']` contains the number of milliseconds Puma spent 73 | waiting for the client to send the request body. 74 | * haproxy: `%Th` (TLS handshake time) and `%Ti` (idle time before request) can 75 | can also be added as headers. 76 | 77 | ## Daemonizing 78 | 79 | I prefer to not daemonize my servers and use something like `runit` or `upstart` to 80 | monitor them as child processes. This gives them fast response to crashes and 81 | makes it easy to figure out what is going on. Additionally, unlike `unicorn`, 82 | puma does not require daemonization to do zero-downtime restarts. 83 | 84 | I see people using daemonization because they start puma directly via capistrano 85 | task and thus want it to live on past the `cap deploy`. To these people I say: 86 | You need to be using a process monitor. Nothing is making sure puma stays up in 87 | this scenario! You're just waiting for something weird to happen, puma to die, 88 | and to get paged at 3am. Do yourself a favor, at least the process monitoring 89 | your OS comes with, be it `sysvinit`, `upstart`, or `systemd`. Or branch out 90 | and use `runit` or hell, even `monit`. 91 | 92 | ## Restarting 93 | 94 | You probably will want to deploy some new code at some point, and you'd like 95 | puma to start running that new code. Minimizing the amount of time the server 96 | is unavailable would be nice as well. Here's how to do it: 97 | 98 | 1. Don't use `preload!`. This dirties the master process and means it will have 99 | to shutdown all the workers and re-exec itself to get your new code. It is not compatible with phased-restart and `prune_bundler` as well. 100 | 101 | 1. Use `prune_bundler`. This makes it so that the cluster master will detach itself 102 | from a Bundler context on start. This allows the cluster workers to load your app 103 | and start a brand new Bundler context within the worker only. This means your 104 | master remains pristine and can live on between new releases of your code. 105 | 106 | 1. Use phased-restart (`SIGUSR1` or `pumactl phased-restart`). This tells the master 107 | to kill off one worker at a time and restart them in your new code. This minimizes 108 | downtime and staggers the restart nicely. **WARNING** This means that both your 109 | old code and your new code will be running concurrently. Most deployment solutions 110 | already cause that, but it's worth warning you about it again. Be careful with your 111 | migrations, etc! 112 | -------------------------------------------------------------------------------- /test/test_thread_pool.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | require "puma/thread_pool" 4 | 5 | class TestThreadPool < Minitest::Test 6 | 7 | def teardown 8 | @pool.shutdown(1) if @pool 9 | end 10 | 11 | def new_pool(min, max, &block) 12 | block = proc { } unless block 13 | @work_mutex = Mutex.new 14 | @work_done = ConditionVariable.new 15 | @pool = Puma::ThreadPool.new(min, max, &block) 16 | end 17 | 18 | def pause 19 | sleep 0.2 20 | end 21 | 22 | def test_append_spawns 23 | saw = [] 24 | thread_name = nil 25 | 26 | pool = new_pool(0, 1) do |work| 27 | @work_mutex.synchronize do 28 | saw << work 29 | thread_name = Thread.current.name if Thread.current.respond_to?(:name) 30 | @work_done.signal 31 | end 32 | end 33 | 34 | pool << 1 35 | 36 | @work_mutex.synchronize do 37 | @work_done.wait(@work_mutex, 5) 38 | assert_equal 1, pool.spawned 39 | assert_equal [1], saw 40 | assert_equal('puma 001', thread_name) if Thread.current.respond_to?(:name) 41 | end 42 | end 43 | 44 | def test_converts_pool_sizes 45 | pool = new_pool('0', '1') 46 | 47 | assert_equal 0, pool.spawned 48 | 49 | pool << 1 50 | 51 | assert_equal 1, pool.spawned 52 | end 53 | 54 | def test_append_queues_on_max 55 | pool = new_pool(0, 0) do 56 | "Hello World!" 57 | end 58 | 59 | pool << 1 60 | pool << 2 61 | pool << 3 62 | 63 | assert_equal 3, pool.backlog 64 | end 65 | 66 | def test_trim 67 | pool = new_pool(0, 1) do |work| 68 | @work_mutex.synchronize do 69 | @work_done.signal 70 | end 71 | end 72 | 73 | pool << 1 74 | 75 | @work_mutex.synchronize do 76 | @work_done.wait(@work_mutex, 5) 77 | assert_equal 1, pool.spawned 78 | end 79 | 80 | pool.trim 81 | pool.instance_variable_get(:@workers).first.join 82 | 83 | assert_equal 0, pool.spawned 84 | end 85 | 86 | def test_trim_leaves_min 87 | pool = new_pool(1, 2) do |work| 88 | @work_mutex.synchronize do 89 | @work_done.signal 90 | end 91 | end 92 | 93 | pool << 1 94 | pool << 2 95 | 96 | @work_mutex.synchronize do 97 | @work_done.wait(@work_mutex, 5) 98 | assert_equal 2, pool.spawned 99 | end 100 | 101 | pool.trim 102 | pause 103 | assert_equal 1, pool.spawned 104 | 105 | 106 | pool.trim 107 | pause 108 | assert_equal 1, pool.spawned 109 | end 110 | 111 | def test_force_trim_doesnt_overtrim 112 | finish = false 113 | pool = new_pool(1, 2) { Thread.pass until finish } 114 | 115 | pool << 1 116 | pool << 2 117 | 118 | assert_equal 2, pool.spawned 119 | pool.trim true 120 | pool.trim true 121 | 122 | finish = true 123 | 124 | pause 125 | 126 | assert_equal 1, pool.spawned 127 | end 128 | 129 | def test_trim_is_ignored_if_no_waiting_threads 130 | finish = false 131 | pool = new_pool(1, 2) { Thread.pass until finish } 132 | 133 | pool << 1 134 | pool << 2 135 | 136 | assert_equal 2, pool.spawned 137 | pool.trim 138 | pool.trim 139 | 140 | assert_equal 0, pool.trim_requested 141 | 142 | finish = true 143 | 144 | pause 145 | end 146 | 147 | def test_autotrim 148 | finish = false 149 | pool = new_pool(1, 2) { Thread.pass until finish } 150 | 151 | pool << 1 152 | pool << 2 153 | 154 | assert_equal 2, pool.spawned 155 | 156 | finish = true 157 | 158 | pause 159 | 160 | assert_equal 2, pool.spawned 161 | 162 | pool.auto_trim! 1 163 | 164 | sleep 1 165 | 166 | pause 167 | 168 | assert_equal 1, pool.spawned 169 | end 170 | 171 | def test_cleanliness 172 | values = [] 173 | n = 100 174 | mutex = Mutex.new 175 | 176 | finished = false 177 | 178 | pool = new_pool(1,1) { 179 | mutex.synchronize { values.push Thread.current[:foo] } 180 | Thread.current[:foo] = :hai 181 | Thread.pass until finished 182 | } 183 | 184 | pool.clean_thread_locals = true 185 | 186 | n.times { pool << 1 } 187 | 188 | finished = true 189 | 190 | pause 191 | 192 | assert_equal n, values.length 193 | 194 | assert_equal [], values.compact 195 | end 196 | 197 | def test_reap_only_dead_threads 198 | pool = new_pool(2,2) { Thread.current.kill } 199 | 200 | assert_equal 2, pool.spawned 201 | 202 | pool << 1 203 | 204 | pause 205 | 206 | assert_equal 2, pool.spawned 207 | 208 | pool.reap 209 | 210 | assert_equal 1, pool.spawned 211 | 212 | pool << 2 213 | 214 | pause 215 | 216 | assert_equal 1, pool.spawned 217 | 218 | pool.reap 219 | 220 | assert_equal 0, pool.spawned 221 | end 222 | 223 | def test_auto_reap_dead_threads 224 | pool = new_pool(2,2) { Thread.current.kill } 225 | 226 | assert_equal 2, pool.spawned 227 | 228 | # TODO: is there a point to these two lines? 229 | pool << 1 230 | pool << 2 231 | 232 | pool.auto_reap! 0.1 233 | 234 | pause 235 | 236 | assert_equal 0, pool.spawned 237 | end 238 | 239 | def test_force_shutdown_immediately 240 | pool = new_pool(0, 1) do |work| 241 | begin 242 | @work_mutex.synchronize do 243 | @work_done.signal 244 | end 245 | sleep 10 # TODO: do something here other than sleep 246 | rescue Puma::ThreadPool::ForceShutdown 247 | end 248 | end 249 | 250 | pool << 1 251 | 252 | @work_mutex.synchronize do 253 | @work_done.wait(@work_mutex, 5) 254 | pool.shutdown(0) 255 | assert_equal 0, pool.spawned 256 | end 257 | end 258 | end 259 | -------------------------------------------------------------------------------- /test/test_rack_handler.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | require "rack/handler/puma" 4 | 5 | class TestHandlerGetStrSym < Minitest::Test 6 | def test_handler 7 | handler = Rack::Handler.get(:puma) 8 | assert_equal Rack::Handler::Puma, handler 9 | handler = Rack::Handler.get('Puma') 10 | assert_equal Rack::Handler::Puma, handler 11 | end 12 | end 13 | 14 | class TestPathHandler < Minitest::Test 15 | def app 16 | Proc.new {|env| @input = env; [200, {}, ["hello world"]]} 17 | end 18 | 19 | def setup 20 | @input = nil 21 | end 22 | 23 | def in_handler(app, options = {}) 24 | options[:Port] ||= 0 25 | options[:Silent] = true 26 | 27 | @launcher = nil 28 | thread = Thread.new do 29 | Rack::Handler::Puma.run(app, options) do |s, p| 30 | @launcher = s 31 | end 32 | end 33 | thread.abort_on_exception = true 34 | 35 | # Wait for launcher to boot 36 | Timeout.timeout(10) do 37 | until @launcher 38 | sleep 1 39 | end 40 | end 41 | sleep 1 42 | 43 | yield @launcher 44 | ensure 45 | @launcher.stop if @launcher 46 | thread.join if thread 47 | end 48 | 49 | def test_handler_boots 50 | host = windows? ? "127.0.1.1" : "0.0.0.0" 51 | opts = { Host: host } 52 | in_handler(app, opts) do |launcher| 53 | hit(["http://#{host}:#{ launcher.connected_port }/test"]) 54 | assert_equal("/test", @input["PATH_INFO"]) 55 | end 56 | end 57 | end 58 | 59 | class TestUserSuppliedOptionsPortIsSet < Minitest::Test 60 | def setup 61 | @options = {} 62 | @options[:user_supplied_options] = [:Port] 63 | end 64 | 65 | def test_port_wins_over_config 66 | user_port = 5001 67 | file_port = 6001 68 | 69 | Dir.mktmpdir do |d| 70 | Dir.chdir(d) do 71 | FileUtils.mkdir("config") 72 | File.open("config/puma.rb", "w") { |f| f << "port #{file_port}" } 73 | 74 | @options[:Port] = user_port 75 | conf = Rack::Handler::Puma.config(->{}, @options) 76 | conf.load 77 | 78 | assert_equal ["tcp://0.0.0.0:#{user_port}"], conf.options[:binds] 79 | end 80 | end 81 | end 82 | end 83 | 84 | class TestUserSuppliedOptionsHostIsSet < Minitest::Test 85 | def setup 86 | @options = {} 87 | @options[:user_supplied_options] = [:Host] 88 | end 89 | 90 | def test_host_uses_supplied_port_default 91 | user_port = rand(1000..9999) 92 | user_host = "123.456.789" 93 | 94 | @options[:Host] = user_host 95 | @options[:Port] = user_port 96 | conf = Rack::Handler::Puma.config(->{}, @options) 97 | conf.load 98 | 99 | assert_equal ["tcp://#{user_host}:#{user_port}"], conf.options[:binds] 100 | end 101 | end 102 | 103 | class TestUserSuppliedOptionsIsEmpty < Minitest::Test 104 | def setup 105 | @options = {} 106 | @options[:user_supplied_options] = [] 107 | end 108 | 109 | def test_config_file_wins_over_port 110 | user_port = 5001 111 | file_port = 6001 112 | 113 | Dir.mktmpdir do |d| 114 | Dir.chdir(d) do 115 | FileUtils.mkdir("config") 116 | File.open("config/puma.rb", "w") { |f| f << "port #{file_port}" } 117 | 118 | @options[:Port] = user_port 119 | conf = Rack::Handler::Puma.config(->{}, @options) 120 | conf.load 121 | 122 | assert_equal ["tcp://0.0.0.0:#{file_port}"], conf.options[:binds] 123 | end 124 | end 125 | end 126 | 127 | def test_default_host_when_using_config_file 128 | user_port = 5001 129 | file_port = 6001 130 | 131 | Dir.mktmpdir do |d| 132 | Dir.chdir(d) do 133 | FileUtils.mkdir("config") 134 | File.open("config/puma.rb", "w") { |f| f << "port #{file_port}" } 135 | 136 | @options[:Host] = "localhost" 137 | @options[:Port] = user_port 138 | conf = Rack::Handler::Puma.config(->{}, @options) 139 | conf.load 140 | 141 | assert_equal ["tcp://localhost:#{file_port}"], conf.options[:binds] 142 | end 143 | end 144 | end 145 | 146 | def test_default_host_when_using_config_file_with_explicit_host 147 | user_port = 5001 148 | file_port = 6001 149 | 150 | Dir.mktmpdir do |d| 151 | Dir.chdir(d) do 152 | FileUtils.mkdir("config") 153 | File.open("config/puma.rb", "w") { |f| f << "port #{file_port}, '1.2.3.4'" } 154 | 155 | @options[:Host] = "localhost" 156 | @options[:Port] = user_port 157 | conf = Rack::Handler::Puma.config(->{}, @options) 158 | conf.load 159 | 160 | assert_equal ["tcp://1.2.3.4:#{file_port}"], conf.options[:binds] 161 | end 162 | end 163 | end 164 | end 165 | 166 | class TestUserSuppliedOptionsIsNotPresent < Minitest::Test 167 | def setup 168 | @options = {} 169 | end 170 | 171 | def test_default_port_when_no_config_file 172 | conf = Rack::Handler::Puma.config(->{}, @options) 173 | conf.load 174 | 175 | assert_equal ["tcp://0.0.0.0:9292"], conf.options[:binds] 176 | end 177 | 178 | def test_config_wins_over_default 179 | file_port = 6001 180 | 181 | Dir.mktmpdir do |d| 182 | Dir.chdir(d) do 183 | FileUtils.mkdir("config") 184 | File.open("config/puma.rb", "w") { |f| f << "port #{file_port}" } 185 | 186 | conf = Rack::Handler::Puma.config(->{}, @options) 187 | conf.load 188 | 189 | assert_equal ["tcp://0.0.0.0:#{file_port}"], conf.options[:binds] 190 | end 191 | end 192 | end 193 | 194 | def test_user_port_wins_over_default_when_user_supplied_is_blank 195 | user_port = 5001 196 | @options[:user_supplied_options] = [] 197 | @options[:Port] = user_port 198 | conf = Rack::Handler::Puma.config(->{}, @options) 199 | conf.load 200 | 201 | assert_equal ["tcp://0.0.0.0:#{user_port}"], conf.options[:binds] 202 | end 203 | 204 | def test_user_port_wins_over_default 205 | user_port = 5001 206 | @options[:Port] = user_port 207 | conf = Rack::Handler::Puma.config(->{}, @options) 208 | conf.load 209 | 210 | assert_equal ["tcp://0.0.0.0:#{user_port}"], conf.options[:binds] 211 | end 212 | 213 | def test_user_port_wins_over_config 214 | user_port = 5001 215 | file_port = 6001 216 | 217 | Dir.mktmpdir do |d| 218 | Dir.chdir(d) do 219 | FileUtils.mkdir("config") 220 | File.open("config/puma.rb", "w") { |f| f << "port #{file_port}" } 221 | 222 | @options[:Port] = user_port 223 | conf = Rack::Handler::Puma.config(->{}, @options) 224 | conf.load 225 | 226 | assert_equal ["tcp://0.0.0.0:#{user_port}"], conf.options[:binds] 227 | end 228 | end 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /test/test_http11.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Evan Phoenix 2 | # Copyright (c) 2005 Zed A. Shaw 3 | 4 | require_relative "helper" 5 | require "digest" 6 | 7 | require "puma/puma_http11" 8 | 9 | class Http11ParserTest < Minitest::Test 10 | 11 | parallelize_me! 12 | 13 | def test_parse_simple 14 | parser = Puma::HttpParser.new 15 | req = {} 16 | http = "GET /?a=1 HTTP/1.1\r\n\r\n" 17 | nread = parser.execute(req, http, 0) 18 | 19 | assert nread == http.length, "Failed to parse the full HTTP request" 20 | assert parser.finished?, "Parser didn't finish" 21 | assert !parser.error?, "Parser had error" 22 | assert nread == parser.nread, "Number read returned from execute does not match" 23 | 24 | assert_equal '/', req['REQUEST_PATH'] 25 | assert_equal 'HTTP/1.1', req['HTTP_VERSION'] 26 | assert_equal '/?a=1', req['REQUEST_URI'] 27 | assert_equal 'GET', req['REQUEST_METHOD'] 28 | assert_nil req['FRAGMENT'] 29 | assert_equal "a=1", req['QUERY_STRING'] 30 | 31 | parser.reset 32 | assert parser.nread == 0, "Number read after reset should be 0" 33 | end 34 | 35 | def test_parse_escaping_in_query 36 | parser = Puma::HttpParser.new 37 | req = {} 38 | http = "GET /admin/users?search=%27%%27 HTTP/1.1\r\n\r\n" 39 | nread = parser.execute(req, http, 0) 40 | 41 | assert nread == http.length, "Failed to parse the full HTTP request" 42 | assert parser.finished?, "Parser didn't finish" 43 | assert !parser.error?, "Parser had error" 44 | assert nread == parser.nread, "Number read returned from execute does not match" 45 | 46 | assert_equal '/admin/users?search=%27%%27', req['REQUEST_URI'] 47 | assert_equal "search=%27%%27", req['QUERY_STRING'] 48 | 49 | parser.reset 50 | assert parser.nread == 0, "Number read after reset should be 0" 51 | end 52 | 53 | def test_parse_absolute_uri 54 | parser = Puma::HttpParser.new 55 | req = {} 56 | http = "GET http://192.168.1.96:3000/api/v1/matches/test?1=1 HTTP/1.1\r\n\r\n" 57 | nread = parser.execute(req, http, 0) 58 | 59 | assert nread == http.length, "Failed to parse the full HTTP request" 60 | assert parser.finished?, "Parser didn't finish" 61 | assert !parser.error?, "Parser had error" 62 | assert nread == parser.nread, "Number read returned from execute does not match" 63 | 64 | assert_equal "GET", req['REQUEST_METHOD'] 65 | assert_equal 'http://192.168.1.96:3000/api/v1/matches/test?1=1', req['REQUEST_URI'] 66 | assert_equal 'HTTP/1.1', req['HTTP_VERSION'] 67 | 68 | assert_nil req['REQUEST_PATH'] 69 | assert_nil req['FRAGMENT'] 70 | assert_nil req['QUERY_STRING'] 71 | 72 | parser.reset 73 | assert parser.nread == 0, "Number read after reset should be 0" 74 | 75 | end 76 | 77 | def test_parse_dumbfuck_headers 78 | parser = Puma::HttpParser.new 79 | req = {} 80 | should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n" 81 | nread = parser.execute(req, should_be_good, 0) 82 | assert_equal should_be_good.length, nread 83 | assert parser.finished? 84 | assert !parser.error? 85 | end 86 | 87 | def test_parse_error 88 | parser = Puma::HttpParser.new 89 | req = {} 90 | bad_http = "GET / SsUTF/1.1" 91 | 92 | error = false 93 | begin 94 | parser.execute(req, bad_http, 0) 95 | rescue 96 | error = true 97 | end 98 | 99 | assert error, "failed to throw exception" 100 | assert !parser.finished?, "Parser shouldn't be finished" 101 | assert parser.error?, "Parser SHOULD have error" 102 | end 103 | 104 | def test_fragment_in_uri 105 | parser = Puma::HttpParser.new 106 | req = {} 107 | get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" 108 | 109 | parser.execute(req, get, 0) 110 | 111 | assert parser.finished? 112 | assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI'] 113 | assert_equal 'posts-17408', req['FRAGMENT'] 114 | end 115 | 116 | # lame random garbage maker 117 | def rand_data(min, max, readable=true) 118 | count = min + ((rand(max)+1) *10).to_i 119 | res = count.to_s + "/" 120 | 121 | if readable 122 | res << Digest::SHA1.hexdigest(rand(count * 100).to_s) * (count / 40) 123 | else 124 | res << Digest::SHA1.digest(rand(count * 100).to_s) * (count / 20) 125 | end 126 | 127 | return res 128 | end 129 | 130 | def test_max_uri_path_length 131 | parser = Puma::HttpParser.new 132 | req = {} 133 | 134 | # Support URI path length to a max of 2048 135 | path = "/" + rand_data(1000, 100) 136 | http = "GET #{path} HTTP/1.1\r\n\r\n" 137 | parser.execute(req, http, 0) 138 | assert_equal path, req['REQUEST_PATH'] 139 | parser.reset 140 | 141 | # Raise exception if URI path length > 2048 142 | path = "/" + rand_data(3000, 100) 143 | http = "GET #{path} HTTP/1.1\r\n\r\n" 144 | assert_raises Puma::HttpParserError do 145 | parser.execute(req, http, 0) 146 | parser.reset 147 | end 148 | end 149 | 150 | def test_horrible_queries 151 | parser = Puma::HttpParser.new 152 | 153 | # then that large header names are caught 154 | 10.times do |c| 155 | get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n" 156 | assert_raises Puma::HttpParserError do 157 | parser.execute({}, get, 0) 158 | parser.reset 159 | end 160 | end 161 | 162 | # then that large mangled field values are caught 163 | 10.times do |c| 164 | get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n" 165 | assert_raises Puma::HttpParserError do 166 | parser.execute({}, get, 0) 167 | parser.reset 168 | end 169 | end 170 | 171 | # then large headers are rejected too 172 | get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n" 173 | get += "X-Test: test\r\n" * (80 * 1024) 174 | assert_raises Puma::HttpParserError do 175 | parser.execute({}, get, 0) 176 | parser.reset 177 | end 178 | 179 | # finally just that random garbage gets blocked all the time 180 | 10.times do |c| 181 | get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n" 182 | assert_raises Puma::HttpParserError do 183 | parser.execute({}, get, 0) 184 | parser.reset 185 | end 186 | end 187 | end 188 | 189 | # https://github.com/puma/puma/issues/1890 190 | def test_trims_whitespace_from_headers 191 | skip("Known failure, see issue 1890 on GitHub") 192 | parser = Puma::HttpParser.new 193 | req = {} 194 | http = "GET / HTTP/1.1\r\nX-Strip-Me: Strip This \r\n\r\n" 195 | 196 | parser.execute(req, http, 0) 197 | 198 | assert_equal "Strip This", req["HTTP_X_STRIP_ME"] 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /test/test_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helper" 4 | 5 | require "puma/configuration" 6 | 7 | class TestConfigFileBase < Minitest::Test 8 | private 9 | 10 | def with_env(env = {}) 11 | original_env = {} 12 | env.each do |k, v| 13 | original_env[k] = ENV[k] 14 | ENV[k] = v 15 | end 16 | yield 17 | ensure 18 | original_env.each do |k, v| 19 | ENV[k] = v 20 | end 21 | end 22 | end 23 | 24 | class TestConfigFile < TestConfigFileBase 25 | parallelize_me! 26 | 27 | def test_app_from_rackup 28 | conf = Puma::Configuration.new do |c| 29 | c.rackup "test/rackup/hello-bind.ru" 30 | end 31 | conf.load 32 | 33 | conf.app 34 | 35 | assert_equal ["tcp://127.0.0.1:9292"], conf.options[:binds] 36 | end 37 | 38 | def test_app_from_app_DSL 39 | conf = Puma::Configuration.new do |c| 40 | c.load "test/config/app.rb" 41 | end 42 | conf.load 43 | 44 | app = conf.app 45 | 46 | assert_equal [200, {}, ["embedded app"]], app.call({}) 47 | end 48 | 49 | def test_ssl_configuration_from_DSL 50 | conf = Puma::Configuration.new do |config| 51 | config.load "test/config/ssl_config.rb" 52 | end 53 | 54 | conf.load 55 | 56 | bind_configuration = conf.options.file_options[:binds].first 57 | app = conf.app 58 | 59 | assert bind_configuration =~ %r{ca=.*ca.crt} 60 | assert bind_configuration =~ /verify_mode=peer/ 61 | 62 | assert_equal [200, {}, ["embedded app"]], app.call({}) 63 | end 64 | 65 | def test_ssl_bind 66 | skip_on :jruby 67 | 68 | conf = Puma::Configuration.new do |c| 69 | c.ssl_bind "0.0.0.0", "9292", { 70 | cert: "/path/to/cert", 71 | key: "/path/to/key", 72 | verify_mode: "the_verify_mode", 73 | } 74 | end 75 | 76 | conf.load 77 | 78 | ssl_binding = "ssl://0.0.0.0:9292?cert=/path/to/cert&key=/path/to/key&verify_mode=the_verify_mode&no_tlsv1=false&no_tlsv1_1=false" 79 | assert_equal [ssl_binding], conf.options[:binds] 80 | end 81 | 82 | def test_ssl_bind_with_cipher_filter 83 | skip_on :jruby 84 | 85 | cipher_filter = "!aNULL:AES+SHA" 86 | conf = Puma::Configuration.new do |c| 87 | c.ssl_bind "0.0.0.0", "9292", { 88 | cert: "cert", 89 | key: "key", 90 | ssl_cipher_filter: cipher_filter, 91 | } 92 | end 93 | 94 | conf.load 95 | 96 | ssl_binding = conf.options[:binds].first 97 | assert ssl_binding.include?("&ssl_cipher_filter=#{cipher_filter}") 98 | end 99 | 100 | def test_lowlevel_error_handler_DSL 101 | conf = Puma::Configuration.new do |c| 102 | c.load "test/config/app.rb" 103 | end 104 | conf.load 105 | 106 | app = conf.options[:lowlevel_error_handler] 107 | 108 | assert_equal [200, {}, ["error page"]], app.call({}) 109 | end 110 | 111 | def test_allow_users_to_override_default_options 112 | conf = Puma::Configuration.new(restart_cmd: 'bin/rails server') 113 | 114 | assert_equal 'bin/rails server', conf.options[:restart_cmd] 115 | end 116 | 117 | def test_overwrite_options 118 | conf = Puma::Configuration.new do |c| 119 | c.workers 3 120 | end 121 | conf.load 122 | 123 | assert_equal conf.options[:workers], 3 124 | conf.options[:workers] += 1 125 | assert_equal conf.options[:workers], 4 126 | end 127 | 128 | def test_explicit_config_files 129 | conf = Puma::Configuration.new(config_files: ['test/config/settings.rb']) do |c| 130 | end 131 | conf.load 132 | assert_match(/:3000$/, conf.options[:binds].first) 133 | end 134 | 135 | def test_parameters_overwrite_files 136 | conf = Puma::Configuration.new(config_files: ['test/config/settings.rb']) do |c| 137 | c.port 3030 138 | end 139 | conf.load 140 | 141 | assert_match(/:3030$/, conf.options[:binds].first) 142 | assert_equal 3, conf.options[:min_threads] 143 | assert_equal 5, conf.options[:max_threads] 144 | end 145 | 146 | def test_config_files_default 147 | conf = Puma::Configuration.new do 148 | end 149 | 150 | assert_equal [nil], conf.config_files 151 | end 152 | 153 | def test_config_files_with_dash 154 | conf = Puma::Configuration.new(config_files: ['-']) do 155 | end 156 | 157 | assert_equal [], conf.config_files 158 | end 159 | 160 | def test_config_files_with_existing_path 161 | conf = Puma::Configuration.new(config_files: ['test/config/settings.rb']) do 162 | end 163 | 164 | assert_equal ['test/config/settings.rb'], conf.config_files 165 | end 166 | 167 | def test_config_files_with_non_existing_path 168 | conf = Puma::Configuration.new(config_files: ['test/config/typo/settings.rb']) do 169 | end 170 | 171 | assert_equal ['test/config/typo/settings.rb'], conf.config_files 172 | end 173 | 174 | def test_config_files_with_integer_convert 175 | conf = Puma::Configuration.new(config_files: ['test/config/with_integer_convert.rb']) do 176 | end 177 | conf.load 178 | 179 | assert_equal 6, conf.options[:persistent_timeout] 180 | assert_equal 3, conf.options[:first_data_timeout] 181 | assert_equal 2, conf.options[:workers] 182 | assert_equal 4, conf.options[:min_threads] 183 | assert_equal 8, conf.options[:max_threads] 184 | assert_equal 90, conf.options[:worker_timeout] 185 | assert_equal 120, conf.options[:worker_boot_timeout] 186 | assert_equal 150, conf.options[:worker_shutdown_timeout] 187 | end 188 | 189 | def test_config_raise_exception_on_sigterm 190 | conf = Puma::Configuration.new do |c| 191 | c.raise_exception_on_sigterm false 192 | end 193 | conf.load 194 | 195 | assert_equal conf.options[:raise_exception_on_sigterm], false 196 | conf.options[:raise_exception_on_sigterm] = true 197 | assert_equal conf.options[:raise_exception_on_sigterm], true 198 | end 199 | end 200 | 201 | # Thread unsafe modification of ENV 202 | class TestEnvModifificationConfig < TestConfigFileBase 203 | def test_double_bind_port 204 | port = (rand(10_000) + 30_000).to_s 205 | with_env("PORT" => port) do 206 | conf = Puma::Configuration.new do |user_config, file_config, default_config| 207 | user_config.bind "tcp://#{Puma::Configuration::DefaultTCPHost}:#{port}" 208 | file_config.load "test/config/app.rb" 209 | end 210 | 211 | conf.load 212 | assert_equal ["tcp://0.0.0.0:#{port}"], conf.options[:binds] 213 | end 214 | end 215 | end 216 | 217 | class TestConfigFileWithFakeEnv < TestConfigFileBase 218 | def setup 219 | FileUtils.mkpath("config/puma") 220 | File.write("config/puma/fake-env.rb", "") 221 | end 222 | 223 | def test_config_files_with_rack_env 224 | with_env('RACK_ENV' => 'fake-env') do 225 | conf = Puma::Configuration.new do 226 | end 227 | 228 | assert_equal ['config/puma/fake-env.rb'], conf.config_files 229 | end 230 | end 231 | 232 | def test_config_files_with_specified_environment 233 | conf = Puma::Configuration.new do 234 | end 235 | 236 | conf.options[:environment] = 'fake-env' 237 | 238 | assert_equal ['config/puma/fake-env.rb'], conf.config_files 239 | end 240 | 241 | def teardown 242 | FileUtils.rm_r("config/puma") 243 | end 244 | end 245 | --------------------------------------------------------------------------------