Commits

Mike Hix committed 71aba2c Draft

Add SSL Redirection Plugin

Comments (0)

Files changed (1)

lib/strelka/app/sslredirection.rb

+# -*- ruby -*-
+# vim: set nosta noet ts=4 sw=4:
+#encoding: utf-8
+
+require 'strelka'
+require 'strelka/app'
+
+# Pluggable control for controlling a client's use of SSL when making requests
+# of a handler.
+module Strelka::App::SSLRedirection
+    extend Strelka::Plugin,
+           Strelka::MethodUtilities,
+           Loggability,
+           Configurability
+    include Strelka::Constants
+
+    # Loggability API -- setu logging under the 'strelka' log host
+    log_to :strelka
+
+    # Strelka Plugins API -- load order
+    run_before :auth,
+        :filters,
+        :negotiation,
+        :parameters,
+        :routing,
+        :sessions,
+        :templating
+
+    # Configurability API -- Specify the section of the config for this plugin.
+    config_key :sslredirection
+
+    # Configuration defualts for this plugin.
+    CONFIG_DEFAULTS = {
+        :lenient => true,
+    }
+
+    ##
+    # The Array of apps that have had the auth plugin installed; this is used to
+    # set up the AuthProvider when the configuration loads later.
+    singleton_attr_accessor :extended_apps
+    self.extended_apps = []
+
+    ### Configurability API -- configure the Auth plugin via the 'auth' section
+    ### of the unified config.
+    def self::configure( config=nil )
+        if config && config[:lenient]
+            self.extended_apps.each { |app| app.lenient = config[:lenient] }
+        end
+    end
+
+    # Class methods to add to app classes that enable Redirection
+    module ClassMethods
+
+        ### Extension callback -- register objects that are extended so when the
+        ### auth plugin is configured, it can set the configured auto provider.
+        def self::extended( obj )
+            super
+            Strelka::App::SSLRedirection.extended_apps << obj
+        end
+
+        @lenient = nil
+
+        # Whether or not this plugin redirects requests made over SSL when SSL
+        # wasn't required.
+        attr_accessor :lenient
+
+        @positive_ssl_criteria = {}
+        @negative_ssl_criteria = {}
+
+        ##
+        # Hashes of criteria for redirection, keyed by request pattern
+        attr_reader :positive_ssl_criteria, :negative_ssl_criteria
+
+        ### Returns +true+ if there are any criteria for determining whether or
+        ### not a redirection should be performed.
+        def has_ssl_criteria?
+            return self.has_positive_ssl_criteria? ||
+                   self.has_negative_ssl_criteria?
+        end
+
+        ### Returns +true+ if the app has been setup so that only some methods
+        ### require redirection.
+        def has_positive_ssl_criteria?
+            return !self.positive_ssl_criteria.empty?
+        end
+
+        ### Retures +true+ if the app has been setup so that all methods but
+        ### ones that match declared criteria require redirection.
+        def has_negative_ssl_criteria?
+            return !self.negative_ssl_criteria.empty?
+        end
+
+        ### :call-seq:
+        ###   require_ssl_for( string )
+        ###   require_ssl_for( regexp )
+        ###   require_ssl_for { |request| ... }
+        ###   require_ssl_for( string ) { |request| ... }
+        ###   require_ssl_for( regexp ) { |request, matchdata| ... }
+        ###
+        ### Constrain ssl redirection to apply only to requests whose
+        ### {#app_path}[rdoc-ref:Strelka::HTTPRequest#app_path] matches
+        ### the given +string+ or +regexp+, and/or for which the given +block+ returns
+        ### a true value. +regexp+ patterns are matched as-is, and +string+ patterns are
+        ### matched exactly via <tt>==</tt> after stripping leading and trailing '/' characters
+        ### from both it and the #app_path.
+
+        def require_ssl_for( *criteria, &block )
+            if self.has_negative_ssl_criteria?
+                raise ScriptError,
+                    "defining both positive and negative SSL redirection criteria is unsupported."
+            end
+
+            criteria << nil if criteria.empty?
+            block ||= Proc.new { true }
+
+            criteria.each do |pattern|
+                pattern.gsub!( %r{^/+|/+$}, '' ) if pattern.respond_to?( :gsub! )
+                self.log.debug "  adding require_ssl for %p" % [ pattern ]
+                self.positive_ssl_criteria[ pattern ] = block
+            end
+        end
+
+        ### :call-seq:
+        ###   no_ssl_for( string )
+        ###   no_ssl_for( regexp )
+        ###   no_ssl_for { |request| ... }
+        ###   no_ssl_for( string ) { |request| ... }
+        ###   no_ssl_for( regexp ) { |request, matchdata| ... }
+        ###
+        ### Constrain ssl redirection to apply to requests *except* those whose
+        ### {#app_path}[rdoc-ref:Strelka::HTTPRequest#app_path] matches
+        ### the given +string+ or +regexp+, and/or for which the given +block+ returns
+        ### a true value.
+        def no_ssl_for( *criteria, &block )
+            if self.has_positive_ssl_criteria?
+                raise ScriptError,
+                    "defining both positive and negative SSL redirection criteria is unsupported."
+            end
+
+            criteria << nil if criteria.empty?
+            block ||= Proc.new { true }
+
+            criteria.each do |pattern|
+                pattern.gsub!( %r{^/+|/+$}, '' ) if pattern.respond_to?( :gsub! )
+                self.log.debug "  adding no_ssl for %p" % [ pattern ]
+                self.negative_ssl_criteria[ pattern ] = block
+            end
+        end
+
+    end
+
+    ######
+    public
+    ######
+
+    ### Plugin API -- Main extension point.
+    def handle_request( request, &block )
+        self.log.debug "[:sslredirection] Redirecting the request if necessary."
+        self.redirect( request )
+        super
+    end
+
+    ### Check the request's scheme, redirecting if necessary before passing the
+    ### request on.
+    def redirect( request )
+        used = ssl_used?( request )
+        required = ssl_required?( request )
+
+        return if required == used
+        return if used and self.class.lenient
+
+        body = "SSL is %s." % [ used ? 'prohibited' : 'required' ]
+        location = request.uri.to_s.sub( /^https?/, used ? 'http' : 'https' )
+
+        self.log.debug 'Redirecting: %s -> %s' % [ request.uri, location ]
+        finish_with( HTTP::REDIRECT, body, { :location => location } )
+    end
+
+    ### Returns +true+ if the request was made using SSL.
+    def ssl_used?( request )
+        # Hopefully someday, mongrel2 will expose this information to handlers.
+        return request.headers[:url_scheme] == 'https'
+    end
+
+    ### Returns +true+ if the given criteria require SSL for this requiest.
+    def ssl_required?( request )
+        self.log.debug "Checking to see if ssl is required for app_path: %p" %
+            [ request.app_path ]
+
+        if self.class.has_positive_ssl_criteria?
+            criteria = self.class.positive_ssl_criteria
+            self.log.debug "    checking %d positive ssl requirement criteria" %
+                [ criteria.length ]
+            return true if criteria.any? do |pattern, block|
+                self.request_matches_criteria( request, pattern, &block )
+            end
+            return false
+
+        elsif self.class.has_negative_ssl_criteria?
+            criteria = self.class.negative_ssl_criteria
+            self.log.debug "    checking %d negative ssl requirement criteria" %
+                [ criteria.length ]
+            return false if criteria.any? do |pattern, block|
+                self.request_matches_criteria( request, pattern, &block )
+            end
+            return true
+
+        else
+            self.log.debug "    no ssl requirement criteria, default to no requirement."
+            return false
+        end
+    end
+
+    #########
+    protected
+    #########
+
+    ### Returns +true+ if there are positive redirection criteria and the
+    ### +request+ matches at least one of them.
+    def request_matches_criteria( request, pattern )
+        self.log.debug "Testing request '%s %s' against pattern: %p" %
+            [ request.verb, request.app_path, pattern ]
+
+        case pattern
+        when nil
+            self.log.debug "  no pattern; calling the block"
+            return yield( request )
+
+        when Regexp
+            self.log.debug "  checking app_path with regexp: %p" % [ pattern ]
+            matchdata = pattern.match( request.app_path ) or return false
+            self.log.debug "  matched: calling the block"
+            return yield( request, matchdata )
+
+        when String
+            self.log.debug "  checking app_path: %p" % [ pattern ]
+            request.app_path.gsub( %r{^/+|/+$}, '' ) == pattern or return false
+            self.log.debug "  matched: calling the block"
+            return yield( request )
+
+        else
+            raise ScriptError,
+                  "don't know how to match a request with a %p" %
+                    [ pattern.class ]
+        end
+    end
+
+
+end # module Strelka::App::SSLRedirection