Skip to content

Commit 4294ddf

Browse files
Ido Kannerlzap
authored andcommitted
Fixes #25293 - Puma support for smart proxy
1 parent 4285fc0 commit 4294ddf

File tree

8 files changed

+159
-40
lines changed

8 files changed

+159
-40
lines changed

bundler.d/puma.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
group :puma do
2+
if RUBY_VERSION < '2.3'
3+
# Important!
4+
# The last actual version that supports v2.0.0 up to v2.2.0 is 3.10.0
5+
# Puma version 3.11.0 changed the usage of socket to a feature that is not
6+
# supported by Ruby 2.2.0 and lower, and it causes a crash on TLS!
7+
gem 'puma', '~>3.10', :require => 'puma'
8+
else
9+
gem 'puma', '~>3.12', :require => 'puma'
10+
end
11+
end

config/settings.yml.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@
5858
# default values for https_port is 8443
5959
#:https_port: 8443
6060

61+
# http server type configuration
62+
# A string that indicates the server type (possible values: 'webrick', 'puma').
63+
# Default value is 'webrick'
64+
#:http_server_type: 'puma'
65+
6166
# Log configuration
6267
# Uncomment and modify if you want to change the location of the log file or use STDOUT or SYSLOG values
6368
#:log_file: /var/log/foreman-proxy/proxy.log

lib/launcher.rb

Lines changed: 110 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
require 'proxy/signal_handler'
55
require 'proxy/log_buffer/trace_decorator'
66
require 'thread'
7+
require 'rack'
8+
require 'webrick'
79

810
module Proxy
911
class Launcher
@@ -13,18 +15,30 @@ class Launcher
1315

1416
def initialize(settings = SETTINGS)
1517
@settings = settings
18+
@settings.http_server_type = Proxy::SETTINGS.http_server_type.to_sym
19+
if @settings.http_server_type == :puma
20+
begin
21+
require 'puma'
22+
require 'rack/handler/puma'
23+
require 'puma-patch'
24+
rescue LoadError
25+
logger.warn 'Puma was requested but not installed, falling back to webrick'
26+
@settings.http_server_type = :webrick
27+
end
28+
end
29+
@servers = []
1630
end
1731

1832
def pid_path
19-
settings.daemon_pid
33+
@settings.daemon_pid
2034
end
2135

2236
def http_enabled?
23-
!settings.http_port.nil?
37+
!@settings.http_port.nil?
2438
end
2539

2640
def https_enabled?
27-
settings.ssl_private_key && settings.ssl_certificate && settings.ssl_ca_file
41+
@settings.ssl_private_key && @settings.ssl_certificate && @settings.ssl_ca_file
2842
end
2943

3044
def plugins
@@ -47,7 +61,7 @@ def http_app(http_port, plugins = http_plugins)
4761

