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.
@@ -0,0 +1,248 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013-2015 all contributors <yahns-public@yhbt.net>
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require 'socket'
5
+ require 'kgio'
6
+ require 'kcar' # gem install kcar
7
+ require 'rack/request'
8
+ require 'timeout'
9
+
10
+ require_relative 'proxy_http_response'
11
+
12
+ class Yahns::ProxyPass # :nodoc:
13
+ class ReqRes < Kgio::Socket
14
+ attr_writer :resbuf
15
+ attr_accessor :proxy_trailers
16
+
17
+ def req_start(c, req, input, chunked)
18
+ @hdr = @resbuf = nil
19
+ @yahns_client = c
20
+ @rrstate = input ? [ req, input, chunked ] : req
21
+ Thread.current[:yahns_queue].queue_add(self, Yahns::Queue::QEV_WR)
22
+ end
23
+
24
+ # we must reinitialize the thread-local rbuf if it may get beyond the
25
+ # current thread
26
+ def detach_rbuf!
27
+ Thread.current[:yahns_rbuf] = ''
28
+ end
29
+
30
+ def yahns_step # yahns event loop entry point
31
+ c = @yahns_client
32
+ case req = @rrstate
33
+ when Kcar::Parser # reading response...
34
+ buf = Thread.current[:yahns_rbuf]
35
+
36
+ case resbuf = @resbuf # where are we at the response?
37
+ when nil # common case, catch the response header in a single read
38
+
39
+ case rv = kgio_tryread(0x2000, buf)
40
+ when String
41
+ if res = req.headers(@hdr = [], rv)
42
+ return c.proxy_response_start(res, rv, req, self)
43
+ else # ugh, big headers or tricked response
44
+ buf = detach_rbuf!
45
+ @resbuf = rv
46
+ end
47
+ # continue looping in middle "case @resbuf" loop
48
+ when :wait_readable
49
+ return rv # spurious wakeup
50
+ when nil then return c.proxy_err_response(502, self, nil, nil)
51
+ end # NOT looping here
52
+
53
+ when String # continue reading trickled response headers from upstream
54
+
55
+ case rv = kgio_tryread(0x2000, buf)
56
+ when String then res = req.headers(@hdr, resbuf << rv) and break
57
+ when :wait_readable then return rv
58
+ when nil then return c.proxy_err_response(502, self, nil, nil)
59
+ end while true
60
+
61
+ return c.proxy_response_start(res, resbuf, req, self)
62
+
63
+ when Yahns::WbufCommon # streaming/buffering the response body
64
+
65
+ return c.proxy_response_finish(req, resbuf, self)
66
+
67
+ end while true # case @resbuf
68
+
69
+ when Array # [ (str|vec), rack.input, chunked? ]
70
+ send_req_body(req) # returns nil or :wait_writable
71
+ when String # buffered request header
72
+ send_req_buf(req)
73
+ end
74
+ rescue => e
75
+ # avoid polluting logs with a giant backtrace when the problem isn't
76
+ # fixable in code.
77
+ e.set_backtrace([]) if Errno::ECONNREFUSED === e
78
+ c.proxy_err_response(502, self, e, nil)
79
+ end
80
+
81
+ # returns :wait_readable if complete, :wait_writable if not
82
+ def send_req_body(req)
83
+ buf, input, chunked = req
84
+
85
+ # get the first buffered chunk or vector
86
+ case rv = String === buf ? kgio_trywrite(buf) : kgio_trywritev(buf)
87
+ when String, Array
88
+ buf = rv # retry inner loop
89
+ when :wait_writable
90
+ req[0] = buf
91
+ return :wait_writable
92
+ when nil
93
+ break # onto writing body
94
+ end while true
95
+
96
+ buf = Thread.current[:yahns_rbuf]
97
+
98
+ # Note: input (env['rack.input']) is fully-buffered by default so
99
+ # we should not be waiting on a slow network resource when reading
100
+ # input. However, some weird configs may disable this on LANs
101
+
102
+ if chunked
103
+ while input.read(0x2000, buf)
104
+ vec = [ "#{buf.size.to_s(16)}\r\n", buf, "\r\n".freeze ]
105
+ case rv = kgio_trywritev(vec)
106
+ when Array
107
+ vec = rv # partial write, retry in case loop
108
+ when :wait_writable
109
+ detach_rbuf!
110
+ req[0] = vec
111
+ return :wait_writable
112
+ when nil
113
+ break # continue onto reading next chunk
114
+ end while true
115
+ end
116
+ close_req_body(input)
117
+
118
+ # note: we do not send any trailer, they are folded into the header
119
+ # because this relies on full request buffering
120
+ send_req_buf("0\r\n\r\n".freeze)
121
+ # prepare_wait_readable already called by send_req_buf
122
+ else # identity request, easy:
123
+ while input.read(0x2000, buf)
124
+ case rv = kgio_trywrite(buf)
125
+ when String
126
+ buf = rv # partial write, retry in case loop
127
+ when :wait_writable
128
+ detach_rbuf!
129
+ req[0] = buf
130
+ return :wait_writable
131
+ when nil
132
+ break # continue onto reading next block
133
+ end while true
134
+ end
135
+
136
+ close_req_body(input)
137
+ prepare_wait_readable
138
+ end
139
+ rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN
140
+ # no more reading off the client socket, just prepare to forward
141
+ # the rejection response from the upstream (if any)
142
+ @yahns_client.to_io.shutdown(Socket::SHUT_RD)
143
+ prepare_wait_readable
144
+ end
145
+
146
+ def prepare_wait_readable
147
+ @rrstate = Kcar::Parser.new
148
+ :wait_readable # all done sending the request, wait for response
149
+ end
150
+
151
+ def close_req_body(input)
152
+ case input
153
+ when Yahns::TeeInput, IO, StringIO
154
+ input.close
155
+ end
156
+ end
157
+
158
+ # n.b. buf must be a detached string not shared with
159
+ # Thread.current[:yahns_rbuf] of any thread
160
+ def send_req_buf(buf)
161
+ case rv = kgio_trywrite(buf)
162
+ when String
163
+ buf = rv # retry inner loop
164
+ when :wait_writable
165
+ @rrstate = buf
166
+ return :wait_writable
167
+ when nil
168
+ return prepare_wait_readable
169
+ end while true
170
+ end
171
+ end # class ReqRes
172
+
173
+ def initialize(dest)
174
+ case dest
175
+ when %r{\Aunix:([^:]+)(?::(/.*))?\z}
176
+ path = $2
177
+ @sockaddr = Socket.sockaddr_un($1)
178
+ when %r{\Ahttp://([^/]+)(/.*)?\z}
179
+ path = $2
180
+ host, port = $1.split(':')
181
+ @sockaddr = Socket.sockaddr_in(port || 80, host)
182
+ else
183
+ raise ArgumentError, "destination must be an HTTP URL or unix: path"
184
+ end
185
+ init_path_vars(path)
186
+ end
187
+
188
+ def init_path_vars(path)
189
+ path ||= '$fullpath'
190
+ # methods from Rack::Request we want:
191
+ allow = %w(fullpath host_with_port host port url path)
192
+ want = path.scan(/\$(\w+)/).flatten! || []
193
+ diff = want - allow
194
+ diff.empty? or
195
+ raise ArgumentError, "vars not allowed: #{diff.uniq.join(' ')}"
196
+
197
+ # kill leading slash just in case...
198
+ @path = path.gsub(%r{\A/(\$(?:fullpath|path))}, '\1')
199
+ end
200
+
201
+ def call(env)
202
+ # 3-way handshake for TCP backends while we generate the request header
203
+ rr = ReqRes.start(@sockaddr)
204
+ c = env['rack.hijack'].call
205
+
206
+ req = Rack::Request.new(env)
207
+ req = @path.gsub(/\$(\w+)/) { req.__send__($1) }
208
+
209
+ # start the connection asynchronously and early so TCP can do a
210
+ case ver = env['HTTP_VERSION']
211
+ when 'HTTP/1.1' # leave alone, response may be chunked
212
+ else # no chunking for HTTP/1.0 and HTTP/0.9
213
+ ver = 'HTTP/1.0'.freeze
214
+ end
215
+
216
+ req = "#{env['REQUEST_METHOD']} #{req} #{ver}\r\n" \
217
+ "X-Forwarded-For: #{env["REMOTE_ADDR"]}\r\n"
218
+
219
+ # pass most HTTP_* headers through as-is
220
+ chunked = false
221
+ env.each do |key, val|
222
+ %r{\AHTTP_(\w+)\z} =~ key or next
223
+ key = $1
224
+ # trailers are folded into the header, so do not send the Trailer:
225
+ # header in the request
226
+ next if /\A(?:VERSION|CONNECTION|KEEP_ALIVE|X_FORWARDED_FOR|TRAILER)/ =~
227
+ key
228
+ 'TRANSFER_ENCODING'.freeze == key && val =~ /\bchunked\b/i and
229
+ chunked = true
230
+ key.tr!('_'.freeze, '-'.freeze)
231
+ req << "#{key}: #{val}\r\n"
232
+ end
233
+
234
+ # special cases which Rack does not prefix:
235
+ ctype = env["CONTENT_TYPE"] and req << "Content-Type: #{ctype}\r\n"
236
+ clen = env["CONTENT_LENGTH"] and req << "Content-Length: #{clen}\r\n"
237
+ input = chunked || (clen && clen.to_i > 0) ? env['rack.input'] : nil
238
+
239
+ # finally, prepare to emit the headers
240
+ rr.req_start(c, req << "\r\n".freeze, input, chunked)
241
+
242
+ # this probably breaks fewer middlewares than returning whatever else...
243
+ [ 500, [], [] ]
244
+ rescue => e
245
+ Yahns::Log.exception(env['rack.logger'], 'proxy_pass', e)
246
+ [ 502, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
247
+ end
248
+ end
@@ -27,20 +27,14 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
27
27
  epoll_ctl(Epoll::CTL_ADD, io, flags)
28
28
  end
29
29
 
30
+ def queue_mod(io, flags)
31
+ epoll_ctl(Epoll::CTL_MOD, io, flags)
32
+ end
33
+
30
34
  def thr_init
31
35
  Thread.current[:yahns_rbuf] = ""
32
36
  Thread.current[:yahns_fdmap] = @fdmap
33
- end
34
-
35
- # use only before hijacking, once hijacked, io may be unusable to us
36
- # It is not safe to call this unless it is an unarmed EPOLLONESHOT
37
- # object.
38
- def queue_del(io)
39
- # order does not really matter here, however Epoll::CTL_DEL
40
- # will free up ~200 bytes of unswappable kernel memory,
41
- # so we call it first
42
- epoll_ctl(Epoll::CTL_DEL, io, 0)
43
- @fdmap.forget(io)
37
+ Thread.current[:yahns_queue] = self
44
38
  end
45
39
 
46
40
  # returns an array of infinitely running threads
@@ -64,13 +58,13 @@ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
64
58
  # expected to work, so we had to erase it from fdmap before hijack
65
59
  when nil, :close
66
60
  # this must be the ONLY place where we call IO#close on
67
- # things that got inside the queue
61
+ # things that got inside the queue AND fdmap
68
62
  @fdmap.sync_close(io)
69
63
  else
70
64
  raise "BUG: #{io.inspect}#yahns_step returned: #{rv.inspect}"
71
65
  end
72
66
  end
73
- rescue => e
67
+ rescue StandardError, LoadError, SyntaxError => e
74
68
  break if closed? # can still happen due to shutdown_timeout
75
69
  Yahns::Log.exception(logger, 'queue loop', e)
76
70
  end while true
@@ -36,15 +36,14 @@ class Yahns::Queue < SleepyPenguin::Kqueue::IO # :nodoc:
36
36
  kevent(Kevent[io.fileno, flags, fflags, 0, 0, io])
37
37
  end
38
38
 
39
+ def queue_mod(io, flags)
40
+ kevent(Kevent[io.fileno, flags, ADD_ONESHOT, 0, 0, io])
41
+ end
42
+
39
43
  def thr_init
40
44
  Thread.current[:yahns_rbuf] = ""
41
45
  Thread.current[:yahns_fdmap] = @fdmap
42
- end
43
-
44
- def queue_del(io)
45
- # do not bother with kevent EV_DELETE, it may be tricky to get right,
46
- # we only did it in epoll since Eric knows the epoll internals well.
47
- @fdmap.forget(io)
46
+ Thread.current[:yahns_queue] = self
48
47
  end
49
48
 
50
49
  # returns an array of infinitely running threads
@@ -67,13 +66,13 @@ class Yahns::Queue < SleepyPenguin::Kqueue::IO # :nodoc:
67
66
  # expected to work, so we had to erase it from fdmap before hijack
68
67
  when nil, :close
69
68
  # this must be the ONLY place where we call IO#close on
70
- # things that got inside the queue
69
+ # things that got inside the queue AND fdmap
71
70
  @fdmap.sync_close(io)
72
71
  else
73
72
  raise "BUG: #{io.inspect}#yahns_step returned: #{rv.inspect}"
74
73
  end
75
74
  end
76
- rescue => e
75
+ rescue StandardError, LoadError, SyntaxError => e
77
76
  break if closed? # can still happen due to shutdown_timeout
78
77
  Yahns::Log.exception(logger, 'queue loop', e)
79
78
  end while true
@@ -32,7 +32,6 @@ module Yahns::RackupHandler # :nodoc:
32
32
  end
33
33
 
34
34
  %w(stderr_path stdout_path).each do |x|
35
- x = x.to_sym
36
35
  val = o[x] and __send__(x, val)
37
36
  end
38
37
  end
@@ -8,7 +8,7 @@ module Yahns::SocketHelper # :nodoc:
8
8
  def so_reuseport
9
9
  if defined?(Socket::SO_REUSEPORT)
10
10
  Socket::SO_REUSEPORT
11
- elsif RUBY_PLATFORM =~ /linux/
11
+ elsif RUBY_PLATFORM.include?('linux')
12
12
  15 # only tested on x86_64 and i686
13
13
  else
14
14
  nil
@@ -108,7 +108,7 @@ module Yahns::SocketHelper # :nodoc:
108
108
  # returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6
109
109
  def tcp_name(sock)
110
110
  port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
111
- /:/ =~ addr ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
111
+ addr.include?(':') ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
112
112
  end
113
113
 
114
114
  # Returns the configuration name of a socket as a string. sock may
@@ -19,7 +19,7 @@ class Yahns::TeeInput < Yahns::StreamInput # :nodoc:
19
19
  def initialize(client, request)
20
20
  @len = request.content_length
21
21
  super
22
- @tmp = client.class.tmpio_for(@len)
22
+ @tmp = client.class.tmpio_for(@len, request.env)
23
23
  end
24
24
 
25
25
  # :call-seq:
data/lib/yahns/tmpio.rb CHANGED
@@ -7,14 +7,15 @@ require 'tmpdir'
7
7
  # well with unlinked files. This one is much shorter, easier
8
8
  # to understand, and slightly faster (no delegation).
9
9
  class Yahns::TmpIO < File # :nodoc:
10
+ include Kgio::PipeMethods
10
11
 
11
12
  # creates and returns a new File object. The File is unlinked
12
13
  # immediately, switched to binary mode, and userspace output
13
14
  # buffering is disabled
14
- def self.new(tmpdir = Dir.tmpdir)
15
+ def self.new(dir)
15
16
  retried = false
16
17
  begin
17
- fp = super("#{tmpdir}/#{rand}", RDWR|CREAT|EXCL|APPEND, 0600)
18
+ fp = super("#{dir || Dir.tmpdir}/#{rand}", RDWR|CREAT|EXCL|APPEND, 0600)
18
19
  rescue Errno::EEXIST
19
20
  retry
20
21
  rescue Errno::EMFILE, Errno::ENFILE
@@ -29,4 +30,7 @@ class Yahns::TmpIO < File # :nodoc:
29
30
  fp.sync = true
30
31
  fp
31
32
  end
33
+
34
+ # pretend we're Tempfile for Rack::TempfileReaper
35
+ alias close! close
32
36
  end
data/lib/yahns/wbuf.rb CHANGED
@@ -29,34 +29,46 @@ require_relative 'wbuf_common'
29
29
  # to be a scalability issue.
30
30
  class Yahns::Wbuf # :nodoc:
31
31
  include Yahns::WbufCommon
32
+ attr_reader :busy
33
+ attr_reader :wbuf_persist
32
34
 
33
- def initialize(body, persist, tmpdir)
35
+ def initialize(body, persist, tmpdir, busy)
34
36
  @tmpio = nil
35
37
  @tmpdir = tmpdir
36
38
  @sf_offset = @sf_count = 0
37
39
  @wbuf_persist = persist # whether or not we keep the connection alive
38
40
  @body = body
39
- @bypass = false
41
+ @busy = busy # may be false
40
42
  end
41
43
 
42
- def wbuf_write(client, buf)
43
- # try to bypass the VFS layer if we're all caught up
44
- case rv = client.kgio_trywrite(buf)
45
- when String
44
+ def wbuf_writev(buf)
45
+ @tmpio.kgio_writev(buf)
46
+ buf.inject(0) { |n, s| n += s.size }
47
+ end
48
+
49
+ def wbuf_write(c, buf)
50
+ # try to bypass the VFS layer and write directly to the socket
51
+ # if we're all caught up
52
+ case rv = String === buf ? c.kgio_trywrite(buf) : c.kgio_trywritev(buf)
53
+ when String, Array
46
54
  buf = rv # retry in loop
47
55
  when nil
48
56
  return # yay! hopefully we don't have to buffer again
49
57
  when :wait_writable, :wait_readable
50
- @bypass = false # ugh, continue to buffering to file
51
- end while @bypass
58
+ @busy = rv
59
+ end until @busy
52
60
 
53
61
  @tmpio ||= Yahns::TmpIO.new(@tmpdir)
54
- @sf_count += @tmpio.write(buf)
55
- case rv = client.trysendfile(@tmpio, @sf_offset, @sf_count)
62
+ @sf_count += String === buf ? @tmpio.write(buf) : wbuf_writev(buf)
63
+
64
+ # we spent some time copying to the FS, try to write to
65
+ # the socket again in case some space opened up...
66
+ case rv = c.trysendfile(@tmpio, @sf_offset, @sf_count)
56
67
  when Integer
57
68
  @sf_count -= rv
58
69
  @sf_offset += rv
59
70
  when :wait_writable, :wait_readable
71
+ @busy = rv
60
72
  return rv
61
73
  else
62
74
  raise "BUG: #{rv.nil ? "EOF" : rv.inspect} on tmpio " \
@@ -65,15 +77,19 @@ class Yahns::Wbuf # :nodoc:
65
77
 
66
78
  # we're all caught up, try to prevent dirty data from getting flushed
67
79
  # to disk if we can help it.
68
- @tmpio = @tmpio.close
80
+ wbuf_abort
69
81
  @sf_offset = 0
70
- @bypass = true
82
+ @busy = false
71
83
  nil
72
84
  end
73
85
 
74
86
  # called by last wbuf_flush
75
87
  def wbuf_close(client)
76
- @tmpio = @tmpio.close if @tmpio
88
+ wbuf_abort
77
89
  wbuf_close_common(client)
78
90
  end
91
+
92
+ def wbuf_abort
93
+ @tmpio = @tmpio.close if @tmpio
94
+ end
79
95
  end