puma 3.12.2 → 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 +106 -6
- data/README.md +91 -43
- data/docs/architecture.md +1 -0
- data/docs/deployment.md +24 -4
- data/docs/images/puma-connection-flow-no-reactor.png +0 -0
- data/docs/images/puma-connection-flow.png +0 -0
- data/docs/images/puma-general-arch.png +0 -0
- data/docs/plugins.md +20 -10
- data/docs/restart.md +4 -2
- data/docs/systemd.md +27 -9
- data/ext/puma_http11/PumaHttp11Service.java +2 -0
- data/ext/puma_http11/extconf.rb +8 -0
- 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 +78 -8
- data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -4
- data/lib/puma.rb +8 -0
- data/lib/puma/accept_nonblock.rb +7 -1
- data/lib/puma/app/status.rb +35 -29
- data/lib/puma/binder.rb +39 -5
- data/lib/puma/cli.rb +4 -0
- data/lib/puma/client.rb +221 -199
- data/lib/puma/cluster.rb +53 -30
- data/lib/puma/configuration.rb +4 -3
- data/lib/puma/const.rb +22 -25
- data/lib/puma/control_cli.rb +21 -4
- data/lib/puma/dsl.rb +297 -75
- data/lib/puma/events.rb +4 -1
- data/lib/puma/io_buffer.rb +1 -6
- data/lib/puma/launcher.rb +95 -53
- data/lib/puma/minissl.rb +35 -17
- 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 +109 -57
- data/lib/puma/runner.rb +4 -3
- data/lib/puma/server.rb +59 -62
- data/lib/puma/single.rb +3 -3
- data/lib/puma/thread_pool.rb +14 -32
- data/lib/puma/util.rb +1 -6
- data/lib/rack/handler/puma.rb +3 -3
- data/tools/docker/Dockerfile +16 -0
- data/tools/jungle/init.d/puma +6 -6
- data/tools/trickletest.rb +0 -1
- metadata +20 -8
- data/lib/puma/compat.rb +0 -14
- data/lib/puma/daemon_ext.rb +0 -33
- data/lib/puma/delegation.rb +0 -13
- data/lib/puma/java_io_buffer.rb +0 -47
- data/lib/puma/rack/backports/uri/common_193.rb +0 -33
@@ -0,0 +1,72 @@
|
|
1
|
+
package org.jruby.puma;
|
2
|
+
|
3
|
+
import org.jruby.*;
|
4
|
+
import org.jruby.anno.JRubyMethod;
|
5
|
+
import org.jruby.runtime.ObjectAllocator;
|
6
|
+
import org.jruby.runtime.ThreadContext;
|
7
|
+
import org.jruby.runtime.builtin.IRubyObject;
|
8
|
+
import org.jruby.util.ByteList;
|
9
|
+
|
10
|
+
/**
|
11
|
+
* @author kares
|
12
|
+
*/
|
13
|
+
public class IOBuffer extends RubyObject {
|
14
|
+
|
15
|
+
private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() {
|
16
|
+
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
|
17
|
+
return new IOBuffer(runtime, klass);
|
18
|
+
}
|
19
|
+
};
|
20
|
+
|
21
|
+
public static void createIOBuffer(Ruby runtime) {
|
22
|
+
RubyModule mPuma = runtime.defineModule("Puma");
|
23
|
+
RubyClass cIOBuffer = mPuma.defineClassUnder("IOBuffer", runtime.getObject(), ALLOCATOR);
|
24
|
+
cIOBuffer.defineAnnotatedMethods(IOBuffer.class);
|
25
|
+
}
|
26
|
+
|
27
|
+
private static final int DEFAULT_SIZE = 4096;
|
28
|
+
|
29
|
+
final ByteList buffer = new ByteList(DEFAULT_SIZE);
|
30
|
+
|
31
|
+
IOBuffer(Ruby runtime, RubyClass klass) {
|
32
|
+
super(runtime, klass);
|
33
|
+
}
|
34
|
+
|
35
|
+
@JRubyMethod
|
36
|
+
public RubyInteger used(ThreadContext context) {
|
37
|
+
return context.runtime.newFixnum(buffer.getRealSize());
|
38
|
+
}
|
39
|
+
|
40
|
+
@JRubyMethod
|
41
|
+
public RubyInteger capacity(ThreadContext context) {
|
42
|
+
return context.runtime.newFixnum(buffer.unsafeBytes().length);
|
43
|
+
}
|
44
|
+
|
45
|
+
@JRubyMethod
|
46
|
+
public IRubyObject reset() {
|
47
|
+
buffer.setRealSize(0);
|
48
|
+
return this;
|
49
|
+
}
|
50
|
+
|
51
|
+
@JRubyMethod(name = { "to_s", "to_str" })
|
52
|
+
public RubyString to_s(ThreadContext context) {
|
53
|
+
return RubyString.newStringShared(context.runtime, buffer.unsafeBytes(), 0, buffer.getRealSize());
|
54
|
+
}
|
55
|
+
|
56
|
+
@JRubyMethod(name = "<<")
|
57
|
+
public IRubyObject add(IRubyObject str) {
|
58
|
+
addImpl(str.convertToString());
|
59
|
+
return this;
|
60
|
+
}
|
61
|
+
|
62
|
+
@JRubyMethod(rest = true)
|
63
|
+
public IRubyObject append(IRubyObject[] strs) {
|
64
|
+
for (IRubyObject str : strs) addImpl(str.convertToString());
|
65
|
+
return this;
|
66
|
+
}
|
67
|
+
|
68
|
+
private void addImpl(RubyString str) {
|
69
|
+
buffer.append(str.getByteList());
|
70
|
+
}
|
71
|
+
|
72
|
+
}
|
@@ -23,6 +23,7 @@ import javax.net.ssl.SSLPeerUnverifiedException;
|
|
23
23
|
import javax.net.ssl.SSLSession;
|
24
24
|
import java.io.FileInputStream;
|
25
25
|
import java.io.IOException;
|
26
|
+
import java.nio.Buffer;
|
26
27
|
import java.nio.ByteBuffer;
|
27
28
|
import java.security.KeyManagementException;
|
28
29
|
import java.security.KeyStore;
|
@@ -65,7 +66,7 @@ public class MiniSSL extends RubyObject {
|
|
65
66
|
|
66
67
|
public void clear() { buffer.clear(); }
|
67
68
|
public void compact() { buffer.compact(); }
|
68
|
-
public void flip() { buffer.flip(); }
|
69
|
+
public void flip() { ((Buffer) buffer).flip(); }
|
69
70
|
public boolean hasRemaining() { return buffer.hasRemaining(); }
|
70
71
|
public int position() { return buffer.position(); }
|
71
72
|
|
@@ -89,7 +90,7 @@ public class MiniSSL extends RubyObject {
|
|
89
90
|
public void resize(int newCapacity) {
|
90
91
|
if (newCapacity > buffer.capacity()) {
|
91
92
|
ByteBuffer dstTmp = ByteBuffer.allocate(newCapacity);
|
92
|
-
|
93
|
+
flip();
|
93
94
|
dstTmp.put(buffer);
|
94
95
|
buffer = dstTmp;
|
95
96
|
} else {
|
@@ -101,7 +102,7 @@ public class MiniSSL extends RubyObject {
|
|
101
102
|
* Drains the buffer to a ByteList, or returns null for an empty buffer
|
102
103
|
*/
|
103
104
|
public ByteList asByteList() {
|
104
|
-
|
105
|
+
flip();
|
105
106
|
if (!buffer.hasRemaining()) {
|
106
107
|
buffer.clear();
|
107
108
|
return null;
|
@@ -158,7 +159,17 @@ public class MiniSSL extends RubyObject {
|
|
158
159
|
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
|
159
160
|
engine = sslCtx.createSSLEngine();
|
160
161
|
|
161
|
-
String[] protocols
|
162
|
+
String[] protocols;
|
163
|
+
if(miniSSLContext.callMethod(threadContext, "no_tlsv1").isTrue()) {
|
164
|
+
protocols = new String[] { "TLSv1.1", "TLSv1.2" };
|
165
|
+
} else {
|
166
|
+
protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" };
|
167
|
+
}
|
168
|
+
|
169
|
+
if(miniSSLContext.callMethod(threadContext, "no_tlsv1_1").isTrue()) {
|
170
|
+
protocols = new String[] { "TLSv1.2" };
|
171
|
+
}
|
172
|
+
|
162
173
|
engine.setEnabledProtocols(protocols);
|
163
174
|
engine.setUseClientMode(false);
|
164
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,26 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
1
5
|
module Puma
|
2
6
|
module App
|
7
|
+
# Check out {#call}'s source code to see what actions this web application
|
8
|
+
# can respond to.
|
3
9
|
class Status
|
4
|
-
def initialize(cli)
|
5
|
-
@cli = cli
|
6
|
-
@auth_token = nil
|
7
|
-
end
|
8
10
|
OK_STATUS = '{ "status": "ok" }'.freeze
|
9
11
|
|
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]]
|
12
|
+
def initialize(cli, token = nil)
|
13
|
+
@cli = cli
|
14
|
+
@auth_token = token
|
24
15
|
end
|
25
16
|
|
26
17
|
def call(env)
|
@@ -31,44 +22,59 @@ module Puma
|
|
31
22
|
case env['PATH_INFO']
|
32
23
|
when /\/stop$/
|
33
24
|
@cli.stop
|
34
|
-
|
25
|
+
rack_response(200, OK_STATUS)
|
35
26
|
|
36
27
|
when /\/halt$/
|
37
28
|
@cli.halt
|
38
|
-
|
29
|
+
rack_response(200, OK_STATUS)
|
39
30
|
|
40
31
|
when /\/restart$/
|
41
32
|
@cli.restart
|
42
|
-
|
33
|
+
rack_response(200, OK_STATUS)
|
43
34
|
|
44
35
|
when /\/phased-restart$/
|
45
36
|
if !@cli.phased_restart
|
46
|
-
|
37
|
+
rack_response(404, '{ "error": "phased restart not available" }')
|
47
38
|
else
|
48
|
-
|
39
|
+
rack_response(200, OK_STATUS)
|
49
40
|
end
|
50
41
|
|
51
42
|
when /\/reload-worker-directory$/
|
52
43
|
if !@cli.send(:reload_worker_directory)
|
53
|
-
|
44
|
+
rack_response(404, '{ "error": "reload_worker_directory not available" }')
|
54
45
|
else
|
55
|
-
|
46
|
+
rack_response(200, OK_STATUS)
|
56
47
|
end
|
57
48
|
|
58
49
|
when /\/gc$/
|
59
50
|
GC.start
|
60
|
-
|
51
|
+
rack_response(200, OK_STATUS)
|
61
52
|
|
62
53
|
when /\/gc-stats$/
|
63
|
-
|
64
|
-
return rack_response(200, json)
|
54
|
+
rack_response(200, GC.stat.to_json)
|
65
55
|
|
66
56
|
when /\/stats$/
|
67
|
-
|
57
|
+
rack_response(200, @cli.stats)
|
68
58
|
else
|
69
59
|
rack_response 404, "Unsupported action", 'text/plain'
|
70
60
|
end
|
71
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
|
72
78
|
end
|
73
79
|
end
|
74
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,7 +50,6 @@ module Puma
|
|
50
50
|
|
51
51
|
def close
|
52
52
|
@ios.each { |i| i.close }
|
53
|
-
@unix_paths.each { |i| File.unlink i }
|
54
53
|
end
|
55
54
|
|
56
55
|
def import_from_env
|
@@ -104,7 +103,17 @@ module Puma
|
|
104
103
|
bak = params.fetch('backlog', 1024).to_i
|
105
104
|
|
106
105
|
io = add_tcp_listener uri.host, uri.port, opt, bak
|
107
|
-
|
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
|
108
117
|
end
|
109
118
|
|
110
119
|
@listeners << [str, io] if io
|
@@ -187,6 +196,9 @@ module Puma
|
|
187
196
|
ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
|
188
197
|
end
|
189
198
|
|
199
|
+
ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
|
200
|
+
ctx.no_tlsv1_1 = true if params['no_tlsv1_1'] == 'true'
|
201
|
+
|
190
202
|
if params['verify_mode']
|
191
203
|
ctx.verify_mode = case params['verify_mode']
|
192
204
|
when "peer"
|
@@ -349,7 +361,7 @@ module Puma
|
|
349
361
|
# Tell the server to listen on +path+ as a UNIX domain socket.
|
350
362
|
#
|
351
363
|
def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
|
352
|
-
@unix_paths << path
|
364
|
+
@unix_paths << path unless File.exist? path
|
353
365
|
|
354
366
|
# Let anyone connect by default
|
355
367
|
umask ||= 0
|
@@ -387,7 +399,7 @@ module Puma
|
|
387
399
|
end
|
388
400
|
|
389
401
|
def inherit_unix_listener(path, fd)
|
390
|
-
@unix_paths << path
|
402
|
+
@unix_paths << path unless File.exist? path
|
391
403
|
|
392
404
|
if fd.kind_of? TCPServer
|
393
405
|
s = fd
|
@@ -403,5 +415,27 @@ module Puma
|
|
403
415
|
s
|
404
416
|
end
|
405
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
|
406
440
|
end
|
407
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,19 +24,24 @@ 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
|
-
#
|
31
|
-
#
|
32
|
-
#
|
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.
|
33
34
|
#
|
34
35
|
# Instances of this class are responsible for knowing if
|
35
36
|
# the header and body are fully buffered via the `try_to_finish` method.
|
36
37
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
37
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
|
+
|
38
43
|
include Puma::Const
|
39
|
-
extend
|
44
|
+
extend Forwardable
|
40
45
|
|
41
46
|
def initialize(io, env=nil)
|
42
47
|
@io = io
|
@@ -54,6 +59,7 @@ module Puma
|
|
54
59
|
@ready = false
|
55
60
|
|
56
61
|
@body = nil
|
62
|
+
@body_read_start = nil
|
57
63
|
@buffer = nil
|
58
64
|
@tempfile = nil
|
59
65
|
|
@@ -64,6 +70,10 @@ module Puma
|
|
64
70
|
|
65
71
|
@peerip = nil
|
66
72
|
@remote_addr_header = nil
|
73
|
+
|
74
|
+
@body_remain = 0
|
75
|
+
|
76
|
+
@in_last_chunk = false
|
67
77
|
end
|
68
78
|
|
69
79
|
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
@@ -73,7 +83,7 @@ module Puma
|
|
73
83
|
|
74
84
|
attr_accessor :remote_addr_header
|
75
85
|
|
76
|
-
|
86
|
+
def_delegators :@io, :closed?
|
77
87
|
|
78
88
|
def inspect
|
79
89
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
@@ -102,6 +112,9 @@ module Puma
|
|
102
112
|
@tempfile = nil
|
103
113
|
@parsed_bytes = 0
|
104
114
|
@ready = false
|
115
|
+
@body_remain = 0
|
116
|
+
@peerip = nil
|
117
|
+
@in_last_chunk = false
|
105
118
|
|
106
119
|
if @buffer
|
107
120
|
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
|
@@ -114,9 +127,16 @@ module Puma
|
|
114
127
|
end
|
115
128
|
|
116
129
|
return false
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
+
|
120
140
|
end
|
121
141
|
end
|
122
142
|
|
@@ -128,165 +148,6 @@ module Puma
|
|
128
148
|
end
|
129
149
|
end
|
130
150
|
|
131
|
-
# The object used for a request with no body. All requests with
|
132
|
-
# no body share this one object since it has no state.
|
133
|
-
EmptyBody = NullIO.new
|
134
|
-
|
135
|
-
def setup_chunked_body(body)
|
136
|
-
@chunked_body = true
|
137
|
-
@partial_part_left = 0
|
138
|
-
@prev_chunk = ""
|
139
|
-
|
140
|
-
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
141
|
-
@body.binmode
|
142
|
-
@tempfile = @body
|
143
|
-
|
144
|
-
return decode_chunk(body)
|
145
|
-
end
|
146
|
-
|
147
|
-
def decode_chunk(chunk)
|
148
|
-
if @partial_part_left > 0
|
149
|
-
if @partial_part_left <= chunk.size
|
150
|
-
@body << chunk[0..(@partial_part_left-3)] # skip the \r\n
|
151
|
-
chunk = chunk[@partial_part_left..-1]
|
152
|
-
else
|
153
|
-
@body << chunk
|
154
|
-
@partial_part_left -= chunk.size
|
155
|
-
return false
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
if @prev_chunk.empty?
|
160
|
-
io = StringIO.new(chunk)
|
161
|
-
else
|
162
|
-
io = StringIO.new(@prev_chunk+chunk)
|
163
|
-
@prev_chunk = ""
|
164
|
-
end
|
165
|
-
|
166
|
-
while !io.eof?
|
167
|
-
line = io.gets
|
168
|
-
if line.end_with?("\r\n")
|
169
|
-
len = line.strip.to_i(16)
|
170
|
-
if len == 0
|
171
|
-
@body.rewind
|
172
|
-
rest = io.read
|
173
|
-
rest = rest[2..-1] if rest.start_with?("\r\n")
|
174
|
-
@buffer = rest.empty? ? nil : rest
|
175
|
-
@requests_served += 1
|
176
|
-
@ready = true
|
177
|
-
return true
|
178
|
-
end
|
179
|
-
|
180
|
-
len += 2
|
181
|
-
|
182
|
-
part = io.read(len)
|
183
|
-
|
184
|
-
unless part
|
185
|
-
@partial_part_left = len
|
186
|
-
next
|
187
|
-
end
|
188
|
-
|
189
|
-
got = part.size
|
190
|
-
|
191
|
-
case
|
192
|
-
when got == len
|
193
|
-
@body << part[0..-3] # to skip the ending \r\n
|
194
|
-
when got <= len - 2
|
195
|
-
@body << part
|
196
|
-
@partial_part_left = len - part.size
|
197
|
-
when got == len - 1 # edge where we get just \r but not \n
|
198
|
-
@body << part[0..-2]
|
199
|
-
@partial_part_left = len - part.size
|
200
|
-
end
|
201
|
-
else
|
202
|
-
@prev_chunk = line
|
203
|
-
return false
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
return false
|
208
|
-
end
|
209
|
-
|
210
|
-
def read_chunked_body
|
211
|
-
while true
|
212
|
-
begin
|
213
|
-
chunk = @io.read_nonblock(4096)
|
214
|
-
rescue Errno::EAGAIN
|
215
|
-
return false
|
216
|
-
rescue SystemCallError, IOError
|
217
|
-
raise ConnectionError, "Connection error detected during read"
|
218
|
-
end
|
219
|
-
|
220
|
-
# No chunk means a closed socket
|
221
|
-
unless chunk
|
222
|
-
@body.close
|
223
|
-
@buffer = nil
|
224
|
-
@requests_served += 1
|
225
|
-
@ready = true
|
226
|
-
raise EOFError
|
227
|
-
end
|
228
|
-
|
229
|
-
return true if decode_chunk(chunk)
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
def setup_body
|
234
|
-
if @env[HTTP_EXPECT] == CONTINUE
|
235
|
-
# TODO allow a hook here to check the headers before
|
236
|
-
# going forward
|
237
|
-
@io << HTTP_11_100
|
238
|
-
@io.flush
|
239
|
-
end
|
240
|
-
|
241
|
-
@read_header = false
|
242
|
-
|
243
|
-
body = @parser.body
|
244
|
-
|
245
|
-
te = @env[TRANSFER_ENCODING2]
|
246
|
-
|
247
|
-
if te && CHUNKED.casecmp(te) == 0
|
248
|
-
return setup_chunked_body(body)
|
249
|
-
end
|
250
|
-
|
251
|
-
@chunked_body = false
|
252
|
-
|
253
|
-
cl = @env[CONTENT_LENGTH]
|
254
|
-
|
255
|
-
unless cl
|
256
|
-
@buffer = body.empty? ? nil : body
|
257
|
-
@body = EmptyBody
|
258
|
-
@requests_served += 1
|
259
|
-
@ready = true
|
260
|
-
return true
|
261
|
-
end
|
262
|
-
|
263
|
-
remain = cl.to_i - body.bytesize
|
264
|
-
|
265
|
-
if remain <= 0
|
266
|
-
@body = StringIO.new(body)
|
267
|
-
@buffer = nil
|
268
|
-
@requests_served += 1
|
269
|
-
@ready = true
|
270
|
-
return true
|
271
|
-
end
|
272
|
-
|
273
|
-
if remain > MAX_BODY
|
274
|
-
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
275
|
-
@body.binmode
|
276
|
-
@tempfile = @body
|
277
|
-
else
|
278
|
-
# The body[0,0] trick is to get an empty string in the same
|
279
|
-
# encoding as body.
|
280
|
-
@body = StringIO.new body[0,0]
|
281
|
-
end
|
282
|
-
|
283
|
-
@body.write body
|
284
|
-
|
285
|
-
@body_remain = remain
|
286
|
-
|
287
|
-
return false
|
288
|
-
end
|
289
|
-
|
290
151
|
def try_to_finish
|
291
152
|
return read_body unless @read_header
|
292
153
|
|
@@ -294,15 +155,14 @@ module Puma
|
|
294
155
|
data = @io.read_nonblock(CHUNK_SIZE)
|
295
156
|
rescue Errno::EAGAIN
|
296
157
|
return false
|
297
|
-
rescue SystemCallError, IOError
|
158
|
+
rescue SystemCallError, IOError, EOFError
|
298
159
|
raise ConnectionError, "Connection error detected during read"
|
299
160
|
end
|
300
161
|
|
301
162
|
# No data means a closed socket
|
302
163
|
unless data
|
303
164
|
@buffer = nil
|
304
|
-
|
305
|
-
@ready = true
|
165
|
+
set_ready
|
306
166
|
raise EOFError
|
307
167
|
end
|
308
168
|
|
@@ -338,8 +198,7 @@ module Puma
|
|
338
198
|
# No data means a closed socket
|
339
199
|
unless data
|
340
200
|
@buffer = nil
|
341
|
-
|
342
|
-
@ready = true
|
201
|
+
set_ready
|
343
202
|
raise EOFError
|
344
203
|
end
|
345
204
|
|
@@ -389,6 +248,84 @@ module Puma
|
|
389
248
|
true
|
390
249
|
end
|
391
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
|
+
|
392
329
|
def read_body
|
393
330
|
if @chunked_body
|
394
331
|
return read_chunked_body
|
@@ -416,8 +353,7 @@ module Puma
|
|
416
353
|
unless chunk
|
417
354
|
@body.close
|
418
355
|
@buffer = nil
|
419
|
-
|
420
|
-
@ready = true
|
356
|
+
set_ready
|
421
357
|
raise EOFError
|
422
358
|
end
|
423
359
|
|
@@ -426,8 +362,7 @@ module Puma
|
|
426
362
|
if remain <= 0
|
427
363
|
@body.rewind
|
428
364
|
@buffer = nil
|
429
|
-
|
430
|
-
@ready = true
|
365
|
+
set_ready
|
431
366
|
return true
|
432
367
|
end
|
433
368
|
|
@@ -436,37 +371,124 @@ module Puma
|
|
436
371
|
false
|
437
372
|
end
|
438
373
|
|
439
|
-
def
|
440
|
-
|
441
|
-
|
442
|
-
|
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)
|
443
393
|
end
|
444
394
|
end
|
445
395
|
|
446
|
-
def
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
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)
|
451
406
|
end
|
452
407
|
|
453
|
-
def
|
454
|
-
|
455
|
-
@
|
456
|
-
|
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
|
457
421
|
end
|
458
|
-
end
|
459
422
|
|
460
|
-
|
461
|
-
|
423
|
+
if @prev_chunk.empty?
|
424
|
+
io = StringIO.new(chunk)
|
425
|
+
else
|
426
|
+
io = StringIO.new(@prev_chunk+chunk)
|
427
|
+
@prev_chunk = ""
|
428
|
+
end
|
462
429
|
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
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
|
450
|
+
|
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
|
467
476
|
end
|
468
477
|
|
469
|
-
|
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
|
470
492
|
end
|
471
493
|
end
|
472
494
|
end
|