Skip to content

Commit

Permalink
Add redirect_http_to_https plugin, helping to ensure future requests …
Browse files Browse the repository at this point in the history
…from the browser are submitted via HTTPS
  • Loading branch information
jeremyevans committed Oct 18, 2023
1 parent f9e5a3f commit b5da06a
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
= master

* Add redirect_http_to_https plugin, helping to ensure future requests from the browser are submitted via HTTPS (jeremyevans)

= 3.73.0 (2023-10-13)

* Support :next_if_not_found option for middleware plugin (jeremyevans) (#334)
Expand Down
99 changes: 99 additions & 0 deletions lib/roda/plugins/redirect_http_to_https.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# frozen-string-literal: true

#
class Roda
module RodaPlugins
# The redirect_http_to_https plugin exposes a +redirect_http_to_https+
# request method that redirects HTTP requests to HTTPS, helping to ensure
# that future requests by the same browser will be submitted securely.
#
# You should use this plugin if you have an application that can receive
# requests using both HTTP and HTTPS, and you want to make sure that all
# or a subset of routes are only handled for HTTPS requests.
#
# The reason this exposes a request method is so that you can choose where
# in your routing tree to do the redirection:
#
# route do |r|
# # routes available via both HTTP and HTTPS
# r.redirect_http_to_https
# # routes available only via HTTPS
# end
#
# If you want to redirect to HTTPS for all routes in the routing tree, you
# can have this as the very first method call in the routing tree. Note that
# in Roda it is possible to handle routing before the normal routing tree
# using before hooks. The static_routing and heartbeat plugins use this
# feature. If you would like to handle routes before the normal routing tree,
# you can setup a before hook:
#
# plugin :hooks
#
# before do
# request.redirect_http_to_https
# end
module RedirectHttpToHttps
status_map = Hash.new(307)
status_map['GET'] = status_map['HEAD'] = 301
status_map.freeze
DEFAULTS = {:status_map => status_map}.freeze
private_constant :DEFAULTS

# Configures redirection from HTTP to HTTPS. Available options:
#
# :body :: The body used in the redirect. If not set, uses an empty body.
# :headers :: Any additional headers used in the redirect response. By default,
# no additional headers are set, the only header used is the Location header.
# :host :: The host to redirect to. If not set, redirects to the same host as the HTTP
# requested to. It is highly recommended that you set this if requests with
# arbitrary Host headers can be submitted to the application.
# :port :: The port to use in the redirect. By default, will not set an explicit port,
# so that it will implicitly use the HTTPS default port of 443.
# :status_map :: A hash mapping request methods to response status codes. By default,
# uses a hash that redirects GET and HEAD requests with a 301 status,
# and other request methods with a 307 status.
def self.configure(app, opts=OPTS)
previous = app.opts[:redirect_http_to_https] || DEFAULTS
opts = app.opts[:redirect_http_to_https] = previous.merge(opts)
opts[:port_string] = opts[:port] ? ":#{opts[:port]}".freeze : "".freeze
opts[:prefix] = opts[:host] ? "https://#{opts[:host]}#{opts[:port_string]}".freeze : nil
opts.freeze
end

module RequestMethods
# Redirect HTTP requests to HTTPS. While this doesn't secure the
# current request, it makes it more likely that the browser will submit
# future requests securely via HTTPS.
def redirect_http_to_https
return if ssl?

opts = roda_class.opts[:redirect_http_to_https]

res = response

if body = opts[:body]
res.write(body)
end

if headers = opts[:headers]
res.headers.merge!(headers)
end

path = if prefix = opts[:prefix]
prefix + fullpath
else
"https://#{host}#{opts[:port_string]}#{fullpath}"
end

unless status = opts[:status_map][@env['REQUEST_METHOD']]
raise RodaError, "redirect_http_to_https :status_map provided does not support #{@env['REQUEST_METHOD']}"
end

redirect(path, status)
end
end
end

register_plugin(:redirect_http_to_https, RedirectHttpToHttps)
end
end
95 changes: 95 additions & 0 deletions spec/plugin/redirect_http_to_https_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
require_relative "../spec_helper"

describe "redirect_http_to_https plugin" do
before do
app(:redirect_http_to_https) do |r|
r.get 'a' do
"a-#{r.ssl?}"
end

r.redirect_http_to_https

"x-#{r.ssl?}"
end
end

it "should not redirect before call to r.redirect_http_to_https" do
body('/a').must_equal 'a-false'
body('/a', 'HTTPS'=>'on').must_equal 'a-true'
end

it "r.redirect_http_to_https redirects HTTP requests to HTTP" do
s, h, b = req('/b', 'HTTP_HOST'=>'foo.com')
s.must_equal 301
h[RodaResponseHeaders::LOCATION].must_equal 'https://foo.com/b'
b.must_be_empty
body('/b', 'HTTPS'=>'on').must_equal 'x-true'
end

it "uses 301 for HEAD redirects by default" do
status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'HEAD').must_equal 301
end

