puma 4.0.1 → 4.2.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +66 -3
- data/README.md +67 -39
- data/docs/plugins.md +20 -10
- data/ext/puma_http11/http11_parser.c +37 -62
- data/ext/puma_http11/http11_parser_common.rl +3 -3
- data/ext/puma_http11/mini_ssl.c +55 -7
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +4 -0
- data/lib/puma.rb +8 -0
- data/lib/puma/accept_nonblock.rb +7 -1
- data/lib/puma/app/status.rb +33 -28
- data/lib/puma/binder.rb +37 -12
- data/lib/puma/cli.rb +4 -0
- data/lib/puma/client.rb +197 -193
- data/lib/puma/cluster.rb +45 -46
- data/lib/puma/configuration.rb +2 -2
- data/lib/puma/const.rb +18 -18
- data/lib/puma/control_cli.rb +11 -4
- data/lib/puma/dsl.rb +261 -81
- data/lib/puma/events.rb +4 -1
- data/lib/puma/launcher.rb +90 -47
- data/lib/puma/minissl.rb +25 -19
- data/lib/puma/plugin.rb +5 -2
- data/lib/puma/plugin/tmp_restart.rb +2 -0
- data/lib/puma/rack/builder.rb +2 -0
- data/lib/puma/rack/urlmap.rb +2 -0
- data/lib/puma/rack_default.rb +2 -0
- data/lib/puma/reactor.rb +6 -5
- data/lib/puma/runner.rb +4 -3
- data/lib/puma/server.rb +33 -23
- data/lib/puma/single.rb +1 -1
- data/lib/puma/thread_pool.rb +9 -31
- data/lib/rack/handler/puma.rb +3 -3
- data/tools/docker/Dockerfile +16 -0
- data/tools/jungle/init.d/puma +1 -1
- data/tools/trickletest.rb +0 -1
- metadata +4 -4
- data/lib/puma/daemon_ext.rb +0 -33
- data/lib/puma/delegation.rb +0 -13
@@ -1,5 +1,5 @@
|
|
1
1
|
%%{
|
2
|
-
|
2
|
+
|
3
3
|
machine puma_parser_common;
|
4
4
|
|
5
5
|
#### HTTP PROTOCOL GRAMMAR
|
@@ -16,7 +16,7 @@
|
|
16
16
|
unreserved = (alpha | digit | safe | extra | national);
|
17
17
|
escape = ("%" xdigit xdigit);
|
18
18
|
uchar = (unreserved | escape | "%");
|
19
|
-
pchar = (uchar | ":" | "@" | "&" | "=" | "+");
|
19
|
+
pchar = (uchar | ":" | "@" | "&" | "=" | "+" | ";");
|
20
20
|
tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");
|
21
21
|
|
22
22
|
# elements
|
@@ -30,7 +30,7 @@
|
|
30
30
|
query = ( uchar | reserved )* %query_string ;
|
31
31
|
param = ( pchar | "/" )* ;
|
32
32
|
params = ( param ( ";" param )* ) ;
|
33
|
-
rel_path = ( path? %request_path
|
33
|
+
rel_path = ( path? %request_path ) ("?" %start_query query)?;
|
34
34
|
absolute_path = ( "/"+ rel_path );
|
35
35
|
|
36
36
|
Request_URI = ( "*" | absolute_uri | absolute_path ) >mark %request_uri;
|
data/ext/puma_http11/mini_ssl.c
CHANGED
@@ -142,7 +142,7 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
|
|
142
142
|
VALUE obj;
|
143
143
|
SSL_CTX* ctx;
|
144
144
|
SSL* ssl;
|
145
|
-
int ssl_options;
|
145
|
+
int min, ssl_options;
|
146
146
|
|
147
147
|
ms_conn* conn = engine_alloc(self, &obj);
|
148
148
|
|
@@ -168,6 +168,9 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
|
|
168
168
|
ID sym_no_tlsv1 = rb_intern("no_tlsv1");
|
169
169
|
VALUE no_tlsv1 = rb_funcall(mini_ssl_ctx, sym_no_tlsv1, 0);
|
170
170
|
|
171
|
+
ID sym_no_tlsv1_1 = rb_intern("no_tlsv1_1");
|
172
|
+
VALUE no_tlsv1_1 = rb_funcall(mini_ssl_ctx, sym_no_tlsv1_1, 0);
|
173
|
+
|
171
174
|
#ifdef HAVE_TLS_SERVER_METHOD
|
172
175
|
ctx = SSL_CTX_new(TLS_server_method());
|
173
176
|
#else
|
@@ -183,12 +186,36 @@ VALUE engine_init_server(VALUE self, VALUE mini_ssl_ctx) {
|
|
183
186
|
SSL_CTX_load_verify_locations(ctx, RSTRING_PTR(ca), NULL);
|
184
187
|
}
|
185
188
|
|
186
|
-
ssl_options
|
189
|
+
ssl_options = SSL_OP_CIPHER_SERVER_PREFERENCE | SSL_OP_SINGLE_ECDH_USE | SSL_OP_NO_COMPRESSION;
|
187
190
|
|
188
|
-
|
191
|
+
#ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION
|
192
|
+
if (RTEST(no_tlsv1_1)) {
|
193
|
+
min = TLS1_2_VERSION;
|
194
|
+
}
|
195
|
+
else if (RTEST(no_tlsv1)) {
|
196
|
+
min = TLS1_1_VERSION;
|
197
|
+
}
|
198
|
+
else {
|
199
|
+
min = TLS1_VERSION;
|
200
|
+
}
|
201
|
+
|
202
|
+
SSL_CTX_set_min_proto_version(ctx, min);
|
203
|
+
|
204
|
+
SSL_CTX_set_options(ctx, ssl_options);
|
205
|
+
|
206
|
+
#else
|
207
|
+
/* As of 1.0.2f, SSL_OP_SINGLE_DH_USE key use is always on */
|
208
|
+
ssl_options |= SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_SINGLE_DH_USE;
|
209
|
+
|
210
|
+
if (RTEST(no_tlsv1)) {
|
189
211
|
ssl_options |= SSL_OP_NO_TLSv1;
|
190
212
|
}
|
213
|
+
if(RTEST(no_tlsv1_1)) {
|
214
|
+
ssl_options |= SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1;
|
215
|
+
}
|
191
216
|
SSL_CTX_set_options(ctx, ssl_options);
|
217
|
+
#endif
|
218
|
+
|
192
219
|
SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF);
|
193
220
|
|
194
221
|
if (!NIL_P(ssl_cipher_filter)) {
|
@@ -458,14 +485,35 @@ void Init_mini_ssl(VALUE puma) {
|
|
458
485
|
// OpenSSL Build / Runtime/Load versions
|
459
486
|
|
460
487
|
/* Version of OpenSSL that Puma was compiled with */
|
461
|
-
|
488
|
+
rb_define_const(mod, "OPENSSL_VERSION", rb_str_new2(OPENSSL_VERSION_TEXT));
|
462
489
|
|
463
490
|
#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x10100000
|
464
|
-
|
465
|
-
|
491
|
+
/* Version of OpenSSL that Puma loaded with */
|
492
|
+
rb_define_const(mod, "OPENSSL_LIBRARY_VERSION", rb_str_new2(OpenSSL_version(OPENSSL_VERSION)));
|
466
493
|
#else
|
467
|
-
|
494
|
+
rb_define_const(mod, "OPENSSL_LIBRARY_VERSION", rb_str_new2(SSLeay_version(SSLEAY_VERSION)));
|
468
495
|
#endif
|
496
|
+
|
497
|
+
#if defined(OPENSSL_NO_SSL3) || defined(OPENSSL_NO_SSL3_METHOD)
|
498
|
+
/* True if SSL3 is not available */
|
499
|
+
rb_define_const(mod, "OPENSSL_NO_SSL3", Qtrue);
|
500
|
+
#else
|
501
|
+
rb_define_const(mod, "OPENSSL_NO_SSL3", Qfalse);
|
502
|
+
#endif
|
503
|
+
|
504
|
+
#if defined(OPENSSL_NO_TLS1) || defined(OPENSSL_NO_TLS1_METHOD)
|
505
|
+
/* True if TLS1 is not available */
|
506
|
+
rb_define_const(mod, "OPENSSL_NO_TLS1", Qtrue);
|
507
|
+
#else
|
508
|
+
rb_define_const(mod, "OPENSSL_NO_TLS1", Qfalse);
|
509
|
+
#endif
|
510
|
+
|
511
|
+
#if defined(OPENSSL_NO_TLS1_1) || defined(OPENSSL_NO_TLS1_1_METHOD)
|
512
|
+
/* True if TLS1_1 is not available */
|
513
|
+
rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qtrue);
|
514
|
+
#else
|
515
|
+
rb_define_const(mod, "OPENSSL_NO_TLS1_1", Qfalse);
|
516
|
+
#endif
|
469
517
|
|
470
518
|
rb_define_singleton_method(mod, "check", noop, 0);
|
471
519
|
|
@@ -166,6 +166,10 @@ public class MiniSSL extends RubyObject {
|
|
166
166
|
protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };
|
167
167
|
}
|
168
168
|
|
169
|
+
if(miniSSLContext.callMethod(threadContext, "no_tlsv1_1").isTrue()) {
|
170
|
+
protocols = new String[] { "TLSv1.2" };
|
171
|
+
}
|
172
|
+
|
169
173
|
engine.setEnabledProtocols(protocols);
|
170
174
|
engine.setUseClientMode(false);
|
171
175
|
|
data/lib/puma.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Standard libraries
|
2
4
|
require 'socket'
|
3
5
|
require 'tempfile'
|
@@ -20,4 +22,10 @@ module Puma
|
|
20
22
|
def self.stats
|
21
23
|
@get_stats.stats
|
22
24
|
end
|
25
|
+
|
26
|
+
# Thread name is new in Ruby 2.3
|
27
|
+
def self.set_thread_name(name)
|
28
|
+
return unless Thread.current.respond_to?(:name=)
|
29
|
+
Thread.current.name = "puma #{name}"
|
30
|
+
end
|
23
31
|
end
|
data/lib/puma/accept_nonblock.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'openssl'
|
2
4
|
|
3
5
|
module OpenSSL
|
@@ -13,7 +15,11 @@ module OpenSSL
|
|
13
15
|
ssl.accept if @start_immediately
|
14
16
|
ssl
|
15
17
|
rescue SSLError => ex
|
16
|
-
|
18
|
+
if ssl
|
19
|
+
ssl.close
|
20
|
+
else
|
21
|
+
sock.close
|
22
|
+
end
|
17
23
|
raise ex
|
18
24
|
end
|
19
25
|
end
|
data/lib/puma/app/status.rb
CHANGED
@@ -1,28 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
|
3
5
|
module Puma
|
4
6
|
module App
|
7
|
+
# Check out {#call}'s source code to see what actions this web application
|
8
|
+
# can respond to.
|
5
9
|
class Status
|
6
|
-
def initialize(cli)
|
7
|
-
@cli = cli
|
8
|
-
@auth_token = nil
|
9
|
-
end
|
10
10
|
OK_STATUS = '{ "status": "ok" }'.freeze
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
return true unless @auth_token
|
16
|
-
env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}")
|
17
|
-
end
|
18
|
-
|
19
|
-
def rack_response(status, body, content_type='application/json')
|
20
|
-
headers = {
|
21
|
-
'Content-Type' => content_type,
|
22
|
-
'Content-Length' => body.bytesize.to_s
|
23
|
-
}
|
24
|
-
|
25
|
-
[status, headers, [body]]
|
12
|
+
def initialize(cli, token = nil)
|
13
|
+
@cli = cli
|
14
|
+
@auth_token = token
|
26
15
|
end
|
27
16
|
|
28
17
|
def call(env)
|
@@ -33,43 +22,59 @@ module Puma
|
|
33
22
|
case env['PATH_INFO']
|
34
23
|
when /\/stop$/
|
35
24
|
@cli.stop
|
36
|
-
|
25
|
+
rack_response(200, OK_STATUS)
|
37
26
|
|
38
27
|
when /\/halt$/
|
39
28
|
@cli.halt
|
40
|
-
|
29
|
+
rack_response(200, OK_STATUS)
|
41
30
|
|
42
31
|
when /\/restart$/
|
43
32
|
@cli.restart
|
44
|
-
|
33
|
+
rack_response(200, OK_STATUS)
|
45
34
|
|
46
35
|
when /\/phased-restart$/
|
47
36
|
if !@cli.phased_restart
|
48
|
-
|
37
|
+
rack_response(404, '{ "error": "phased restart not available" }')
|
49
38
|
else
|
50
|
-
|
39
|
+
rack_response(200, OK_STATUS)
|
51
40
|
end
|
52
41
|
|
53
42
|
when /\/reload-worker-directory$/
|
54
43
|
if !@cli.send(:reload_worker_directory)
|
55
|
-
|
44
|
+
rack_response(404, '{ "error": "reload_worker_directory not available" }')
|
56
45
|
else
|
57
|
-
|
46
|
+
rack_response(200, OK_STATUS)
|
58
47
|
end
|
59
48
|
|
60
49
|
when /\/gc$/
|
61
50
|
GC.start
|
62
|
-
|
51
|
+
rack_response(200, OK_STATUS)
|
63
52
|
|
64
53
|
when /\/gc-stats$/
|
65
|
-
|
54
|
+
rack_response(200, GC.stat.to_json)
|
66
55
|
|
67
56
|
when /\/stats$/
|
68
|
-
|
57
|
+
rack_response(200, @cli.stats)
|
69
58
|
else
|
70
59
|
rack_response 404, "Unsupported action", 'text/plain'
|
71
60
|
end
|
72
61
|
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def authenticate(env)
|
66
|
+
return true unless @auth_token
|
67
|
+
env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}")
|
68
|
+
end
|
69
|
+
|
70
|
+
def rack_response(status, body, content_type='application/json')
|
71
|
+
headers = {
|
72
|
+
'Content-Type' => content_type,
|
73
|
+
'Content-Length' => body.bytesize.to_s
|
74
|
+
}
|
75
|
+
|
76
|
+
[status, headers, [body]]
|
77
|
+
end
|
73
78
|
end
|
74
79
|
end
|
75
80
|
end
|
data/lib/puma/binder.rb
CHANGED
@@ -42,7 +42,7 @@ module Puma
|
|
42
42
|
@ios = []
|
43
43
|
end
|
44
44
|
|
45
|
-
attr_reader :
|
45
|
+
attr_reader :ios
|
46
46
|
|
47
47
|
def env(sock)
|
48
48
|
@envs.fetch(sock, @proto_env)
|
@@ -50,14 +50,6 @@ module Puma
|
|
50
50
|
|
51
51
|
def close
|
52
52
|
@ios.each { |i| i.close }
|
53
|
-
@unix_paths.each do |i|
|
54
|
-
# Errno::ENOENT is intermittently raised
|
55
|
-
begin
|
56
|
-
unix_socket = UNIXSocket.new i
|
57
|
-
unix_socket.close
|
58
|
-
rescue Errno::ENOENT
|
59
|
-
end
|
60
|
-
end
|
61
53
|
end
|
62
54
|
|
63
55
|
def import_from_env
|
@@ -111,7 +103,17 @@ module Puma
|
|
111
103
|
bak = params.fetch('backlog', 1024).to_i
|
112
104
|
|
113
105
|
io = add_tcp_listener uri.host, uri.port, opt, bak
|
114
|
-
|
106
|
+
|
107
|
+
@ios.each do |i|
|
108
|
+
next unless TCPServer === i
|
109
|
+
addr = if i.local_address.ipv6?
|
110
|
+
"[#{i.local_address.ip_unpack[0]}]:#{i.local_address.ip_unpack[1]}"
|
111
|
+
else
|
112
|
+
i.local_address.ip_unpack.join(':')
|
113
|
+
end
|
114
|
+
|
115
|
+
logger.log "* Listening on tcp://#{addr}"
|
116
|
+
end
|
115
117
|
end
|
116
118
|
|
117
119
|
@listeners << [str, io] if io
|
@@ -195,6 +197,7 @@ module Puma
|
|
195
197
|
end
|
196
198
|
|
197
199
|
ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
|
200
|
+
ctx.no_tlsv1_1 = true if params['no_tlsv1_1'] == 'true'
|
198
201
|
|
199
202
|
if params['verify_mode']
|
200
203
|
ctx.verify_mode = case params['verify_mode']
|
@@ -358,7 +361,7 @@ module Puma
|
|
358
361
|
# Tell the server to listen on +path+ as a UNIX domain socket.
|
359
362
|
#
|
360
363
|
def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
|
361
|
-
@unix_paths << path
|
364
|
+
@unix_paths << path unless File.exist? path
|
362
365
|
|
363
366
|
# Let anyone connect by default
|
364
367
|
umask ||= 0
|
@@ -396,7 +399,7 @@ module Puma
|
|
396
399
|
end
|
397
400
|
|
398
401
|
def inherit_unix_listener(path, fd)
|
399
|
-
@unix_paths << path
|
402
|
+
@unix_paths << path unless File.exist? path
|
400
403
|
|
401
404
|
if fd.kind_of? TCPServer
|
402
405
|
s = fd
|
@@ -412,5 +415,27 @@ module Puma
|
|
412
415
|
s
|
413
416
|
end
|
414
417
|
|
418
|
+
def close_listeners
|
419
|
+
@listeners.each do |l, io|
|
420
|
+
io.close
|
421
|
+
uri = URI.parse(l)
|
422
|
+
next unless uri.scheme == 'unix'
|
423
|
+
unix_path = "#{uri.host}#{uri.path}"
|
424
|
+
File.unlink unix_path if @unix_paths.include? unix_path
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
def close_unix_paths
|
429
|
+
@unix_paths.each { |up| File.unlink(up) if File.exist? up }
|
430
|
+
end
|
431
|
+
|
432
|
+
def redirects_for_restart
|
433
|
+
redirects = {:close_others => true}
|
434
|
+
@listeners.each_with_index do |(l, io), i|
|
435
|
+
ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
|
436
|
+
redirects[io.to_i] = io.to_i
|
437
|
+
end
|
438
|
+
redirects
|
439
|
+
end
|
415
440
|
end
|
416
441
|
end
|
data/lib/puma/cli.rb
CHANGED
@@ -161,6 +161,10 @@ module Puma
|
|
161
161
|
user_config.prune_bundler
|
162
162
|
end
|
163
163
|
|
164
|
+
o.on "--extra-runtime-dependencies GEM1,GEM2", "Defines any extra needed gems when using --prune-bundler" do |arg|
|
165
|
+
c.extra_runtime_dependencies arg.split(',')
|
166
|
+
end
|
167
|
+
|
164
168
|
o.on "-q", "--quiet", "Do not log requests internally (default true)" do
|
165
169
|
user_config.quiet
|
166
170
|
end
|
data/lib/puma/client.rb
CHANGED
@@ -9,8 +9,8 @@ class IO
|
|
9
9
|
end
|
10
10
|
|
11
11
|
require 'puma/detect'
|
12
|
-
require 'puma/delegation'
|
13
12
|
require 'tempfile'
|
13
|
+
require 'forwardable'
|
14
14
|
|
15
15
|
if Puma::IS_JRUBY
|
16
16
|
# We have to work around some OpenSSL buffer/io-readiness bugs
|
@@ -24,11 +24,11 @@ module Puma
|
|
24
24
|
class ConnectionError < RuntimeError; end
|
25
25
|
|
26
26
|
# An instance of this class represents a unique request from a client.
|
27
|
-
# For example a web request from a browser or from CURL.
|
27
|
+
# For example, this could be a web request from a browser or from CURL.
|
28
28
|
#
|
29
29
|
# An instance of `Puma::Client` can be used as if it were an IO object
|
30
|
-
# by the reactor
|
31
|
-
# on any non-IO objects it polls. For example nio4r internally calls
|
30
|
+
# by the reactor. The reactor is expected to call `#to_io`
|
31
|
+
# on any non-IO objects it polls. For example, nio4r internally calls
|
32
32
|
# `IO::try_convert` (which may call `#to_io`) when a new socket is
|
33
33
|
# registered.
|
34
34
|
#
|
@@ -36,8 +36,12 @@ module Puma
|
|
36
36
|
# the header and body are fully buffered via the `try_to_finish` method.
|
37
37
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
38
38
|
class Client
|
39
|
+
# The object used for a request with no body. All requests with
|
40
|
+
# no body share this one object since it has no state.
|
41
|
+
EmptyBody = NullIO.new
|
42
|
+
|
39
43
|
include Puma::Const
|
40
|
-
extend
|
44
|
+
extend Forwardable
|
41
45
|
|
42
46
|
def initialize(io, env=nil)
|
43
47
|
@io = io
|
@@ -68,6 +72,8 @@ module Puma
|
|
68
72
|
@remote_addr_header = nil
|
69
73
|
|
70
74
|
@body_remain = 0
|
75
|
+
|
76
|
+
@in_last_chunk = false
|
71
77
|
end
|
72
78
|
|
73
79
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
@@ -77,7 +83,7 @@ module Puma
|
|
77
83
|
|
78
84
|
attr_accessor :remote_addr_header
|
79
85
|
|
80
|
-
|
86
|
+
def_delegators :@io, :closed?
|
81
87
|
|
82
88
|
def inspect
|
83
89
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
@@ -108,6 +114,7 @@ module Puma
|
|
108
114
|
@ready = false
|
109
115
|
@body_remain = 0
|
110
116
|
@peerip = nil
|
117
|
+
@in_last_chunk = false
|
111
118
|
|
112
119
|
if @buffer
|
113
120
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
@@ -141,166 +148,6 @@ module Puma
|
|
141
148
|
end
|
142
149
|
end
|
143
150
|
|
144
|
-
# The object used for a request with no body. All requests with
|
145
|
-
# no body share this one object since it has no state.
|
146
|
-
EmptyBody = NullIO.new
|
147
|
-
|
148
|
-
def setup_chunked_body(body)
|
149
|
-
@chunked_body = true
|
150
|
-
@partial_part_left = 0
|
151
|
-
@prev_chunk = ""
|
152
|
-
|
153
|
-
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
154
|
-
@body.binmode
|
155
|
-
@tempfile = @body
|
156
|
-
|
157
|
-
return decode_chunk(body)
|
158
|
-
end
|
159
|
-
|
160
|
-
def decode_chunk(chunk)
|
161
|
-
if @partial_part_left > 0
|
162
|
-
if @partial_part_left <= chunk.size
|
163
|
-
if @partial_part_left > 2
|
164
|
-
@body << chunk[0..(@partial_part_left-3)] # skip the \r\n
|
165
|
-
end
|
166
|
-
chunk = chunk[@partial_part_left..-1]
|
167
|
-
@partial_part_left = 0
|
168
|
-
else
|
169
|
-
@body << chunk
|
170
|
-
@partial_part_left -= chunk.size
|
171
|
-
return false
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
if @prev_chunk.empty?
|
176
|
-
io = StringIO.new(chunk)
|
177
|
-
else
|
178
|
-
io = StringIO.new(@prev_chunk+chunk)
|
179
|
-
@prev_chunk = ""
|
180
|
-
end
|
181
|
-
|
182
|
-
while !io.eof?
|
183
|
-
line = io.gets
|
184
|
-
if line.end_with?("\r\n")
|
185
|
-
len = line.strip.to_i(16)
|
186
|
-
if len == 0
|
187
|
-
@body.rewind
|
188
|
-
rest = io.read
|
189
|
-
rest = rest[2..-1] if rest.start_with?("\r\n")
|
190
|
-
@buffer = rest.empty? ? nil : rest
|
191
|
-
set_ready
|
192
|
-
return true
|
193
|
-
end
|
194
|
-
|
195
|
-
len += 2
|
196
|
-
|
197
|
-
part = io.read(len)
|
198
|
-
|
199
|
-
unless part
|
200
|
-
@partial_part_left = len
|
201
|
-
next
|
202
|
-
end
|
203
|
-
|
204
|
-
got = part.size
|
205
|
-
|
206
|
-
case
|
207
|
-
when got == len
|
208
|
-
@body << part[0..-3] # to skip the ending \r\n
|
209
|
-
when got <= len - 2
|
210
|
-
@body << part
|
211
|
-
@partial_part_left = len - part.size
|
212
|
-
when got == len - 1 # edge where we get just \r but not \n
|
213
|
-
@body << part[0..-2]
|
214
|
-
@partial_part_left = len - part.size
|
215
|
-
end
|
216
|
-
else
|
217
|
-
@prev_chunk = line
|
218
|
-
return false
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
return false
|
223
|
-
end
|
224
|
-
|
225
|
-
def read_chunked_body
|
226
|
-
while true
|
227
|
-
begin
|
228
|
-
chunk = @io.read_nonblock(4096)
|
229
|
-
rescue IO::WaitReadable
|
230
|
-
return false
|
231
|
-
rescue SystemCallError, IOError
|
232
|
-
raise ConnectionError, "Connection error detected during read"
|
233
|
-
end
|
234
|
-
|
235
|
-
# No chunk means a closed socket
|
236
|
-
unless chunk
|
237
|
-
@body.close
|
238
|
-
@buffer = nil
|
239
|
-
set_ready
|
240
|
-
raise EOFError
|
241
|
-
end
|
242
|
-
|
243
|
-
return true if decode_chunk(chunk)
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
def setup_body
|
248
|
-
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
249
|
-
|
250
|
-
if @env[HTTP_EXPECT] == CONTINUE
|
251
|
-
# TODO allow a hook here to check the headers before
|
252
|
-
# going forward
|
253
|
-
@io << HTTP_11_100
|
254
|
-
@io.flush
|
255
|
-
end
|
256
|
-
|
257
|
-
@read_header = false
|
258
|
-
|
259
|
-
body = @parser.body
|
260
|
-
|
261
|
-
te = @env[TRANSFER_ENCODING2]
|
262
|
-
|
263
|
-
if te && CHUNKED.casecmp(te) == 0
|
264
|
-
return setup_chunked_body(body)
|
265
|
-
end
|
266
|
-
|
267
|
-
@chunked_body = false
|
268
|
-
|
269
|
-
cl = @env[CONTENT_LENGTH]
|
270
|
-
|
271
|
-
unless cl
|
272
|
-
@buffer = body.empty? ? nil : body
|
273
|
-
@body = EmptyBody
|
274
|
-
set_ready
|
275
|
-
return true
|
276
|
-
end
|
277
|
-
|
278
|
-
remain = cl.to_i - body.bytesize
|
279
|
-
|
280
|
-
if remain <= 0
|
281
|
-
@body = StringIO.new(body)
|
282
|
-
@buffer = nil
|
283
|
-
set_ready
|
284
|
-
return true
|
285
|
-
end
|
286
|
-
|
287
|
-
if remain > MAX_BODY
|
288
|
-
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
289
|
-
@body.binmode
|
290
|
-
@tempfile = @body
|
291
|
-
else
|
292
|
-
# The body[0,0] trick is to get an empty string in the same
|
293
|
-
# encoding as body.
|
294
|
-
@body = StringIO.new body[0,0]
|
295
|
-
end
|
296
|
-
|
297
|
-
@body.write body
|
298
|
-
|
299
|
-
@body_remain = remain
|
300
|
-
|
301
|
-
return false
|
302
|
-
end
|
303
|
-
|
304
151
|
def try_to_finish
|
305
152
|
return read_body unless @read_header
|
306
153
|
|
@@ -401,6 +248,84 @@ module Puma
|
|
401
248
|
true
|
402
249
|
end
|
403
250
|
|
251
|
+
def write_error(status_code)
|
252
|
+
begin
|
253
|
+
@io << ERROR_RESPONSE[status_code]
|
254
|
+
rescue StandardError
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def peerip
|
259
|
+
return @peerip if @peerip
|
260
|
+
|
261
|
+
if @remote_addr_header
|
262
|
+
hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
|
263
|
+
@peerip = hdr
|
264
|
+
return hdr
|
265
|
+
end
|
266
|
+
|
267
|
+
@peerip ||= @io.peeraddr.last
|
268
|
+
end
|
269
|
+
|
270
|
+
private
|
271
|
+
|
272
|
+
def setup_body
|
273
|
+
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
274
|
+
|
275
|
+
if @env[HTTP_EXPECT] == CONTINUE
|
276
|
+
# TODO allow a hook here to check the headers before
|
277
|
+
# going forward
|
278
|
+
@io << HTTP_11_100
|
279
|
+
@io.flush
|
280
|
+
end
|
281
|
+
|
282
|
+
@read_header = false
|
283
|
+
|
284
|
+
body = @parser.body
|
285
|
+
|
286
|
+
te = @env[TRANSFER_ENCODING2]
|
287
|
+
|
288
|
+
if te && CHUNKED.casecmp(te) == 0
|
289
|
+
return setup_chunked_body(body)
|
290
|
+
end
|
291
|
+
|
292
|
+
@chunked_body = false
|
293
|
+
|
294
|
+
cl = @env[CONTENT_LENGTH]
|
295
|
+
|
296
|
+
unless cl
|
297
|
+
@buffer = body.empty? ? nil : body
|
298
|
+
@body = EmptyBody
|
299
|
+
set_ready
|
300
|
+
return true
|
301
|
+
end
|
302
|
+
|
303
|
+
remain = cl.to_i - body.bytesize
|
304
|
+
|
305
|
+
if remain <= 0
|
306
|
+
@body = StringIO.new(body)
|
307
|
+
@buffer = nil
|
308
|
+
set_ready
|
309
|
+
return true
|
310
|
+
end
|
311
|
+
|
312
|
+
if remain > MAX_BODY
|
313
|
+
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
314
|
+
@body.binmode
|
315
|
+
@tempfile = @body
|
316
|
+
else
|
317
|
+
# The body[0,0] trick is to get an empty string in the same
|
318
|
+
# encoding as body.
|
319
|
+
@body = StringIO.new body[0,0]
|
320
|
+
end
|
321
|
+
|
322
|
+
@body.write body
|
323
|
+
|
324
|
+
@body_remain = remain
|
325
|
+
|
326
|
+
return false
|
327
|
+
end
|
328
|
+
|
404
329
|
def read_body
|
405
330
|
if @chunked_body
|
406
331
|
return read_chunked_body
|
@@ -446,45 +371,124 @@ module Puma
|
|
446
371
|
false
|
447
372
|
end
|
448
373
|
|
449
|
-
def
|
450
|
-
|
451
|
-
|
374
|
+
def read_chunked_body
|
375
|
+
while true
|
376
|
+
begin
|
377
|
+
chunk = @io.read_nonblock(4096)
|
378
|
+
rescue IO::WaitReadable
|
379
|
+
return false
|
380
|
+
rescue SystemCallError, IOError
|
381
|
+
raise ConnectionError, "Connection error detected during read"
|
382
|
+
end
|
383
|
+
|
384
|
+
# No chunk means a closed socket
|
385
|
+
unless chunk
|
386
|
+
@body.close
|
387
|
+
@buffer = nil
|
388
|
+
set_ready
|
389
|
+
raise EOFError
|
390
|
+
end
|
391
|
+
|
392
|
+
return true if decode_chunk(chunk)
|
452
393
|
end
|
453
|
-
@requests_served += 1
|
454
|
-
@ready = true
|
455
394
|
end
|
456
395
|
|
457
|
-
def
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
396
|
+
def setup_chunked_body(body)
|
397
|
+
@chunked_body = true
|
398
|
+
@partial_part_left = 0
|
399
|
+
@prev_chunk = ""
|
400
|
+
|
401
|
+
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
402
|
+
@body.binmode
|
403
|
+
@tempfile = @body
|
404
|
+
|
405
|
+
return decode_chunk(body)
|
462
406
|
end
|
463
407
|
|
464
|
-
def
|
465
|
-
|
466
|
-
@
|
467
|
-
|
408
|
+
def decode_chunk(chunk)
|
409
|
+
if @partial_part_left > 0
|
410
|
+
if @partial_part_left <= chunk.size
|
411
|
+
if @partial_part_left > 2
|
412
|
+
@body << chunk[0..(@partial_part_left-3)] # skip the \r\n
|
413
|
+
end
|
414
|
+
chunk = chunk[@partial_part_left..-1]
|
415
|
+
@partial_part_left = 0
|
416
|
+
else
|
417
|
+
@body << chunk if @partial_part_left > 2 # don't include the last \r\n
|
418
|
+
@partial_part_left -= chunk.size
|
419
|
+
return false
|
420
|
+
end
|
468
421
|
end
|
469
|
-
end
|
470
422
|
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
423
|
+
if @prev_chunk.empty?
|
424
|
+
io = StringIO.new(chunk)
|
425
|
+
else
|
426
|
+
io = StringIO.new(@prev_chunk+chunk)
|
427
|
+
@prev_chunk = ""
|
475
428
|
end
|
476
|
-
end
|
477
429
|
|
478
|
-
|
479
|
-
|
430
|
+
while !io.eof?
|
431
|
+
line = io.gets
|
432
|
+
if line.end_with?("\r\n")
|
433
|
+
len = line.strip.to_i(16)
|
434
|
+
if len == 0
|
435
|
+
@in_last_chunk = true
|
436
|
+
@body.rewind
|
437
|
+
rest = io.read
|
438
|
+
last_crlf_size = "\r\n".bytesize
|
439
|
+
if rest.bytesize < last_crlf_size
|
440
|
+
@buffer = nil
|
441
|
+
@partial_part_left = last_crlf_size - rest.bytesize
|
442
|
+
return false
|
443
|
+
else
|
444
|
+
@buffer = rest[last_crlf_size..-1]
|
445
|
+
@buffer = nil if @buffer.empty?
|
446
|
+
set_ready
|
447
|
+
return true
|
448
|
+
end
|
449
|
+
end
|
480
450
|
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
451
|
+
len += 2
|
452
|
+
|
453
|
+
part = io.read(len)
|
454
|
+
|
455
|
+
unless part
|
456
|
+
@partial_part_left = len
|
457
|
+
next
|
458
|
+
end
|
459
|
+
|
460
|
+
got = part.size
|
461
|
+
|
462
|
+
case
|
463
|
+
when got == len
|
464
|
+
@body << part[0..-3] # to skip the ending \r\n
|
465
|
+
when got <= len - 2
|
466
|
+
@body << part
|
467
|
+
@partial_part_left = len - part.size
|
468
|
+
when got == len - 1 # edge where we get just \r but not \n
|
469
|
+
@body << part[0..-2]
|
470
|
+
@partial_part_left = len - part.size
|
471
|
+
end
|
472
|
+
else
|
473
|
+
@prev_chunk = line
|
474
|
+
return false
|
475
|
+
end
|
485
476
|
end
|
486
477
|
|
487
|
-
|
478
|
+
if @in_last_chunk
|
479
|
+
set_ready
|
480
|
+
true
|
481
|
+
else
|
482
|
+
false
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
def set_ready
|
487
|
+
if @body_read_start
|
488
|
+
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
|
489
|
+
end
|
490
|
+
@requests_served += 1
|
491
|
+
@ready = true
|
488
492
|
end
|
489
493
|
end
|
490
494
|
end
|