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 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