yahns 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ed39f1e1cd058304e952eec64aa936763d1afd90
4
- data.tar.gz: 97d35c0cbf569626a2ad5670f558c09736c76d97
3
+ metadata.gz: 2e1ce64c8f47f927355913c766a4c7cc3d007089
4
+ data.tar.gz: 29d5d3c75d4b05671d62f30987c71afa544d6950
5
5
  SHA512:
6
- metadata.gz: bf372119bfdf2226d133e1b0ff4a74f51d2f79811901da42f601f4c92f872c13c2a31fbd872eb38b1f7fa2f34db5871037ebc4d1be282fdc14a690664238fe07
7
- data.tar.gz: 5e4af052c543e3154ad7c9a5abdd4af95aa9c711a53236e9cd5b74ada45585e7ae4d6b0fbd279dc6fd19422a946aa1e2de5241c0fd5439d25579029e627a8fb7
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
@@ -4,7 +4,7 @@
4
4
  CONSTANT = "Yahns::VERSION"
5
5
  RVF = "lib/yahns/version.rb"
6
6
  GVF = "GIT-VERSION-FILE"
7
- DEF_VER = "v1.6.0"
7
+ DEF_VER = "v1.7.0"
8
8
  vn = DEF_VER
9
9
 
10
10
  # First see if there is a version file (included in release tarballs),
data/extras/proxy_pass.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: binary -*-
2
- # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
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{\Ahttp://([^/]+)(/.*)\z}
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.to_sym) }
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
- ERROR_502
197
+ [ 502, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
192
198
  end
193
199
 
194
200
  def send_body(input, ures, chunked)
@@ -38,4 +38,4 @@ module Yahns::ClientExpireTCPI # :nodoc:
38
38
  end
39
39
  # FreeBSD has "struct tcp_info", too, but does not support all the fields
40
40
  # Linux does as of FreeBSD 9 (haven't checked FreeBSD 10, yet).
41
- end if RUBY_PLATFORM =~ /linux/
41
+ end if RUBY_PLATFORM.include?('linux')
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 == nil
22
- return var if @block == nil
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
- /:/ =~ addr ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
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), "a")
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)
@@ -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 false to keep looping inside yahns_step
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
- http_response_write(*k.app.call(@hs.env))
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] = hijack_proc(env)
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
- # we must issue EPOLL_CTL_DEL before hijacking (if we issue it at all),
261
- # because the hijacker may close use before we get back to the epoll worker
262
- # loop. EPOLL_CTL_DEL saves about 200 bytes of unswappable kernel memory,
263
- # so it can matter if we have lots of hijacked sockets.
264
- self.class.queue.queue_del(self) # EPOLL_CTL_DEL
265
- @input = @input.close if @input
266
- @hs = nil # no need for the HTTP parser anymore
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)
@@ -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
- attr_writer :output_buffer_tmpdir
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 == nil
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
- if len # Content-Length given
82
- len <= @client_body_buffer_size ? StringIO.new("")
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
- def output_buffer_tmpdir
96
- @output_buffer_tmpdir || Dir.tmpdir
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
@@ -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
@@ -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