Skip to content

Commit a125bf2

Browse files
author
Ido Kanner
committed
Fixes #25293 - Puma support for smart proxy
1 parent f84bd40 commit a125bf2

File tree

7 files changed

+133
-30
lines changed

7 files changed

+133
-30
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ bundler.d/*.local.rb
1616
pkg/
1717
test/tmp/*
1818
/tmp/
19+
ssl/

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.0', :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: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1+
require 'bundler'
12
require 'proxy/log'
23
require 'proxy/sd_notify'
34
require 'proxy/settings'
45
require 'proxy/signal_handler'
56
require 'thread'
7+
require 'rack'
8+
require 'webrick'
9+
begin
10+
require 'puma'
11+
require 'rack/handler/puma'
12+
require 'puma_patch'
13+
$HAS_PUMA = true
14+
rescue LoadError
15+
$stderr.puts 'Puma was requested but not installed'
16+
$HAS_PUMA = false
17+
end
618

719
module Proxy
820
class Launcher
@@ -12,18 +24,23 @@ class Launcher
1224

1325
def initialize(settings = SETTINGS)
1426
@settings = settings
27+
@settings.http_server_type = Proxy::SETTINGS.http_server_type.to_sym
28+
if @settings.http_server_type == :puma && !$HAS_PUMA
29+
logger.warn 'Puma was requested but not installed, falling back to webrick'
30+
@settings.http_server_type = :webrick
31+
end
1532
end
1633

1734
def pid_path
18-
settings.daemon_pid
35+
@settings.daemon_pid
1936
end
2037

2138
def http_enabled?
22-
!settings.http_port.nil?
39+
!@settings.http_port.nil?
2340
end
2441

2542
def https_enabled?
26-
settings.ssl_private_key && settings.ssl_certificate && settings.ssl_ca_file
43+
@settings.ssl_private_key && @settings.ssl_certificate && @settings.ssl_ca_file
2744
end
2845

2946
def plugins
@@ -46,7 +63,7 @@ def http_app(http_port, plugins = http_plugins)
4663

4764
{
4865
:app => app,
49-
:server => :webrick,
66+
:server => @settings.http_server_type,
5067
:DoNotListen => true,
5168
:Port => http_port, # only being used to correctly log http port being used
5269
:Logger => ::Proxy::LogBuffer::Decorator.instance,
@@ -72,8 +89,8 @@ def https_app(https_port, plugins = https_plugins)
7289
ssl_options |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
7390
ssl_options |= OpenSSL::SSL::OP_NO_TLSv1 if defined?(OpenSSL::SSL::OP_NO_TLSv1)
7491

75-
if Proxy::SETTINGS.tls_disabled_versions
76-
Proxy::SETTINGS.tls_disabled_versions.each do |version|
92+
if @settings.tls_disabled_versions
93+
@settings.tls_disabled_versions.each do |version|
7794
constant = OpenSSL::SSL.const_get("OP_NO_TLSv#{version.to_s.gsub(/\./, '_')}") rescue nil
7895

7996
if constant
@@ -85,21 +102,31 @@ def https_app(https_port, plugins = https_plugins)
85102
end
86103
end
87104

88-
{
105+
app_details = {
89106
:app => app,
90-
:server => :webrick,
107+
:server => @settings.http_server_type,
91108
:DoNotListen => true,
92109
:Port => https_port, # only being used to correctly log https port being used
93110
:Logger => ::Proxy::LogBuffer::Decorator.instance,
94111
:ServerSoftware => "foreman-proxy/#{Proxy::VERSION}",
95112
:SSLEnable => true,
96113
:SSLVerifyClient => OpenSSL::SSL::VERIFY_PEER,
97-
:SSLPrivateKey => load_ssl_private_key(settings.ssl_private_key),
98-
:SSLCertificate => load_ssl_certificate(settings.ssl_certificate),
99-
:SSLCACertificateFile => settings.ssl_ca_file,
114+
:SSLCACertificateFile => @settings.ssl_ca_file,
100115
:SSLOptions => ssl_options,
101116
:daemonize => false
102117
}
118+
case @settings.http_server_type
119+
when :webrick
120+
app_details[:SSLPrivateKey] = load_ssl_private_key(@settings.ssl_private_key)
121+
app_details[:SSLCertificate] = load_ssl_certificate(@settings.ssl_certificate)
122+
when :puma
123+
app_details[:SSLArgs] = {
124+
:ca => @settings.ssl_ca_file,
125+
:key => @settings.ssl_private_key,
126+
:cert => @settings.ssl_certificate
127+
}
128+
end
129+
app_details
103130
end
104131

105132
def load_ssl_private_key(path)
@@ -147,30 +174,83 @@ def write_pid
147174
retry
148175
end
149176

150-
def webrick_server(app, addresses, port)
177+
def add_puma_server(app, addresses, port, conn_type)
178+
# IMPORTANT:
179+
# The following code takes only a single host.
180+
# The current reason for it, is that "run" is blocking, and in order to
181+
# add support for more hosts, additional threads requires to be created
182+
address = addresses.first
183+
address = '0.0.0.0' if address == '*'
184+
if conn_type == :ssl
185+
host = "ssl://#{address}/"
186+
require 'cgi'
187+
query_list = []
188+
app[:SSLArgs].each_pair do |name, value|
189+
query_list << "#{CGI::escape(name.to_s)}=#{CGI::escape(value)}"
190+
end
191+
host = "#{host}?#{query_list.join('&')}"
192+
else
193+
host = address
194+
end
195+
Rack::Handler::Puma.run(app[:app],
196+
Verbose: true,
197+
Port: port,
198+
Host: host
199+
)
200+
end
201+
202+
def add_webrick_server(app, addresses, port)
151203
server = ::WEBrick::HTTPServer.new(app)
152-
addresses.each {|a| server.listen(a, port)}
153-
server.mount "/", Rack::Handler::WEBrick, app[:app]
204+
addresses.each { |a| server.listen(a, port) }
205+
server.mount '/', Rack::Handler::WEBrick, app[:app]
154206
server
155207
end
156208

209+
def add_threaded_server(server_name, conn_type, app, addresses, port)
210+
case server_name
211+
when :webrick
212+
Thread.new do
213+
add_webrick_server(app, addresses, port).start
214+
end
215+
when :puma
216+
Thread.new do
217+
add_puma_server(app, addresses, port, conn_type)
218+
end
219+
end
220+
end
221+
157222
def launch
158223
raise Exception.new("Both http and https are disabled, unable to start.") unless http_enabled? || https_enabled?
159224

160-
if settings.daemon
225+
if @settings.daemon
161226
check_pid
162227
Process.daemon
163228
write_pid
164229
end
165230

166231
::Proxy::PluginInitializer.new(::Proxy::Plugins.instance).initialize_plugins
167232

168-
http_app = http_app(settings.http_port)
169-
https_app = https_app(settings.https_port)
170-
install_webrick_callback!(http_app, https_app)
233+
http_app = http_app(@settings.http_port)
234+
https_app = https_app(@settings.https_port)
235+
install_http_server_callback!(http_app, https_app)
236+
237+
http_server_name = @settings.http_server_type
238+
https_server_name = @settings.http_server_type
239+
if https_app
240+
t1 = add_threaded_server(https_server_name,
241+
:ssl,
242+
https_app,
243+
@settings.bind_host,
244+
@settings.https_port)
245+
end
171246

172-
t1 = Thread.new { webrick_server(https_app, settings.bind_host, settings.https_port).start } unless https_app.nil?
173-
t2 = Thread.new { webrick_server(http_app, settings.bind_host, settings.http_port).start } unless http_app.nil?
247+
if http_app
248+
t2 = add_threaded_server(http_server_name,
249+
:tcp,
250+
http_app,
251+
@settings.bind_host,
252+
@settings.http_port)
253+
end
174254

175255
Proxy::SignalHandler.install_traps
176256

@@ -187,19 +267,19 @@ def launch
187267
exit(1)
188268
end
189269

190-
def install_webrick_callback!(*apps)
270+
def install_http_server_callback!(*apps)
191271
apps.compact!
192272

193-
# track how many webrick apps are still starting up
194-
@pending_webrick = apps.size
195-
@pending_webrick_lock = Mutex.new
273+
# track how many apps are still starting up
274+
@pending_server = apps.size
275+
@pending_server_lock = Mutex.new
196276

197277
apps.each do |app|
198278
# add a callback to each server, decrementing the pending counter
199279
app[:StartCallback] = lambda do
200-
@pending_webrick_lock.synchronize do
201-
@pending_webrick -= 1
202-
launched(apps) if @pending_webrick.zero?
280+
@pending_server_lock.synchronize do
281+
@pending_server -= 1
282+
launched(apps) if @pending_server.zero?
203283
end
204284
end
205285
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
:log_level => "INFO",

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)