yahns 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Documentation/yahns_config.txt +3 -0
- data/GIT-VERSION-GEN +1 -1
- data/extras/proxy_pass.rb +22 -16
- data/lib/yahns/client_expire_tcpi.rb +1 -1
- data/lib/yahns/config.rb +4 -5
- data/lib/yahns/fdmap.rb +9 -0
- data/lib/yahns/http_client.rb +19 -19
- data/lib/yahns/http_context.rb +11 -18
- data/lib/yahns/http_response.rb +2 -2
- data/lib/yahns/openssl_client.rb +26 -6
- data/lib/yahns/proxy_http_response.rb +293 -0
- data/lib/yahns/proxy_pass.rb +248 -0
- data/lib/yahns/queue_epoll.rb +7 -13
- data/lib/yahns/queue_kqueue.rb +7 -8
- data/lib/yahns/rackup_handler.rb +0 -1
- data/lib/yahns/socket_helper.rb +2 -2
- data/lib/yahns/tee_input.rb +1 -1
- data/lib/yahns/tmpio.rb +6 -2
- data/lib/yahns/wbuf.rb +29 -13
- data/test/helper.rb +10 -0
- data/test/test_extras_proxy_pass.rb +3 -0
- data/test/test_input.rb +50 -1
- data/test/test_proxy_pass.rb +611 -0
- data/test/test_rack_hijack.rb +14 -10
- data/test/test_server.rb +3 -1
- data/test/test_ssl.rb +72 -0
- data/test/test_tmpio.rb +20 -0
- data/test/test_wbuf.rb +4 -3
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e1ce64c8f47f927355913c766a4c7cc3d007089
|
4
|
+
data.tar.gz: 29d5d3c75d4b05671d62f30987c71afa544d6950
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5fdfdb0cf9f3baf831cdf5d8af0f6779f2efc930c6736bdd5f31a18ea4248cf93643476208bfa93e2d43eb9026988b14a084cd708121098489bec599bf55f8d1
|
7
|
+
data.tar.gz: 6c1f462a5a96ae40eed022c339994e2254ddd3fdb9b95a453ddda67ab04b509badf35868a69840b97a28a66a613fc20407f9104ac6eb769d11ca72a1d553d10d
|
@@ -208,6 +208,9 @@ Ruby it is running under.
|
|
208
208
|
if input_buffering is false. This also governs the size of an
|
209
209
|
individual read(2) system call when reading a request body.
|
210
210
|
|
211
|
+
There is generally no need to change this value and this directive
|
212
|
+
may be removed in the future.
|
213
|
+
|
211
214
|
Default: 8192 bytes (8 kilobytes)
|
212
215
|
|
213
216
|
* client_header_buffer_size INTEGER
|
data/GIT-VERSION-GEN
CHANGED
data/extras/proxy_pass.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- encoding: binary -*-
|
2
|
-
# Copyright (C) 2013
|
2
|
+
# Copyright (C) 2013-2015 all contributors <yahns-public@yhbt.net>
|
3
3
|
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
4
4
|
require 'time'
|
5
5
|
require 'socket'
|
@@ -16,9 +16,6 @@ require 'timeout'
|
|
16
16
|
# cheap on GNU/Linux...
|
17
17
|
# This is totally untested but currently doesn't serve anything important.
|
18
18
|
class ProxyPass # :nodoc:
|
19
|
-
CHUNK_SIZE = 16384
|
20
|
-
ERROR_502 = [ 502, {'Content-Length'=>'0','Content-Type'=>'text/plain'}, [] ]
|
21
|
-
|
22
19
|
class ConnPool
|
23
20
|
def initialize
|
24
21
|
@mtx = Mutex.new
|
@@ -115,29 +112,38 @@ class ProxyPass # :nodoc:
|
|
115
112
|
|
116
113
|
def initialize(dest, timeout = 5)
|
117
114
|
case dest
|
118
|
-
when %r{\
|
115
|
+
when %r{\Aunix:([^:]+)(?::(/.*))?\z}
|
116
|
+
path = $2
|
117
|
+
@sockaddr = Socket.sockaddr_un($1)
|
118
|
+
when %r{\Ahttp://([^/]+)(/.*)?\z}
|
119
119
|
path = $2
|
120
120
|
host, port = $1.split(':')
|
121
121
|
@sockaddr = Socket.sockaddr_in(port || 80, host)
|
122
|
-
|
123
|
-
# methods from Rack::Request we want:
|
124
|
-
allow = %w(fullpath host_with_port host port url path)
|
125
|
-
@path = path
|
126
|
-
want = path.scan(/\$(\w+)/).flatten! || []
|
127
|
-
diff = want - allow
|
128
|
-
diff.empty? or
|
129
|
-
raise ArgumentError, "vars not allowed: #{diff.uniq.join(' ')}"
|
130
122
|
else
|
131
|
-
raise ArgumentError, "destination must be an HTTP URL"
|
123
|
+
raise ArgumentError, "destination must be an HTTP URL or unix: path"
|
132
124
|
end
|
125
|
+
init_path_vars(path)
|
133
126
|
@pool = ConnPool.new
|
134
127
|
@timeout = timeout
|
135
128
|
end
|
136
129
|
|
130
|
+
def init_path_vars(path)
|
131
|
+
path ||= '$fullpath'
|
132
|
+
# methods from Rack::Request we want:
|
133
|
+
allow = %w(fullpath host_with_port host port url path)
|
134
|
+
want = path.scan(/\$(\w+)/).flatten! || []
|
135
|
+
diff = want - allow
|
136
|
+
diff.empty? or
|
137
|
+
raise ArgumentError, "vars not allowed: #{diff.uniq.join(' ')}"
|
138
|
+
|
139
|
+
# kill leading slash just in case...
|
140
|
+
@path = path.gsub(%r{\A/(\$(?:fullpath|path))}, '\1')
|
141
|
+
end
|
142
|
+
|
137
143
|
def call(env)
|
138
144
|
request_method = env['REQUEST_METHOD']
|
139
145
|
req = Rack::Request.new(env)
|
140
|
-
path = @path.gsub(/\$(\w+)/) { req.__send__($1
|
146
|
+
path = @path.gsub(/\$(\w+)/) { req.__send__($1) }
|
141
147
|
req = "#{request_method} #{path} HTTP/1.1\r\n" \
|
142
148
|
"X-Forwarded-For: #{env["REMOTE_ADDR"]}\r\n"
|
143
149
|
|
@@ -188,7 +194,7 @@ class ProxyPass # :nodoc:
|
|
188
194
|
logger = env['rack.logger'] and
|
189
195
|
Yahns::Log.exception(logger, 'proxy_pass', e)
|
190
196
|
end
|
191
|
-
|
197
|
+
[ 502, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
|
192
198
|
end
|
193
199
|
|
194
200
|
def send_body(input, ures, chunked)
|
data/lib/yahns/config.rb
CHANGED
@@ -18,8 +18,8 @@ class Yahns::Config # :nodoc:
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def _check_in_block(ctx, var)
|
21
|
-
if ctx
|
22
|
-
return var if @block
|
21
|
+
if ctx.nil?
|
22
|
+
return var if @block.nil?
|
23
23
|
msg = "#{var} must be called outside of #{@block.type}"
|
24
24
|
else
|
25
25
|
ctx = Array(ctx)
|
@@ -236,7 +236,7 @@ class Yahns::Config # :nodoc:
|
|
236
236
|
def canonicalize_tcp(addr, port)
|
237
237
|
packed = Socket.pack_sockaddr_in(port, addr)
|
238
238
|
port, addr = Socket.unpack_sockaddr_in(packed)
|
239
|
-
|
239
|
+
addr.include?(':') ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
|
240
240
|
end
|
241
241
|
|
242
242
|
def queue(*args, &block)
|
@@ -400,9 +400,8 @@ class Yahns::Config # :nodoc:
|
|
400
400
|
var = _check_in_block(:app, :errors)
|
401
401
|
if String === val
|
402
402
|
# we've already bound working_directory by the time we get here
|
403
|
-
val = File.open(File.expand_path(val), "
|
403
|
+
val = File.open(File.expand_path(val), "ab")
|
404
404
|
val.close_on_exec = val.sync = true
|
405
|
-
val.binmode
|
406
405
|
else
|
407
406
|
rt = [ :puts, :write, :flush ] # match Rack::Lint
|
408
407
|
rt.all? { |m| val.respond_to?(m) } or raise ArgumentError,
|
data/lib/yahns/fdmap.rb
CHANGED
@@ -59,6 +59,15 @@ class Yahns::Fdmap # :nodoc:
|
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
|
+
# used by proxy to re-enable an existing client
|
63
|
+
def remember(io)
|
64
|
+
fd = io.fileno
|
65
|
+
@fdmap_mtx.synchronize do
|
66
|
+
@count += 1
|
67
|
+
@fdmap_ary[fd] = io
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
62
71
|
# this is only called in Errno::EMFILE/Errno::ENFILE situations
|
63
72
|
# and graceful shutdown
|
64
73
|
def desperate_expire(timeout)
|
data/lib/yahns/http_client.rb
CHANGED
@@ -57,7 +57,7 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
|
|
57
57
|
"Content-Length:#{len} too large (>#{mbs})", []
|
58
58
|
end
|
59
59
|
@state = :body
|
60
|
-
@input = k.tmpio_for(len)
|
60
|
+
@input = k.tmpio_for(len, @hs.env)
|
61
61
|
|
62
62
|
rbuf = Thread.current[:yahns_rbuf]
|
63
63
|
@hs.filter_body(rbuf, @hs.buf)
|
@@ -179,8 +179,9 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
|
|
179
179
|
handle_error(e)
|
180
180
|
end
|
181
181
|
|
182
|
+
# only called when buffering slow clients
|
182
183
|
# returns :wait_readable, :wait_writable, :ignore, or nil for epoll
|
183
|
-
# returns
|
184
|
+
# returns true to keep looping inside yahns_step
|
184
185
|
def r100_done
|
185
186
|
k = self.class
|
186
187
|
case k.input_buffering
|
@@ -193,7 +194,9 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
|
|
193
194
|
mkinput_preread # keep looping (@state == :body)
|
194
195
|
true
|
195
196
|
else # :lazy, false
|
196
|
-
|
197
|
+
r = k.app.call(env = @hs.env)
|
198
|
+
return :ignore if env.include?(RACK_HIJACK_IO)
|
199
|
+
http_response_write(*r)
|
197
200
|
end
|
198
201
|
end
|
199
202
|
|
@@ -205,7 +208,7 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
|
|
205
208
|
# check_client_connection
|
206
209
|
if input
|
207
210
|
env[REMOTE_ADDR] = @kgio_addr
|
208
|
-
env[RACK_HIJACK] =
|
211
|
+
env[RACK_HIJACK] = self
|
209
212
|
env[RACK_INPUT] = input
|
210
213
|
|
211
214
|
if k.check_client_connection && @hs.headers?
|
@@ -225,14 +228,6 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
|
|
225
228
|
http_response_write(status, headers, body)
|
226
229
|
end
|
227
230
|
|
228
|
-
# this is the env["rack.hijack"] callback exposed to the Rack app
|
229
|
-
def hijack_proc(env)
|
230
|
-
proc do
|
231
|
-
hijack_cleanup
|
232
|
-
env[RACK_HIJACK_IO] = self
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
231
|
# called automatically by kgio_write
|
237
232
|
def kgio_wait_writable(timeout = self.class.client_timeout)
|
238
233
|
super timeout
|
@@ -256,14 +251,19 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc:
|
|
256
251
|
end
|
257
252
|
|
258
253
|
# allow releasing some memory if rack.hijack is used
|
254
|
+
# n.b. we no longer issue EPOLL_CTL_DEL because it becomes more expensive
|
255
|
+
# (and complicated) as our hijack support will allow "un-hijacking"
|
256
|
+
# the socket.
|
259
257
|
def hijack_cleanup
|
260
|
-
#
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
258
|
+
# prevent socket from holding process up
|
259
|
+
Thread.current[:yahns_fdmap].forget(self)
|
260
|
+
@input = nil # keep env["rack.input"] accessible, though
|
261
|
+
end
|
262
|
+
|
263
|
+
# this is the env["rack.hijack"] callback exposed to the Rack app
|
264
|
+
def call
|
265
|
+
hijack_cleanup
|
266
|
+
@hs.env[RACK_HIJACK_IO] = self
|
267
267
|
end
|
268
268
|
|
269
269
|
def response_hijacked(fn)
|
data/lib/yahns/http_context.rb
CHANGED
@@ -18,7 +18,7 @@ module Yahns::HttpContext # :nodoc:
|
|
18
18
|
attr_reader :app
|
19
19
|
attr_reader :app_defaults
|
20
20
|
attr_writer :input_buffer_tmpdir
|
21
|
-
|
21
|
+
attr_accessor :output_buffer_tmpdir
|
22
22
|
|
23
23
|
def http_ctx_init(yahns_rack)
|
24
24
|
@yahns_rack = yahns_rack
|
@@ -46,7 +46,7 @@ module Yahns::HttpContext # :nodoc:
|
|
46
46
|
|
47
47
|
def __wrap_app(app)
|
48
48
|
# input_buffering == false is handled in http_client
|
49
|
-
return app if @client_max_body_size
|
49
|
+
return app if @client_max_body_size.nil?
|
50
50
|
|
51
51
|
require_relative 'cap_input'
|
52
52
|
return app if @input_buffering == true
|
@@ -77,22 +77,15 @@ module Yahns::HttpContext # :nodoc:
|
|
77
77
|
@app_defaults["rack.errors"]
|
78
78
|
end
|
79
79
|
|
80
|
-
def tmpio_for(len)
|
81
|
-
|
82
|
-
|
83
|
-
: Yahns::TmpIO.new(input_buffer_tmpdir)
|
84
|
-
else # chunked, unknown length
|
85
|
-
mbs = @client_max_body_size
|
86
|
-
tmpdir = input_buffer_tmpdir
|
87
|
-
mbs ? Yahns::CapInput.new(mbs, tmpdir) : Yahns::TmpIO.new(tmpdir)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def input_buffer_tmpdir
|
92
|
-
@input_buffer_tmpdir || Dir.tmpdir
|
93
|
-
end
|
80
|
+
def tmpio_for(len, env)
|
81
|
+
# short requests are most common
|
82
|
+
return StringIO.new('') if len && len <= @client_body_buffer_size;
|
94
83
|
|
95
|
-
|
96
|
-
|
84
|
+
# too big or chunked, unknown length
|
85
|
+
tmp = @input_buffer_tmpdir
|
86
|
+
mbs = @client_max_body_size
|
87
|
+
tmp = mbs ? Yahns::CapInput.new(mbs, tmp) : Yahns::TmpIO.new(tmp)
|
88
|
+
(env['rack.tempfiles'] ||= []) << tmp
|
89
|
+
tmp
|
97
90
|
end
|
98
91
|
end
|
data/lib/yahns/http_response.rb
CHANGED
@@ -67,7 +67,7 @@ module Yahns::HttpResponse # :nodoc:
|
|
67
67
|
alive = Yahns::StreamFile.new(body, alive, offset, count)
|
68
68
|
body = nil
|
69
69
|
end
|
70
|
-
wbuf = Yahns::Wbuf.new(body, alive, self.class.output_buffer_tmpdir)
|
70
|
+
wbuf = Yahns::Wbuf.new(body, alive, self.class.output_buffer_tmpdir, ret)
|
71
71
|
rv = wbuf.wbuf_write(self, header)
|
72
72
|
body.each { |chunk| rv = wbuf.wbuf_write(self, chunk) } if body
|
73
73
|
wbuf_maybe(wbuf, rv)
|
@@ -199,7 +199,7 @@ module Yahns::HttpResponse # :nodoc:
|
|
199
199
|
chunk = rv # hope the skb grows when we loop into the trywrite
|
200
200
|
when :wait_writable, :wait_readable
|
201
201
|
if k.output_buffering
|
202
|
-
wbuf = Yahns::Wbuf.new(body, alive, k.output_buffer_tmpdir)
|
202
|
+
wbuf = Yahns::Wbuf.new(body, alive, k.output_buffer_tmpdir, rv)
|
203
203
|
rv = wbuf.wbuf_write(self, chunk)
|
204
204
|
break
|
205
205
|
else
|
data/lib/yahns/openssl_client.rb
CHANGED
@@ -8,6 +8,31 @@ require_relative 'sendfile_compat'
|
|
8
8
|
module Yahns::OpenSSLClient # :nodoc:
|
9
9
|
include Yahns::SendfileCompat
|
10
10
|
|
11
|
+
def self.included(cls)
|
12
|
+
# Forward these methods to OpenSSL::SSL::SSLSocket so hijackers
|
13
|
+
# can rely on stdlib methods instead of ugly kgio stuff that
|
14
|
+
# we hope to phase out.
|
15
|
+
# This is a bit weird, since OpenSSL::SSL::SSLSocket wraps
|
16
|
+
# our actual socket, too, so we must take care to not blindly
|
17
|
+
# use method_missing and cause infinite recursion
|
18
|
+
%w(sync= read write readpartial write_nonblock read_nonblock
|
19
|
+
print printf puts gets readlines readline getc
|
20
|
+
readchar ungetc eof eof? << flush
|
21
|
+
sysread syswrite).map!(&:to_sym).each do |m|
|
22
|
+
cls.__send__(:define_method, m) { |*a| @ssl.__send__(m, *a) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# block captures, ugh, but nobody really uses them
|
26
|
+
%w(each each_line each_byte).map!(&:to_sym).each do |m|
|
27
|
+
cls.__send__(:define_method, m) { |*a, &b| @ssl.__send__(m, *a, &b) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# this is special, called during IO initialization in Ruby
|
32
|
+
def sync
|
33
|
+
defined?(@ssl) ? @ssl.sync : super
|
34
|
+
end
|
35
|
+
|
11
36
|
def yahns_init_ssl(ssl_ctx)
|
12
37
|
@need_accept = true
|
13
38
|
@ssl = OpenSSL::SSL::SSLSocket.new(self, ssl_ctx)
|
@@ -42,13 +67,8 @@ module Yahns::OpenSSLClient # :nodoc:
|
|
42
67
|
@ssl.read_nonblock(len, buf, exception: false)
|
43
68
|
end
|
44
69
|
|
45
|
-
def shutdown(*args)
|
46
|
-
@ssl.shutdown(*args)
|
47
|
-
super # BasicSocket#shutdown
|
48
|
-
end
|
49
|
-
|
50
70
|
def close
|
51
|
-
@ssl.close
|
71
|
+
@ssl.close # flushes SSLSocket
|
52
72
|
super # IO#close
|
53
73
|
end
|
54
74
|
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# Copyright (C) 2015 all contributors <yahns-public@yhbt.net>
|
3
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
4
|
+
|
5
|
+
# loaded by yahns/proxy_pass, this relies on Yahns::HttpResponse for
|
6
|
+
# constants.
|
7
|
+
module Yahns::HttpResponse # :nodoc:
|
8
|
+
|
9
|
+
# write everything in buf to our client socket (or wbuf, if it exists)
|
10
|
+
# it may return a newly-created wbuf or nil
|
11
|
+
def proxy_write(wbuf, buf, alive)
|
12
|
+
unless wbuf
|
13
|
+
# no write buffer, try to write directly to the client socket
|
14
|
+
case rv = String === buf ? kgio_trywrite(buf) : kgio_trywritev(buf)
|
15
|
+
when nil then return # done writing buf, likely
|
16
|
+
when String, Array # partial write, hope the skb grows
|
17
|
+
buf = rv
|
18
|
+
when :wait_writable, :wait_readable
|
19
|
+
wbuf = Yahns::Wbuf.new(nil, alive, self.class.output_buffer_tmpdir, rv)
|
20
|
+
buf = buf.join if Array === buf
|
21
|
+
break
|
22
|
+
end while true
|
23
|
+
end
|
24
|
+
|
25
|
+
wbuf.wbuf_write(self, buf)
|
26
|
+
wbuf.busy ? wbuf : nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def proxy_err_response(code, req_res, exc, wbuf)
|
30
|
+
logger = @hs.env['rack.logger']
|
31
|
+
case exc
|
32
|
+
when nil
|
33
|
+
logger.error('premature upstream EOF')
|
34
|
+
when Kcar::ParserError
|
35
|
+
logger.error("upstream response error: #{exc.message}")
|
36
|
+
else
|
37
|
+
Yahns::Log.exception(logger, 'upstream error', exc)
|
38
|
+
end
|
39
|
+
# try to write something, but don't care if we fail
|
40
|
+
Integer === code and
|
41
|
+
kgio_trywrite("HTTP/1.1 #{CODES[code]}\r\n\r\n") rescue nil
|
42
|
+
|
43
|
+
shutdown rescue nil
|
44
|
+
req_res.shutdown rescue nil
|
45
|
+
nil # signal close of req_res from yahns_step in yahns/proxy_pass.rb
|
46
|
+
ensure
|
47
|
+
wbuf.wbuf_abort if wbuf
|
48
|
+
end
|
49
|
+
|
50
|
+
def wait_on_upstream(req_res, alive, wbuf)
|
51
|
+
req_res.resbuf = wbuf || Yahns::Wbuf.new(nil, alive,
|
52
|
+
self.class.output_buffer_tmpdir,
|
53
|
+
false)
|
54
|
+
:wait_readable # self remains in :ignore, wait on upstream
|
55
|
+
end
|
56
|
+
|
57
|
+
# returns :wait_readable if we need to read more from req_res
|
58
|
+
# returns :ignore if we yield control to the client(self)
|
59
|
+
# returns nil if completely done
|
60
|
+
def proxy_response_start(res, tip, kcar, req_res)
|
61
|
+
status, headers = res
|
62
|
+
si = status.to_i
|
63
|
+
status = CODES[si] || status
|
64
|
+
env = @hs.env
|
65
|
+
have_body = !Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(si) &&
|
66
|
+
env[REQUEST_METHOD] != HEAD
|
67
|
+
flags = MSG_DONTWAIT
|
68
|
+
alive = @hs.next? && self.class.persistent_connections
|
69
|
+
|
70
|
+
res = "HTTP/1.1 #{status}\r\n"
|
71
|
+
headers.each do |key,value| # n.b.: headers is an Array of 2-element Arrays
|
72
|
+
case key
|
73
|
+
when /\A(?:Connection|Keep-Alive)\z/i
|
74
|
+
next # do not let some upstream headers leak through
|
75
|
+
when %r{\AContent-Length\z}i
|
76
|
+
flags |= MSG_MORE if have_body && value.to_i > 0
|
77
|
+
end
|
78
|
+
|
79
|
+
res << "#{key}: #{value}\r\n"
|
80
|
+
end
|
81
|
+
|
82
|
+
# For now, do not add a Date: header, assume upstream already did it
|
83
|
+
# but do not care if they did not
|
84
|
+
res << (alive ? CONN_KA : CONN_CLOSE)
|
85
|
+
|
86
|
+
# send the headers
|
87
|
+
case rv = kgio_syssend(res, flags)
|
88
|
+
when nil then break # all done, likely
|
89
|
+
when String # partial write, highly unlikely
|
90
|
+
flags = MSG_DONTWAIT
|
91
|
+
res = rv # hope the skb grows
|
92
|
+
when :wait_writable, :wait_readable # highly unlikely in real apps
|
93
|
+
wbuf = proxy_write(nil, res, alive)
|
94
|
+
break # keep buffering as much as possible
|
95
|
+
end while true
|
96
|
+
|
97
|
+
rbuf = Thread.current[:yahns_rbuf]
|
98
|
+
tip = tip.empty? ? [] : [ tip ]
|
99
|
+
|
100
|
+
if have_body
|
101
|
+
if len = kcar.body_bytes_left
|
102
|
+
|
103
|
+
case tmp = tip.shift || req_res.kgio_tryread(0x2000, rbuf)
|
104
|
+
when String
|
105
|
+
len = kcar.body_bytes_left -= tmp.size
|
106
|
+
wbuf = proxy_write(wbuf, tmp, alive)
|
107
|
+
when nil # premature EOF
|
108
|
+
return proxy_err_response(nil, req_res, nil, wbuf)
|
109
|
+
when :wait_readable
|
110
|
+
return wait_on_upstream(req_res, alive, wbuf)
|
111
|
+
end until len == 0
|
112
|
+
|
113
|
+
elsif kcar.chunked? # nasty chunked body
|
114
|
+
req_res.proxy_trailers = nil # define to avoid warnings for now
|
115
|
+
buf = ''
|
116
|
+
case tmp = tip.shift || req_res.kgio_tryread(0x2000, rbuf)
|
117
|
+
when String
|
118
|
+
kcar.filter_body(buf, tmp)
|
119
|
+
wbuf = proxy_write(wbuf, chunk_out(buf), alive) unless buf.empty?
|
120
|
+
when nil # premature EOF
|
121
|
+
return proxy_err_response(nil, req_res, nil, wbuf)
|
122
|
+
when :wait_readable
|
123
|
+
return wait_on_upstream(req_res, alive, wbuf)
|
124
|
+
end until kcar.body_eof?
|
125
|
+
|
126
|
+
buf = tmp
|
127
|
+
req_res.proxy_trailers = [ buf, tlr = [] ]
|
128
|
+
rbuf = Thread.current[:yahns_rbuf] = ''
|
129
|
+
until kcar.trailers(tlr, buf)
|
130
|
+
case rv = req_res.kgio_tryread(0x2000, rbuf)
|
131
|
+
when String
|
132
|
+
buf << rv
|
133
|
+
when :wait_readable
|
134
|
+
return wait_on_upstream(req_res, alive, wbuf)
|
135
|
+
when nil # premature EOF
|
136
|
+
return proxy_err_response(nil, req_res, nil, wbuf)
|
137
|
+
end # no loop here
|
138
|
+
end
|
139
|
+
wbuf = proxy_write(wbuf, trailer_out(tlr), alive)
|
140
|
+
|
141
|
+
else # no Content-Length or Transfer-Encoding: chunked, wait on EOF!
|
142
|
+
|
143
|
+
case tmp = tip.shift || req_res.kgio_tryread(0x2000, rbuf)
|
144
|
+
when String
|
145
|
+
wbuf = proxy_write(wbuf, tmp, alive)
|
146
|
+
when nil
|
147
|
+
req_res.shutdown
|
148
|
+
break
|
149
|
+
when :wait_readable
|
150
|
+
return wait_on_upstream(req_res, alive, wbuf)
|
151
|
+
end while true
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
return proxy_busy_mod_done(alive) unless wbuf
|
157
|
+
req_res.resbuf = wbuf
|
158
|
+
proxy_busy_mod_blocked(wbuf, wbuf.busy)
|
159
|
+
rescue => e
|
160
|
+
proxy_err_response(502, req_res, e, wbuf)
|
161
|
+
end
|
162
|
+
|
163
|
+
def proxy_response_finish(kcar, wbuf, req_res)
|
164
|
+
rbuf = Thread.current[:yahns_rbuf]
|
165
|
+
if len = kcar.body_bytes_left
|
166
|
+
|
167
|
+
case tmp = req_res.kgio_tryread(0x2000, rbuf)
|
168
|
+
when String
|
169
|
+
len = kcar.body_bytes_left -= tmp.size
|
170
|
+
wbuf.wbuf_write(self, tmp)
|
171
|
+
when nil # premature EOF
|
172
|
+
return proxy_err_response(nil, req_res, nil, wbuf)
|
173
|
+
when :wait_readable
|
174
|
+
return :wait_readable # self remains in :ignore, wait on upstream
|
175
|
+
end while len != 0
|
176
|
+
|
177
|
+
elsif kcar.chunked? # nasty chunked body
|
178
|
+
buf = ''
|
179
|
+
|
180
|
+
unless req_res.proxy_trailers
|
181
|
+
# are we done dechunking the main body, yet?
|
182
|
+
case tmp = req_res.kgio_tryread(0x2000, rbuf)
|
183
|
+
when String
|
184
|
+
kcar.filter_body(buf, tmp)
|
185
|
+
buf.empty? or wbuf.wbuf_write(self, chunk_out(buf))
|
186
|
+
when nil # premature EOF
|
187
|
+
return proxy_err_response(nil, req_res, nil, wbuf)
|
188
|
+
when :wait_readable
|
189
|
+
return :wait_readable # self remains in :ignore, wait on upstream
|
190
|
+
end until kcar.body_eof?
|
191
|
+
req_res.proxy_trailers = [ tmp, [] ] # onto trailers!
|
192
|
+
rbuf = Thread.current[:yahns_rbuf] = ''
|
193
|
+
end
|
194
|
+
|
195
|
+
buf, tlr = *req_res.proxy_trailers
|
196
|
+
until kcar.trailers(tlr, buf)
|
197
|
+
case rv = req_res.kgio_tryread(0x2000, rbuf)
|
198
|
+
when String
|
199
|
+
buf << rv
|
200
|
+
when :wait_readable
|
201
|
+
return :wait_readable
|
202
|
+
when nil # premature EOF
|
203
|
+
return proxy_err_response(nil, req_res, nil, wbuf)
|
204
|
+
end # no loop here
|
205
|
+
end
|
206
|
+
wbuf.wbuf_write(self, trailer_out(tlr))
|
207
|
+
|
208
|
+
else # no Content-Length or Transfer-Encoding: chunked, wait on EOF!
|
209
|
+
|
210
|
+
case tmp = req_res.kgio_tryread(0x2000, rbuf)
|
211
|
+
when String
|
212
|
+
wbuf.wbuf_write(self, tmp)
|
213
|
+
when nil
|
214
|
+
req_res.shutdown
|
215
|
+
break
|
216
|
+
when :wait_readable
|
217
|
+
return :wait_readable # self remains in :ignore, wait on upstream
|
218
|
+
end while true
|
219
|
+
|
220
|
+
end
|
221
|
+
|
222
|
+
busy = wbuf.busy and return proxy_busy_mod_blocked(wbuf, busy)
|
223
|
+
proxy_busy_mod_done(wbuf.wbuf_persist) # returns nil
|
224
|
+
end
|
225
|
+
|
226
|
+
def proxy_wait_next(qflags)
|
227
|
+
Thread.current[:yahns_fdmap].remember(self)
|
228
|
+
# We must allocate a new, empty request object here to avoid a TOCTTOU
|
229
|
+
# in the following timeline
|
230
|
+
#
|
231
|
+
# original thread: | another thread
|
232
|
+
# HttpClient#yahns_step |
|
233
|
+
# r = k.app.call(env = @hs.env) # socket hijacked into epoll queue
|
234
|
+
# <thread is scheduled away> | epoll_wait readiness
|
235
|
+
# | ReqRes#yahns_step
|
236
|
+
# | proxy dispatch ...
|
237
|
+
# | proxy_busy_mod_done
|
238
|
+
# ************************** DANGER BELOW ********************************
|
239
|
+
# | HttpClient#yahns_step
|
240
|
+
# | # clears env
|
241
|
+
# sees empty env: |
|
242
|
+
# return :ignore if env.include?('rack.hijack_io') |
|
243
|
+
#
|
244
|
+
# In other words, we cannot touch the original env seen by the
|
245
|
+
# original thread since it must see the 'rack.hijack_io' value
|
246
|
+
# because both are operating in the same Yahns::HttpClient object.
|
247
|
+
# This will happen regardless of GVL existence
|
248
|
+
hs = Unicorn::HttpRequest.new
|
249
|
+
hs.buf.replace(@hs.buf)
|
250
|
+
@hs = hs
|
251
|
+
|
252
|
+
# n.b. we may not touch anything in this object once we call queue_mod,
|
253
|
+
# another thread is likely to take it!
|
254
|
+
Thread.current[:yahns_queue].queue_mod(self, qflags)
|
255
|
+
end
|
256
|
+
|
257
|
+
def proxy_busy_mod_done(alive)
|
258
|
+
case http_response_done(alive)
|
259
|
+
when :wait_readable then proxy_wait_next(Yahns::Queue::QEV_RD)
|
260
|
+
when :wait_writable then proxy_wait_next(Yahns::Queue::QEV_WR)
|
261
|
+
when :close then close
|
262
|
+
end
|
263
|
+
|
264
|
+
nil # close the req_res, too
|
265
|
+
end
|
266
|
+
|
267
|
+
def proxy_busy_mod_blocked(wbuf, busy)
|
268
|
+
q = Thread.current[:yahns_queue]
|
269
|
+
# we are completely done reading and buffering the upstream response,
|
270
|
+
# but have not completely written the response to the client,
|
271
|
+
# yield control to the client socket:
|
272
|
+
@state = wbuf
|
273
|
+
case busy
|
274
|
+
when :wait_readable then q.queue_mod(self, Yahns::Queue::QEV_RD)
|
275
|
+
when :wait_writable then q.queue_mod(self, Yahns::Queue::QEV_WR)
|
276
|
+
else
|
277
|
+
abort "BUG: invalid wbuf.busy: #{busy.inspect}"
|
278
|
+
end
|
279
|
+
# no touching self after queue_mod
|
280
|
+
:ignore
|
281
|
+
end
|
282
|
+
|
283
|
+
# n.b.: we can use String#size for optimized dispatch under YARV instead
|
284
|
+
# of String#bytesize because all the IO read methods return a binary
|
285
|
+
# string when given a maximum read length
|
286
|
+
def chunk_out(buf)
|
287
|
+
[ "#{buf.size.to_s(16)}\r\n", buf, "\r\n".freeze ]
|
288
|
+
end
|
289
|
+
|
290
|
+
def trailer_out(tlr)
|
291
|
+
"0\r\n#{tlr.map! do |k,v| "#{k}: #{v}\r\n" end.join}\r\n"
|
292
|
+
end
|
293
|
+
end
|