lib/roda/plugins/redirect_http_to_https.rb



# 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