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
* text eol=lf
*.png binary

View file

@ -1,11 +1,9 @@
dist: trusty
sudo: false
group: beta
dist: xenial
language: ruby
cache: bundler
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')";
if [ "$rv" \< "2.3" ]; then gem update --system 2.7.8 --no-document
@ -13,13 +11,19 @@ before_install:
fi
- ruby -v && gem --version && bundle version
before_script:
- bundle exec rake compile
script:
- bundle exec rake
rvm:
- 2.2.10
- 2.3.8
- 2.4.5
- 2.5.3
- 2.6
- ruby-head
- jruby-9.2.0.0
matrix:
fast_finish: true
@ -30,15 +34,21 @@ matrix:
os: osx
- rvm: 2.5.3
os: osx
- rvm: jruby-9.2.5.0
dist: trusty
sudo: false
- rvm: jruby-head
- rvm: rbx-3
dist: trusty
sudo: false
allow_failures:
- rvm: 2.6
- rvm: ruby-head
- rvm: ruby-head
env: RUBYOPT="--jit"
- rvm: jruby-head
- rvm: rbx-3
dist: trusty
sudo: false
env:
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)
[![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)
[![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)
[![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.
@ -25,7 +25,7 @@ On MRI, there is a Global VM Lock (GVL) that ensures only one thread can run Rub
```
$ gem install puma
$ puma <any rackup (*.ru) file>
```
```
## 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'
```
#### 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'
```
#####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'
```
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
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]
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
desc "Run the integration tests"
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.
* 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
* 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
* Worker threads pop work off the "todo" set for processing
* 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
workers down and the threads up.
#### Ubuntu / Systemd (Systemctl) Installation
See [systemd.md](systemd.md)
#### 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.
But since so many apps are waiting on IO from DBs, etc., they can utilize threads
to make better use of the process.
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
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.
**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
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.
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
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

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)).
**Note**: Hot and phased restarts are only available on MRI, not on JRuby. They are also unavailable on Windows servers.
### 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.

View file

@ -32,21 +32,26 @@ Type=simple
# Preferably configure a non-privileged user
# User=
# The path to the puma application root
# Also replace the "<WD>" place holders below with this path.
WorkingDirectory=
# The path to the your application code root directory.
# Also replace the "<YOUR_APP_PATH>" place holders below with this path.
# Example /home/username/myapp
WorkingDirectory=<YOUR_APP_PATH>
# Helpful for debugging socket activation, etc.
# Environment=PUMA_DEBUG=1
# The command to start Puma. This variant uses a binstub generated via
# `bundle binstubs puma --path ./sbin` in the WorkingDirectory
# (replace "<WD>" below)
ExecStart=<WD>/sbin/puma -b tcp://0.0.0.0:9292 -b ssl://0.0.0.0:9293?key=key.pem&cert=cert.pem
# SystemD will not run puma even if it is in your path. You must specify
# an absolute URL to puma. For example /usr/local/bin/puma
# Alternatively, create a binstub with `bundle binstubs puma --path ./sbin` in the WorkingDirectory
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: 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
@ -247,6 +252,12 @@ PIDFile=<WD>/shared/tmp/pids/puma.pid
# reconsider if you actually need the forking config.
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]
WantedBy=multi-user.target
~~~~

View file

@ -80,8 +80,10 @@
# can also use the "ssl_bind" option.
#
# ssl_bind '127.0.0.1', '9292', {
# cert: path_to_cert,
# 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:
# keystore: path_to_keystore,
@ -180,7 +182,8 @@
# 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.
# 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

View file

@ -11,7 +11,7 @@ require 'redis'
# 1. Add this plugin to your 'lib' directory
# 2. In the `puma.rb` config file add the following lines
# === Plugins ===
# require './lib/puma/plugins/redis_stop_puma'
# require './lib/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
# will restart

View file

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

View file

