puma 3.12.0 → 4.3.8
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +164 -0
- data/README.md +76 -48
- data/docs/architecture.md +1 -0
- data/docs/deployment.md +24 -4
- data/docs/plugins.md +20 -10
- data/docs/restart.md +4 -2
- data/docs/systemd.md +27 -9
- data/docs/tcp_mode.md +96 -0
- data/ext/puma_http11/PumaHttp11Service.java +2 -0
- data/ext/puma_http11/extconf.rb +13 -0
- data/ext/puma_http11/http11_parser.c +40 -63
- data/ext/puma_http11/http11_parser.java.rl +21 -37
- data/ext/puma_http11/http11_parser.rl +3 -1
- data/ext/puma_http11/http11_parser_common.rl +3 -3
- data/ext/puma_http11/mini_ssl.c +86 -4
- data/ext/puma_http11/org/jruby/puma/Http11.java +106 -114
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
- data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -4
- data/ext/puma_http11/puma_http11.c +3 -0
- data/lib/puma.rb +8 -0
- data/lib/puma/accept_nonblock.rb +7 -1
- data/lib/puma/app/status.rb +37 -29
- data/lib/puma/binder.rb +47 -68
- data/lib/puma/cli.rb +6 -0
- data/lib/puma/client.rb +244 -199
- data/lib/puma/cluster.rb +55 -30
- data/lib/puma/commonlogger.rb +2 -0
- data/lib/puma/configuration.rb +6 -3
- data/lib/puma/const.rb +32 -18
- data/lib/puma/control_cli.rb +41 -14
- data/lib/puma/detect.rb +2 -0
- data/lib/puma/dsl.rb +311 -77
- data/lib/puma/events.rb +6 -1
- data/lib/puma/io_buffer.rb +3 -6
- data/lib/puma/jruby_restart.rb +2 -0
- data/lib/puma/launcher.rb +99 -55
- data/lib/puma/minissl.rb +37 -17
- data/lib/puma/minissl/context_builder.rb +76 -0
- data/lib/puma/null_io.rb +2 -0
- data/lib/puma/plugin.rb +7 -2
- data/lib/puma/plugin/tmp_restart.rb +2 -0
- data/lib/puma/rack/builder.rb +4 -1
- data/lib/puma/rack/urlmap.rb +2 -0
- data/lib/puma/rack_default.rb +2 -0
- data/lib/puma/reactor.rb +112 -57
- data/lib/puma/runner.rb +13 -3
- data/lib/puma/server.rb +119 -48
- data/lib/puma/single.rb +5 -3
- data/lib/puma/state_file.rb +2 -0
- data/lib/puma/tcp_logger.rb +2 -0
- data/lib/puma/thread_pool.rb +17 -33
- data/lib/puma/util.rb +2 -6
- data/lib/rack/handler/puma.rb +6 -3
- data/tools/docker/Dockerfile +16 -0
- data/tools/jungle/init.d/puma +6 -6
- data/tools/trickletest.rb +0 -1
- metadata +26 -14
- data/lib/puma/compat.rb +0 -14
- data/lib/puma/convenient.rb +0 -23
- data/lib/puma/daemon_ext.rb +0 -31
- data/lib/puma/delegation.rb +0 -11
- data/lib/puma/java_io_buffer.rb +0 -45
- data/lib/puma/rack/backports/uri/common_193.rb +0 -33
@@ -10,6 +10,7 @@
|
|
10
10
|
#include "ext_help.h"
|
11
11
|
#include <assert.h>
|
12
12
|
#include <string.h>
|
13
|
+
#include <ctype.h>
|
13
14
|
#include "http11_parser.h"
|
14
15
|
|
15
16
|
#ifndef MANAGED_STRINGS
|
@@ -200,6 +201,8 @@ void http_field(puma_parser* hp, const char *field, size_t flen,
|
|
200
201
|
f = rb_str_new(hp->buf, new_size);
|
201
202
|
}
|
202
203
|
|
204
|
+
while (vlen > 0 && isspace(value[vlen - 1])) vlen--;
|
205
|
+
|
203
206
|
/* check for duplicate header */
|
204
207
|
v = rb_hash_aref(hp->request, f);
|
205
208
|
|
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,26 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Puma
|
2
4
|
module App
|
5
|
+
# Check out {#call}'s source code to see what actions this web application
|
6
|
+
# can respond to.
|
3
7
|
class Status
|
4
|
-
def initialize(cli)
|
5
|
-
@cli = cli
|
6
|
-
@auth_token = nil
|
7
|
-
end
|
8
8
|
OK_STATUS = '{ "status": "ok" }'.freeze
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
return true unless @auth_token
|
14
|
-
env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}")
|
15
|
-
end
|
16
|
-
|
17
|
-
def rack_response(status, body, content_type='application/json')
|
18
|
-
headers = {
|
19
|
-
'Content-Type' => content_type,
|
20
|
-
'Content-Length' => body.bytesize.to_s
|
21
|
-
}
|
22
|
-
|
23
|
-
[status, headers, [body]]
|
10
|
+
def initialize(cli, token = nil)
|
11
|
+
@cli = cli
|
12
|
+
@auth_token = token
|
24
13
|
end
|
25
14
|
|
26
15
|
def call(env)
|
@@ -28,47 +17,66 @@ module Puma
|
|
28
17
|
return rack_response(403, 'Invalid auth token', 'text/plain')
|
29
18
|
end
|
30
19
|
|
20
|
+
if env['PATH_INFO'] =~ /\/(gc-stats|stats|thread-backtraces)$/
|
21
|
+
require 'json'
|
22
|
+
end
|
23
|
+
|
31
24
|
case env['PATH_INFO']
|
32
25
|
when /\/stop$/
|
33
26
|
@cli.stop
|
34
|
-
|
27
|
+
rack_response(200, OK_STATUS)
|
35
28
|
|
36
29
|
when /\/halt$/
|
37
30
|
@cli.halt
|
38
|
-
|
31
|
+
rack_response(200, OK_STATUS)
|
39
32
|
|
40
33
|
when /\/restart$/
|
41
34
|
@cli.restart
|
42
|
-
|
35
|
+
rack_response(200, OK_STATUS)
|
43
36
|
|
44
37
|
when /\/phased-restart$/
|
45
38
|
if !@cli.phased_restart
|
46
|
-
|
39
|
+
rack_response(404, '{ "error": "phased restart not available" }')
|
47
40
|
else
|
48
|
-
|
41
|
+
rack_response(200, OK_STATUS)
|
49
42
|
end
|
50
43
|
|
51
44
|
when /\/reload-worker-directory$/
|
52
45
|
if !@cli.send(:reload_worker_directory)
|
53
|
-
|
46
|
+
rack_response(404, '{ "error": "reload_worker_directory not available" }')
|
54
47
|
else
|
55
|
-
|
48
|
+
rack_response(200, OK_STATUS)
|
56
49
|
end
|
57
50
|
|
58
51
|
when /\/gc$/
|
59
52
|
GC.start
|
60
|
-
|
53
|
+
rack_response(200, OK_STATUS)
|
61
54
|
|
62
55
|
when /\/gc-stats$/
|
63
|
-
|
64
|
-
return rack_response(200, json)
|
56
|
+
rack_response(200, GC.stat.to_json)
|
65
57
|
|
66
58
|
when /\/stats$/
|
67
|
-
|
59
|
+
rack_response(200, @cli.stats)
|
68
60
|
else
|
69
61
|
rack_response 404, "Unsupported action", 'text/plain'
|
70
62
|
end
|
71
63
|
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def authenticate(env)
|
68
|
+
return true unless @auth_token
|
69
|
+
env['QUERY_STRING'].to_s.split(/&;/).include?("token=#{@auth_token}")
|
70
|
+
end
|
71
|
+
|
72
|
+
def rack_response(status, body, content_type='application/json')
|
73
|
+
headers = {
|
74
|
+
'Content-Type' => content_type,
|
75
|
+
'Content-Length' => body.bytesize.to_s
|
76
|
+
}
|
77
|
+
|
78
|
+
[status, headers, [body]]
|
79
|
+
end
|
72
80
|
end
|
73
81
|
end
|
74
82
|
end
|
data/lib/puma/binder.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'uri'
|
2
4
|
require 'socket'
|
3
5
|
|
4
6
|
require 'puma/const'
|
5
7
|
require 'puma/util'
|
8
|
+
require 'puma/minissl/context_builder'
|
6
9
|
|
7
10
|
module Puma
|
8
11
|
class Binder
|
@@ -40,7 +43,7 @@ module Puma
|
|
40
43
|
@ios = []
|
41
44
|
end
|
42
45
|
|
43
|
-
attr_reader :
|
46
|
+
attr_reader :ios
|
44
47
|
|
45
48
|
def env(sock)
|
46
49
|
@envs.fetch(sock, @proto_env)
|
@@ -48,7 +51,6 @@ module Puma
|
|
48
51
|
|
49
52
|
def close
|
50
53
|
@ios.each { |i| i.close }
|
51
|
-
@unix_paths.each { |i| File.unlink i }
|
52
54
|
end
|
53
55
|
|
54
56
|
def import_from_env
|
@@ -90,19 +92,29 @@ module Puma
|
|
90
92
|
case uri.scheme
|
91
93
|
when "tcp"
|
92
94
|
if fd = @inherited_fds.delete(str)
|
93
|
-
logger.log "* Inherited #{str}"
|
94
95
|
io = inherit_tcp_listener uri.host, uri.port, fd
|
96
|
+
logger.log "* Inherited #{str}"
|
95
97
|
elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
|
96
|
-
logger.log "* Activated #{str}"
|
97
98
|
io = inherit_tcp_listener uri.host, uri.port, sock
|
99
|
+
logger.log "* Activated #{str}"
|
98
100
|
else
|
99
101
|
params = Util.parse_query uri.query
|
100
102
|
|
101
103
|
opt = params.key?('low_latency')
|
102
104
|
bak = params.fetch('backlog', 1024).to_i
|
103
105
|
|
104
|
-
logger.log "* Listening on #{str}"
|
105
106
|
io = add_tcp_listener uri.host, uri.port, opt, bak
|
107
|
+
|
108
|
+
@ios.each do |i|
|
109
|
+
next unless TCPServer === i
|
110
|
+
addr = if i.local_address.ipv6?
|
111
|
+
"[#{i.local_address.ip_unpack[0]}]:#{i.local_address.ip_unpack[1]}"
|
112
|
+
else
|
113
|
+
i.local_address.ip_unpack.join(':')
|
114
|
+
end
|
115
|
+
|
116
|
+
logger.log "* Listening on tcp://#{addr}"
|
117
|
+
end
|
106
118
|
end
|
107
119
|
|
108
120
|
@listeners << [str, io] if io
|
@@ -110,14 +122,12 @@ module Puma
|
|
110
122
|
path = "#{uri.host}#{uri.path}".gsub("%20", " ")
|
111
123
|
|
112
124
|
if fd = @inherited_fds.delete(str)
|
113
|
-
logger.log "* Inherited #{str}"
|
114
125
|
io = inherit_unix_listener path, fd
|
126
|
+
logger.log "* Inherited #{str}"
|
115
127
|
elsif sock = @activated_sockets.delete([ :unix, path ])
|
116
|
-
logger.log "* Activated #{str}"
|
117
128
|
io = inherit_unix_listener path, sock
|
129
|
+
logger.log "* Activated #{str}"
|
118
130
|
else
|
119
|
-
logger.log "* Listening on #{str}"
|
120
|
-
|
121
131
|
umask = nil
|
122
132
|
mode = nil
|
123
133
|
backlog = 1024
|
@@ -139,76 +149,23 @@ module Puma
|
|
139
149
|
end
|
140
150
|
|
141
151
|
io = add_unix_listener path, umask, mode, backlog
|
152
|
+
logger.log "* Listening on #{str}"
|
142
153
|
end
|
143
154
|
|
144
155
|
@listeners << [str, io]
|
145
156
|
when "ssl"
|
146
157
|
params = Util.parse_query uri.query
|
147
|
-
|
148
|
-
|
149
|
-
MiniSSL.check
|
150
|
-
|
151
|
-
ctx = MiniSSL::Context.new
|
152
|
-
|
153
|
-
if defined?(JRUBY_VERSION)
|
154
|
-
unless params['keystore']
|
155
|
-
@events.error "Please specify the Java keystore via 'keystore='"
|
156
|
-
end
|
157
|
-
|
158
|
-
ctx.keystore = params['keystore']
|
159
|
-
|
160
|
-
unless params['keystore-pass']
|
161
|
-
@events.error "Please specify the Java keystore password via 'keystore-pass='"
|
162
|
-
end
|
163
|
-
|
164
|
-
ctx.keystore_pass = params['keystore-pass']
|
165
|
-
ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
|
166
|
-
else
|
167
|
-
unless params['key']
|
168
|
-
@events.error "Please specify the SSL key via 'key='"
|
169
|
-
end
|
170
|
-
|
171
|
-
ctx.key = params['key']
|
172
|
-
|
173
|
-
unless params['cert']
|
174
|
-
@events.error "Please specify the SSL cert via 'cert='"
|
175
|
-
end
|
176
|
-
|
177
|
-
ctx.cert = params['cert']
|
178
|
-
|
179
|
-
if ['peer', 'force_peer'].include?(params['verify_mode'])
|
180
|
-
unless params['ca']
|
181
|
-
@events.error "Please specify the SSL ca via 'ca='"
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
ctx.ca = params['ca'] if params['ca']
|
186
|
-
ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
|
187
|
-
end
|
188
|
-
|
189
|
-
if params['verify_mode']
|
190
|
-
ctx.verify_mode = case params['verify_mode']
|
191
|
-
when "peer"
|
192
|
-
MiniSSL::VERIFY_PEER
|
193
|
-
when "force_peer"
|
194
|
-
MiniSSL::VERIFY_PEER | MiniSSL::VERIFY_FAIL_IF_NO_PEER_CERT
|
195
|
-
when "none"
|
196
|
-
MiniSSL::VERIFY_NONE
|
197
|
-
else
|
198
|
-
@events.error "Please specify a valid verify_mode="
|
199
|
-
MiniSSL::VERIFY_NONE
|
200
|
-
end
|
201
|
-
end
|
158
|
+
ctx = MiniSSL::ContextBuilder.new(params, @events).context
|
202
159
|
|
203
160
|
if fd = @inherited_fds.delete(str)
|
204
161
|
logger.log "* Inherited #{str}"
|
205
162
|
io = inherit_ssl_listener fd, ctx
|
206
163
|
elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
|
207
|
-
logger.log "* Activated #{str}"
|
208
164
|
io = inherit_ssl_listener sock, ctx
|
165
|
+
logger.log "* Activated #{str}"
|
209
166
|
else
|
210
|
-
logger.log "* Listening on #{str}"
|
211
167
|
io = add_ssl_listener uri.host, uri.port, ctx
|
168
|
+
logger.log "* Listening on #{str}"
|
212
169
|
end
|
213
170
|
|
214
171
|
@listeners << [str, io] if io
|
@@ -348,7 +305,7 @@ module Puma
|
|
348
305
|
# Tell the server to listen on +path+ as a UNIX domain socket.
|
349
306
|
#
|
350
307
|
def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
|
351
|
-
@unix_paths << path
|
308
|
+
@unix_paths << path unless File.exist? path
|
352
309
|
|
353
310
|
# Let anyone connect by default
|
354
311
|
umask ||= 0
|
@@ -386,7 +343,7 @@ module Puma
|
|
386
343
|
end
|
387
344
|
|
388
345
|
def inherit_unix_listener(path, fd)
|
389
|
-
@unix_paths << path
|
346
|
+
@unix_paths << path unless File.exist? path
|
390
347
|
|
391
348
|
if fd.kind_of? TCPServer
|
392
349
|
s = fd
|
@@ -402,5 +359,27 @@ module Puma
|
|
402
359
|
s
|
403
360
|
end
|
404
361
|
|
362
|
+
def close_listeners
|
363
|
+
@listeners.each do |l, io|
|
364
|
+
io.close
|
365
|
+
uri = URI.parse(l)
|
366
|
+
next unless uri.scheme == 'unix'
|
367
|
+
unix_path = "#{uri.host}#{uri.path}"
|
368
|
+
File.unlink unix_path if @unix_paths.include? unix_path
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def close_unix_paths
|
373
|
+
@unix_paths.each { |up| File.unlink(up) if File.exist? up }
|
374
|
+
end
|
375
|
+
|
376
|
+
def redirects_for_restart
|
377
|
+
redirects = {:close_others => true}
|
378
|
+
@listeners.each_with_index do |(l, io), i|
|
379
|
+
ENV["PUMA_INHERIT_#{i}"] = "#{io.to_i}:#{l}"
|
380
|
+
redirects[io.to_i] = io.to_i
|
381
|
+
end
|
382
|
+
redirects
|
383
|
+
end
|
405
384
|
end
|
406
385
|
end
|
data/lib/puma/cli.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'optparse'
|
2
4
|
require 'uri'
|
3
5
|
|
@@ -159,6 +161,10 @@ module Puma
|
|
159
161
|
user_config.prune_bundler
|
160
162
|
end
|
161
163
|
|
164
|
+
o.on "--extra-runtime-dependencies GEM1,GEM2", "Defines any extra needed gems when using --prune-bundler" do |arg|
|
165
|
+
user_config.extra_runtime_dependencies arg.split(',')
|
166
|
+
end
|
167
|
+
|
162
168
|
o.on "-q", "--quiet", "Do not log requests internally (default true)" do
|
163
169
|
user_config.quiet
|
164
170
|
end
|
data/lib/puma/client.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class IO
|
2
4
|
# We need to use this for a jruby work around on both 1.8 and 1.9.
|
3
5
|
# So this either creates the constant (on 1.8), or harmlessly
|
@@ -7,8 +9,8 @@ class IO
|
|
7
9
|
end
|
8
10
|
|
9
11
|
require 'puma/detect'
|
10
|
-
require 'puma/delegation'
|
11
12
|
require 'tempfile'
|
13
|
+
require 'forwardable'
|
12
14
|
|
13
15
|
if Puma::IS_JRUBY
|
14
16
|
# We have to work around some OpenSSL buffer/io-readiness bugs
|
@@ -22,19 +24,24 @@ module Puma
|
|
22
24
|
class ConnectionError < RuntimeError; end
|
23
25
|
|
24
26
|
# An instance of this class represents a unique request from a client.
|
25
|
-
# 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.
|
26
28
|
#
|
27
29
|
# An instance of `Puma::Client` can be used as if it were an IO object
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
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
|
+
# `IO::try_convert` (which may call `#to_io`) when a new socket is
|
33
|
+
# registered.
|
31
34
|
#
|
32
35
|
# Instances of this class are responsible for knowing if
|
33
36
|
# the header and body are fully buffered via the `try_to_finish` method.
|
34
37
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
35
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
|
+
|
36
43
|
include Puma::Const
|
37
|
-
extend
|
44
|
+
extend Forwardable
|
38
45
|
|
39
46
|
def initialize(io, env=nil)
|
40
47
|
@io = io
|
@@ -52,6 +59,7 @@ module Puma
|
|
52
59
|
@ready = false
|
53
60
|
|
54
61
|
@body = nil
|
62
|
+
@body_read_start = nil
|
55
63
|
@buffer = nil
|
56
64
|
@tempfile = nil
|
57
65
|
|
@@ -62,6 +70,10 @@ module Puma
|
|
62
70
|
|
63
71
|
@peerip = nil
|
64
72
|
@remote_addr_header = nil
|
73
|
+
|
74
|
+
@body_remain = 0
|
75
|
+
|
76
|
+
@in_last_chunk = false
|
65
77
|
end
|
66
78
|
|
67
79
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
@@ -71,7 +83,7 @@ module Puma
|
|
71
83
|
|
72
84
|
attr_accessor :remote_addr_header
|
73
85
|
|
74
|
-
|
86
|
+
def_delegators :@io, :closed?
|
75
87
|
|
76
88
|
def inspect
|
77
89
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
@@ -100,6 +112,9 @@ module Puma
|
|
100
112
|
@tempfile = nil
|
101
113
|
@parsed_bytes = 0
|
102
114
|
@ready = false
|
115
|
+
@body_remain = 0
|
116
|
+
@peerip = nil
|
117
|
+
@in_last_chunk = false
|
103
118
|
|
104
119
|
if @buffer
|
105
120
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
@@ -112,9 +127,16 @@ module Puma
|
|
112
127
|
end
|
113
128
|
|
114
129
|
return false
|
115
|
-
|
116
|
-
|
117
|
-
|
130
|
+
else
|
131
|
+
begin
|
132
|
+
if fast_check &&
|
133
|
+
IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
|
134
|
+
return try_to_finish
|
135
|
+
end
|
136
|
+
rescue IOError
|
137
|
+
# swallow it
|
138
|
+
end
|
139
|
+
|
118
140
|
end
|
119
141
|
end
|
120
142
|
|
@@ -126,180 +148,21 @@ module Puma
|
|
126
148
|
end
|
127
149
|
end
|
128
150
|
|
129
|
-
# The object used for a request with no body. All requests with
|
130
|
-
# no body share this one object since it has no state.
|
131
|
-
EmptyBody = NullIO.new
|
132
|
-
|
133
|
-
def setup_chunked_body(body)
|
134
|
-
@chunked_body = true
|
135
|
-
@partial_part_left = 0
|
136
|
-
@prev_chunk = ""
|
137
|
-
|
138
|
-
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
139
|
-
@body.binmode
|
140
|
-
@tempfile = @body
|
141
|
-
|
142
|
-
return decode_chunk(body)
|
143
|
-
end
|
144
|
-
|
145
|
-
def decode_chunk(chunk)
|
146
|
-
if @partial_part_left > 0
|
147
|
-
if @partial_part_left <= chunk.size
|
148
|
-
@body << chunk[0..(@partial_part_left-3)] # skip the \r\n
|
149
|
-
chunk = chunk[@partial_part_left..-1]
|
150
|
-
else
|
151
|
-
@body << chunk
|
152
|
-
@partial_part_left -= chunk.size
|
153
|
-
return false
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
if @prev_chunk.empty?
|
158
|
-
io = StringIO.new(chunk)
|
159
|
-
else
|
160
|
-
io = StringIO.new(@prev_chunk+chunk)
|
161
|
-
@prev_chunk = ""
|
162
|
-
end
|
163
|
-
|
164
|
-
while !io.eof?
|
165
|
-
line = io.gets
|
166
|
-
if line.end_with?("\r\n")
|
167
|
-
len = line.strip.to_i(16)
|
168
|
-
if len == 0
|
169
|
-
@body.rewind
|
170
|
-
rest = io.read
|
171
|
-
@buffer = rest.empty? ? nil : rest
|
172
|
-
@requests_served += 1
|
173
|
-
@ready = true
|
174
|
-
return true
|
175
|
-
end
|
176
|
-
|
177
|
-
len += 2
|
178
|
-
|
179
|
-
part = io.read(len)
|
180
|
-
|
181
|
-
unless part
|
182
|
-
@partial_part_left = len
|
183
|
-
next
|
184
|
-
end
|
185
|
-
|
186
|
-
got = part.size
|
187
|
-
|
188
|
-
case
|
189
|
-
when got == len
|
190
|
-
@body << part[0..-3] # to skip the ending \r\n
|
191
|
-
when got <= len - 2
|
192
|
-
@body << part
|
193
|
-
@partial_part_left = len - part.size
|
194
|
-
when got == len - 1 # edge where we get just \r but not \n
|
195
|
-
@body << part[0..-2]
|
196
|
-
@partial_part_left = len - part.size
|
197
|
-
end
|
198
|
-
else
|
199
|
-
@prev_chunk = line
|
200
|
-
return false
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
return false
|
205
|
-
end
|
206
|
-
|
207
|
-
def read_chunked_body
|
208
|
-
while true
|
209
|
-
begin
|
210
|
-
chunk = @io.read_nonblock(4096)
|
211
|
-
rescue Errno::EAGAIN
|
212
|
-
return false
|
213
|
-
rescue SystemCallError, IOError
|
214
|
-
raise ConnectionError, "Connection error detected during read"
|
215
|
-
end
|
216
|
-
|
217
|
-
# No chunk means a closed socket
|
218
|
-
unless chunk
|
219
|
-
@body.close
|
220
|
-
@buffer = nil
|
221
|
-
@requests_served += 1
|
222
|
-
@ready = true
|
223
|
-
raise EOFError
|
224
|
-
end
|
225
|
-
|
226
|
-
return true if decode_chunk(chunk)
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
def setup_body
|
231
|
-
if @env[HTTP_EXPECT] == CONTINUE
|
232
|
-
# TODO allow a hook here to check the headers before
|
233
|
-
# going forward
|
234
|
-
@io << HTTP_11_100
|
235
|
-
@io.flush
|
236
|
-
end
|
237
|
-
|
238
|
-
@read_header = false
|
239
|
-
|
240
|
-
body = @parser.body
|
241
|
-
|
242
|
-
te = @env[TRANSFER_ENCODING2]
|
243
|
-
|
244
|
-
if te && CHUNKED.casecmp(te) == 0
|
245
|
-
return setup_chunked_body(body)
|
246
|
-
end
|
247
|
-
|
248
|
-
@chunked_body = false
|
249
|
-
|
250
|
-
cl = @env[CONTENT_LENGTH]
|
251
|
-
|
252
|
-
unless cl
|
253
|
-
@buffer = body.empty? ? nil : body
|
254
|
-
@body = EmptyBody
|
255
|
-
@requests_served += 1
|
256
|
-
@ready = true
|
257
|
-
return true
|
258
|
-
end
|
259
|
-
|
260
|
-
remain = cl.to_i - body.bytesize
|
261
|
-
|
262
|
-
if remain <= 0
|
263
|
-
@body = StringIO.new(body)
|
264
|
-
@buffer = nil
|
265
|
-
@requests_served += 1
|
266
|
-
@ready = true
|
267
|
-
return true
|
268
|
-
end
|
269
|
-
|
270
|
-
if remain > MAX_BODY
|
271
|
-
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
272
|
-
@body.binmode
|
273
|
-
@tempfile = @body
|
274
|
-
else
|
275
|
-
# The body[0,0] trick is to get an empty string in the same
|
276
|
-
# encoding as body.
|
277
|
-
@body = StringIO.new body[0,0]
|
278
|
-
end
|
279
|
-
|
280
|
-
@body.write body
|
281
|
-
|
282
|
-
@body_remain = remain
|
283
|
-
|
284
|
-
return false
|
285
|
-
end
|
286
|
-
|
287
151
|
def try_to_finish
|
288
152
|
return read_body unless @read_header
|
289
153
|
|
290
154
|
begin
|
291
155
|
data = @io.read_nonblock(CHUNK_SIZE)
|
292
|
-
rescue
|
156
|
+
rescue IO::WaitReadable
|
293
157
|
return false
|
294
|
-
rescue SystemCallError, IOError
|
158
|
+
rescue SystemCallError, IOError, EOFError
|
295
159
|
raise ConnectionError, "Connection error detected during read"
|
296
160
|
end
|
297
161
|
|
298
162
|
# No data means a closed socket
|
299
163
|
unless data
|
300
164
|
@buffer = nil
|
301
|
-
|
302
|
-
@ready = true
|
165
|
+
set_ready
|
303
166
|
raise EOFError
|
304
167
|
end
|
305
168
|
|
@@ -335,8 +198,7 @@ module Puma
|
|
335
198
|
# No data means a closed socket
|
336
199
|
unless data
|
337
200
|
@buffer = nil
|
338
|
-
|
339
|
-
@ready = true
|
201
|
+
set_ready
|
340
202
|
raise EOFError
|
341
203
|
end
|
342
204
|
|
@@ -386,6 +248,92 @@ module Puma
|
|
386
248
|
true
|
387
249
|
end
|
388
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
|
289
|
+
if te.include?(",")
|
290
|
+
te.split(",").each do |part|
|
291
|
+
if CHUNKED.casecmp(part.strip) == 0
|
292
|
+
return setup_chunked_body(body)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
elsif CHUNKED.casecmp(te) == 0
|
296
|
+
return setup_chunked_body(body)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
@chunked_body = false
|
301
|
+
|
302
|
+
cl = @env[CONTENT_LENGTH]
|
303
|
+
|
304
|
+
unless cl
|
305
|
+
@buffer = body.empty? ? nil : body
|
306
|
+
@body = EmptyBody
|
307
|
+
set_ready
|
308
|
+
return true
|
309
|
+
end
|
310
|
+
|
311
|
+
remain = cl.to_i - body.bytesize
|
312
|
+
|
313
|
+
if remain <= 0
|
314
|
+
@body = StringIO.new(body)
|
315
|
+
@buffer = nil
|
316
|
+
set_ready
|
317
|
+
return true
|
318
|
+
end
|
319
|
+
|
320
|
+
if remain > MAX_BODY
|
321
|
+
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
322
|
+
@body.binmode
|
323
|
+
@tempfile = @body
|
324
|
+
else
|
325
|
+
# The body[0,0] trick is to get an empty string in the same
|
326
|
+
# encoding as body.
|
327
|
+
@body = StringIO.new body[0,0]
|
328
|
+
end
|
329
|
+
|
330
|
+
@body.write body
|
331
|
+
|
332
|
+
@body_remain = remain
|
333
|
+
|
334
|
+
return false
|
335
|
+
end
|
336
|
+
|
389
337
|
def read_body
|
390
338
|
if @chunked_body
|
391
339
|
return read_chunked_body
|
@@ -403,7 +351,7 @@ module Puma
|
|
403
351
|
|
404
352
|
begin
|
405
353
|
chunk = @io.read_nonblock(want)
|
406
|
-
rescue
|
354
|
+
rescue IO::WaitReadable
|
407
355
|
return false
|
408
356
|
rescue SystemCallError, IOError
|
409
357
|
raise ConnectionError, "Connection error detected during read"
|
@@ -413,8 +361,7 @@ module Puma
|
|
413
361
|
unless chunk
|
414
362
|
@body.close
|
415
363
|
@buffer = nil
|
416
|
-
|
417
|
-
@ready = true
|
364
|
+
set_ready
|
418
365
|
raise EOFError
|
419
366
|
end
|
420
367
|
|
@@ -423,8 +370,7 @@ module Puma
|
|
423
370
|
if remain <= 0
|
424
371
|
@body.rewind
|
425
372
|
@buffer = nil
|
426
|
-
|
427
|
-
@ready = true
|
373
|
+
set_ready
|
428
374
|
return true
|
429
375
|
end
|
430
376
|
|
@@ -433,37 +379,136 @@ module Puma
|
|
433
379
|
false
|
434
380
|
end
|
435
381
|
|
436
|
-
def
|
437
|
-
|
438
|
-
|
439
|
-
|
382
|
+
def read_chunked_body
|
383
|
+
while true
|
384
|
+
begin
|
385
|
+
chunk = @io.read_nonblock(4096)
|
386
|
+
rescue IO::WaitReadable
|
387
|
+
return false
|
388
|
+
rescue SystemCallError, IOError
|
389
|
+
raise ConnectionError, "Connection error detected during read"
|
390
|
+
end
|
391
|
+
|
392
|
+
# No chunk means a closed socket
|
393
|
+
unless chunk
|
394
|
+
@body.close
|
395
|
+
@buffer = nil
|
396
|
+
set_ready
|
397
|
+
raise EOFError
|
398
|
+
end
|
399
|
+
|
400
|
+
if decode_chunk(chunk)
|
401
|
+
@env[CONTENT_LENGTH] = @chunked_content_length
|
402
|
+
return true
|
403
|
+
end
|
440
404
|
end
|
441
405
|
end
|
442
406
|
|
443
|
-
def
|
444
|
-
|
445
|
-
|
446
|
-
|
407
|
+
def setup_chunked_body(body)
|
408
|
+
@chunked_body = true
|
409
|
+
@partial_part_left = 0
|
410
|
+
@prev_chunk = ""
|
411
|
+
|
412
|
+
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
413
|
+
@body.binmode
|
414
|
+
@tempfile = @body
|
415
|
+
|
416
|
+
@chunked_content_length = 0
|
417
|
+
|
418
|
+
if decode_chunk(body)
|
419
|
+
@env[CONTENT_LENGTH] = @chunked_content_length
|
420
|
+
return true
|
447
421
|
end
|
448
422
|
end
|
449
423
|
|
450
|
-
def
|
451
|
-
|
452
|
-
@io << ERROR_500_RESPONSE
|
453
|
-
rescue StandardError
|
454
|
-
end
|
424
|
+
def write_chunk(str)
|
425
|
+
@chunked_content_length += @body.write(str)
|
455
426
|
end
|
456
427
|
|
457
|
-
def
|
458
|
-
|
428
|
+
def decode_chunk(chunk)
|
429
|
+
if @partial_part_left > 0
|
430
|
+
if @partial_part_left <= chunk.size
|
431
|
+
if @partial_part_left > 2
|
432
|
+
write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
|
433
|
+
end
|
434
|
+
chunk = chunk[@partial_part_left..-1]
|
435
|
+
@partial_part_left = 0
|
436
|
+
else
|
437
|
+
write_chunk(chunk) if @partial_part_left > 2 # don't include the last \r\n
|
438
|
+
@partial_part_left -= chunk.size
|
439
|
+
return false
|
440
|
+
end
|
441
|
+
end
|
459
442
|
|
460
|
-
if @
|
461
|
-
|
462
|
-
|
463
|
-
|
443
|
+
if @prev_chunk.empty?
|
444
|
+
io = StringIO.new(chunk)
|
445
|
+
else
|
446
|
+
io = StringIO.new(@prev_chunk+chunk)
|
447
|
+
@prev_chunk = ""
|
464
448
|
end
|
465
449
|
|
466
|
-
|
450
|
+
while !io.eof?
|
451
|
+
line = io.gets
|
452
|
+
if line.end_with?("\r\n")
|
453
|
+
len = line.strip.to_i(16)
|
454
|
+
if len == 0
|
455
|
+
@in_last_chunk = true
|
456
|
+
@body.rewind
|
457
|
+
rest = io.read
|
458
|
+
last_crlf_size = "\r\n".bytesize
|
459
|
+
if rest.bytesize < last_crlf_size
|
460
|
+
@buffer = nil
|
461
|
+
@partial_part_left = last_crlf_size - rest.bytesize
|
462
|
+
return false
|
463
|
+
else
|
464
|
+
@buffer = rest[last_crlf_size..-1]
|
465
|
+
@buffer = nil if @buffer.empty?
|
466
|
+
set_ready
|
467
|
+
return true
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
len += 2
|
472
|
+
|
473
|
+
part = io.read(len)
|
474
|
+
|
475
|
+
unless part
|
476
|
+
@partial_part_left = len
|
477
|
+
next
|
478
|
+
end
|
479
|
+
|
480
|
+
got = part.size
|
481
|
+
|
482
|
+
case
|
483
|
+
when got == len
|
484
|
+
write_chunk(part[0..-3]) # to skip the ending \r\n
|
485
|
+
when got <= len - 2
|
486
|
+
write_chunk(part)
|
487
|
+
@partial_part_left = len - part.size
|
488
|
+
when got == len - 1 # edge where we get just \r but not \n
|
489
|
+
write_chunk(part[0..-2])
|
490
|
+
@partial_part_left = len - part.size
|
491
|
+
end
|
492
|
+
else
|
493
|
+
@prev_chunk = line
|
494
|
+
return false
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
if @in_last_chunk
|
499
|
+
set_ready
|
500
|
+
true
|
501
|
+
else
|
502
|
+
false
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
def set_ready
|
507
|
+
if @body_read_start
|
508
|
+
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
|
509
|
+
end
|
510
|
+
@requests_served += 1
|
511
|
+
@ready = true
|
467
512
|
end
|
468
513
|
end
|
469
514
|
end
|