diff --git a/CHANGELOG b/CHANGELOG index 1ab29e06..fd7558e0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ = master +* Add hsts plugin for setting Strict-Transport-Security header (jeremyevans) + * Remove documentation from the gem to reduce gem size by 25% (jeremyevans) = 3.83.0 (2024-08-12) diff --git a/lib/roda/plugins/hsts.rb b/lib/roda/plugins/hsts.rb new file mode 100644 index 00000000..2693b921 --- /dev/null +++ b/lib/roda/plugins/hsts.rb @@ -0,0 +1,35 @@ +# frozen-string-literal: true + +# +class Roda + module RodaPlugins + # The hsts plugin allows for easily configuring an appropriate + # Strict-Transport-Security response header for the application: + # + # plugin :hsts + # # Strict-Transport-Security: max-age=63072000; includeSubDomains + # + # plugin :hsts, preload: true + # # Strict-Transport-Security: max-age=63072000; includeSubDomains; preload + # + # plugin :hsts, max_age: 31536000, subdomains: false + # # Strict-Transport-Security: max-age=31536000 + module Hsts + # Ensure default_headers plugin is loaded first + def self.load_dependencies(app, opts=OPTS) + app.plugin :default_headers + end + + # Configure the Strict-Transport-Security header. Options: + # :max_age :: Set max-age in seconds (default is 63072000, two years) + # :preload :: Set preload, so the domain can be included in HSTS preload lists + # :subdomains :: Set to false to not set includeSubDomains. By default, + # includeSubDomains is set to enforce HTTPS for subdomains. + def self.configure(app, opts=OPTS) + app.plugin :default_headers, RodaResponseHeaders::STRICT_TRANSPORT_SECURITY => "max-age=#{opts[:max_age]||63072000}#{'; includeSubDomains' unless opts[:subdomains] == false}#{'; preload' if opts[:preload]}".freeze + end + end + + register_plugin(:hsts, Hsts) + end +end diff --git a/lib/roda/response.rb b/lib/roda/response.rb index 46c60c4b..1df12102 100644 --- a/lib/roda/response.rb +++ b/lib/roda/response.rb @@ -15,7 +15,7 @@ module RodaResponseHeaders %w'Allow Cache-Control Content-Disposition Content-Encoding Content-Length Content-Security-Policy Content-Security-Policy-Report-Only Content-Type ETag Expires Last-Modified Link Location Set-Cookie Transfer-Encoding Vary - Permissions-Policy Permissions-Policy-Report-Only'. + Permissions-Policy Permissions-Policy-Report-Only Strict-Transport-Security'. each do |value| value = value.downcase if downcase const_set(value.gsub('-', '_').upcase!.to_sym, value.freeze) diff --git a/spec/plugin/hsts_spec.rb b/spec/plugin/hsts_spec.rb new file mode 100644 index 00000000..e87d7762 --- /dev/null +++ b/spec/plugin/hsts_spec.rb @@ -0,0 +1,27 @@ +require_relative "../spec_helper" + +describe "default_headers plugin" do + def app(opts={}) + super(:bare) do + plugin :hsts, opts + route do |r| + '' + end + end + end + + it "sets appropriate headers for the response" do + app + req[1][RodaResponseHeaders::STRICT_TRANSPORT_SECURITY].must_equal "max-age=63072000; includeSubDomains" + end + + it "supports :preload option" do + app(preload: true) + req[1][RodaResponseHeaders::STRICT_TRANSPORT_SECURITY].must_equal "max-age=63072000; includeSubDomains; preload" + end + + it "supports subdomains: false option" do + app(subdomains: false) + req[1][RodaResponseHeaders::STRICT_TRANSPORT_SECURITY].must_equal "max-age=63072000" + end +end diff --git a/www/pages/documentation.erb b/www/pages/documentation.erb index 2c6f7a44..d4cc9e1a 100644 --- a/www/pages/documentation.erb +++ b/www/pages/documentation.erb @@ -108,6 +108,7 @@