Skip to content

Commit

Permalink
Fixes #25293 - Puma support for smart proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
Ido Kanner authored and lzap committed Nov 19, 2018
1 parent 986c95c commit df44aad
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 38 deletions.
11 changes: 11 additions & 0 deletions bundler.d/puma.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
group :puma do
if RUBY_VERSION < '2.3'
# Important!
# The last actual version that supports v2.0.0 up to v2.2.0 is 3.10.0
# Puma version 3.11.0 changed the usage of socket to a feature that is not
# supported by Ruby 2.2.0 and lower, and it causes a crash on TLS!
gem 'puma', '~>3.10', :require => 'puma'
else
gem 'puma', '~>3.12', :require => 'puma'
end
end
5 changes: 5 additions & 0 deletions config/settings.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@
# default values for https_port is 8443
#:https_port: 8443

# http server type configuration
# A string that indicates the server type (possible values: 'webrick', 'puma').
# Default value is 'webrick'
#:http_server_type: 'puma'

# Log configuration
# Uncomment and modify if you want to change the location of the log file or use STDOUT or SYSLOG values
#:log_file: /var/log/foreman-proxy/proxy.log
Expand Down
137 changes: 109 additions & 28 deletions lib/launcher.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
require 'bundler'
require 'proxy/log'
require 'proxy/sd_notify'
require 'proxy/settings'
require 'proxy/signal_handler'
require 'proxy/log_buffer/trace_decorator'
require 'thread'
require 'rack'
require 'webrick'
begin
require 'puma'
require 'rack/handler/puma'
require 'puma_patch'
$HAS_PUMA = true
rescue LoadError
$stderr.puts 'Puma was requested but not installed'
$HAS_PUMA = false
end

module Proxy
class Launcher
Expand All @@ -13,18 +25,24 @@ class Launcher

def initialize(settings = SETTINGS)
@settings = settings
@settings.http_server_type = Proxy::SETTINGS.http_server_type.to_sym
if @settings.http_server_type == :puma && !$HAS_PUMA
logger.warn 'Puma was requested but not installed, falling back to webrick'
@settings.http_server_type = :webrick
end
@servers = []
end

def pid_path
settings.daemon_pid
@settings.daemon_pid
end

def http_enabled?
!settings.http_port.nil?
!@settings.http_port.nil?
end

def https_enabled?
settings.ssl_private_key && settings.ssl_certificate && settings.ssl_ca_file
@settings.ssl_private_key && @settings.ssl_certificate && @settings.ssl_ca_file
end

def plugins
Expand All @@ -47,7 +65,7 @@ def http_app(http_port, plugins = http_plugins)

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

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

if constant
Expand All @@ -87,21 +105,31 @@ def https_app(https_port, plugins = https_plugins)
end
end