4862
{
4963
:app => app,
50-
:server => :webrick,
64+
:server => @settings.http_server_type,
5165
:DoNotListen => true,
5266
:Port => http_port, # only being used to correctly log http port being used
5367
:Logger => ::Proxy::LogBuffer::TraceDecorator.instance,
@@ -74,8 +88,8 @@ def https_app(https_port, plugins = https_plugins)
7488
ssl_options |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
7589
ssl_options |= OpenSSL::SSL::OP_NO_TLSv1 if defined?(OpenSSL::SSL::OP_NO_TLSv1)
7690

77-
if Proxy::SETTINGS.tls_disabled_versions
78-
Proxy::SETTINGS.tls_disabled_versions.each do |version|
91+
if @settings.tls_disabled_versions
92+
@settings.tls_disabled_versions.each do |version|
7993
constant = OpenSSL::SSL.const_get("OP_NO_TLSv#{version.to_s.tr('.', '_')}") rescue nil
8094

8195
if constant
@@ -87,21 +101,31 @@ def https_app(https_port, plugins = https_plugins)
87101
end
88102
end
89103

90-
{
104+
app_details = {
91105
:app => app,
92-
:server => :webrick,
106+
:server => @settings.http_server_type,
93107
:DoNotListen => true,
94108
:Port => https_port, # only being used to correctly log https port being used
95109
:Logger => ::Proxy::LogBuffer::Decorator.instance,
96110
:ServerSoftware => "foreman-proxy/#{Proxy::VERSION}",
97111
:SSLEnable => true,
98112
:SSLVerifyClient => OpenSSL::SSL::VERIFY_PEER,
99-
:SSLPrivateKey => load_ssl_private_key(settings.ssl_private_key),
100-
:SSLCertificate => load_ssl_certificate(settings.ssl_certificate),
101-
:SSLCACertificateFile => settings.ssl_ca_file,
113+
:SSLCACertificateFile => @settings.ssl_ca_file,
102114
:SSLOptions => ssl_options,
103115
:daemonize => false
104116
}
117+
case @settings.http_server_type
118+
when :webrick
119+
app_details[:SSLPrivateKey] = load_ssl_private_key(@settings.ssl_private_key)
120+
app_details[:SSLCertificate] = load_ssl_certificate(@settings.ssl_certificate)
121+
when :puma
122+
app_details[:SSLArgs] = {
123+
:ca => @settings.ssl_ca_file,
124+
:key => @settings.ssl_private_key,
125+
:cert => @settings.ssl_certificate
126+
}
127+
end
128+
app_details
105129
end
106130

107131
def load_ssl_private_key(path)
@@ -149,34 +173,90 @@ def write_pid
149173
retry
150174
end
151175

152-
def webrick_server(app, addresses, port)
176+
def add_puma_server(app, address, port, conn_type)
177+
logger.debug "Launching Puma listener at #{address} port #{port}"
178+
if conn_type == :ssl
179+
require 'cgi'
180+
query_list = app[:SSLArgs].to_a.map do |x|
181+
"#{CGI::escape(x[0].to_s)}=#{CGI::escape(x[1])}"
182+
end
183+
host = "ssl://#{address}/?#{query_list.join('&')}"
184+
else
185+
host = address
186+
end
187+
logger.debug "Host URL: #{host}"
188+
# the following lines are from lib/rack/handler/puma.rb#run
189+
options = {Verbose: true, Port: port, Host: host}
190+
conf = Rack::Handler::Puma.config(app[:app], options)
191+
events = ::Puma::Events.new(::Proxy::LogBuffer::Decorator.instance, ::Proxy::LogBuffer::Decorator.instance)
192+
launcher = ::Puma::Launcher.new(conf, :events => events)
193+
@servers << launcher
194+
launcher.run
195+
end
196+
197+
def add_webrick_server(app, addresses, port)
153198
server = ::WEBrick::HTTPServer.new(app)
154-
addresses.each {|a| server.listen(a, port)}
155-
server.mount "/", Rack::Handler::WEBrick, app[:app]
199+
addresses.each do |address|
200+
logger.debug "Launching Webrick listener at #{address} port #{port}"
201+
server.listen(address, port)
202+
end
203+
server.mount '/', Rack::Handler::WEBrick, app[:app]
156204
server
157205
end
158206

207+
def add_threaded_server(server_name, conn_type, app, addresses, port)
208+
result = []
209+
case server_name
210+
when :webrick
211+
result << Thread.new do
212+
@servers << add_webrick_server(app, addresses, port).start
213+
end
214+
when :puma
215+
addresses.map{|a| a == '*' ? ['0.0.0.0', '[::1]'] : a}.flatten.each do |address|
216+
result << Thread.new do
217+
add_puma_server(app, address, port, conn_type)
218+
end
219+
end
220+
end
221+
result
222+
end
223+
159224
def launch
160225
raise Exception.new("Both http and https are disabled, unable to start.") unless http_enabled? || https_enabled?
161226

162-
if settings.daemon
227+
if @settings.daemon
163228
check_pid
164229
Process.daemon
165230
write_pid
166231
end
167232

168233
::Proxy::PluginInitializer.new(::Proxy::Plugins.instance).initialize_plugins
169234

170-
http_app = http_app(settings.http_port)
171-
https_app = https_app(settings.https_port)
172-
install_webrick_callback!(http_app, https_app)
173-
174-
t1 = Thread.new { webrick_server(https_app, settings.bind_host, settings.https_port).start } unless https_app.nil?
175-
t2 = Thread.new { webrick_server(http_app, settings.bind_host, settings.http_port).start } unless http_app.nil?
235+
http_app = http_app(@settings.http_port)
236+
https_app = https_app(@settings.https_port)
237+
install_http_server_callback!(http_app, https_app)
238+
239+
http_server_name = @settings.http_server_type
240+
https_server_name = @settings.http_server_type
241+
threads = []
242+
if https_app
243+
threads += add_threaded_server(https_server_name,
244+
:ssl,
245+
https_app,
246+
@settings.bind_host,
247+
@settings.https_port)
248+
end
176249

177-
Proxy::SignalHandler.install_traps
250+
if http_app
251+
threads += add_threaded_server(http_server_name,
252+
:tcp,
253+
http_app,
254+
@settings.bind_host,
255+
@settings.http_port)
256+
end
178257

179-
(t1 || t2).join
258+
Proxy::SignalHandler.install_traps(@servers)
259+
threads.each(&:join)
180260
rescue SignalException => e
181261
logger.debug("Caught #{e}. Exiting")
182262
raise
@@ -189,19 +269,19 @@ def launch
189269
exit(1)
190270
end
191271

192-
def install_webrick_callback!(*apps)
272+
def install_http_server_callback!(*apps)
193273
apps.compact!
194274

195-
# track how many webrick apps are still starting up
196-
@pending_webrick = apps.size
197-
@pending_webrick_lock = Mutex.new
275+
# track how many apps are still starting up
276+
@pending_server = apps.size
277+
@pending_server_lock = Mutex.new
198278

199279
apps.each do |app|
200280
# add a callback to each server, decrementing the pending counter
201281
app[:StartCallback] = lambda do
202-
@pending_webrick_lock.synchronize do
203-
@pending_webrick -= 1
204-
launched(apps) if @pending_webrick.zero?
282+
@pending_server_lock.synchronize do
283+
@pending_server -= 1
284+
launched(apps) if @pending_server.zero?
205285
end
206286
end
207287
end

lib/proxy/log_buffer/decorator.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ def exception(context_message, exception, options = {})
120120
end
121121
end
122122

123+
# for Puma Event compatibility
124+
def sync=(_); end
125+
alias_method :log, :info
126+
alias_method :puts, :info
127+
123128
def method_missing(symbol, *args);
124129
@logger.send(symbol, *args)
125130
end

lib/proxy/settings/global.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module ::Proxy::Settings
22
class Global < ::OpenStruct
33
DEFAULT_SETTINGS = {
44
:settings_directory => Pathname.new(__FILE__).join("..","..","..","..","config","settings.d").expand_path.to_s,
5+
:http_server_type => :webrick,
56
:https_port => 8443,
67
:log_file => "/var/log/foreman-proxy/proxy.log",
78
:file_rolling_keep => 6,

lib/proxy/signal_handler.rb

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
class Proxy::SignalHandler
44
include ::Proxy::Log
55

6-
def self.install_traps
6+
def self.install_traps(servers)
77
handler = new
88
handler.install_ttin_trap unless RUBY_PLATFORM =~ /mingw/
9-
handler.install_int_trap
10-
handler.install_term_trap
9+
handler.install_int_trap(servers)
10+
handler.install_term_trap(servers)
1111
handler.install_usr1_trap unless RUBY_PLATFORM =~ /mingw/
1212
end
1313

@@ -25,12 +25,24 @@ def install_ttin_trap
2525
end
2626
end
2727

28-
def install_int_trap
29-
trap(:INT) { exit(0) }
28+
def install_int_trap(servers)
29+
trap(:INT) do
30+
servers.each do |server|
31+
server.shutdown if server.respond_to?(:shutdown)
32+
server.stop if server.respond_to?(:stop)
33+
end
34+
exit(0)
35+
end
3036
end
3137

32-
def install_term_trap
33-
trap(:TERM) { exit(0) }
38+
def install_term_trap(servers)
39+
trap(:TERM) do
40+
servers.each do |server|
41+
server.shutdown if server.respond_to?(:shutdown)
42+
server.stop if server.respond_to?(:stop)
43+
end
44+
exit(0)
45+
end
3446
end
3547

3648
def install_usr1_trap

test/launcher_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ def test_check_pid_exits_program
4040
FileUtils.rm_f @launcher.pid_path
4141
end
4242

43-
def test_install_webrick_callback
43+
def test_install_http_server_callback
4444
app1 = {app: 1}
4545
app2 = {app: 2}
46-
@launcher.install_webrick_callback!(app1, nil, app2)
46+
@launcher.install_http_server_callback!(app1, nil, app2)
4747
@launcher.expects(:launched).never
4848
app1[:StartCallback].call
4949
@launcher.expects(:launched).with([app1, app2])

test/test_helper.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ def launch(protocol: 'https', plugins: [], settings: {})
4343
@t = Thread.new do
4444
launcher = Proxy::Launcher.new(@settings)
4545
app = launcher.public_send("#{protocol}_app", port, plugins)
46-
launcher.webrick_server(app.merge(AccessLog: [Logger.new('/dev/null')]), ['localhost'], port).start
46+
case @settings.http_server_type
47+
when :webrick
48+
launcher.add_webrick_server(app.merge(AccessLog: [Logger.new('/dev/null')]), ['localhost'], port).start
49+
when :puma
50+
launcher.add_puma_server(app.merge(AccessLog: [Logger.new('/dev/null')]), ['localhost'], port).start
51+
end
4752
end
4853
Timeout::timeout(2) do
4954
sleep(0.1) until can_connect?('localhost', @settings.send("#{protocol}_port"))

0 commit comments

Comments
 (0)