Skip to content

Commit df44aad

Browse files
Ido Kannerlzap
authored andcommitted
Fixes #25293 - Puma support for smart proxy
1 parent 986c95c commit df44aad

File tree

7 files changed

+151
-38
lines changed

7 files changed

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

820
module Proxy
921
class Launcher
@@ -13,18 +25,24 @@ class Launcher
1325

1426
def initialize(settings = SETTINGS)
1527
@settings = settings
28+
@settings.http_server_type = Proxy::SETTINGS.http_server_type.to_sym
29+
if @settings.http_server_type == :puma && !$HAS_PUMA
30+
logger.warn 'Puma was requested but not installed, falling back to webrick'
31+
@settings.http_server_type = :webrick
32+
end
33+
@servers = []
1634
end
1735

1836
def pid_path
19-
settings.daemon_pid
37+
@settings.daemon_pid
2038
end
2139

2240
def http_enabled?
23-
!settings.http_port.nil?
41+
!@settings.http_port.nil?
2442
end
2543

2644
def https_enabled?
27-
settings.ssl_private_key && settings.ssl_certificate && settings.ssl_ca_file
45+
@settings.ssl_private_key && @settings.ssl_certificate && @settings.ssl_ca_file
2846
end
2947

3048
def plugins
@@ -47,7 +65,7 @@ def http_app(http_port, plugins = http_plugins)
4765

4866
{
4967
:app => app,
50-
:server => :webrick,
68+
:server => @settings.http_server_type,
5169
:DoNotListen => true,
5270
:Port => http_port, # only being used to correctly log http port being used
5371
:Logger => ::Proxy::LogBuffer::TraceDecorator.instance,
@@ -74,8 +92,8 @@ def https_app(https_port, plugins = https_plugins)
7492
ssl_options |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
7593
ssl_options |= OpenSSL::SSL::OP_NO_TLSv1 if defined?(OpenSSL::SSL::OP_NO_TLSv1)
7694

77-
if Proxy::SETTINGS.tls_disabled_versions
78-
Proxy::SETTINGS.tls_disabled_versions.each do |version|
95+
if @settings.tls_disabled_versions
96+
@settings.tls_disabled_versions.each do |version|
7997
constant = OpenSSL::SSL.const_get("OP_NO_TLSv#{version.to_s.gsub(/\./, '_')}") rescue nil
8098

8199
if constant
@@ -87,21 +105,31 @@ def https_app(https_port, plugins = https_plugins)
87105
end
88106
end
89107

90-
{
108+
app_details = {
91109
:app => app,
92-
:server => :webrick,
110+
:server => @settings.http_server_type,
93111
:DoNotListen => true,
94112
:Port => https_port, # only being used to correctly log https port being used
95113
:Logger => ::Proxy::LogBuffer::Decorator.instance,
96114
:ServerSoftware => "foreman-proxy/#{Proxy::VERSION}",
97115
:SSLEnable => true,
98116
: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,
117+
:SSLCACertificateFile => @settings.ssl_ca_file,
102118
:SSLOptions => ssl_options,
103119
:daemonize => false
104120
}
121+
case @settings.http_server_type
122+
when :webrick
123+
app_details[:SSLPrivateKey] = load_ssl_private_key(@settings.ssl_private_key)
124+
app_details[:SSLCertificate] = load_ssl_certificate(@settings.ssl_certificate)
125+
when :puma
126+
app_details[:SSLArgs] = {
127+
:ca => @settings.ssl_ca_file,
128+
:key => @settings.ssl_private_key,
129+
:cert => @settings.ssl_certificate
130+
}
131+
end
132+
app_details
105133
end
106134

107135
def load_ssl_private_key(path)
@@ -149,32 +177,85 @@ def write_pid
149177
retry
150178
end
151179

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

212+
def add_threaded_server(server_name, conn_type, app, addresses, port)
213+
case server_name
214+
when :webrick
215+
Thread.new do
216+
@servers << add_webrick_server(app, addresses, port).start
217+
end
218+
when :puma
219+
Thread.new do
220+
add_puma_server(app, addresses, port, conn_type)
221+
end
222+
end
223+
end
224+
159225
def launch
160226
raise Exception.new("Both http and https are disabled, unable to start.") unless http_enabled? || https_enabled?
161227

162-
if settings.daemon
228+
if @settings.daemon
163229
check_pid
164230
Process.daemon
165231
write_pid
166232
end
167233

168234
::Proxy::PluginInitializer.new(::Proxy::Plugins.instance).initialize_plugins
169235

170-
http_app = http_app(settings.http_port)
171-
https_app = https_app(settings.https_port)
172-
install_webrick_callback!(http_app, https_app)
236+
http_app = http_app(@settings.http_port)
237+
https_app = https_app(@settings.https_port)
238+
install_http_server_callback!(http_app, https_app)
239+
240+
http_server_name = @settings.http_server_type
241+
https_server_name = @settings.http_server_type
242+
if https_app
243+
t1 = add_threaded_server(https_server_name,
244+
:ssl,
245+
https_app,
246+
@settings.bind_host,
247+
@settings.https_port)
248+
end
173249

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?
250+
if http_app
251+
t2 = add_threaded_server(http_server_name,
252+
:tcp,
253+
http_app,
254+
@settings.bind_host,
255+
@settings.http_port)
256+
end
176257

177-
Proxy::SignalHandler.install_traps
258+
Proxy::SignalHandler.install_traps(@servers)
178259

179260
(t1 || t2).join
180261
rescue SignalException => e
@@ -189,19 +270,19 @@ def launch
189270
exit(1)
190271
end
191272

192-
def install_webrick_callback!(*apps)
273+
def install_http_server_callback!(*apps)
193274
apps.compact!
194275

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

199280
apps.each do |app|
200281
# add a callback to each server, decrementing the pending counter
201282
app[:StartCallback] = lambda do
202-
@pending_webrick_lock.synchronize do
203-
@pending_webrick -= 1
204-
launched(apps) if @pending_webrick.zero?
283+
@pending_server_lock.synchronize do
284+
@pending_server -= 1
285+
launched(apps) if @pending_server.zero?
205286
end
206287
end
207288
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: 17 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,22 @@ 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
32+
end
33+
exit(0)
34+
end
3035
end
3136

32-
def install_term_trap
33-
trap(:TERM) { exit(0) }
37+
def install_term_trap(servers)
38+
trap(:TERM) do
39+
servers.each do |server|
40+
server.shutdown
41+
end
42+
exit(0)
43+
end
3444
end
3545

3646
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)