Skip to content

Commit

Permalink
implement method+path combination constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
wyattisimo committed May 8, 2013
1 parent eebb2c1 commit 10594b6
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 8 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,36 @@ config.middleware.use Rack::SslEnforcer, :except_methods => ['GET', 'HEAD']
Note: The `:hosts` constraint takes precedence over the `:path` constraint. Please see the tests for examples.


### Method + Path combination constraints

You can enforce SSL connections only for certain HTTP method and path combinations with `:only_methods_with_paths`,
or prevent enforcement of SSL connections for certain HTTP method and path combinations with `:except_methods_with_paths`.
The combination constraint must be a `Hash` with methods as keys and paths as values.
Method constraints can be a `Symbol`, a `String` or an array of `Symbol` or `String`.
Path constraints can be a `String`, a `Regex` or an array of `String` or `Regex`.

Examples:

```ruby
# Enforce SSL on all requests except HEAD requests to anything under /users or /widgets
config.middleware.use Rack::SslEnforcer, :except_methods_with_paths => {'HEAD' => ['/users', '/widgets']}

# Enforce SSL only on POST and PUT requests to anything under /admin
config.middleware.use Rack::SslEnforcer, :only_methods_with_paths => {['POST', 'PUT'] => '/admin'}

# Enforce SSL only on POST, PUT, and PATCH requests to anything under /admin
# and POST, PUT, and PATCH requests to any path matching `/users/`
config.middleware.use Rack::SslEnforcer, :only_methods_with_paths => {[:post, :put, :patch] => ['/admin', /users/]}

# Multiple combination constraints... you get the idea
config.middleware.use Rack::SslEnforcer, :only_methods_with_paths => {
:post => ['/admin', /users/],
[:put, :patch] => /users/,
:get => '/secrets'
}
```


### Environment constraints

You can enforce SSL connections only for certain environments with `:only_environments` or prevent certain environments from being forced to SSL with `:except_environments`.
Expand Down
37 changes: 30 additions & 7 deletions lib/rack/ssl-enforcer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ module Rack
class SslEnforcer

CONSTRAINTS_BY_TYPE = {
:hosts => [:only_hosts, :except_hosts],
:path => [:only, :except],
:methods => [:only_methods, :except_methods],
:environments => [:only_environments, :except_environments]
:hosts => [:only_hosts, :except_hosts],
:path => [:only, :except],
:methods => [:only_methods, :except_methods],
:methods_with_paths => [:only_methods_with_paths, :except_methods_with_paths],
:environments => [:only_environments, :except_environments]
}

# Warning: If you set the option force_secure_cookies to false, make sure that your cookies
Expand Down Expand Up @@ -119,9 +120,31 @@ def enforce_ssl_for?(keys)
true
else
provided_keys.all? do |key|
rules = [@options[key]].flatten.compact
rules.send([:except_hosts, :except_environments, :except].include?(key) ? :all? : :any?) do |rule|
SslEnforcerConstraint.new(key, rule, @request).matches?
constraint_type = key.to_s[0, key.to_s.index('_') || key.to_s.length]

if key.to_s =~ /methods_with_paths/
# Match method and path constraints separately, then combine the results.
@options[key].send(constraint_type == 'except' ? :all? : :any?) do |combo_rule|

method_rules = [combo_rule[0]].flatten.compact
method_constraint = "#{constraint_type}_methods".to_sym
method_match = method_rules.send(constraint_type == 'except' ? :all? : :any?) do |method_rule|
SslEnforcerConstraint.new(method_constraint, method_rule, @request).matches?
end

path_rules = [combo_rule[1]].flatten.compact
path_constraint = constraint_type.to_sym
path_match = path_rules.send(constraint_type == 'except' ? :all? : :any?) do |path_rule|
SslEnforcerConstraint.new(path_constraint, path_rule, @request).matches?
end

