From 7acf608c7b10051dea05de9c6986b4868659f5ff Mon Sep 17 00:00:00 2001 From: Ewoud Kohl van Wijngaarden Date: Fri, 2 Oct 2020 22:24:01 +0200 Subject: [PATCH 1/4] Move lib/launcher.rb to lib/proxy/launcher.rb This matches the module layout --- bin/smart-proxy | 1 + lib/{ => proxy}/launcher.rb | 0 lib/smart_proxy_for_testing.rb | 1 - lib/smart_proxy_main.rb | 1 - test/launcher_test.rb | 2 +- 5 files changed, 2 insertions(+), 3 deletions(-) rename lib/{ => proxy}/launcher.rb (100%) diff --git a/bin/smart-proxy b/bin/smart-proxy index 78e6c6282..5c6abb31d 100755 --- a/bin/smart-proxy +++ b/bin/smart-proxy @@ -3,4 +3,5 @@ $LOAD_PATH.unshift(*Dir[File.expand_path('../lib', __dir__), File.expand_path('../modules', __dir__)]) require 'smart_proxy_main' +require 'proxy/launcher' Proxy::Launcher.new.launch diff --git a/lib/launcher.rb b/lib/proxy/launcher.rb similarity index 100% rename from lib/launcher.rb rename to lib/proxy/launcher.rb diff --git a/lib/smart_proxy_for_testing.rb b/lib/smart_proxy_for_testing.rb index b8a8d9333..721753e94 100644 --- a/lib/smart_proxy_for_testing.rb +++ b/lib/smart_proxy_for_testing.rb @@ -21,7 +21,6 @@ require 'proxy/provider' require 'proxy/error' require 'proxy/request' -require 'launcher' require 'sinatra/base' require 'sinatra/authorization' diff --git a/lib/smart_proxy_main.rb b/lib/smart_proxy_main.rb index 6428cda65..b38dbb32e 100644 --- a/lib/smart_proxy_main.rb +++ b/lib/smart_proxy_main.rb @@ -1,7 +1,6 @@ APP_ROOT = "#{__dir__}/.." require 'smart_proxy' -require 'launcher' require 'fileutils' require 'pathname' diff --git a/test/launcher_test.rb b/test/launcher_test.rb index ef9cc7621..d621fa312 100644 --- a/test/launcher_test.rb +++ b/test/launcher_test.rb @@ -1,5 +1,5 @@ require 'test_helper' -require 'launcher' +require 'proxy/launcher' class LauncherTest < Test::Unit::TestCase def setup From c7dc87847df9e55b81ebdb641f95c17f0e38d167 Mon Sep 17 00:00:00 2001 From: Ewoud Kohl van Wijngaarden Date: Fri, 2 Oct 2020 19:34:53 +0200 Subject: [PATCH 2/4] Fixes #25293 - Support Puma To support Puma, this takes the approach of introducing a Rack application that understands the HTTP request scheme. Depending on that, it responds either with the HTTP or HTTPS app. This means all the responsibility of binding to HTTP and HTTPS is shifted to the application server. To use this: bundle exec puma -b tcp://127.0.0.1:8000 -b 'ssl://127.0.0.1:8443?key=config/key.pem&cert=config/cert.pem' Note that this doesn't set the secure ciphers nor protocols. It also requires Puma to be built with OpenSSL extensions to be able to bind on HTTPS. --- config.ru | 8 +++++--- lib/proxy/app.rb | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 lib/proxy/app.rb diff --git a/config.ru b/config.ru index 691ab4671..da9336468 100644 --- a/config.ru +++ b/config.ru @@ -1,5 +1,7 @@ -$LOAD_PATH.unshift *Dir[File.expand_path('lib', __dir__), File.expand_path('modules', __dir__)] +$LOAD_PATH.unshift(*Dir[File.expand_path('lib', __dir__), File.expand_path('modules', __dir__)]) require 'smart_proxy_main' -::Proxy::PluginInitializer.new(::Proxy::Plugins.instance).initialize_plugins -::Proxy::Plugins.instance.select { |p| p[:state] == :running && p[:https_enabled] }.each { |p| instance_eval(p[:class].https_rackup) } +require 'proxy/app' +plugins = ::Proxy::Plugins.instance +::Proxy::PluginInitializer.new(plugins).initialize_plugins +run ::Proxy::App.new(plugins) diff --git a/lib/proxy/app.rb b/lib/proxy/app.rb new file mode 100644 index 000000000..5ca001086 --- /dev/null +++ b/lib/proxy/app.rb @@ -0,0 +1,29 @@ +module Proxy + class App + def initialize(plugins) + @apps = {} + + http_plugins = plugins.select { |p| p[:state] == :running && p[:http_enabled] } + if http_plugins.any? + @apps['http'] = Rack::Builder.new do + http_plugins.each { |p| instance_eval(p[:class].http_rackup) } + end + end + + https_plugins = plugins.select { |p| p[:state] == :running && p[:https_enabled] } + if https_plugins.any? + @apps['https'] = Rack::Builder.new do + https_plugins.each { |p| instance_eval(p[:class].https_rackup) } + end + end + end + + def call(env) + # TODO: Respect X-Forwarded-Proto? + scheme = env['rack.url_scheme'] + app = @apps[scheme] + fail "Unsupported URL scheme #{scheme}" unless app + app.call(env) + end + end +end From 669d4eda8e0e4fd7bdcb9f26c080d45bb0d1adbb Mon Sep 17 00:00:00 2001 From: Ewoud Kohl van Wijngaarden Date: Fri, 2 Oct 2020 21:18:56 +0200 Subject: [PATCH 3/4] Extract webrick launcher to a separate file --- lib/proxy/launcher.rb | 136 ++----------------------------- lib/proxy/launcher/webrick.rb | 148 ++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 128 deletions(-) create mode 100644 lib/proxy/launcher/webrick.rb diff --git a/lib/proxy/launcher.rb b/lib/proxy/launcher.rb index 020968e7e..f7b8a6a03 100644 --- a/lib/proxy/launcher.rb +++ b/lib/proxy/launcher.rb @@ -2,7 +2,6 @@ require 'proxy/settings' require 'proxy/signal_handler' require 'proxy/log_buffer/trace_decorator' -require 'sd_notify' CIPHERS = ['ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES256-GCM-SHA384', 'AES128-GCM-SHA256', 'AES256-GCM-SHA384', 'AES128-SHA256', @@ -14,7 +13,7 @@ class Launcher attr_reader :settings - def initialize(settings = SETTINGS) + def initialize(settings = ::Proxy::SETTINGS) @settings = settings end @@ -31,94 +30,7 @@ def https_enabled? end def plugins - ::Proxy::Plugins.instance.select { |p| p[:state] == :running } - end - - def http_plugins - plugins.select { |p| p[:http_enabled] }.map { |p| p[:class] } - end - - def https_plugins - plugins.select { |p| p[:https_enabled] }.map { |p| p[:class] } - end - - def http_app(http_port, plugins = http_plugins) - return nil unless http_enabled? - app = Rack::Builder.new do - plugins.each { |p| instance_eval(p.http_rackup) } - end - - { - :app => app, - :server => :webrick, - :DoNotListen => true, - :Port => http_port, # only being used to correctly log http port being used - :Logger => ::Proxy::LogBuffer::TraceDecorator.instance, - :AccessLog => [], - :ServerSoftware => "foreman-proxy/#{Proxy::VERSION}", - :daemonize => false, - } - end - - def https_app(https_port, plugins = https_plugins) - unless https_enabled? - logger.warn "Missing SSL setup, https is disabled." - return nil - end - - app = Rack::Builder.new do - plugins.each { |p| instance_eval(p.https_rackup) } - end - - ssl_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] - ssl_options |= OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE if defined?(OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE) - # This is required to disable SSLv3 on Ruby 1.8.7 - ssl_options |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2) - 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) - ssl_options |= OpenSSL::SSL::OP_NO_TLSv1_1 if defined?(OpenSSL::SSL::OP_NO_TLSv1_1) - - Proxy::SETTINGS.tls_disabled_versions&.each do |version| - constant = OpenSSL::SSL.const_get("OP_NO_TLSv#{version.to_s.tr('.', '_')}") rescue nil - - if constant - logger.info "TLSv#{version} will be disabled." - ssl_options |= constant - else - logger.warn "TLSv#{version} was not found." - end - end - - { - :app => app, - :server => :webrick, - :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, - :SSLOptions => ssl_options, - :SSLCiphers => CIPHERS - Proxy::SETTINGS.ssl_disabled_ciphers, - :daemonize => false, - } - end - - def load_ssl_private_key(path) - OpenSSL::PKey::RSA.new(File.read(path)) - rescue Exception => e - logger.error "Unable to load private SSL key. Are the values correct in settings.yml and do permissions allow reading?", e - raise e - end - - def load_ssl_certificate(path) - OpenSSL::X509::Certificate.new(File.read(path)) - rescue Exception => e - logger.error "Unable to load SSL certificate. Are the values correct in settings.yml and do permissions allow reading?", e - raise e + ::Proxy::Plugins.instance end def pid_status @@ -152,11 +64,8 @@ def write_pid retry end - def webrick_server(app, addresses, port) - server = ::WEBrick::HTTPServer.new(app) - addresses.each { |a| server.listen(a, port) } - server.mount "/", Rack::Handler::WEBrick, app[:app] - server + def ciphers + CIPHERS - settings.ssl_disabled_ciphers end def launch @@ -168,18 +77,12 @@ def launch write_pid end - ::Proxy::PluginInitializer.new(::Proxy::Plugins.instance).initialize_plugins + ::Proxy::PluginInitializer.new(plugins).initialize_plugins - http_app = http_app(settings.http_port) - https_app = https_app(settings.https_port) - install_webrick_callback!(http_app, https_app) + require 'proxy/launcher/webrick' + launcher = ::Launcher::Webrick.new(self) - 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? - - Proxy::SignalHandler.install_traps - - (t1 || t2).join + launcher.launch rescue SignalException => e logger.debug("Caught #{e}. Exiting") raise @@ -191,28 +94,5 @@ def launch puts "Errors detected on startup, see log for details. Exiting: #{e}" exit(1) end - - def install_webrick_callback!(*apps) - apps.compact! - - # track how many webrick apps are still starting up - @pending_webrick = apps.size - @pending_webrick_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? - end - end - end - end - - def launched(apps) - logger.info("Smart proxy has launched on #{apps.size} socket(s), waiting for requests") - SdNotify.ready - end end end diff --git a/lib/proxy/launcher/webrick.rb b/lib/proxy/launcher/webrick.rb new file mode 100644 index 000000000..8adc4e349 --- /dev/null +++ b/lib/proxy/launcher/webrick.rb @@ -0,0 +1,148 @@ +require 'sd_notify' + +module Launcher + class Webrick + include ::Proxy::Log + + attr_reader :launcher + + def initialize(launcher) + @launcher = launcher + end + + def launch + http_app = build_http_app + https_app = build_https_app + install_webrick_callback!(http_app, https_app) + + 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? + + Proxy::SignalHandler.install_traps + + (t1 || t2).join + end + + private + + def settings + launcher.settings + end + + def webrick_server(app, addresses, port) + server = ::WEBrick::HTTPServer.new(app) + addresses.each { |a| server.listen(a, port) } + server.mount "/", Rack::Handler::WEBrick, app[:app] + server + end + + def build_http_app + return unless launcher.http_enabled? + + plugins = launcher.plugins.select { |p| p[:state] == :running && p[:http_enabled] } + return unless plugins.any? + + app = Rack::Builder.new do + plugins.each { |p| instance_eval(p[:class].http_rackup) } + end + + { + :app => app, + :server => :webrick, + :DoNotListen => true, + :Port => settings.http_port, # only being used to correctly log http port being used + :Logger => ::Proxy::LogBuffer::TraceDecorator.instance, + :AccessLog => [], + :ServerSoftware => "foreman-proxy/#{Proxy::VERSION}", + :daemonize => false, + } + end + + def build_https_app + unless launcher.https_enabled? + logger.warn "Missing SSL setup, https is disabled." + return + end + + plugins = launcher.plugins.select { |p| p[:state] == :running && p[:https_enabled] } + return unless plugins.any? + + app = Rack::Builder.new do + plugins.each { |p| instance_eval(p[:class].https_rackup) } + end + + ssl_options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] + ssl_options |= OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE if defined?(OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE) + # This is required to disable SSLv3 on Ruby 1.8.7 + ssl_options |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2) + 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) + ssl_options |= OpenSSL::SSL::OP_NO_TLSv1_1 if defined?(OpenSSL::SSL::OP_NO_TLSv1_1) + + settings.tls_disabled_versions&.each do |version| + constant = OpenSSL::SSL.const_get("OP_NO_TLSv#{version.to_s.tr('.', '_')}") rescue nil + + if constant + logger.info "TLSv#{version} will be disabled." + ssl_options |= constant + else + logger.warn "TLSv#{version} was not found." + end + end + + { + :app => app, + :server => :webrick, + :DoNotListen => true, + :Port => settings.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, + :SSLOptions => ssl_options, + :SSLCiphers => launcher.ciphers, + :daemonize => false, + } + end + + def load_ssl_private_key(path) + OpenSSL::PKey::RSA.new(File.read(path)) + rescue Exception => e + logger.error "Unable to load private SSL key. Are the values correct in settings.yml and do permissions allow reading?", e + raise e + end + + def load_ssl_certificate(path) + OpenSSL::X509::Certificate.new(File.read(path)) + rescue Exception => e + logger.error "Unable to load SSL certificate. Are the values correct in settings.yml and do permissions allow reading?", e + raise e + end + + def install_webrick_callback!(*apps) + apps.compact! + + # track how many webrick apps are still starting up + @pending_webrick = apps.size + @pending_webrick_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? + end + end + end + end + + def launched(apps) + logger.info("Smart proxy has launched on #{apps.size} socket(s), waiting for requests") + SdNotify.ready + end + end +end From 8742b814209a8da7f7c0408b6c46b91685a60ff8 Mon Sep 17 00:00:00 2001 From: Ewoud Kohl van Wijngaarden Date: Fri, 2 Oct 2020 22:05:19 +0200 Subject: [PATCH 4/4] Add a Puma launcher --- lib/proxy/launcher.rb | 12 ++++- lib/proxy/launcher/puma.rb | 87 ++++++++++++++++++++++++++++++++++++ lib/proxy/settings/global.rb | 1 + 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 lib/proxy/launcher/puma.rb diff --git a/lib/proxy/launcher.rb b/lib/proxy/launcher.rb index f7b8a6a03..2568d3588 100644 --- a/lib/proxy/launcher.rb +++ b/lib/proxy/launcher.rb @@ -79,8 +79,16 @@ def launch ::Proxy::PluginInitializer.new(plugins).initialize_plugins - require 'proxy/launcher/webrick' - launcher = ::Launcher::Webrick.new(self) + case settings.http_server_type + when 'webrick' + require 'proxy/launcher/webrick' + launcher = ::Launcher::Webrick.new(self) + when 'puma' + require 'proxy/launcher/puma' + launcher = ::Launcher::Puma.new(self) + else + fail "Unknown http_server_type: #{settings.http_server_type}" + end launcher.launch rescue SignalException => e diff --git a/lib/proxy/launcher/puma.rb b/lib/proxy/launcher/puma.rb new file mode 100644 index 000000000..ebe4de403 --- /dev/null +++ b/lib/proxy/launcher/puma.rb @@ -0,0 +1,87 @@ +require 'puma' +require 'puma/configuration' +require 'proxy/app' + +module Launcher + class Puma + attr_reader :launcher + + def initialize(launcher) + @launcher = launcher + end + + def launch + ::Puma::Launcher.new(conf).run + end + + private + + def conf + ::Puma::Configuration.new do |user_config| + user_config.environment('production') + user_config.app(app) + + if launcher.http_enabled? + bind_hosts do |host| + user_config.bind "tcp://#{host}:#{settings.http_port}" + end + end + + if launcher.https_enabled? + ssl_options = { + key: settings.ssl_private_key, + cert: settings.ssl_certificate, + ca: settings.ssl_ca_file, + ssl_cipher_filter: launcher.ciphers.join(':'), + verify_mode: 'peer', + no_tlsv1: true, + no_tlsv1_1: true, + } + + bind_hosts do |host| + user_config.ssl_bind(host, settings.https_port, ssl_options) + end + end + + user_config.on_restart do + ::Proxy::LogBuffer::Decorator.instance.roll_log = true + end + + begin + user_config.plugin('systemd') + rescue ::Puma::UnknownPlugin + end + end + end + + def app + ::Proxy::App.new(launcher.plugins) + end + + def binds + end + + def bind_hosts + settings.bind_host.each do |host| + if host == '*' + yield ipv6_enabled? ? '[::]' : '0.0.0.0' + else + begin + addr = IPAddr.new(host) + yield addr.ipv6? ? "[#{addr}]" : addr.to_s + rescue IPAddr::InvalidAddressError + yield host + end + end + end + end + + def settings + launcher.settings + end + + def ipv6_enabled? + File.exist?('/proc/net/if_inet6') || (RUBY_PLATFORM =~ /cygwin|mswin|mingw|bccwin|wince|emx/) + end + end +end diff --git a/lib/proxy/settings/global.rb b/lib/proxy/settings/global.rb index 6b227b329..a7b4e64a0 100644 --- a/lib/proxy/settings/global.rb +++ b/lib/proxy/settings/global.rb @@ -2,6 +2,7 @@ module ::Proxy::Settings class Global < ::OpenStruct DEFAULT_SETTINGS = { :settings_directory => Pathname.new(__dir__).join("..", "..", "..", "config", "settings.d").expand_path.to_s, + :http_server_type => 'puma', :https_port => 8443, :log_file => "/var/log/foreman-proxy/proxy.log", :file_rolling_keep => 6,