1
0
Fork 0
mirror of https://github.com/puma/puma.git synced 2022-11-09 13:48:40 -05:00

Merge branch 'master' into control-exception-on-sigterm

This commit is contained in:
Michał Kulesza 2019-02-21 19:30:00 +01:00
commit 58d9824920
37 changed files with 616 additions and 228 deletions

1
.gitattributes vendored
View file

@ -1,2 +1,3 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text eol=lf * text eol=lf
*.png binary

View file

@ -1,11 +1,9 @@
dist: trusty dist: xenial
sudo: false
group: beta
language: ruby language: ruby
cache: bundler cache: bundler
before_install: before_install:
# rubygems 2.7.8 and greater include bundler # rubygems 2.7.8 and greater include bundler, leave 2.6.0 untouched
- | - |
rv="$(ruby -e 'STDOUT.write RUBY_VERSION')"; rv="$(ruby -e 'STDOUT.write RUBY_VERSION')";
if [ "$rv" \< "2.3" ]; then gem update --system 2.7.8 --no-document if [ "$rv" \< "2.3" ]; then gem update --system 2.7.8 --no-document
@ -13,13 +11,19 @@ before_install:
fi fi
- ruby -v && gem --version && bundle version - ruby -v && gem --version && bundle version
before_script:
- bundle exec rake compile
script:
- bundle exec rake
rvm: rvm:
- 2.2.10 - 2.2.10
- 2.3.8 - 2.3.8
- 2.4.5 - 2.4.5
- 2.5.3 - 2.5.3
- 2.6
- ruby-head - ruby-head
- jruby-9.2.0.0
matrix: matrix:
fast_finish: true fast_finish: true
@ -30,15 +34,21 @@ matrix:
os: osx os: osx
- rvm: 2.5.3 - rvm: 2.5.3
os: osx os: osx
- rvm: jruby-9.2.5.0
dist: trusty
sudo: false
- rvm: jruby-head - rvm: jruby-head
- rvm: rbx-3 dist: trusty
sudo: false
allow_failures: allow_failures:
- rvm: 2.6
- rvm: ruby-head - rvm: ruby-head
- rvm: ruby-head - rvm: ruby-head
env: RUBYOPT="--jit" env: RUBYOPT="--jit"
- rvm: jruby-head - rvm: jruby-head
- rvm: rbx-3 dist: trusty
sudo: false
env: env:
global: global:

View file

