1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
| | # -*- encoding: binary -*-
# Copyright (C) 2013-2016 all contributors <yahns-public@yhbt.net>
# License: GPL-3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
# frozen_string_literal: true
require 'socket'
require 'rack/request'
require 'timeout'
require_relative 'proxy_http_response'
require_relative 'req_res'
class Yahns::ProxyPass # :nodoc:
def initialize(dest, opts = {})
case dest
when %r{\Aunix:([^:]+)(?::(/.*))?\z}
path = $2
@sockaddr = Socket.sockaddr_un($1)
when %r{\Ahttp://([^/]+)(/.*)?\z}
path = $2
host, port = $1.split(':')
@sockaddr = Socket.sockaddr_in(port || 80, host)
else
raise ArgumentError, "destination must be an HTTP URL or unix: path"
end
@response_headers = opts[:response_headers] || {}
# It's wrong to send the backend Server tag through. Let users say
# { "Server => "yahns" } if they want to advertise for us, but don't
# advertise by default (for security)
@response_headers['Server'] ||= :ignore
init_path_vars(path)
end
def init_path_vars(path)
path ||= '$fullpath'
# methods from Rack::Request we want:
allow = %w(fullpath host_with_port host port url path)
want = path.scan(/\$(\w+)/).flatten! || []
diff = want - allow
diff.empty? or
raise ArgumentError, "vars not allowed: #{diff.uniq.join(' ')}"
# kill leading slash just in case...
@path = path.gsub(%r{\A/(\$(?:fullpath|path))}, '\1')
end
def call(env)
# 3-way handshake for TCP backends while we generate the request header
rr = Yahns::ReqRes.start(@sockaddr)
c = env['rack.hijack'].call
req = Rack::Request.new(env)
req = @path.gsub(/\$(\w+)/) { req.__send__($1) }
# start the connection asynchronously and early so TCP can do a
case ver = env['HTTP_VERSION']
when 'HTTP/1.1' # leave alone, response may be chunked
else # no chunking for HTTP/1.0 and HTTP/0.9
ver = 'HTTP/1.0'.freeze
end
addr = env['REMOTE_ADDR']
xff = env['HTTP_X_FORWARDED_FOR']
xff = xff =~ /\S/ ? "#{xff}, #{addr}" : addr
req = "#{env['REQUEST_METHOD']} #{req} #{ver}\r\n" \
"X-Forwarded-Proto: #{env['rack.url_scheme']}\r\n" \
"X-Forwarded-For: #{xff}\r\n".dup
# pass most HTTP_* headers through as-is
chunked = false
env.each do |key, val|
%r{\AHTTP_(\w+)\z} =~ key or next
key = $1
# trailers are folded into the header, so do not send the Trailer:
# header in the request
next if /\A(?:VERSION|CONNECTION|KEEP_ALIVE|X_FORWARDED_FOR|TRAILER)/ =~
key
'TRANSFER_ENCODING'.freeze == key && val =~ /\bchunked\b/i and
chunked = true
key.tr!('_'.freeze, '-'.freeze)
req << "#{key}: #{val}\r\n"
end
# special cases which Rack does not prefix:
ctype = env["CONTENT_TYPE"] and req << "Content-Type: #{ctype}\r\n"
clen = env["CONTENT_LENGTH"] and req << "Content-Length: #{clen}\r\n"
input = chunked || (clen && clen.to_i > 0) ? env['rack.input'] : nil
env['yahns.proxy_pass.response_headers'] = @response_headers
# finally, prepare to emit the headers
rr.req_start(c, req << "\r\n".freeze, input, chunked)
# this probably breaks fewer middlewares than returning whatever else...
[ 500, [], [] ]
rescue => e
Yahns::Log.exception(env['rack.logger'], 'proxy_pass', e)
[ 502, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
end
end
|