@ -142,6 +142,7 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
VALUE obj;
SSL_CTX* ctx;
SSL* ssl;
int ssl_options;
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");
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());
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_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);
if (!NIL_P(ssl_cipher_filter)) {
@ -189,12 +199,18 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
DH *dh = get_dh1024();
SSL_CTX_set_tmp_dh(ctx, dh);
#ifndef OPENSSL_NO_ECDH
EC_KEY *ecdh = EC_KEY_new_by_curve_name(NID_secp521r1);
#if OPENSSL_VERSION_NUMBER < 0x10002000L
// 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) {
SSL_CTX_set_tmp_ecdh(ctx, 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
ssl = SSL_new(ctx);
@ -217,7 +233,7 @@ VALUE engine_init_client(VALUE klass) {
VALUE 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);
SSL_set_app_data(conn->ssl, 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);
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.setUseClientMode(false);

View file

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

View file

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

View file

@ -295,12 +295,14 @@ module Puma
def ssl_bind(host, port, opts)
verify = opts.fetch(:verify_mode, 'none')
no_tlsv1 = opts.fetch(:no_tlsv1, 'false')
if defined?(JRUBY_VERSION)
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
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
@ -375,6 +377,21 @@ module Puma
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.
def directory(dir)
@options[:directory] = dir.to_s
@ -444,6 +461,13 @@ module Puma
# that have not checked in within the given +timeout+.
# This mitigates hung processes. Default value is 60 seconds.
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)
end

View file

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

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
require 'io/wait'
rescue LoadError
rescue LoadError
end
module Puma
@ -177,6 +177,11 @@ module Puma
class Context
attr_accessor :verify_mode
attr_reader :no_tlsv1
def initialize
@no_tlsv1 = false
end
if defined?(JRUBY_VERSION)
# 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
end
def check
raise "Key not configured" unless @key
raise "Cert not configured" unless @cert
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
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
# 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.
# 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
#
# 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.
#
# 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>], [], []]`.
#
# 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,
# 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
@ -110,9 +110,9 @@ module Puma
# 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
# 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 client object is removed from the timeout aray, a 408 response is written.
# Then it's connection is closed, and the object is removed from the `sockets` array
# the first element in the `@timeout` array has exceed its allowed time. If so,
# the client object is removed from the timeout array, a 408 response is written.
# Then its connection is closed, and the object is removed from the `sockets` array
# that watches for new data.
#
# 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
# 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.
#
# 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'
unless Puma.const_defined? "IOBuffer"
require 'puma/io_buffer'
end
require 'socket'
module Puma
@ -79,7 +75,6 @@ module Puma
@first_data_timeout = options.fetch(:first_data_timeout, FIRST_DATA_TIMEOUT)
@binder = Binder.new(events)
@own_binder = true
@leak_stack_on_error = true
@ -102,7 +97,6 @@ module Puma
def inherit_binder(bind)
@binder = bind
@own_binder = false
end
def tcp_mode!
@ -270,10 +264,11 @@ module Puma
Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
end
@notify.close
if @status != :restart and @own_binder
@binder.close
# Prevent can't modify frozen IOError (RuntimeError)
begin
@notify.close
rescue IOError
# no biggy
end
end
@ -398,7 +393,10 @@ module Puma
end
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
rescue SystemCallError
# nothing
@ -430,10 +428,6 @@ module Puma
ensure
@check.close
@notify.close
if @status != :restart and @own_binder
@binder.close
end
end
@events.fire :state, :done
@ -942,6 +936,10 @@ module Puma
@events.debug "Drained #{count} additional connections."
end
if @status != :restart
@binder.close
end
if @thread_pool
if timeout = @options[:force_shutdown_after]
@thread_pool.shutdown timeout.to_i

View file

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

View file

@ -194,6 +194,9 @@ module Puma
# method would not block and another request would be added into the reactor
# 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.
#
# Returns the current number of busy threads, or +nil+ if shutting down.
#
def wait_until_not_full
@mutex.synchronize do
while true
@ -203,7 +206,8 @@ module Puma
# is work queued that cannot be handled by waiting
# threads, then accept more work until we would
# 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
end

View file

@ -1,11 +1,6 @@
# 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 'puma/rack/backports/uri/common_193'
else
require 'uri/common'
end
require 'uri/common'
module Puma
module Util

View file

@ -71,7 +71,28 @@ if ENV['CI']
Minitest::Retry.use!
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
# optional suffix kwarg is appended to the skip message
# optional suffix bt should generally not used
@ -98,6 +119,10 @@ module SkipTestsBasedOnRubyEngine
end
skip skip_msg, bt if skip_msg
end
def next_port(incr = 1)
@@next_port += incr
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(ssl_cipher_list, ctx.ssl_cipher_list)
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

View file

@ -40,8 +40,11 @@ class TestCLI < Minitest::Test
end
def test_control_for_tcp
url = "tcp://127.0.0.1:9877/"
cli = Puma::CLI.new ["-b", "tcp://127.0.0.1:9876",
tcp = next_port
cntl = next_port
url = "tcp://127.0.0.1:#{cntl}/"
cli = Puma::CLI.new ["-b", "tcp://127.0.0.1:#{tcp}",
"--control", url,
"--control-token", "",
"test/rackup/lobster.ru"], @events
@ -53,18 +56,22 @@ class TestCLI < Minitest::Test
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"
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 }', Puma.stats
ensure
cli.launcher.stop
t.join
end
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}"
cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}",
@ -102,7 +109,7 @@ class TestCLI < Minitest::Test
end
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}"
cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}",
@ -126,7 +133,7 @@ class TestCLI < Minitest::Test
end
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}"
cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}",
@ -148,21 +155,20 @@ class TestCLI < Minitest::Test
t.join
end
def test_control_gc_stats
skip_on :jruby, :windows, suffix: " - Puma::Binder::UNIXServer is not defined"
url = "unix://#{@tmp_path}"
cli = Puma::CLI.new ["-b", "unix://#{@tmp_path2}",
"--control", url,
def control_gc_stats(uri, cntl)
cli = Puma::CLI.new ["-b", uri,
"--control", cntl,
"--control-token", "",
"test/rackup/lobster.ru"], @events
t = Thread.new { cli.run }
t.abort_on_exception = true
t = Thread.new do
Thread.current.abort_on_exception = true
cli.run
end
wait_booted
s = UNIXSocket.new @tmp_path
s = yield
s << "GET /gc-stats HTTP/1.0\r\n\r\n"
body = s.read
s.close
@ -177,15 +183,16 @@ class TestCLI < Minitest::Test
end
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"
body = s.read # Ignored
s.close
s = UNIXSocket.new @tmp_path
s = yield
s << "GET /gc-stats HTTP/1.0\r\n\r\n"
body = s.read
s.close
lines = body.split("\r\n")
json_line = lines.detect { |l| l[0] == "{" }
pairs = json_line.scan(/\"[^\"]+\": [^,]+/)
@ -197,15 +204,35 @@ class TestCLI < Minitest::Test
gc_count_after = gc_stats["count"].to_i
# 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
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
skip_on :jruby
url = "tcp://127.0.0.1:8232"
skip_on :jruby, suffix: " - Unknown issue"
cli = Puma::CLI.new ["--state", @tmp_path, "--control", "auto"]
cli.launcher.write_state
@ -221,7 +248,7 @@ class TestCLI < Minitest::Test
end
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",
"--state", @tmp_path ]
cli.launcher.write_state
@ -233,7 +260,7 @@ class TestCLI < Minitest::Test
end
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.launcher.write_state

View file

@ -43,6 +43,41 @@ class TestConfigFile < Minitest::Test
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
conf = Puma::Configuration.new do |c|
c.load "test/config/app.rb"

View file

@ -2,16 +2,17 @@ require_relative "helper"
require "puma/cli"
require "puma/control_cli"
require "open3"
# These don't run on travis because they're too fragile
class TestIntegration < Minitest::Test
def setup
@state_path = "test/test_puma.state"
@bind_path = "test/test_server.sock"
@control_path = "test/test_control.sock"
@token = "xxyyzz"
@tcp_port = 9998
@server = nil
@ -41,6 +42,7 @@ class TestIntegration < Minitest::Test
end
def server(argv)
@tcp_port = next_port
base = "#{Gem.ruby} -Ilib bin/puma"
base.prepend("bundle exec ") if defined?(Bundler)
cmd = "#{base} -b tcp://127.0.0.1:#{@tcp_port} #{argv}"
@ -52,6 +54,7 @@ class TestIntegration < Minitest::Test
end
def start_forked_server(argv)
@tcp_port = next_port
pid = fork do
exec "#{Gem.ruby} -I lib/ bin/puma -b tcp://127.0.0.1:#{@tcp_port} #{argv}"
end
@ -67,7 +70,6 @@ class TestIntegration < Minitest::Test
end
def restart_server_and_listen(argv)
skip_on :windows
server(argv)
s = connect
initial_reply = read_body(s)
@ -115,7 +117,7 @@ class TestIntegration < Minitest::Test
end
def test_stop_via_pumactl
skip_on :jruby, :windows
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
conf = Puma::Configuration.new do |c|
c.quiet
@ -148,7 +150,10 @@ class TestIntegration < Minitest::Test
end
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|
c.quiet
@ -157,7 +162,7 @@ class TestIntegration < Minitest::Test
c.activate_control_app "unix://#{@control_path}", :auth_token => @token
c.workers 2
c.worker_shutdown_timeout 1
c.rackup "test/rackup/hello-stuck.ru"
c.rackup rackup
end
l = Puma::Launcher.new conf, :events => @events
@ -195,11 +200,11 @@ class TestIntegration < Minitest::Test
end
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
# do not want to accidently kill a valid other process
io = IO.popen("ls")
io = IO.popen(windows? ? "dir" : "ls")
safe_pid = io.pid
Process.wait safe_pid
@ -210,17 +215,19 @@ class TestIntegration < Minitest::Test
ccli.run
end
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)
end
def test_restart_closes_keepalive_sockets
skip_unless_signal_exist? :USR2
_, new_reply = restart_server_and_listen("-q test/rackup/hello.ru")
assert_equal "Hello World", new_reply
end
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")
assert_equal "Hello World", new_reply
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
# next workers ENV
skip_on :jruby
skip_unless_signal_exist? :USR2
initial_reply, new_reply = restart_server_and_listen("-q test/rackup/hello-env.ru")
@ -239,7 +247,7 @@ class TestIntegration < Minitest::Test
end
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")
_, status = stop_forked_server(pid)
@ -248,11 +256,38 @@ class TestIntegration < Minitest::Test
end
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")
_, status = stop_forked_server(pid)
assert_equal 15, status
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