constraint_type == 'except' ? method_match || path_match : method_match && path_match
end
else
rules = [@options[key]].flatten.compact
rules.send(constraint_type == 'except' ? :all? : :any?) do |rule|
SslEnforcerConstraint.new(key, rule, @request).matches?
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/rack/ssl-enforcer/constraint.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class SslEnforcerConstraint
def initialize(name, rule, request)
@name = name
@rule = name =~ /methods$/ && (rule.is_a?(String) || rule.is_a?(Symbol)) ? rule.to_s.upcase : rule
@rule = name.to_s =~ /methods$/ && (rule.is_a?(String) || rule.is_a?(Symbol)) ? rule.to_s.upcase : rule
@request = request
end

Expand Down
92 changes: 92 additions & 0 deletions test/rack-ssl-enforcer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,98 @@ class TestRackSslEnforcer < Test::Unit::TestCase
end
end

context ':only_methods_with_paths' do
setup { mock_app :only_methods_with_paths => {
[:post, :put] => '/admin',
:get => ['/secrets', /users/]
} }

should 'redirect to HTTPS for POST /admin/account' do
post 'http://www.example.org/admin/account'
assert_equal 301, last_response.status
assert_equal 'https://www.example.org/admin/account', last_response.location
end

should 'redirect to HTTPS for PUT /admin/account' do
put 'http://www.example.org/admin/account'
assert_equal 301, last_response.status
assert_equal 'https://www.example.org/admin/account', last_response.location
end

should 'redirect to HTTPS for GET /secrets' do
get 'http://www.example.org/secrets'
assert_equal 301, last_response.status
assert_equal 'https://www.example.org/secrets', last_response.location
end

should 'redirect to HTTPS for GET /foo/users/bar' do
get 'http://www.example.org/foo/users/bar'
assert_equal 301, last_response.status
assert_equal 'https://www.example.org/foo/users/bar', last_response.location
end

should 'not redirect for GET /admin/account' do
get 'http://www.example.org/admin/account'
assert_equal 200, last_response.status
assert_equal 'Hello world!', last_response.body
end

should 'not redirect for POST /secrets' do
post 'http://www.example.org/secrets'
assert_equal 200, last_response.status
assert_equal 'Hello world!', last_response.body
end

should 'not redirect for POST /foo/users/bar' do
post 'http://www.example.org/foo/users/bar'
assert_equal 200, last_response.status
assert_equal 'Hello world!', last_response.body
end
end

context ':except_methods_with_paths' do
setup { mock_app :except_methods_with_paths => {
[:post, :get] => '/public',
:get => ['/admin', /users/]
} }

should 'not redirect for POST /public/stuff' do
post 'http://www.example.org/public/stuff'
assert_equal 200, last_response.status
assert_equal 'Hello world!', last_response.body
end

should 'not redirect for GET /public/stuff' do
get 'http://www.example.org/public/stuff'
assert_equal 200, last_response.status
assert_equal 'Hello world!', last_response.body
end

should 'not redirect for GET /admin/account' do
get 'http://www.example.org/admin/account'
assert_equal 200, last_response.status
assert_equal 'Hello world!', last_response.body
end

should 'not redirect for GET /foo/users/bar' do
get 'http://www.example.org/foo/users/bar'
assert_equal 200, last_response.status
assert_equal 'Hello world!', last_response.body
end

should 'redirect to HTTPS for POST /foobar' do
post 'http://www.example.org/foobar'
assert_equal 301, last_response.status
assert_equal 'https://www.example.org/foobar', last_response.location
end

should 'redirect to HTTPS for GET /foobar' do
get 'http://www.example.org/foobar'
assert_equal 301, last_response.status
assert_equal 'https://www.example.org/foobar', last_response.location
end
end

context 'complex example' do
setup { mock_app :only => '/cart', :ignore => %r{/assets}, :strict => true }

Expand Down

0 comments on commit 10594b6

Please sign in to comment.