From 9afea181455ea43e67fdc48fefbf8814ed73601b Mon Sep 17 00:00:00 2001 From: Lukas Hellebrandt Date: Tue, 18 Aug 2026 16:48:23 +0200 Subject: [PATCH] Fixes #38120 - Support both Sinatra 2/Rack 2 and Sinatra 4/Rack 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow smart-proxy to run on either Sinatra 2 + Rack 2 or Sinatra 4 + Rack 3, depending on what is installed. This enables Debian 13 support (which ships Rack 3) without forcing all platforms to upgrade simultaneously. gemspec: Widen constraints to rack >= 2.0 < 4, sinatra >= 2.0 < 5, add rackup (needed for Rack 3, harmless on Rack 2). launcher.rb: Resolve the WEBrick handler at load time via require/rescue LoadError — Rackup::Handler::WEBrick for Rack 3, Rack::Handler::WEBrick for Rack 2. hsts_middleware.rb: Use unconditional lowercase header key. Rack 3 requires it; Rack 2 does case-insensitive lookups internally so lowercase works for both. registration_api.rb: Add missing helpers ::Proxy::Helpers so logger resolves to Proxy::LogBuffer::Decorator (which implements #exception) instead of Sinatra's built-in logger. Under Sinatra 2 this was masked by Rack::NullLogger swallowing unknown methods; Sinatra 4 uses a real ::Logger that crashes on #exception. smart_proxy_main.rb: Fix Sinatra environment setting from :env (a no-op custom attribute) to :environment (the real Sinatra setting). Without this, Sinatra defaults to development mode, which in Sinatra 4 enables Rack::Protection::HostAuthorization that rejects requests whose Host header isn't localhost — breaking all deployments where Foreman reaches smart-proxy by hostname. Note: Sinatra 4's Rack::Protection::HostAuthorization can restrict accepted Host headers to a configured allow-list, but in production mode it allows all hosts by default. This means we don't yet benefit from the additional security Sinatra 4 offers here. Configuring permitted hosts (e.g. via foremanctl or the installer) should be addressed as follow-up work; for now this is not a regression since Sinatra 2 never had this protection at all. --- lib/launcher.rb | 10 +++++++++- lib/proxy/hsts_middleware.rb | 8 ++++++-- lib/smart_proxy_main.rb | 6 +++++- modules/registration/registration_api.rb | 5 +++++ smart_proxy.gemspec | 5 +++-- 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/launcher.rb b/lib/launcher.rb index 0967485bb..996810599 100644 --- a/lib/launcher.rb +++ b/lib/launcher.rb @@ -1,5 +1,13 @@ require 'openssl' require 'proxy/log' + +begin + require 'rackup/handler/webrick' + WEBRICK_HANDLER = Rackup::Handler::WEBrick +rescue LoadError + require 'rack' + WEBRICK_HANDLER = Rack::Handler::WEBrick +end require 'proxy/settings' require 'proxy/signal_handler' require 'proxy/log_buffer/trace_decorator' @@ -191,7 +199,7 @@ def webrick_server(app, addresses, port) rescue ::OpenSSL::SSL::SSLError => e raise "Invalid tls_ciphers value '#{app[:SSLCiphers]}': #{e.message}" end - server.mount "/", Rack::Handler::WEBrick, app[:app] + server.mount "/", WEBRICK_HANDLER, app[:app] # WEBrick 1.9.x does not support :SSLMinVersion in its config hash, so we # apply min_version= directly on the SSL context after WEBrick creates it. diff --git a/lib/proxy/hsts_middleware.rb b/lib/proxy/hsts_middleware.rb index 4b982deef..1aa0085b6 100644 --- a/lib/proxy/hsts_middleware.rb +++ b/lib/proxy/hsts_middleware.rb @@ -4,14 +4,18 @@ module Proxy # is needed. # https://www.tenable.com/plugins/nessus/142960 class HstsMiddleware + # Lowercase is required by Rack 3 (https://github.com/rack/rack/issues/1592); + # Rack 2 doesn't care about header key case, so this works for both. + HEADER_KEY = 'strict-transport-security'.freeze + def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) - if env['HTTPS'] == 'on' && !headers.include?('Strict-Transport-Security') - headers['Strict-Transport-Security'] = 'max-age=31536000' + if env['HTTPS'] == 'on' && !headers.include?(HEADER_KEY) + headers[HEADER_KEY] = 'max-age=31536000' end [status, headers, body] end diff --git a/lib/smart_proxy_main.rb b/lib/smart_proxy_main.rb index 581a07c6e..14a979111 100644 --- a/lib/smart_proxy_main.rb +++ b/lib/smart_proxy_main.rb @@ -46,7 +46,11 @@ module Proxy ::Sinatra::Base.use ::Proxy::RequestIdMiddleware ::Sinatra::Base.use ::Proxy::LoggerMiddleware ::Sinatra::Base.use ::Proxy::HstsMiddleware - ::Sinatra::Base.set :env, :production + # Without this, Sinatra defaults to :development, which in Sinatra 4 + # enables strict Rack::Protection::HostAuthorization that rejects + # requests whose Host header isn't localhost — breaking all real + # deployments where Foreman reaches smart-proxy by hostname. + ::Sinatra::Base.set :environment, :production ::Sinatra::Base.register ::Sinatra::Authorization require 'root/root' diff --git a/modules/registration/registration_api.rb b/modules/registration/registration_api.rb index a45af152b..6fa43c16d 100644 --- a/modules/registration/registration_api.rb +++ b/modules/registration/registration_api.rb @@ -1,6 +1,11 @@ require 'registration/proxy_request' class Proxy::Registration::Api < ::Sinatra::Base + # Needed so `logger` resolves to Proxy::LogBuffer::Decorator (which implements + # #exception, used in the rescue blocks below) instead of Sinatra's own null + # logger, which is a plain ::Logger with no #exception method as of Sinatra 4. + helpers ::Proxy::Helpers + # Cache for the global registration script (GET /register). # # The script is identical for all hosts sharing the same registration diff --git a/smart_proxy.gemspec b/smart_proxy.gemspec index 4dc05065d..cb6b7b58c 100644 --- a/smart_proxy.gemspec +++ b/smart_proxy.gemspec @@ -17,10 +17,11 @@ Gem::Specification.new do |s| s.add_dependency 'json' s.add_dependency 'logging' s.add_dependency 'ostruct' - s.add_dependency 'rack', '>= 1.3' + s.add_dependency 'rack', '>= 2.0', '< 4' + s.add_dependency 'rackup' s.add_dependency 'rexml', '~> 3.2' s.add_dependency 'sd_notify', '~> 0.1' - s.add_dependency 'sinatra', '~> 2.0' + s.add_dependency 'sinatra', '>= 2.0', '< 5' s.add_dependency 'webrick', '~> 1.0' s.description = <<~EOF Foreman Proxy is used via The Foreman Project, it allows Foreman to manage