{
app_details = {
:app => app,
:server => :webrick,
:server => @settings.http_server_type,
:DoNotListen => true,
:Port => https_port, # only being used to correctly log https port being used
:Logger => ::Proxy::LogBuffer::Decorator.instance,
:ServerSoftware => "foreman-proxy/#{Proxy::VERSION}",
:SSLEnable => true,
:SSLVerifyClient => OpenSSL::SSL::VERIFY_PEER,
:SSLPrivateKey => load_ssl_private_key(settings.ssl_private_key),
:SSLCertificate => load_ssl_certificate(settings.ssl_certificate),
:SSLCACertificateFile => settings.ssl_ca_file,
:SSLCACertificateFile => @settings.ssl_ca_file,
:SSLOptions => ssl_options,
:daemonize => false
}
case @settings.http_server_type
when :webrick
app_details[:SSLPrivateKey] = load_ssl_private_key(@settings.ssl_private_key)
app_details[:SSLCertificate] = load_ssl_certificate(@settings.ssl_certificate)
when :puma
app_details[:SSLArgs] = {
:ca => @settings.ssl_ca_file,
:key => @settings.ssl_private_key,
:cert => @settings.ssl_certificate
}
end
app_details
end

def load_ssl_private_key(path)
Expand Down Expand Up @@ -149,32 +177,85 @@ def write_pid
retry
end

def webrick_server(app, addresses, port)
def add_puma_server(app, addresses, port, conn_type)
# IMPORTANT:
# The following code takes only a single host.
# The current reason for it, is that "run" is blocking, and in order to
# add support for more hosts, additional threads requires to be created
address = addresses.first
address = '0.0.0.0' if address == '*'
if conn_type == :ssl
host = "ssl://#{address}/"
require 'cgi'
query_list = []
app[:SSLArgs].each_pair do |name, value|
query_list << "#{CGI::escape(name.to_s)}=#{CGI::escape(value)}"
end
host = "#{host}?#{query_list.join('&')}"
else
host = address
end
Rack::Handler::Puma.run(app[:app],
Verbose: true,
Port: port,
Host: host
)
end

def add_webrick_server(app, addresses, port)
server = ::WEBrick::HTTPServer.new(app)
addresses.each {|a| server.listen(a, port)}
server.mount "/", Rack::Handler::WEBrick, app[:app]
addresses.each { |a| server.listen(a, port) }
server.mount '/', Rack::Handler::WEBrick, app[:app]
server
end

def add_threaded_server(server_name, conn_type, app, addresses, port)
case server_name
when :webrick
Thread.new do
@servers << add_webrick_server(app, addresses, port).start
end
when :puma
Thread.new do
add_puma_server(app, addresses, port, conn_type)
end
end
end

def launch
raise Exception.new("Both http and https are disabled, unable to start.") unless http_enabled? || https_enabled?

if settings.daemon
if @settings.daemon
check_pid
Process.daemon
write_pid
end

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

http_app = http_app(settings.http_port)
https_app = https_app(settings.https_port)
install_webrick_callback!(http_app, https_app)
http_app = http_app(@settings.http_port)
https_app = https_app(@settings.https_port)
install_http_server_callback!(http_app, https_app)

http_server_name = @settings.http_server_type
https_server_name = @settings.http_server_type
if https_app
t1 = add_threaded_server(https_server_name,
:ssl,
https_app,
@settings.bind_host,
@settings.https_port)
end

t1 = Thread.new { webrick_server(https_app, settings.bind_host, settings.https_port).start } unless https_app.nil?
t2 = Thread.new { webrick_server(http_app, settings.bind_host, settings.http_port).start } unless http_app.nil?
if http_app
t2 = add_threaded_server(http_server_name,
:tcp,
http_app,
@settings.bind_host,
@settings.http_port)
end

Proxy::SignalHandler.install_traps
Proxy::SignalHandler.install_traps(@servers)

(t1 || t2).join
rescue SignalException => e
Expand All @@ -189,19 +270,19 @@ def launch
exit(1)
end

def install_webrick_callback!(*apps)
def install_http_server_callback!(*apps)
apps.compact!

# track how many webrick apps are still starting up
@pending_webrick = apps.size
@pending_webrick_lock = Mutex.new
# track how many apps are still starting up
@pending_server = apps.size
@pending_server_lock = Mutex.new

apps.each do |app|
# add a callback to each server, decrementing the pending counter
app[:StartCallback] = lambda do
@pending_webrick_lock.synchronize do
@pending_webrick -= 1
launched(apps) if @pending_webrick.zero?
@pending_server_lock.synchronize do
@pending_server -= 1
launched(apps) if @pending_server.zero?
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/proxy/settings/global.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module ::Proxy::Settings
class Global < ::OpenStruct
DEFAULT_SETTINGS = {
:settings_directory => Pathname.new(__FILE__).join("..","..","..","..","config","settings.d").expand_path.to_s,
:http_server_type => :webrick,
:https_port => 8443,
:log_file => "/var/log/foreman-proxy/proxy.log",
:file_rolling_keep => 6,
Expand Down
24 changes: 17 additions & 7 deletions lib/proxy/signal_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
class Proxy::SignalHandler
include ::Proxy::Log

def self.install_traps
def self.install_traps(servers)
handler = new
handler.install_ttin_trap unless RUBY_PLATFORM =~ /mingw/
handler.install_int_trap
handler.install_term_trap
handler.install_int_trap(servers)
handler.install_term_trap(servers)
handler.install_usr1_trap unless RUBY_PLATFORM =~ /mingw/
end

Expand All @@ -25,12 +25,22 @@ def install_ttin_trap
end
end

def install_int_trap
trap(:INT) { exit(0) }
def install_int_trap(servers)
trap(:INT) do
servers.each do |server|
server.shutdown
end
exit(0)
end
end

def install_term_trap
trap(:TERM) { exit(0) }
def install_term_trap(servers)
trap(:TERM) do
servers.each do |server|
server.shutdown
end
exit(0)
end
end

def install_usr1_trap
Expand Down
4 changes: 2 additions & 2 deletions test/launcher_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ def test_check_pid_exits_program
FileUtils.rm_f @launcher.pid_path
end

def test_install_webrick_callback
def test_install_http_server_callback
app1 = {app: 1}
app2 = {app: 2}
@launcher.install_webrick_callback!(app1, nil, app2)
@launcher.install_http_server_callback!(app1, nil, app2)
@launcher.expects(:launched).never
app1[:StartCallback].call
@launcher.expects(:launched).with([app1, app2])
Expand Down
7 changes: 6 additions & 1 deletion test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ def launch(protocol: 'https', plugins: [], settings: {})
@t = Thread.new do
launcher = Proxy::Launcher.new(@settings)
app = launcher.public_send("#{protocol}_app", port, plugins)
launcher.webrick_server(app.merge(AccessLog: [Logger.new('/dev/null')]), ['localhost'], port).start
case @settings.http_server_type
when :webrick
launcher.add_webrick_server(app.merge(AccessLog: [Logger.new('/dev/null')]), ['localhost'], port).start
when :puma
launcher.add_puma_server(app.merge(AccessLog: [Logger.new('/dev/null')]), ['localhost'], port).start
end
end
Timeout::timeout(2) do
sleep(0.1) until can_connect?('localhost', @settings.send("#{protocol}_port"))
Expand Down

0 comments on commit df44aad

Please sign in to comment.