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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
| | # -*- encoding: binary -*-
# Copyright (C) 2017 all contributors <yahns-public@yhbt.net>
# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
# frozen_string_literal: true
require_relative 'rack'
require_relative 'proxy_pass'
# Basically, a lazy way to setup ProxyPass to hand off some (or all)
# requests to any HTTP server backend (e.g. varnish, etc)
class Yahns::RackProxy < Yahns::Rack # :nodoc:
# the key is the destination returned by the top-level config.ru
# and the value is a splattable array for spawning another process
# via Process.exec
# {
# # [ key, backend URL, ] => %w(splattable array for Process.exec),
# [:pass, 'http://127.0.0.1:9292/' ] => %w(rackup /path/to/config.ru)
# [:lsock, 'unix:/path/to/sock' ] => %w(bleh -l /path/to/sock ...)
#
# # Users of Ruby 2.3+ can shorten their config when
# # running systemd-aware daemons which will bind to
# # a random TCP port:
# :pri => %w(blah -c conf.rb config.ru),
# :alt => %w(blah -c /path/to/alt.conf.rb alt.ru),
# :psgi => %w(blah foo.psgi),
# ...
# }
# By default, proxy all requests by using the :pass return value
# Users can selectively process requests for non-buggy code in
# the core yahns processes.
PROXY_ALL = lambda { |env| :pass } # :nodoc:
attr_reader :submasters # :nodoc: see http_context.rb /submasters
# every declaration of this in yahns_config is unique:
def self.instance_key(*args)
args.object_id
end
def initialize(mapping = { :pass => %w(true) }, ru = PROXY_ALL, opts = {})
sd_env = {
'LISTEN_FDS' => '1',
'LISTEN_PID' => lambda { "#$$" }
}
@submasters = []
case mapping
when Hash # multiple HTTP backends running different commands
# nothing to do { key: splattable array for Process.spawn }
when Array # only one backend
mapping = { :pass => mapping }
else
raise ArgumentError, "#{mapping.inspect} must be an Array or Hash"
end
@proxy_pass_map = {}
mapping.each do |key, cmd|
case key
when Array
key, addr, ppopts = key
ppopts ||= {}
when Symbol # OK
ppopts = {}
else
raise ArgumentError, "#{key.inspect} is not a symbol"
end
Array === cmd or raise ArgumentError,
"#{cmd.inspect} must be a splattable array for Process.exec"
@proxy_pass_map[key] and raise ArgumentError,
"#{key.inspect} may not be repeated in mapping"
cmd = cmd.dup
if addr
env = {}
rdr = {}
else
if RUBY_VERSION.to_f < 2.3 && @submasters.empty? # only warn once
warn "Ruby < 2.3 may crash when emulating systemd to pass FDs\n",
" http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/69895\n"
end
# nope, no UNIXServer support, maybe not worth it to deal
# with FS perms in containers.
# TODO: try TCP Fast Open (Linux)
srv = random_tcp_listener(ppopts)
addr = srv.addr
addr = "http://#{addr[3]}:#{addr[1]}/"
env = sd_env
rdr = { 3 => srv }
end
# never pass YAHNS_FD to children, they do not inherit what we use
# for SIGUSR2 upgrades
env['YAHNS_FD'] = nil
case cmd[0]
when Hash
cmd[0] = cmd[0].merge(env)
else
cmd.unshift(env)
end
rdr[:close_others] = true
case cmd[-1]
when Hash
cmd[-1] = cmd[-1].merge(rdr)
else
cmd << rdr
end
@submasters << Yahns::Submaster.new(key, cmd)
@proxy_pass_map[key] = Yahns::ProxyPass.new(addr, ppopts)
end
super(ru, opts) # Yahns::Rack#initialize
end
def build_app!
super # Yahns::Rack#build_app!
proxy_app = @app
# wrap the (possibly-)user-supplied app
@app = lambda do |env|
res = proxy_app.call(env)
# standard Rack responses may be handled in yahns proper:
Array === res and return res
# the response is :pass or another Symbol, not a proper Rack response!
# shove the env over to the appropriate Yahns::ProxyPass which
# talks to a backend HTTP process:
ppass = @proxy_pass_map[res] and return ppass.call(env)
# oops, user screwed up :<
logger = env['rack.logger'] and
logger.error("bad response from user-supplied proxy: #{res.inspect}")
[ 500, [ %w(Content-Type text/plain) ], [] ]
end
end
def random_tcp_listener(opts) # TODO: should we support options?
srv = TCPServer.new('127.0.0.1', 0) # 0: bind random port
srv.close_on_exec = true
srv.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 1)
srv.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, 1)
# Deferring accepts slows down core yahns, but it's useful for
# less-sophisticated upstream (backend) servers:
Socket.const_defined?(:TCP_DEFER_ACCEPT) and
srv.setsockopt(:IPPROTO_TCP, :TCP_DEFER_ACCEPT, 1)
srv.listen(1024)
srv
end
end
# register ourselves
Yahns::Config::APP_CLASS[:rack_proxy] = Yahns::RackProxy
|