View file

@ -658,6 +658,83 @@ EOF
assert_equal "hello", body
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
body = nil
@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
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

View file

@ -1,6 +1,8 @@
require_relative "helper"
require "puma/minissl"
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
@ -113,13 +115,15 @@ class TestPumaServerSSL < Minitest::Test
def test_ssl_v3_rejection
@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
Net::HTTP::Get.new '/'
end
end
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
@ -147,7 +151,7 @@ class TestPumaServerSSLClient < Minitest::Test
events = SSLEventsHelper.new STDOUT, STDERR
server = Puma::Server.new app, events
ssl_listener = server.add_ssl_listener host, port, ctx
server.add_ssl_listener host, port, ctx
server.run
http = Net::HTTP.new host, port

View file

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

View file

@ -47,11 +47,11 @@ do_start_one() {
PIDFILE=$1/tmp/puma/pid
if [ -e $PIDFILE ]; then
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
do_start_one_do $1
else
do_restart_one $1
else
do_start_one_do $1
fi
else
do_start_one_do $1
@ -106,8 +106,6 @@ do_stop_one() {
if [ -e $PIDFILE ]; then
PID=`cat $PIDFILE`
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`"
if [ "$USE_LOCAL_BUNDLE" -eq 1 ]; then
cd $1 && bundle exec pumactl --state $STATEFILE stop
@ -116,6 +114,8 @@ do_stop_one() {
fi
# Many daemons don't delete their pidfiles when they exit.
rm -f $PIDFILE $STATEFILE
else
log_daemon_msg "---> Puma $1 isn't running."
fi
else
log_daemon_msg "---> No puma here..."