it "uses 307 for POST redirects by default" do
status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'POST').must_equal 307
end

it "includes query string when redirecting" do
header(RodaResponseHeaders::LOCATION, '/b', 'HTTP_HOST'=>'foo.com', 'QUERY_STRING'=>'foo=bar').must_equal 'https://foo.com/b?foo=bar'
end

it "supports :body option" do
@app.plugin :redirect_http_to_https, :body=>'RTHS'
s, h, b = req('/b', 'HTTP_HOST'=>'foo.com')
s.must_equal 301
h[RodaResponseHeaders::LOCATION].must_equal 'https://foo.com/b'
b.must_equal ['RTHS']
end

it "supports :headers option" do
@app.plugin :redirect_http_to_https, :headers=>{'foo'=>'bar'}
s, h, b = req('/b', 'HTTP_HOST'=>'foo.com')
s.must_equal 301
h[RodaResponseHeaders::LOCATION].must_equal 'https://foo.com/b'
h['foo'].must_equal 'bar'
b.must_be_empty
end

it "supports :host option" do
@app.plugin :redirect_http_to_https, :host=>'bar.foo.com'
s, h, b = req('/b', 'HTTP_HOST'=>'foo.com')
s.must_equal 301
h[RodaResponseHeaders::LOCATION].must_equal 'https://bar.foo.com/b'
b.must_be_empty
end

it "supports :port option" do
@app.plugin :redirect_http_to_https, :port=>444
s, h, b = req('/b', 'HTTP_HOST'=>'foo.com')
s.must_equal 301
h[RodaResponseHeaders::LOCATION].must_equal 'https://foo.com:444/b'
b.must_be_empty
end

it "supports :host and :port options together" do
@app.plugin :redirect_http_to_https, :host=>'bar.foo.com', :port=>444
s, h, b = req('/b', 'HTTP_HOST'=>'foo.com')
s.must_equal 301
h[RodaResponseHeaders::LOCATION].must_equal 'https://bar.foo.com:444/b'
b.must_be_empty
end

it "supports :status_map option" do
map = Hash.new(302)
map['GET'] = 301
@app.plugin :redirect_http_to_https, :status_map=>map
status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'GET').must_equal 301
status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'HEAD').must_equal 302
end

it "raise for :status_map that does not handle request mthod" do
@app.plugin :redirect_http_to_https, :status_map=>{'GET'=>302}
status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'GET').must_equal 302
proc{status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'HEAD')}.must_raise Roda::RodaError
end
end
1 change: 1 addition & 0 deletions www/pages/documentation.erb
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
<li><a href="rdoc/classes/Roda/RodaPlugins/ModuleInclude.html">module_include</a>: Adds request_module and response_module class methods for adding modules/methods to request/response classes.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/PlainHashResponseHeaders.html">plain_hash_response_headers</a>: Uses plain hashes for response headers on Rack 3, for much better performance.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/R.html">r</a>: Adds r method for accessing the request, useful when r local variable is not in scope.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/RedirectHttpToHttps.html">redirect_http_to_https</a>: Adds request method to redirect HTTP requests to the same location using HTTPS.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/RequestAref.html">request_aref</a>: Adds configurable handling for [] and []= request methods.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/RequestHeaders.html">request_headers</a>: Adds a headers method to the request object, for easier access to request headers.</li>
<li><a href="rdoc/classes/Roda/RodaPlugins/ResponseRequest.html">response_request</a>: Gives response object access to request object.</li>
Expand Down

0 comments on commit b5da06a

Please sign in to comment.