@ -7,8 +7,8 @@
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/puma/puma?utm\_source=badge&utm\_medium=badge&utm\_campaign=pr-badge) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/puma/puma?utm\_source=badge&utm\_medium=badge&utm\_campaign=pr-badge)
[![Build Status](https://secure.travis-ci.org/puma/puma.svg)](http://travis-ci.org/puma/puma) [![Build Status](https://secure.travis-ci.org/puma/puma.svg)](http://travis-ci.org/puma/puma)
[![AppVeyor](https://img.shields.io/appveyor/ci/nateberkopec/puma.svg)](https://ci.appveyor.com/project/nateberkopec/puma) [![AppVeyor](https://img.shields.io/appveyor/ci/nateberkopec/puma.svg)](https://ci.appveyor.com/project/nateberkopec/puma)
[![Dependency Status](https://gemnasium.com/puma/puma.svg)](https://gemnasium.com/puma/puma)
[![Code Climate](https://codeclimate.com/github/puma/puma.svg)](https://codeclimate.com/github/puma/puma) [![Code Climate](https://codeclimate.com/github/puma/puma.svg)](https://codeclimate.com/github/puma/puma)
[![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=puma&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=puma&package-manager=bundler&version-scheme=semver)
Puma is a **simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications** in development and production. Puma is a **simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications** in development and production.
@ -25,7 +25,7 @@ On MRI, there is a Global VM Lock (GVL) that ensures only one thread can run Rub
``` ```
$ gem install puma $ gem install puma
$ puma <any rackup (*.ru) file> $ puma <any rackup (*.ru) file>
``` ```
## Frameworks ## Frameworks
@ -160,18 +160,31 @@ Need a bit of security? Use SSL sockets:
``` ```
$ puma -b 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert' $ puma -b 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert'
``` ```
#### Controlling SSL Cipher Suites #### Controlling SSL Cipher Suites
Need to use or avoid specific SSL cipher suites? Use ssl_cipher_filter or ssl_cipher_list options.
#####Ruby: Need to use or avoid specific SSL cipher suites? Use `ssl_cipher_filter` or `ssl_cipher_list` options.
##### Ruby:
``` ```
$ puma -b 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert&ssl_cipher_filter=!aNULL:AES+SHA' $ puma -b 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert&ssl_cipher_filter=!aNULL:AES+SHA'
``` ```
#####JRuby:
##### JRuby:
``` ```
$ puma -b 'ssl://127.0.0.1:9292?keystore=path_to_keystore&keystore-pass=keystore_password&ssl_cipher_list=TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA' $ puma -b 'ssl://127.0.0.1:9292?keystore=path_to_keystore&keystore-pass=keystore_password&ssl_cipher_list=TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA'
``` ```
See https://www.openssl.org/docs/man1.0.2/apps/ciphers.html for cipher filter format and full list of cipher suites. See https://www.openssl.org/docs/man1.0.2/apps/ciphers.html for cipher filter format and full list of cipher suites.
Don't want to use insecure TLSv1.0 ?
```
$ puma -b 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert&no_tlsv1=true'
```
### Control/Status Server ### Control/Status Server
Puma has a built-in status/control app that can be used to query and control Puma itself. Puma has a built-in status/control app that can be used to query and control Puma itself.

View file

@ -73,16 +73,6 @@ else
task :test => [:compile] task :test => [:compile]
end end
task :test => [:ensure_no_puma_gem]
task :ensure_no_puma_gem do
Bundler.with_clean_env do
out = `gem list puma`.strip
if !$?.success? || out != ""
abort "No other puma version should be installed to avoid false positives or loading it by accident but found #{out}"
end
end
end
namespace :test do namespace :test do
desc "Run the integration tests" desc "Run the integration tests"
task :integration do task :integration do

View file

@ -20,6 +20,7 @@ Clustered mode is shown/discussed here. Single mode is analogous to having a sin
* By default, a single, separate thread is used to receive HTTP requests across the socket. * By default, a single, separate thread is used to receive HTTP requests across the socket.
* When at least one worker thread is available for work, a connection is accepted and placed in this request buffer * When at least one worker thread is available for work, a connection is accepted and placed in this request buffer
* This thread waits for entire HTTP requests to be received over the connection * This thread waits for entire HTTP requests to be received over the connection
* 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)
* Once received, the connection is pushed into the "todo" set * Once received, the connection is pushed into the "todo" set
* Worker threads pop work off the "todo" set for processing * Worker threads pop work off the "todo" set for processing
* The thread processes the request via the rack application (which generates the HTTP response) * The thread processes the request via the rack application (which generates the HTTP response)

View file

@ -38,22 +38,42 @@ Here are some rules of thumb:
* As you grow more confident in the thread safety of your app, you can tune the * As you grow more confident in the thread safety of your app, you can tune the
workers down and the threads up. workers down and the threads up.
#### Ubuntu / Systemd (Systemctl) Installation
See [systemd.md](systemd.md)
#### Worker utilization #### Worker utilization
**How do you know if you're got enough (or too many workers)?** **How do you know if you've got enough (or too many workers)?**
A good question. Due to MRI's GIL, only one thread can be executing Ruby code at a time. A good question. Due to MRI's GIL, only one thread can be executing Ruby code at a time.
But since so many apps are waiting on IO from DBs, etc., they can utilize threads But since so many apps are waiting on IO from DBs, etc., they can utilize threads
to make better use of the process. to make better use of the process.
The rule of thumb is you never want processes that are pegged all the time. This The rule of thumb is you never want processes that are pegged all the time. This
means that there is more work to do that the process can get through. On the other means that there is more work to do than the process can get through. On the other
hand, if you have processes that sit around doing nothing, then they're just eating hand, if you have processes that sit around doing nothing, then they're just eating
up resources. up resources.
Watching your CPU utilization over time and aim for about 70% on average. This means Watch your CPU utilization over time and aim for about 70% on average. This means
you've got capacity still but aren't starving threads. you've got capacity still but aren't starving threads.
**Measuring utilization**
Using a timestamp header from an upstream proxy server (eg. nginx or haproxy), it's
possible to get an indication of how long requests have been waiting for a Puma
thread to become available.
* Have your upstream proxy set a header with the time it received the request:
* nginx: `proxy_set_header X-Request-Start "${msec}";`
* haproxy: `http-request set-header X-Request-Start "%t";`
* In your Rack middleware, determine the amount of time elapsed since `X-Request-Start`.
* To improve accuracy, you will want to subtract time spent waiting for slow clients:
* `env['puma.request_body_wait']` contains the number of milliseconds Puma spent
waiting for the client to send the request body.
* haproxy: `%Th` (TLS handshake time) and `%Ti` (idle time before request) can
can also be added as headers.
## Daemonizing ## Daemonizing
I prefer to not daemonize my servers and use something like `runit` or `upstart` to I prefer to not daemonize my servers and use something like `runit` or `upstart` to
@ -62,7 +82,7 @@ makes it easy to figure out what is going on. Additionally, unlike `unicorn`,
puma does not require daemonization to do zero-downtime restarts. puma does not require daemonization to do zero-downtime restarts.
I see people using daemonization because they start puma directly via capistrano I see people using daemonization because they start puma directly via capistrano
task and thus want it to live on past the `cap deploy`. To this people I said: task and thus want it to live on past the `cap deploy`. To these people I say:
You need to be using a process monitor. Nothing is making sure puma stays up in You need to be using a process monitor. Nothing is making sure puma stays up in
this scenario! You're just waiting for something weird to happen, puma to die, this scenario! You're just waiting for something weird to happen, puma to die,
and to get paged at 3am. Do yourself a favor, at least the process monitoring and to get paged at 3am. Do yourself a favor, at least the process monitoring

View file

@ -22,6 +22,8 @@ But again beware, upgrading an application sometimes involves upgrading the data
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)). 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)).
**Note**: Hot and phased restarts are only available on MRI, not on JRuby. They are also unavailable on Windows servers.
### Release Directory ### Release Directory
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. 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.

View file

@ -32,21 +32,26 @@ Type=simple
# Preferably configure a non-privileged user # Preferably configure a non-privileged user
# User= # User=
# The path to the puma application root # The path to the your application code root directory.
# Also replace the "<WD>" place holders below with this path. # Also replace the "<YOUR_APP_PATH>" place holders below with this path.
WorkingDirectory= # Example /home/username/myapp
WorkingDirectory=<YOUR_APP_PATH>
# Helpful for debugging socket activation, etc. # Helpful for debugging socket activation, etc.
# Environment=PUMA_DEBUG=1 # Environment=PUMA_DEBUG=1
# The command to start Puma. This variant uses a binstub generated via # SystemD will not run puma even if it is in your path. You must specify
# `bundle binstubs puma --path ./sbin` in the WorkingDirectory # an absolute URL to puma. For example /usr/local/bin/puma
# (replace "<WD>" below) # Alternatively, create a binstub with `bundle binstubs puma --path ./sbin` in the WorkingDirectory
ExecStart=<WD>/sbin/puma -b tcp://0.0.0.0:9292 -b ssl://0.0.0.0:9293?key=key.pem&cert=cert.pem ExecStart=/<FULLPATH>/bin/puma -C <YOUR_APP_PATH>/puma.rb
# Variant: Rails start.
# ExecStart=/<FULLPATH>/bin/puma -C <YOUR_APP_PATH>/config/puma.rb ../config.ru
# Variant: Use config file with `bind` directives instead:
# ExecStart=<WD>/sbin/puma -C config.rb
# Variant: Use `bundle exec --keep-file-descriptors puma` instead of binstub # Variant: Use `bundle exec --keep-file-descriptors puma` instead of binstub
# Variant: Specify directives inline.
# ExecStart=/<FULLPATH>/puma -b tcp://0.0.0.0:9292 -b ssl://0.0.0.0:9293?key=key.pem&cert=cert.pem
Restart=always Restart=always
@ -247,6 +252,12 @@ PIDFile=<WD>/shared/tmp/pids/puma.pid
# reconsider if you actually need the forking config. # reconsider if you actually need the forking config.
Restart=no Restart=no
# `puma_ctl restart` wouldn't work without this. It's because `pumactl`
# changes PID on restart and systemd stops the service afterwards
# because of the PID change. This option prevents stopping after PID
# change.
RemainAfterExit=yes
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
~~~~ ~~~~

View file

@ -80,8 +80,10 @@
# can also use the "ssl_bind" option. # can also use the "ssl_bind" option.
# #
# ssl_bind '127.0.0.1', '9292', { # ssl_bind '127.0.0.1', '9292', {
# cert: path_to_cert,
# key: path_to_key, # key: path_to_key,
# cert: path_to_cert # ssl_cipher_filter: cipher_filter, # optional
# verify_mode: verify_mode, # default 'none'
# } # }
# for JRuby additional keys are required: # for JRuby additional keys are required:
# keystore: path_to_keystore, # keystore: path_to_keystore,
@ -180,7 +182,8 @@
# the given timeout. If not the worker process will be restarted. This is # the given timeout. If not the worker process will be restarted. This is
# not a request timeout, it is to protect against a hung or dead process. # not a request timeout, it is to protect against a hung or dead process.
# Setting this value will not protect against slow requests. # Setting this value will not protect against slow requests.
# Default value is 60 seconds. #
# The minimum value is 6 seconds, the default value is 60 seconds.
# #
# worker_timeout 60 # worker_timeout 60

View file

@ -11,7 +11,7 @@ require 'redis'
# 1. Add this plugin to your 'lib' directory # 1. Add this plugin to your 'lib' directory
# 2. In the `puma.rb` config file add the following lines # 2. In the `puma.rb` config file add the following lines
# === Plugins === # === Plugins ===
# require './lib/puma/plugins/redis_stop_puma' # require './lib/puma/plugin/redis_stop_puma'
# plugin 'redis_stop_puma' # plugin 'redis_stop_puma'
# 3. Now, when you set the redis key "puma::restart::web.1", your web.1 dyno # 3. Now, when you set the redis key "puma::restart::web.1", your web.1 dyno
# will restart # will restart

View file

@ -6,11 +6,13 @@ import org.jruby.Ruby;
import org.jruby.runtime.load.BasicLibraryService; import org.jruby.runtime.load.BasicLibraryService;
import org.jruby.puma.Http11; import org.jruby.puma.Http11;
import org.jruby.puma.IOBuffer;
import org.jruby.puma.MiniSSL; import org.jruby.puma.MiniSSL;
public class PumaHttp11Service implements BasicLibraryService { public class PumaHttp11Service implements BasicLibraryService {
public boolean basicLoad(final Ruby runtime) throws IOException { public boolean basicLoad(final Ruby runtime) throws IOException {
Http11.createHttp11(runtime); Http11.createHttp11(runtime);
IOBuffer.createIOBuffer(runtime);
MiniSSL.createMiniSSL(runtime); MiniSSL.createMiniSSL(runtime);
return true; return true;
} }

View file

@ -142,6 +142,7 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
VALUE obj; VALUE obj;
SSL_CTX* ctx; SSL_CTX* ctx;
SSL* ssl; SSL* ssl;
int ssl_options;
ms_conn* conn = engine_alloc(self, &obj); ms_conn* conn = engine_alloc(self, &obj);
@ -164,6 +165,10 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
ID sym_ssl_cipher_filter = rb_intern("ssl_cipher_filter"); ID sym_ssl_cipher_filter = rb_intern("ssl_cipher_filter");
VALUE ssl_cipher_filter = rb_funcall(mini_ssl_ctx, sym_ssl_cipher_filter, 0); VALUE ssl_cipher_filter = rb_funcall(mini_ssl_ctx, sym_ssl_cipher_filter, 0);
ID sym_no_tlsv1 = rb_intern("no_tlsv1");
VALUE no_tlsv1 = rb_funcall(mini_ssl_ctx, sym_no_tlsv1, 0);
ctx = SSL_CTX_new(SSLv23_server_method()); ctx = SSL_CTX_new(SSLv23_server_method());
conn->ctx = ctx; conn->ctx = ctx;
@ -175,7 +180,12 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
SSL_CTX_load_verify_locations(ctx, RSTRING_PTR(ca), NULL); SSL_CTX_load_verify_locations(ctx, RSTRING_PTR(ca), NULL);
} }
SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_SINGLE_DH_USE | SSL_OP_SINGLE_ECDH_USE | SSL_OP_NO_COMPRESSION); ssl_options = SSL_OP_CIPHER_SERVER_PREFERENCE | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_SINGLE_DH_USE | SSL_OP_SINGLE_ECDH_USE | SSL_OP_NO_COMPRESSION;
if(RTEST(no_tlsv1)) {
ssl_options |= SSL_OP_NO_TLSv1;
}
SSL_CTX_set_options(ctx, ssl_options);
SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
if (!NIL_P(ssl_cipher_filter)) { if (!NIL_P(ssl_cipher_filter)) {
@ -189,12 +199,18 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
DH *dh = get_dh1024(); DH *dh = get_dh1024();
SSL_CTX_set_tmp_dh(ctx, dh); SSL_CTX_set_tmp_dh(ctx, dh);
#ifndef OPENSSL_NO_ECDH #if OPENSSL_VERSION_NUMBER < 0x10002000L
EC_KEY *ecdh = EC_KEY_new_by_curve_name(NID_secp521r1); // Remove this case if OpenSSL 1.0.1 (now EOL) support is no
// longer needed.
EC_KEY *ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
if (ecdh) { if (ecdh) {
SSL_CTX_set_tmp_ecdh(ctx, ecdh); SSL_CTX_set_tmp_ecdh(ctx, ecdh);
EC_KEY_free(ecdh); EC_KEY_free(ecdh);
} }
#elif OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
// Prior to OpenSSL 1.1.0, servers must manually enable server-side ECDH
// negotiation.
SSL_CTX_set_ecdh_auto(ctx, 1);
#endif #endif
ssl = SSL_new(ctx); ssl = SSL_new(ctx);
@ -217,7 +233,7 @@ VALUE engine_init_client(VALUE klass) {
VALUE obj; VALUE obj;
ms_conn* conn = engine_alloc(klass, &obj); ms_conn* conn = engine_alloc(klass, &obj);
conn->ctx = SSL_CTX_new(DTLSv1_method()); conn->ctx = SSL_CTX_new(DTLS_method());
conn->ssl = SSL_new(conn->ctx); conn->ssl = SSL_new(conn->ctx);
SSL_set_app_data(conn->ssl, NULL); SSL_set_app_data(conn->ssl, NULL);
SSL_set_verify(conn->ssl, SSL_VERIFY_NONE, NULL); SSL_set_verify(conn->ssl, SSL_VERIFY_NONE, NULL);

View file

@ -0,0 +1,72 @@
package org.jruby.puma;
import org.jruby.*;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;
/**
* @author kares
*/
public class IOBuffer extends RubyObject {
private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
return new IOBuffer(runtime, klass);
}
};
public static void createIOBuffer(Ruby runtime) {
RubyModule mPuma = runtime.defineModule("Puma");
RubyClass cIOBuffer = mPuma.defineClassUnder("IOBuffer", runtime.getObject(), ALLOCATOR);
cIOBuffer.defineAnnotatedMethods(IOBuffer.class);
}
private static final int DEFAULT_SIZE = 4096;
final ByteList buffer = new ByteList(DEFAULT_SIZE);
IOBuffer(Ruby runtime, RubyClass klass) {
super(runtime, klass);
}
@JRubyMethod
public RubyInteger used(ThreadContext context) {
return context.runtime.newFixnum(buffer.getRealSize());
}
@JRubyMethod
public RubyInteger capacity(ThreadContext context) {
return context.runtime.newFixnum(buffer.unsafeBytes().length);
}
@JRubyMethod
public IRubyObject reset() {
buffer.setRealSize(0);
return this;
}
@JRubyMethod(name = { "to_s", "to_str" })
public RubyString to_s(ThreadContext context) {
return RubyString.newStringShared(context.runtime, buffer.unsafeBytes(), 0, buffer.getRealSize());
}
@JRubyMethod(name = "<<")
public IRubyObject add(IRubyObject str) {
addImpl(str.convertToString());
return this;
}
@JRubyMethod(rest = true)
public IRubyObject append(IRubyObject[] strs) {
for (IRubyObject str : strs) addImpl(str.convertToString());
return this;
}
private void addImpl(RubyString str) {
buffer.append(str.getByteList());
}
}

View file

@ -158,7 +158,13 @@ public class MiniSSL extends RubyObject {
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
engine = sslCtx.createSSLEngine(); engine = sslCtx.createSSLEngine();
String[] protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }; String[] protocols;
if(miniSSLContext.callMethod(threadContext, "no_tlsv1").isTrue()) {
protocols = new String[] { "TLSv1.1", "TLSv1.2" };
} else {
protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };
}
engine.setEnabledProtocols(protocols); engine.setEnabledProtocols(protocols);
engine.setUseClientMode(false); engine.setUseClientMode(false);

View file

@ -187,6 +187,8 @@ module Puma
ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter'] ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
end end
ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
if params['verify_mode'] if params['verify_mode']
ctx.verify_mode = case params['verify_mode'] ctx.verify_mode = case params['verify_mode']
when "peer" when "peer"

View file

@ -147,8 +147,11 @@ module Puma
def decode_chunk(chunk) def decode_chunk(chunk)
if @partial_part_left > 0 if @partial_part_left > 0
if @partial_part_left <= chunk.size if @partial_part_left <= chunk.size
@body << chunk[0..(@partial_part_left-3)] # skip the \r\n if @partial_part_left > 2
@body << chunk[0..(@partial_part_left-3)] # skip the \r\n
end
chunk = chunk[@partial_part_left..-1] chunk = chunk[@partial_part_left..-1]
@partial_part_left = 0
else else
@body << chunk @body << chunk
@partial_part_left -= chunk.size @partial_part_left -= chunk.size
@ -172,8 +175,7 @@ module Puma
rest = io.read rest = io.read
rest = rest[2..-1] if rest.start_with?("\r\n") rest = rest[2..-1] if rest.start_with?("\r\n")
@buffer = rest.empty? ? nil : rest @buffer = rest.empty? ? nil : rest
@requests_served += 1 set_ready
@ready = true
return true return true
end end
@ -211,7 +213,7 @@ module Puma
while true while true
begin begin
chunk = @io.read_nonblock(4096) chunk = @io.read_nonblock(4096)
rescue Errno::EAGAIN rescue IO::WaitReadable
return false return false
rescue SystemCallError, IOError rescue SystemCallError, IOError
raise ConnectionError, "Connection error detected during read" raise ConnectionError, "Connection error detected during read"
@ -221,8 +223,7 @@ module Puma
unless chunk unless chunk
@body.close @body.close
@buffer = nil @buffer = nil
@requests_served += 1 set_ready
@ready = true
raise EOFError raise EOFError
end end
@ -231,6 +232,8 @@ module Puma
end end
def setup_body def setup_body
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
if @env[HTTP_EXPECT] == CONTINUE if @env[HTTP_EXPECT] == CONTINUE
# TODO allow a hook here to check the headers before # TODO allow a hook here to check the headers before
# going forward # going forward
@ -255,8 +258,7 @@ module Puma
unless cl unless cl
@buffer = body.empty? ? nil : body @buffer = body.empty? ? nil : body
@body = EmptyBody @body = EmptyBody
@requests_served += 1 set_ready
@ready = true
return true return true
end end
@ -265,8 +267,7 @@ module Puma
if remain <= 0 if remain <= 0
@body = StringIO.new(body) @body = StringIO.new(body)
@buffer = nil @buffer = nil
@requests_served += 1 set_ready
@ready = true
return true return true
end end
@ -301,8 +302,7 @@ module Puma
# No data means a closed socket # No data means a closed socket
unless data unless data
@buffer = nil @buffer = nil
@requests_served += 1 set_ready
@ready = true
raise EOFError raise EOFError
end end
@ -338,8 +338,7 @@ module Puma
# No data means a closed socket # No data means a closed socket
unless data unless data
@buffer = nil @buffer = nil
@requests_served += 1 set_ready
@ready = true
raise EOFError raise EOFError
end end
@ -416,8 +415,7 @@ module Puma
unless chunk unless chunk
@body.close @body.close
@buffer = nil @buffer = nil
@requests_served += 1 set_ready
@ready = true
raise EOFError raise EOFError
end end
@ -426,8 +424,7 @@ module Puma
if remain <= 0 if remain <= 0
@body.rewind @body.rewind
@buffer = nil @buffer = nil
@requests_served += 1 set_ready
@ready = true
return true return true
end end
@ -436,6 +433,14 @@ module Puma
false false
end end
def set_ready
if @body_read_start
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
end
@requests_served += 1
@ready = true
end
def write_400 def write_400
begin begin
@io << ERROR_400_RESPONSE @io << ERROR_400_RESPONSE

View file

@ -295,12 +295,14 @@ module Puma
def ssl_bind(host, port, opts) def ssl_bind(host, port, opts)
verify = opts.fetch(:verify_mode, 'none') verify = opts.fetch(:verify_mode, 'none')
no_tlsv1 = opts.fetch(:no_tlsv1, 'false')
if defined?(JRUBY_VERSION) if defined?(JRUBY_VERSION)
keystore_additions = "keystore=#{opts[:keystore]}&keystore-pass=#{opts[:keystore_pass]}" keystore_additions = "keystore=#{opts[:keystore]}&keystore-pass=#{opts[:keystore_pass]}"
bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&#{keystore_additions}&verify_mode=#{verify}" bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&#{keystore_additions}&verify_mode=#{verify}&no_tlsv1=#{no_tlsv1}"
else else
bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&verify_mode=#{verify}" ssl_cipher_filter = "&ssl_cipher_filter=#{opts[:ssl_cipher_filter]}" if opts[:ssl_cipher_filter]
bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}#{ssl_cipher_filter}&verify_mode=#{verify}&no_tlsv1=#{no_tlsv1}"
end end
end end
@ -375,6 +377,21 @@ module Puma
alias_method :after_worker_boot, :after_worker_fork alias_method :after_worker_boot, :after_worker_fork
# Code to run out-of-band when the worker is idle.
# These hooks run immediately after a request has finished
# processing and there are no busy threads on the worker.
# The worker doesn't accept new requests until this code finishes.
#
# This hook is useful for running out-of-band garbage collection
# or scheduling asynchronous tasks to execute after a response.
#
# This can be called multiple times to add hooks.
#
def out_of_band(&block)
@options[:out_of_band] ||= []
@options[:out_of_band] << block
end
# The directory to operate out of. # The directory to operate out of.
def directory(dir) def directory(dir)
@options[:directory] = dir.to_s @options[:directory] = dir.to_s
@ -444,6 +461,13 @@ module Puma
# that have not checked in within the given +timeout+. # that have not checked in within the given +timeout+.
# This mitigates hung processes. Default value is 60 seconds. # This mitigates hung processes. Default value is 60 seconds.
def worker_timeout(timeout) def worker_timeout(timeout)
timeout = Integer(timeout)
min = Cluster::WORKER_CHECK_INTERVAL
if timeout <= min
raise "The minimum worker_timeout must be greater than the worker reporting interval (#{min})"
end
@options[:worker_timeout] = Integer(timeout) @options[:worker_timeout] = Integer(timeout)
end end

View file

@ -1,9 +1,4 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'puma/detect' require 'puma/detect'
require 'puma/puma_http11'
if Puma.jruby?
require 'puma/java_io_buffer'
else
require 'puma/puma_http11'
end

View file

@ -1,47 +0,0 @@
# frozen_string_literal: true
require 'java'
# Conservative native JRuby/Java implementation of IOBuffer
# backed by a ByteArrayOutputStream and conversion between
# Ruby String and Java bytes
module Puma
class JavaIOBuffer < java.io.ByteArrayOutputStream
field_reader :buf
end
class IOBuffer
BUF_DEFAULT_SIZE = 4096
def initialize
@buf = JavaIOBuffer.new(BUF_DEFAULT_SIZE)
end
def reset
@buf.reset
end
def <<(str)
bytes = str.to_java_bytes
@buf.write(bytes, 0, bytes.length)
end
def append(*strs)
strs.each { |s| self << s; }
end
def to_s
String.from_java_bytes @buf.to_byte_array
end
alias_method :to_str, :to_s
def used
@buf.size
end
def capacity
@buf.buf.length
end
end
end

View file

@ -2,7 +2,7 @@
begin begin
require 'io/wait' require 'io/wait'
rescue LoadError rescue LoadError
end end
module Puma module Puma
@ -177,6 +177,11 @@ module Puma
class Context class Context
attr_accessor :verify_mode attr_accessor :verify_mode
attr_reader :no_tlsv1
def initialize
@no_tlsv1 = false
end
if defined?(JRUBY_VERSION) if defined?(JRUBY_VERSION)
# jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair # jruby-specific Context properties: java uses a keystore and password pair rather than a cert/key pair
@ -215,11 +220,18 @@ module Puma
@ca = ca @ca = ca
end end
def check def check
raise "Key not configured" unless @key raise "Key not configured" unless @key
raise "Cert not configured" unless @cert raise "Cert not configured" unless @cert
end end
end end
def no_tlsv1=(tlsv1)
raise ArgumentError, "Invalid value of no_tlsv1" unless ['true', 'false', true, false].include?(tlsv1)
@no_tlsv1 = tlsv1
end
end end
VERIFY_NONE = 0 VERIFY_NONE = 0

View file

@ -1,33 +0,0 @@
# :stopdoc:
require 'uri/common'
# Issue:
# http://bugs.ruby-lang.org/issues/5925
#
# Relevant commit:
# https://github.com/ruby/ruby/commit/edb7cdf1eabaff78dfa5ffedfbc2e91b29fa9ca1
module URI
begin
256.times do |i|
TBLENCWWWCOMP_[i.chr] = '%%%02X' % i
end
TBLENCWWWCOMP_[' '] = '+'
TBLENCWWWCOMP_.freeze
256.times do |i|
h, l = i>>4, i&15
TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
end
TBLDECWWWCOMP_['+'] = ' '
TBLDECWWWCOMP_.freeze
rescue Exception
end
end
# :startdoc:

View file

@ -23,7 +23,7 @@ module Puma
# #
# When the request is written to by the client then the `IO.select` will "wake up" and # When the request is written to by the client then the `IO.select` will "wake up" and
# return the references to any objects that caused it to "wake". The reactor # return the references to any objects that caused it to "wake". The reactor
# then loops through each of these request objects, and sees if they're complete. If they # then loops through each of these request objects, and sees if they're complete. If they
# have a full header and body then the reactor passes the request to a thread pool. # have a full header and body then the reactor passes the request to a thread pool.
# Once in a thread pool, a "worker thread" can run the the application's Ruby code against the request. # Once in a thread pool, a "worker thread" can run the the application's Ruby code against the request.
# #
@ -38,7 +38,7 @@ module Puma
# Creates an instance of Puma::Reactor # Creates an instance of Puma::Reactor
# #
# The `server` argument is an instance of `Puma::Server` # The `server` argument is an instance of `Puma::Server`
# this is used to write a response for "low level errors" # that is used to write a response for "low level errors"
# when there is an exception inside of the reactor. # when there is an exception inside of the reactor.
# #
# The `app_pool` is an instance of `Puma::ThreadPool`. # The `app_pool` is an instance of `Puma::ThreadPool`.
@ -94,7 +94,7 @@ module Puma
# `ready` output looks like this: `[[#<Puma::Client:0x3fdc1103bee8 @ready=false>], [], []]`. # `ready` output looks like this: `[[#<Puma::Client:0x3fdc1103bee8 @ready=false>], [], []]`.
# #
# Each element in the first entry is iterated over. The `Puma::Client` object is not # Each element in the first entry is iterated over. The `Puma::Client` object is not
# the `@ready` pipe, so the reactor checks to see if it has the fully header and body with # the `@ready` pipe, so the reactor checks to see if it has the full header and body with
# the `Puma::Client#try_to_finish` method. If the full request has been sent, # the `Puma::Client#try_to_finish` method. If the full request has been sent,
# then the request is passed off to the `@app_pool` thread pool so that a "worker thread" # then the request is passed off to the `@app_pool` thread pool so that a "worker thread"
# can pick up the request and begin to execute application logic. This is done # can pick up the request and begin to execute application logic. This is done
@ -110,9 +110,9 @@ module Puma
# In addition to being woken via a write to one of the sockets the `IO.select` will # In addition to being woken via a write to one of the sockets the `IO.select` will
# periodically "time out" of the sleep. One of the functions of this is to check for # periodically "time out" of the sleep. One of the functions of this is to check for
# any requests that have "timed out". At the end of the loop it's checked to see if # any requests that have "timed out". At the end of the loop it's checked to see if
# the first element in the `@timeout` array has exceed it's allowed time. If so, # the first element in the `@timeout` array has exceed its allowed time. If so,
# the client object is removed from the timeout aray, a 408 response is written. # the client object is removed from the timeout array, a 408 response is written.
# Then it's connection is closed, and the object is removed from the `sockets` array # Then its connection is closed, and the object is removed from the `sockets` array
# that watches for new data. # that watches for new data.
# #
# This behavior loops until all the objects that have timed out have been removed. # This behavior loops until all the objects that have timed out have been removed.
@ -294,7 +294,7 @@ module Puma
# #
# The main body of the reactor loop is in `run_internal` and it # The main body of the reactor loop is in `run_internal` and it
# will sleep on `IO.select`. When a new connection is added to the # will sleep on `IO.select`. When a new connection is added to the
# reactor it cannot be added directly to the `sockets` aray, because # reactor it cannot be added directly to the `sockets` array, because
# the `IO.select` will not be watching for it yet. # the `IO.select` will not be watching for it yet.
# #
# Instead what needs to happen is that `IO.select` needs to be woken up, # Instead what needs to happen is that `IO.select` needs to be woken up,

View file

@ -16,10 +16,6 @@ require 'puma/util'
require 'puma/puma_http11' require 'puma/puma_http11'
unless Puma.const_defined? "IOBuffer"
require 'puma/io_buffer'
end
require 'socket' require 'socket'
module Puma module Puma
@ -79,7 +75,6 @@ module Puma
@first_data_timeout = options.fetch(:first_data_timeout, FIRST_DATA_TIMEOUT) @first_data_timeout = options.fetch(:first_data_timeout, FIRST_DATA_TIMEOUT)
@binder = Binder.new(events) @binder = Binder.new(events)
@own_binder = true
@leak_stack_on_error = true @leak_stack_on_error = true
@ -102,7 +97,6 @@ module Puma
def inherit_binder(bind) def inherit_binder(bind)
@binder = bind @binder = bind
@own_binder = false
end end
def tcp_mode! def tcp_mode!
@ -270,10 +264,11 @@ module Puma
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
end end
@notify.close # Prevent can't modify frozen IOError (RuntimeError)
begin
if @status != :restart and @own_binder @notify.close
@binder.close rescue IOError
# no biggy
end end
end end
@ -398,7 +393,10 @@ module Puma
end end
pool << client pool << client
pool.wait_until_not_full busy_threads = pool.wait_until_not_full
if busy_threads == 0
@options[:out_of_band].each(&:call) if @options[:out_of_band]
end
end end
rescue SystemCallError rescue SystemCallError
# nothing # nothing
@ -430,10 +428,6 @@ module Puma
ensure ensure
@check.close @check.close
@notify.close @notify.close
if @status != :restart and @own_binder
@binder.close
end
end end
@events.fire :state, :done @events.fire :state, :done
@ -942,6 +936,10 @@ module Puma
@events.debug "Drained #{count} additional connections." @events.debug "Drained #{count} additional connections."
end end
if @status != :restart
@binder.close
end
if @thread_pool if @thread_pool
if timeout = @options[:force_shutdown_after] if timeout = @options[:force_shutdown_after]
@thread_pool.shutdown timeout.to_i @thread_pool.shutdown timeout.to_i

View file

@ -26,7 +26,7 @@ module Puma
end end
def stop def stop
@server.stop false @server.stop(false) if @server
end end
def halt def halt
@ -36,7 +36,7 @@ module Puma
def stop_blocked def stop_blocked
log "- Gracefully stopping, waiting for requests to finish" log "- Gracefully stopping, waiting for requests to finish"
@control.stop(true) if @control @control.stop(true) if @control
@server.stop(true) @server.stop(true) if @server
end end
def jruby_daemon? def jruby_daemon?

View file

@ -194,6 +194,9 @@ module Puma
# method would not block and another request would be added into the reactor # method would not block and another request would be added into the reactor
# by the server. This would continue until a fully bufferend request # by the server. This would continue until a fully bufferend request
# makes it through the reactor and can then be processed by the thread pool. # makes it through the reactor and can then be processed by the thread pool.
#
# Returns the current number of busy threads, or +nil+ if shutting down.
#
def wait_until_not_full def wait_until_not_full
@mutex.synchronize do @mutex.synchronize do
while true while true
@ -203,7 +206,8 @@ module Puma
# is work queued that cannot be handled by waiting # is work queued that cannot be handled by waiting
# threads, then accept more work until we would # threads, then accept more work until we would
# spin up the max number of threads. # spin up the max number of threads.
return if @todo.size - @waiting < @max - @spawned busy_threads = @spawned - @waiting + @todo.size
return busy_threads if @max > busy_threads
@not_full.wait @mutex @not_full.wait @mutex
end end

View file

@ -1,11 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
if major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125 require 'uri/common'
require 'puma/rack/backports/uri/common_193'
else
require 'uri/common'
end
module Puma module Puma
module Util module Util

View file

@ -71,7 +71,28 @@ if ENV['CI']
Minitest::Retry.use! Minitest::Retry.use!
end end
module SkipTestsBasedOnRubyEngine module TestSkips
@@next_port = 9000
# usage: skip NO_FORK_MSG unless HAS_FORK
# windows >= 2.6 fork is not defined, < 2.6 fork raises NotImplementedError
HAS_FORK = ::Process.respond_to? :fork
NO_FORK_MSG = "Kernel.fork isn't available on the #{RUBY_PLATFORM} platform"
# socket is required by puma
# usage: skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
UNIX_SKT_EXIST = Object.const_defined? :UNIXSocket
UNIX_SKT_MSG = "UnixSockets aren't available on the #{RUBY_PLATFORM} platform"
# usage: skip_unless_signal_exist? :USR2
def skip_unless_signal_exist?(sig, bt: caller)
signal = sig.to_s
unless Signal.list.key? signal
skip "Signal #{signal} isn't available on the #{RUBY_PLATFORM} platform", bt
end
end
# called with one or more params, like skip_on :jruby, :windows # called with one or more params, like skip_on :jruby, :windows
# optional suffix kwarg is appended to the skip message # optional suffix kwarg is appended to the skip message
# optional suffix bt should generally not used # optional suffix bt should generally not used
@ -98,6 +119,10 @@ module SkipTestsBasedOnRubyEngine
end end
skip skip_msg, bt if skip_msg skip skip_msg, bt if skip_msg
end end
def next_port(incr = 1)
@@next_port += incr
end
end end
Minitest::Test.include SkipTestsBasedOnRubyEngine Minitest::Test.include TestSkips

4
test/rackup/10seconds.ru Normal file
View file

@ -0,0 +1,4 @@
run lambda { |env|
sleep 10
[200, {}, ["Hello World"]]
}

View file

@ -0,0 +1 @@
run lambda { |env| sleep 10; [200, {"Content-Type" => "text/plain"}, ["Hello World"]] }

View file

@ -54,4 +54,43 @@ class TestBinder < Minitest::Test
assert_equal(keystore, ctx.keystore) assert_equal(keystore, ctx.keystore)
assert_equal(ssl_cipher_list, ctx.ssl_cipher_list) assert_equal(ssl_cipher_list, ctx.ssl_cipher_list)
end end
def test_binder_parses_tlsv1_disabled
skip_on :jruby
key = File.expand_path "../../examples/puma/puma_keypair.pem", __FILE__
cert = File.expand_path "../../examples/puma/cert_puma.pem", __FILE__
@binder.parse(["ssl://0.0.0.0?key=#{key}&cert=#{cert}&no_tlsv1=true"], @events)
ssl = @binder.instance_variable_get(:@ios).first
ctx = ssl.instance_variable_get(:@ctx)
assert_equal(true, ctx.no_tlsv1)
end
def test_binder_parses_tlsv1_enabled
skip_on :jruby
key = File.expand_path "../../examples/puma/puma_keypair.pem", __FILE__
cert = File.expand_path "../../examples/puma/cert_puma.pem", __FILE__
@binder.parse(["ssl://0.0.0.0?key=#{key}&cert=#{cert}&no_tlsv1=false"], @events)
ssl = @binder.instance_variable_get(:@ios).first
ctx = ssl.instance_variable_get(:@ctx)
refute(ctx.no_tlsv1)
end
def test_binder_parses_tlsv1_unspecified_defaults_to_enabled
skip_on :jruby
key = File.expand_path "../../examples/puma/puma_keypair.pem", __FILE__
cert = File.expand_path "../../examples/puma/cert_puma.pem", __FILE__
@binder.parse(["ssl://0.0.0.0?key=#{key}&cert=#{cert}"], @events)
ssl = @binder.instance_variable_get(:@ios).first
ctx = ssl.instance_variable_get(:@ctx)
refute(ctx.no_tlsv1)
end
end end

View file

@ -40,8 +40,11 @@ class TestCLI < Minitest::Test
end end
def test_control_for_tcp def test_control_for_tcp
url = "tcp://127.0.0.1:9877/" tcp = next_port
cli = Puma::CLI.new ["-b", "tcp://127.0.0.1:9876", cntl = next_port
url = "tcp://127.0.0.1:#{cntl}/"
cli = Puma::CLI.new ["-b", "tcp://127.0.0.1:#{tcp}",
"--control", url, "--control", url,
"--control-token", "", "--control-token", "",
"test/rackup/lobster.ru"], @events "test/rackup/lobster.ru"], @events
@ -53,18 +56,22 @@ class TestCLI < Minitest::Test
wait_booted wait_booted
s = TCPSocket.new "127.0.0.1", 9877 s = TCPSocket.new "127.0.0.1", cntl
s << "GET /stats HTTP/1.0\r\n\r\n" s << "GET /stats HTTP/1.0\r\n\r\n"
body = s.read body = s.read
s.close
assert_equal '{ "backlog": 0, "running": 0, "pool_capacity": 16, "max_threads": 16 }', body.split(/\r?\n/).last assert_equal '{ "backlog": 0, "running": 0, "pool_capacity": 16, "max_threads": 16 }', body.split(/\r?\n/).last
assert_equal '{ "backlog": 0, "running": 0, "pool_capacity": 16, "max_threads": 16 }', Puma.stats assert_equal '{ "backlog": 0, "running": 0, "pool_capacity": 16, "max_threads": 16 }', Puma.stats
ensure
cli.launcher.stop cli.launcher.stop
t.join t.join
end end
def test_control_clustered def test_control_clustered
skip_on :jruby, :windows, suffix: " - Puma::Binder::UNIXServer is not defined" skip NO_FORK_MSG unless HAS_FORK
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
url = "unix://#{@tmp_path}" url = "unix://#{@tmp_path}"
cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}", cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}",
@ -102,7 +109,7 @@ class TestCLI < Minitest::Test
end end
def test_control def test_control
skip_on :jruby, :windows, suffix: " - Puma::Binder::UNIXServer is not defined" skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
url = "unix://#{@tmp_path}" url = "unix://#{@tmp_path}"
cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}", cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}",
@ -126,7 +133,7 @@ class TestCLI < Minitest::Test
end end
def test_control_stop def test_control_stop
skip_on :jruby, :windows, suffix: " - Puma::Binder::UNIXServer is not defined" skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
url = "unix://#{@tmp_path}" url = "unix://#{@tmp_path}"
cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}", cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}",
@ -148,21 +155,20 @@ class TestCLI < Minitest::Test
t.join t.join
end end
def test_control_gc_stats def control_gc_stats(uri, cntl)
skip_on :jruby, :windows, suffix: " - Puma::Binder::UNIXServer is not defined" cli = Puma::CLI.new ["-b", uri,
url = "unix://#{@tmp_path}" "--control", cntl,
cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}",
"--control", url,
"--control-token", "", "--control-token", "",
"test/rackup/lobster.ru"], @events "test/rackup/lobster.ru"], @events
t = Thread.new { cli.run } t = Thread.new do
t.abort_on_exception = true Thread.current.abort_on_exception = true
cli.run
end
wait_booted wait_booted
s = UNIXSocket.new @tmp_path s = yield
s << "GET /gc-stats HTTP/1.0\r\n\r\n" s << "GET /gc-stats HTTP/1.0\r\n\r\n"
body = s.read body = s.read
s.close s.close
@ -177,15 +183,16 @@ class TestCLI < Minitest::Test
end end
gc_count_before = gc_stats["count"].to_i gc_count_before = gc_stats["count"].to_i
s = UNIXSocket.new @tmp_path s = yield
s << "GET /gc HTTP/1.0\r\n\r\n" s << "GET /gc HTTP/1.0\r\n\r\n"
body = s.read # Ignored body = s.read # Ignored
s.close s.close
s = UNIXSocket.new @tmp_path s = yield
s << "GET /gc-stats HTTP/1.0\r\n\r\n" s << "GET /gc-stats HTTP/1.0\r\n\r\n"
body = s.read body = s.read
s.close s.close
lines = body.split("\r\n") lines = body.split("\r\n")
json_line = lines.detect { |l| l[0] == "{" } json_line = lines.detect { |l| l[0] == "{" }
pairs = json_line.scan(/\"[^\"]+\": [^,]+/) pairs = json_line.scan(/\"[^\"]+\": [^,]+/)
@ -197,15 +204,35 @@ class TestCLI < Minitest::Test
gc_count_after = gc_stats["count"].to_i gc_count_after = gc_stats["count"].to_i
# Hitting the /gc route should increment the count by 1 # Hitting the /gc route should increment the count by 1
assert_equal gc_count_before + 1, gc_count_after assert(gc_count_before < gc_count_after, "make sure a gc has happened")
cli.launcher.stop ensure
cli.launcher.stop if cli
t.join t.join
end end
def test_control_gc_stats_tcp
skip_on :jruby, suffix: " - Hitting /gc route does not increment count"
uri = "tcp://127.0.0.1:#{next_port}/"
cntl_port = next_port
cntl = "tcp://127.0.0.1:#{cntl_port}/"
control_gc_stats(uri, cntl) { TCPSocket.new "127.0.0.1", cntl_port }
end
def test_control_gc_stats_unix
skip_on :jruby, suffix: " - Hitting /gc route does not increment count"
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
uri = "unix://#{@tmp_path2}"
cntl = "unix://#{@tmp_path}"
control_gc_stats(uri, cntl) { UNIXSocket.new @tmp_path }
end
def test_tmp_control def test_tmp_control
skip_on :jruby skip_on :jruby, suffix: " - Unknown issue"
url = "tcp://127.0.0.1:8232"
cli = Puma::CLI.new ["--state", @tmp_path, "--control", "auto"] cli = Puma::CLI.new ["--state", @tmp_path, "--control", "auto"]
cli.launcher.write_state cli.launcher.write_state
@ -221,7 +248,7 @@ class TestCLI < Minitest::Test
end end
def test_state_file_callback_filtering def test_state_file_callback_filtering
skip_on :jruby, :windows, suffix: " - worker mode not supported" skip NO_FORK_MSG unless HAS_FORK
cli = Puma::CLI.new [ "--config", "test/config/state_file_testing_config.rb", cli = Puma::CLI.new [ "--config", "test/config/state_file_testing_config.rb",
"--state", @tmp_path ] "--state", @tmp_path ]
cli.launcher.write_state cli.launcher.write_state
@ -233,7 +260,7 @@ class TestCLI < Minitest::Test
end end
def test_state def test_state
url = "tcp://127.0.0.1:8232" url = "tcp://127.0.0.1:#{next_port}"
cli = Puma::CLI.new ["--state", @tmp_path, "--control", url] cli = Puma::CLI.new ["--state", @tmp_path, "--control", url]
cli.launcher.write_state cli.launcher.write_state

View file

@ -43,6 +43,41 @@ class TestConfigFile < Minitest::Test
end end
end end
def test_ssl_bind
skip_on :jruby
conf = Puma::Configuration.new do |c|
c.ssl_bind "0.0.0.0", "9292", {
cert: "/path/to/cert",
key: "/path/to/key",
verify_mode: "the_verify_mode",
}
end
conf.load
ssl_binding = "ssl://0.0.0.0:9292?cert=/path/to/cert&key=/path/to/key&verify_mode=the_verify_mode&no_tlsv1=false"
assert_equal [ssl_binding], conf.options[:binds]
end
def test_ssl_bind_with_cipher_filter
skip_on :jruby
cipher_filter = "!aNULL:AES+SHA"
conf = Puma::Configuration.new do |c|
c.ssl_bind "0.0.0.0", "9292", {
cert: "cert",
key: "key",
ssl_cipher_filter: cipher_filter,
}
end
conf.load
ssl_binding = conf.options[:binds].first
assert ssl_binding.include?("&ssl_cipher_filter=#{cipher_filter}")
end
def test_lowlevel_error_handler_DSL def test_lowlevel_error_handler_DSL
conf = Puma::Configuration.new do |c| conf = Puma::Configuration.new do |c|
c.load "test/config/app.rb" c.load "test/config/app.rb"

View file

@ -2,16 +2,17 @@ require_relative "helper"
require "puma/cli" require "puma/cli"
require "puma/control_cli" require "puma/control_cli"
require "open3"
# These don't run on travis because they're too fragile # These don't run on travis because they're too fragile
class TestIntegration < Minitest::Test class TestIntegration < Minitest::Test
def setup def setup
@state_path = "test/test_puma.state" @state_path = "test/test_puma.state"
@bind_path = "test/test_server.sock" @bind_path = "test/test_server.sock"
@control_path = "test/test_control.sock" @control_path = "test/test_control.sock"
@token = "xxyyzz" @token = "xxyyzz"
@tcp_port = 9998
@server = nil @server = nil
@ -41,6 +42,7 @@ class TestIntegration < Minitest::Test
end end
def server(argv) def server(argv)
@tcp_port = next_port
base = "#{Gem.ruby} -Ilib bin/puma" base = "#{Gem.ruby} -Ilib bin/puma"
base.prepend("bundle exec ") if defined?(Bundler) base.prepend("bundle exec ") if defined?(Bundler)
cmd = "#{base} -b tcp://127.0.0.1:#{@tcp_port} #{argv}" cmd = "#{base} -b tcp://127.0.0.1:#{@tcp_port} #{argv}"
@ -52,6 +54,7 @@ class TestIntegration < Minitest::Test
end end
def start_forked_server(argv) def start_forked_server(argv)
@tcp_port = next_port
pid = fork do pid = fork do
exec "#{Gem.ruby} -I lib/ bin/puma -b tcp://127.0.0.1:#{@tcp_port} #{argv}" exec "#{Gem.ruby} -I lib/ bin/puma -b tcp://127.0.0.1:#{@tcp_port} #{argv}"
end end
@ -67,7 +70,6 @@ class TestIntegration < Minitest::Test
end end
def restart_server_and_listen(argv) def restart_server_and_listen(argv)
skip_on :windows
server(argv) server(argv)
s = connect s = connect
initial_reply = read_body(s) initial_reply = read_body(s)
@ -115,7 +117,7 @@ class TestIntegration < Minitest::Test
end end
def test_stop_via_pumactl def test_stop_via_pumactl
skip_on :jruby, :windows skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
conf = Puma::Configuration.new do |c| conf = Puma::Configuration.new do |c|
c.quiet c.quiet
@ -148,7 +150,10 @@ class TestIntegration < Minitest::Test
end end
def test_phased_restart_via_pumactl def test_phased_restart_via_pumactl
skip_on :jruby, :windows, :ci, suffix: " - UNIX sockets are not recommended" skip NO_FORK_MSG unless HAS_FORK
# hello-stuck-ci uses sleep 10, hello-stuck uses sleep 60
rackup = "test/rackup/hello-stuck#{ ENV['CI'] ? '-ci' : '' }.ru"
conf = Puma::Configuration.new do |c| conf = Puma::Configuration.new do |c|
c.quiet c.quiet
@ -157,7 +162,7 @@ class TestIntegration < Minitest::Test
c.activate_control_app "unix://#{@control_path}", :auth_token => @token c.activate_control_app "unix://#{@control_path}", :auth_token => @token
c.workers 2 c.workers 2
c.worker_shutdown_timeout 1 c.worker_shutdown_timeout 1
c.rackup "test/rackup/hello-stuck.ru" c.rackup rackup
end end
l = Puma::Launcher.new conf, :events => @events l = Puma::Launcher.new conf, :events => @events
@ -195,11 +200,11 @@ class TestIntegration < Minitest::Test
end end
def test_kill_unknown_via_pumactl def test_kill_unknown_via_pumactl
skip_on :jruby, :windows skip_on :jruby
# we run ls to get a 'safe' pid to pass off as puma in cli stop # we run ls to get a 'safe' pid to pass off as puma in cli stop
# do not want to accidently kill a valid other process # do not want to accidently kill a valid other process
io = IO.popen("ls") io = IO.popen(windows? ? "dir" : "ls")
safe_pid = io.pid safe_pid = io.pid
Process.wait safe_pid Process.wait safe_pid
@ -210,17 +215,19 @@ class TestIntegration < Minitest::Test
ccli.run ccli.run
end end
sout.rewind sout.rewind
assert_match(/No pid '\d+' found/, sout.readlines.join("")) # windows bad URI(is not URI?)
assert_match(/No pid '\d+' found|bad URI\(is not URI\?\)/, sout.readlines.join(""))
assert_equal(1, e.status) assert_equal(1, e.status)
end end
def test_restart_closes_keepalive_sockets def test_restart_closes_keepalive_sockets
skip_unless_signal_exist? :USR2
_, new_reply = restart_server_and_listen("-q test/rackup/hello.ru") _, new_reply = restart_server_and_listen("-q test/rackup/hello.ru")
assert_equal "Hello World", new_reply assert_equal "Hello World", new_reply
end end
def test_restart_closes_keepalive_sockets_workers def test_restart_closes_keepalive_sockets_workers
skip_on :jruby skip NO_FORK_MSG unless HAS_FORK
_, new_reply = restart_server_and_listen("-q -w 2 test/rackup/hello.ru") _, new_reply = restart_server_and_listen("-q -w 2 test/rackup/hello.ru")
assert_equal "Hello World", new_reply assert_equal "Hello World", new_reply
end end
@ -230,6 +237,7 @@ class TestIntegration < Minitest::Test
# jruby has a bug where setting `nil` into the ENV or `delete` do not change the # jruby has a bug where setting `nil` into the ENV or `delete` do not change the
# next workers ENV # next workers ENV
skip_on :jruby skip_on :jruby
skip_unless_signal_exist? :USR2
initial_reply, new_reply = restart_server_and_listen("-q test/rackup/hello-env.ru") initial_reply, new_reply = restart_server_and_listen("-q test/rackup/hello-env.ru")
@ -239,7 +247,7 @@ class TestIntegration < Minitest::Test
end end
def test_term_signal_exit_code_in_single_mode def test_term_signal_exit_code_in_single_mode
skip_on :jruby, :windows skip NO_FORK_MSG unless HAS_FORK
pid = start_forked_server("test/rackup/hello.ru") pid = start_forked_server("test/rackup/hello.ru")
_, status = stop_forked_server(pid) _, status = stop_forked_server(pid)
@ -248,11 +256,38 @@ class TestIntegration < Minitest::Test
end end
def test_term_signal_exit_code_in_clustered_mode def test_term_signal_exit_code_in_clustered_mode
skip_on :jruby, :windows skip NO_FORK_MSG unless HAS_FORK
pid = start_forked_server("-w 2 test/rackup/hello.ru") pid = start_forked_server("-w 2 test/rackup/hello.ru")
_, status = stop_forked_server(pid) _, status = stop_forked_server(pid)
assert_equal 15, status assert_equal 15, status
end end
def test_not_accepts_new_connections_after_term_signal
skip_on :jruby, :windows
server('test/rackup/10seconds.ru')
_stdin, curl_stdout, _stderr, curl_wait_thread = Open3.popen3("curl 127.0.0.1:#{@tcp_port}")
sleep 1 # ensure curl send a request
Process.kill(:TERM, @server.pid)
true while @server.gets !~ /Gracefully stopping/ # wait for server to begin graceful shutdown
# Invoke a request which must be rejected
_stdin, _stdout, rejected_curl_stderr, rejected_curl_wait_thread = Open3.popen3("curl 127.0.0.1:#{@tcp_port}")
assert nil != Process.getpgid(@server.pid) # ensure server is still running
assert nil != Process.getpgid(rejected_curl_wait_thread[:pid]) # ensure first curl invokation still in progress
curl_wait_thread.join
rejected_curl_wait_thread.join
assert_match /Hello World/, curl_stdout.read
assert_match /Connection refused/, rejected_curl_stderr.read
Process.wait(@server.pid)
@server = nil # prevent `#teardown` from killing already killed server
end
end end

View file

@ -658,6 +658,83 @@ EOF
assert_equal "hello", body assert_equal "hello", body
end end
def test_chunked_request_pause_between_cr_lf_after_size_of_second_chunk
body = nil
@server.app = proc { |env|
body = env['rack.input'].read
[200, {}, [""]]
}
@server.add_tcp_listener @host, @port
@server.run
part1 = 'a' * 4200
chunked_body = "#{part1.size.to_s(16)}\r\n#{part1}\r\n1\r\nb\r\n0\r\n\r\n"
sock = TCPSocket.new @host, @server.connected_port
sock << "PUT /path HTTP/1.1\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n"
sleep 0.1
sock << chunked_body[0..-10]
sleep 0.1
sock << chunked_body[-9..-1]
data = sock.read
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal (part1 + 'b'), body
end
def test_chunked_request_pause_between_closing_cr_lf
body = nil
@server.app = proc { |env|
body = env['rack.input'].read
[200, {}, [""]]
}
@server.add_tcp_listener @host, @port
@server.run
sock = TCPSocket.new @host, @server.connected_port
sock << "PUT /path HTTP/1.1\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r"
sleep 1
sock << "\n0\r\n\r\n"
data = sock.read
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal 'hello', body
end
def test_chunked_request_pause_before_closing_cr_lf
body = nil
@server.app = proc { |env|
body = env['rack.input'].read
[200, {}, [""]]
}
@server.add_tcp_listener @host, @port
@server.run
sock = TCPSocket.new @host, @server.connected_port
sock << "PUT /path HTTP/1.1\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello"
sleep 1
sock << "\r\n0\r\n\r\n"
data = sock.read
assert_equal "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", data
assert_equal 'hello', body
end
def test_chunked_request_header_case def test_chunked_request_header_case
body = nil body = nil
@server.app = proc { |env| @server.app = proc { |env|
@ -740,4 +817,44 @@ EOF
assert_equal "HTTP/1.0 200 OK\r\nX-Empty-Header: \r\n\r\n", data assert_equal "HTTP/1.0 200 OK\r\nX-Empty-Header: \r\n\r\n", data
end end
def test_request_body_wait
request_body_wait = nil
@server.app = proc { |env|
request_body_wait = env['puma.request_body_wait']
[204, {}, []]
}
@server.add_tcp_listener @host, @port
@server.run
sock = TCPSocket.new @host, @server.connected_port
sock << "POST / HTTP/1.1\r\nHost: test.com\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nh"
sleep 1
sock << "ello"
sock.gets
assert request_body_wait >= 1000
end
def test_request_body_wait_chunked
request_body_wait = nil
@server.app = proc { |env|
request_body_wait = env['puma.request_body_wait']
[204, {}, []]
}
@server.add_tcp_listener @host, @port
@server.run
sock = TCPSocket.new @host, @server.connected_port
sock << "GET / HTTP/1.1\r\nConnection: close\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nh\r\n"
sleep 1
sock << "4\r\nello\r\n0\r\n"
sock.gets
assert request_body_wait >= 1000
end
end end

View file

@ -1,6 +1,8 @@
require_relative "helper" require_relative "helper"
require "puma/minissl" require "puma/minissl"
require "puma/puma_http11" require "puma/puma_http11"
# net/http (loaded in helper) does not necessarily load OpenSSL
require "openssl" unless Object.const_defined? :OpenSSL
#——————————————————————————————————————————————————————————————————————————————— #———————————————————————————————————————————————————————————————————————————————
# NOTE: ALL TESTS BYPASSED IF DISABLE_SSL IS TRUE # NOTE: ALL TESTS BYPASSED IF DISABLE_SSL IS TRUE
@ -113,13 +115,15 @@ class TestPumaServerSSL < Minitest::Test
def test_ssl_v3_rejection def test_ssl_v3_rejection
@http.ssl_version= :SSLv3 @http.ssl_version= :SSLv3
assert_raises(OpenSSL::SSL::SSLError) do # Ruby 2.4.5 on Travis raises ArgumentError
assert_raises(OpenSSL::SSL::SSLError, ArgumentError) do
@http.start do @http.start do
Net::HTTP::Get.new '/' Net::HTTP::Get.new '/'
end end
end end
unless Puma.jruby? unless Puma.jruby?
assert_match(/wrong version number|no protocols available/, @events.error.message) if @events.error msg = /wrong version number|no protocols available|version too low|unknown SSL method/
assert_match(msg, @events.error.message) if @events.error
end end
end end
@ -147,7 +151,7 @@ class TestPumaServerSSLClient < Minitest::Test
events = SSLEventsHelper.new STDOUT, STDERR events = SSLEventsHelper.new STDOUT, STDERR
server = Puma::Server.new app, events server = Puma::Server.new app, events
ssl_listener = server.add_ssl_listener host, port, ctx server.add_ssl_listener host, port, ctx
server.run server.run
http = Net::HTTP.new host, port http = Net::HTTP.new host, port

View file

@ -7,8 +7,7 @@ class TestPumaUnixSocket < Minitest::Test
Path = "test/puma.sock" Path = "test/puma.sock"
def setup def setup
# UNIX sockets are not recommended on JRuby or Windows skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
skip_on :jruby, :windows, suffix: " - UNIX sockets are not recommended"
@server = Puma::Server.new App @server = Puma::Server.new App
@server.add_unix_listener Path @server.add_unix_listener Path
@server.run @server.run

View file

@ -47,11 +47,11 @@ do_start_one() {
PIDFILE=$1/tmp/puma/pid PIDFILE=$1/tmp/puma/pid
if [ -e $PIDFILE ]; then if [ -e $PIDFILE ]; then
PID=`cat $PIDFILE` PID=`cat $PIDFILE`
# If the puma isn't running, run it, otherwise restart it. # If the puma is running, restart it, otherwise run it.
if ps -p $PID > /dev/null; then if ps -p $PID > /dev/null; then
do_start_one_do $1
else
do_restart_one $1 do_restart_one $1
else
do_start_one_do $1
fi fi
else else
do_start_one_do $1 do_start_one_do $1
@ -106,8 +106,6 @@ do_stop_one() {
if [ -e $PIDFILE ]; then if [ -e $PIDFILE ]; then
PID=`cat $PIDFILE` PID=`cat $PIDFILE`
if ps -p $PID > /dev/null; then if ps -p $PID > /dev/null; then
log_daemon_msg "---> Puma $1 isn't running."
else
log_daemon_msg "---> About to kill PID `cat $PIDFILE`" log_daemon_msg "---> About to kill PID `cat $PIDFILE`"
if [ "$USE_LOCAL_BUNDLE" -eq 1 ]; then if [ "$USE_LOCAL_BUNDLE" -eq 1 ]; then
cd $1 && bundle exec pumactl --state $STATEFILE stop cd $1 && bundle exec pumactl --state $STATEFILE stop
@ -116,6 +114,8 @@ do_stop_one() {
fi fi
# Many daemons don't delete their pidfiles when they exit. # Many daemons don't delete their pidfiles when they exit.
rm -f $PIDFILE $STATEFILE rm -f $PIDFILE $STATEFILE
else
log_daemon_msg "---> Puma $1 isn't running."
fi fi
else else
log_daemon_msg "---> No puma here..." log_daemon_msg "---> No puma here..."