1 # -*- encoding: binary -*-
6 require 'kcar' # https://yhbt.net/kcar/ -- gem install kcar
8 # This is lightly tested and has an unstable configuration interface.
9 # ***** Do not rely on anything under the ReverseProxy namespace! *****
11 # A reverse proxy implementation for \Rainbows! It is a Rack application
12 # compatible and optimized for most \Rainbows! concurrency models.
14 # It makes HTTP/1.0 connections without keepalive to backends, so
15 # it is only recommended for proxying to upstreams on the same LAN
16 # or machine. It can proxy to TCP hosts as well as UNIX domain sockets.
18 # Currently it only does simple round-robin balancing and does not
19 # know to retry connections from failed backends.
21 # Buffering-behavior is currently dependent on the concurrency model selected:
23 # Fully-buffered (uploads and response bodies):
24 # Coolio, EventMachine, NeverBlock, CoolioThreadSpawn, CoolioThreadPool
25 # If you're proxying to Unicorn, fully-buffered is the way to go.
27 # Buffered input only (uploads, but not response bodies):
28 # ThreadSpawn, ThreadPool, FiberSpawn, FiberPool, CoolioFiberSpawn
30 # It is not recommended to use Base, WriterThreadSpawn or WriterThreadPool
31 # to host this application. However, you may proxy to a backend running
32 # one of these concurrency models with a fully-buffering concurrency model.
34 # See the {example config}[link:examples/reverse_proxy.ru] for a sample
37 # TODO: Revactor support
38 # TODO: Support HTTP trailers
39 # TODO: optional streaming input for synchronous
40 # TODO: error handling
42 # WARNING! this is only lightly tested and has no automated tests, yet!
43 class Rainbows::ReverseProxy
44 autoload :MultiThread, 'rainbows/reverse_proxy/multi_thread'
45 autoload :Synchronous, 'rainbows/reverse_proxy/synchronous'
46 autoload :Coolio, 'rainbows/reverse_proxy/coolio'
47 autoload :EventMachine, 'rainbows/reverse_proxy/event_machine'
48 autoload :EvClient, 'rainbows/reverse_proxy/ev_client'
50 E502 = [ 502, [ %w(Content-Length 0), %w(Content-Type text/plain) ], [] ]
54 upstreams = opts[:upstreams]
56 upstreams.each do |url|
57 url, cfg = *url if Array === url
58 if url =~ %r{\Ahttp://}
60 host = uri.host =~ %r{\A\[([a-fA-F0-9:]+)\]\z} ? $1 : uri.host
61 sockaddr = Socket.sockaddr_in(uri.port, host)
63 path = url.gsub(%r{\Aunix:}, "") # nginx compat
64 %r{\A~} =~ path and path = File.expand_path(path)
65 sockaddr = Socket.sockaddr_un(path)
67 ((cfg && cfg[:weight]) || 1).times { @upstreams << sockaddr }
72 # detects the concurrency model at first run and replaces itself
75 case model = env["rainbows.model"]
76 when :EventMachine, :NeverBlock
78 when :Coolio, :CoolioThreadPool, :CoolioThreadSpawn
80 when :RevFiberSpawn, :Rev, :RevThreadPool, :RevThreadSpawn
81 warn "#{model} is not *well* supported with #{self.class}"
82 warn "Switch to #{model.to_s.gsub(/Rev/, 'Coolio')}!"
85 warn "Revactor is not *well* supported with #{self.class} yet"
87 when :FiberSpawn, :FiberPool, :CoolioFiberSpawn
89 Synchronous::UpstreamSocket.
90 __send__(:include, Rainbows::Fiber::IO::Methods)
91 when :WriterThreadSpawn, :WriterThreadPool
92 warn "#{model} is not recommended for use with #{self.class}"
97 extend(MultiThread) if env["rack.multithread"]
100 @lock.synchronize {} # wait for the first locker to finish
105 # returns request headers for sending to the upstream as a string
106 def build_headers(env, input)
107 remote_addr = env['REMOTE_ADDR']
108 xff = env['HTTP_X_FORWARDED_FOR']
109 xff = xff ? "#{xff},#{remote_addr}" : remote_addr
110 req = "#{env['REQUEST_METHOD']} #{env['REQUEST_URI']} HTTP/1.0\r\n" \
111 "Connection: close\r\n" \
112 "X-Forwarded-For: #{xff}\r\n"
113 env.each do |key, value|
114 %r{\AHTTP_(\w+)\z} =~ key or next
116 next if %r{\A(?:VERSION|CONNECTION|KEEP_ALIVE|X_FORWARDED_FOR)\z}x =~ key
117 key.tr!('_'.freeze, '-'.freeze)
118 req << "#{key}: #{value}\r\n"
120 input and req << (input.respond_to?(:size) ?
121 "Content-Length: #{input.size}\r\n" :
122 "Transfer-Encoding: chunked\r\n".freeze)
126 def pick_upstream(env) # +env+ is reserved for future expansion
128 @upstreams[@nr %= @upstreams.size]
131 def prepare_input!(env)
132 if cl = env['CONTENT_LENGTH']
135 elsif %r{\Achunked\z}i =~ env.delete('HTTP_TRANSFER_ENCODING')
136 # do people use multiple transfer-encodings?
141 input = env['rack.input']
142 if input.respond_to?(:rewind)
143 if input.respond_to?(:size)
144 input.size # TeeInput-specific behavior
147 return SizedInput.new(input, size)
150 tmp = size && size < 0x4000 ? StringIO.new("") : Unicorn::TmpIO.new
151 each_block(input) { |x| tmp.syswrite(x) }
159 def initialize(input, n)
163 while input.read(16384, buf)
168 @input, @size = input, n
176 class UpstreamSocket < Kgio::Socket
177 alias readpartial kgio_read!