diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9cced4a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +rvm: + - 1.8.7 + - 1.9.2 + - 1.9.3 + - ruby-head + - ree + - rbx-18mode + - rbx-19mode + - jruby +branches: + only: + - master +# notifications: +# recipients: +# - thibaud@thibaud.me +# - rymai@rymai.me diff --git a/Guardfile b/Guardfile index ef6fb34..14ae14e 100644 --- a/Guardfile +++ b/Guardfile @@ -1,8 +1,5 @@ -# A sample Guardfile -# More info at http://github.com/guard/guard#readme - -guard 'test' do - watch(%r{^lib/(.*)\.rb}) { "test/rack-ssl-enforcer_test.rb" } +guard :test do + watch(%r{^lib/(.*)\.rb}) { "test" } watch('test/helper.rb') { "test" } watch(%r{^test/(.*)_test\.rb}) end diff --git a/LICENSE b/LICENSE index 2a68fe6..d703009 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009 Tobias Matthies +Copyright (c) 2009-2012 Tobias Matthies Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md new file mode 100644 index 0000000..a3d7a6d --- /dev/null +++ b/README.md @@ -0,0 +1,187 @@ +# Rack::SslEnforcer + +Rack::SslEnforcer is a simple Rack middleware to enforce SSL connections. As of Version 0.2.0, Rack::SslEnforcer marks +Cookies as secure by default (HSTS must be set manually). + +Tested against Ruby 1.8.7, 1.9.2, 1.9.3, ruby-head, REE and the latest versions of Rubinius & JRuby. + +## Installation + +The simplest way to install Rack::SslEnforcer is to use [Bundler](http://gembundler.com/). + +Add Rack::SslEnforcer to your `Gemfile`: + +```ruby + gem 'rack-ssl-enforcer' +``` + +## Basic Usage + +If you don't use Bundler, be sure to require Rack::SslEnforcer manually before actually using the middleware: + +```ruby + require 'rack/ssl-enforcer' + use Rack::SslEnforcer +``` + +To use Rack::SslEnforcer in your Rails application, add the following line to your application config file (`config/application.rb` for Rails 3, `config/environment.rb` for Rails 2): + +```ruby +config.middleware.use Rack::SslEnforcer +``` + +If all you want is SSL for your whole application, you are done! Otherwise, you can specify some options described below. + +## Options + +### Host contraints + +You can enforce SSL connections only for certain hosts with `:only_hosts`, or prevent certain hosts from being forced to SSL with `:except_hosts`. Constraints can be a `String`, a `Regex` or an array of `String` or `Regex` (possibly mixed), as shown in the following examples: + +```ruby +config.middleware.use Rack::SslEnforcer, :only_hosts => 'api.example.com' +# Please note that, for instance, both http://help.example.com/demo and https://help.example.com/demo would be accessible here + +config.middleware.use Rack::SslEnforcer, :except_hosts => /[help|blog]\.example\.com$/ + +config.middleware.use Rack::SslEnforcer, :only_hosts => [/[secure|admin]\.example\.org$/, 'api.example.com'] +``` + +### Path contraints + +You can enforce SSL connections only for certain paths with `:only`, or prevent certain paths from being forced to SSL with `:except`. Constraints can be a `String`, a `Regex` or an array of `String` or `Regex` (possibly mixed), as shown in the following examples: + +```ruby +config.middleware.use Rack::SslEnforcer, :only => '/login' +# Please note that, for instance, both http://example.com/demo and https://example.com/demo would be accessible here + +config.middleware.use Rack::SslEnforcer, :only => %r{^/admin/} + +config.middleware.use Rack::SslEnforcer, :except => ['/demo', %r{^/public/}] +``` + +### Method contraints + +You can enforce SSL connections only for certain HTTP methods with `:only_methods`, or prevent certain HTTP methods from being forced to SSL with `:except_methods`. Constraints can be a `String` or an array of `String`, as shown in the following examples: + +```ruby +# constraint as a String +config.middleware.use Rack::SslEnforcer, :only_methods => 'POST' +# Please note that, for instance, GET requests would be accessible via SSL and non-SSL connection here + +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. + +### Force-redirection to non-SSL connection if constraint is not matched + +Use the `:strict` option to force non-SSL connection for all requests not matching the constraints you set. Examples: + +```ruby +config.middleware.use Rack::SslEnforcer, :only => ["/login", /\.xml$/], :strict => true +# https://example.com/demo would be redirected to http://example.com/demo + +config.middleware.use Rack::SslEnforcer, :except_hosts => 'demo.example.com', :strict => true +# https://demo.example.com would be redirected to http://demo.example.com +``` + +### Automatic method contraints + +In the case where you have matching URLs with different HTTP methods – for instance Rails RESTful routes: `GET /users`, `POST /users`, `GET /user/:id` and `PUT /user/:id` – you may need to force POST and PUT requests to SSL connection but redirect to non-SSL connection on GET. + +```ruby +config.middleware.use Rack::SslEnforcer, :only => [%r{^/users/}], :mixed => true +``` + +The above will allow you to POST/PUT from the secure/non-secure URLs keeping the original schema. + +### HTTP Strict Transport Security (HSTS) + +To set HSTS expiry and subdomain inclusion (defaults respectively to `one year` and `true`). + +```ruby +config.middleware.use Rack::SslEnforcer, :hsts => { :expires => 500, :subdomains => false } +config.middleware.use Rack::SslEnforcer, :hsts => true # equivalent to { :expires => 31536000, :subdomains => true } +``` +Please note that the strict option disables HSTS. + +### Redirect to specific URL (e.g. if you're using a proxy) + +You might need the `:redirect_to` option if the requested URL can't be determined. + +```ruby +config.middleware.use Rack::SslEnforcer, :redirect_to => 'https://example.org' +``` + +### Custom HTTP port + +If you're using a different port than the default (80) for HTTP, you can specify it with the `:http_port` option: + +```ruby +config.middleware.use Rack::SslEnforcer, :http_port => 8080 +``` + +### Custom HTTPS port + +If you're using a different port than the default (443) for HTTPS, you can specify it with the `:https_port` option: + +```ruby +config.middleware.use Rack::SslEnforcer, :https_port => 444 +``` + +### Secure cookies disabling + +Finally you might want to share a cookie based session between HTTP and HTTPS. +This is not possible by default with Rack::SslEnforcer for [security reasons](http://en.wikipedia.org/wiki/HTTP_cookie#Cookie_theft_and_session_hijacking). + +Nevertheless, you can set the `:force_secure_cookies` option to `false` in order to be able to share a cookie based session between HTTP and HTTPS: + +```ruby +config.middleware.use Rack::SslEnforcer, :only => "/login", :force_secure_cookies => false +``` + +But be aware that if you do so, you have to make sure that the content of you cookie is encoded. +This can be done using a coder with [Rack::Session::Cookie](https://github.com/rack/rack/blob/master/lib/rack/session/cookie.rb#L28-42). + +## Deployment + +If you run your application behind a proxy (e.g. Nginx) you may need to do some configuration on that side. If you don't you may experience an infinite redirect loop. + +The reason this happens is that Rack::SslEnforcer can't detect if you are running SSL or not. The solution is to have your front-end server send extra headers for Rack::SslEnforcer to identify the request protocol. + +### Nginx + +In the `location` block for your app's SSL configuration, include the following proxy header configuration: + +`proxy_set_header X-Forwarded-Proto https;` + +This makes sure that Rack::SslEnforcer knows it's being accessed over SSL. Just restart Nginx for these changes to take effect. + +## TODO + +* Cleanup tests + +## Contributors + +* [Dan Mayer](http://github.com/danmayer) +* [Rémy Coutable](http://github.com/rymai) +* [Thibaud Guillaume-Gentil](http://github.com/thibaudgg) +* [Paul Annesley](https://github.com/pda) +* [Saimon Moore](https://github.com/saimonmoore) + +## Credits + +Flagging cookies as secure functionality and HSTS support is greatly inspired by [Joshua Peek's Rack::SSL](https://github.com/josh/rack-ssl). + +## Note on Patches / Pull Requests + +* Fork the project. +* Code your feature addition or bug fix. +* **Add tests for it.** This is important so we don't break it in a future version unintentionally. +* Commit, do not mess with Rakefile or version number. If you want to have your own version, that's fine but bump version in a commit by itself so we can ignore it when merging. +* Send a pull request. Bonus points for topic branches. + +## Copyright + +Copyright (c) 2010-2012 Tobias Matthies. See LICENSE for details. diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index e4599bd..0000000 --- a/README.rdoc +++ /dev/null @@ -1,130 +0,0 @@ -= Rack::SslEnforcer - -Rack::SslEnforcer is a simple Rack middleware to enforce SSL connections. As of Version 0.2.0, Rack::SslEnforcer marks -Cookies as secure by default (HSTS must be set manually). - -Tested on Ruby 1.8.7, 1.9.2, REE, JRuby and Rubinius. - -== Installation - - gem install rack-ssl-enforcer - - -== Basic Usage - - require 'rack/ssl-enforcer' - use Rack::SslEnforcer - -Or, if you are using Bundler, just add this to your Gemfile: - - gem 'rack-ssl-enforcer' - -To use Rack::SslEnforcer in your Rails application, add the following line to your application -config file (config/application.rb for Rails 3, config/environment.rb for Rails 2): - - config.middleware.use Rack::SslEnforcer - -If all you want is SSL for your whole application, you are done! However, you can specify some - - -== Options - -You might need the :redirect_to option if the requested URL can't be determined (e.g. if using a proxy). - - config.middleware.use Rack::SslEnforcer, :redirect_to => 'https://example.org' - -You can also define specific regex patterns or paths or hosts or methods to redirect. - - config.middleware.use Rack::SslEnforcer, :only => /^\/admin\// - config.middleware.use Rack::SslEnforcer, :only => "/login" - config.middleware.use Rack::SslEnforcer, :only => ["/login", /\.xml$/] - config.middleware.use Rack::SslEnforcer, :only_hosts => 'api.example.com' - config.middleware.use Rack::SslEnforcer, :only_hosts => [/[www|api]\.example\.org$/, 'example.com'] - config.middleware.use Rack::SslEnforcer, :except_hosts => 'help.example.com' - config.middleware.use Rack::SslEnforcer, :except_hosts => /[help|blog]\.example\.com$/ - config.middleware.use Rack::SslEnforcer, :only_methods => 'POST' - config.middleware.use Rack::SslEnforcer, :only_methods => ['POST', 'PUT'] - config.middleware.use Rack::SslEnforcer, :except_methods => 'GET' - config.middleware.use Rack::SslEnforcer, :except_methods => ['GET', 'HEAD'] - -Note: hosts options take precedence over the path options. See tests for examples. - -Use the :strict option to force http for all requests not matching your :only specification - - config.middleware.use Rack::SslEnforcer, :only => ["/login", /\.xml$/], :strict => true - config.middleware.use Rack::SslEnforcer, :only_hosts => 'api.example.com', :strict => true - -Or in the case where you have matching urls with different methods (Rails RESTful routes: get#users post#users || get#user/:id put#user/:id) you may need to post and put to secure but redirect to http on get. - - config.middleware.use Rack::SslEnforcer, :only => [%r{^/users/}], :mixed => true - -The above will allow you to post/put from the secure/non-secure urls keeping the original schema. - -To set HSTS expiry and subdomain inclusion (defaults: one year, true). Strict option disables HSTS. - - config.middleware.use Rack::SslEnforcer, :hsts => { :expires => 500, :subdomains => false } - config.middleware.use Rack::SslEnforcer, :hsts => true # equivalent to { :expires => 31536000, :subdomains => true } - -Finally you might want to share a cookie based session between http and https. -This is not possible by default with Rack::SslEnforcer for security reasons. -See: [http://en.wikipedia.org/wiki/HTTP_cookie#Cookie_theft_and_session_hijacking] - -Nevertheless, you can set the option :force_secure_cookies to false in order to be able to share a cookie based session between http and https: - - config.middleware.use Rack::SslEnforcer, :only => "/login", :force_secure_cookies => false - -But be aware that if you do so, you have to make sure that the content of you cookie is encoded. -This can be done using a coder with Rack::Session::Cookie. -See: [https://github.com/rack/rack/blob/master/lib/rack/session/cookie.rb#L28-42] - - -== Deployment - -If you run your application behind a proxy (e.g. Nginx) you may need to do some configuration on that side. If you don't you may experience an infinite redirect loop. - -The reason this happens is that Rack::SslEnforcer can't detect if you are running SSL or not. The solution is to have your front-end server send extra headers for Rack::SslEnforcer to identify the request protocol. - -=== Nginx - -In the `location` block for your app's SSL configuration, include the following proxy header configuration: - - proxy_set_header X-Forwarded-Proto https; - -This makes sure that Rack::SslEnforcer knows it's being accessed over SSL. Just restart Nginx for these changes to take effect. - - -== TODO - -* Add configuration option to specify local http / https ports -* Cleanup tests - - -== Contributors - -* {Dan Mayer}[http://github.com/danmayer] -* {Rémy Coutable}[http://github.com/rymai] -* {Thibaud Guillaume-Gentil}[http://github.com/thibaudgg] -* {Paul Annesley}[https://github.com/pda] -* {Saimon Moore}[https://github.com/saimonmoore] - - -== Credits - -Flagging cookies as secure functionality and HSTS support is greatly inspired by {Joshua Peek's Rack::SSL}[https://github.com/josh/rack-ssl] - - -== Note on Patches/Pull Requests - -* Fork the project. -* Make your feature addition or bug fix. -* Add tests for it. This is important so I don't break it in a - future version unintentionally. -* Commit, do not mess with rakefile, version, or history. - (if you want to have your own version, - that is fine but bump version in a commit by itself I can ignore when I pull) -* Send me a pull request. Bonus points for topic branches. - - -== Copyright - -Copyright (c) 2010 Tobias Matthies. See LICENSE for details. diff --git a/Rakefile b/Rakefile index 550353c..115f0a2 100644 --- a/Rakefile +++ b/Rakefile @@ -35,15 +35,29 @@ Rake::RDocTask.new do |rdoc| end namespace(:test) do - desc "Run all specs on multiple ruby versions (requires rvm)" + desc "Run all tests on multiple ruby versions (requires rvm)" task(:portability) do - %w[1.8.7 1.9.2 ree jruby rbx].each do |version| + travis_config_file = File.expand_path("../.travis.yml", __FILE__) + begin + travis_options ||= YAML::load_file(travis_config_file) + rescue => ex + puts "Travis config file '#{travis_config_file}' could not be found: #{ex.message}" + return + end + + travis_options['rvm'].each do |version| system <<-BASH bash -c 'source ~/.rvm/scripts/rvm; rvm #{version}; - echo "--------- version #{version} ----------\n"; - bundle install; - rake test' + ruby_version_string_size=`ruby -v | wc -m` + echo; + for ((c=1; c<$ruby_version_string_size; c++)); do echo -n "="; done + echo; + echo "`ruby -v`"; + for ((c=1; c<$ruby_version_string_size; c++)); do echo -n "="; done + echo; + RBXOPT="-Xrbc.db" bundle install; + RBXOPT="-Xrbc.db" rake test 2>&1;' BASH end end diff --git a/lib/rack/ssl-enforcer.rb b/lib/rack/ssl-enforcer.rb index cc12547..aacd896 100644 --- a/lib/rack/ssl-enforcer.rb +++ b/lib/rack/ssl-enforcer.rb @@ -1,38 +1,45 @@ +require 'rack/ssl-enforcer/constraint' + module Rack + class SslEnforcer + CONSTRAINTS_BY_TYPE = { + :hosts => [:only_hosts, :except_hosts], + :path => [:only, :except], + :methods => [:only_methods, :except_methods] + } + # Warning: If you set the option force_secure_cookies to false, make sure that your cookies # are encoded and that you understand the consequences (see documentation) def initialize(app, options={}) default_options = { - :redirect_to => nil, - :only => nil, - :only_hosts => nil, - :except => nil, - :except_hosts => nil, - :strict => false, - :mixed => false, - :hsts => nil, - :http_port => nil, - :https_port => nil, + :redirect_to => nil, + :strict => false, + :mixed => false, + :hsts => nil, + :http_port => nil, + :https_port => nil, :force_secure_cookies => true } + CONSTRAINTS_BY_TYPE.values.each { |constraint| default_options[constraint] = nil } + @app, @options = app, default_options.merge(options) end def call(env) - @req = Rack::Request.new(env) - if enforce_ssl?(@req) - scheme = 'https' unless ssl_request?(env) - elsif ssl_request?(env) && enforcement_non_ssl?(env) - scheme = 'http' + @request = Rack::Request.new(env) + scheme = if enforce_ssl? + 'https' + elsif enforce_non_ssl? + 'http' end - if scheme - location = replace_scheme(@req, scheme) + if scheme && scheme != current_scheme + location = replace_scheme(@request, scheme) body = "
You are being redirected." [301, { 'Content-Type' => 'text/html', 'Location' => location }, [body]] - elsif ssl_request?(env) + elsif ssl_request? status, headers, body = @app.call(env) flag_cookies_as_secure!(headers) if @options[:force_secure_cookies] set_hsts_headers!(headers) if @options[:hsts] && !@options[:strict] @@ -44,98 +51,43 @@ def call(env) private - def enforcement_non_ssl?(env) - true if @options[:strict] || @options[:mixed] && !(env['REQUEST_METHOD'] == 'PUT' || env['REQUEST_METHOD'] == 'POST') + def enforce_non_ssl? + true if @options[:strict] || @options[:mixed] && !(@request.request_method == 'PUT' || @request.request_method == 'POST') end - def ssl_request?(env) - scheme(env) == 'https' + def ssl_request? + current_scheme == 'https' end # Fixed in rack >= 1.3 - def scheme(env) - if env['HTTPS'] == 'on' + def current_scheme + if @request.env['HTTPS'] == 'on' 'https' - elsif env['HTTP_X_FORWARDED_PROTO'] - env['HTTP_X_FORWARDED_PROTO'].split(',')[0] + elsif @request.env['HTTP_X_FORWARDED_PROTO'] + @request.env['HTTP_X_FORWARDED_PROTO'].split(',')[0] else - env['rack.url_scheme'] + @request.scheme end end - def matches?(key, pattern, req) - if pattern.is_a?(Regexp) - case key - when :only - req.path =~ pattern - when :except - req.path !~ pattern - when :only_hosts - req.host =~ pattern - when :except_hosts - req.host !~ pattern - end + def enforce_ssl_for?(keys) + provided_keys = keys.select { |key| @options[key] } + if provided_keys.empty? + true else - case key - when :only - req.path[0,pattern.length] == pattern - when :except - req.path[0,pattern.length] != pattern - when :only_hosts - req.host == pattern - when :except_hosts - req.host != pattern - when :only_methods - req.env['REQUEST_METHOD'] == pattern - when :except_methods - req.env['REQUEST_METHOD'] != pattern - end - end - end - - def enforce_ssl_for?(keys, req) - if keys.any? { |option| @options[option] } - keys.any? do |key| + provided_keys.all? do |key| rules = [@options[key]].flatten.compact - unless rules.empty? - rules.send(key == :except_hosts || key == :except ? "all?" : "any?") do |pattern| - matches?(key, pattern, req) - end + rules.send([:except_hosts, :except].include?(key) ? :all? : :any?) do |rule| + SslEnforcerConstraint.new(key, rule, @request).matches? end end - else - false end end - def enforce_ssl?(req) - enforce = false - keys_by_type = { - :hosts => [:only_hosts, :except_hosts], :path => [:only, :except], - :methods => [:only_methods, :except_methods] - } - - if !keys_by_type.values.flatten.compact.any? { |option| @options[option] } - return true - end - - keys_by_type.keys.each do |type| - enforce = enforce_ssl_for?(keys_by_type[type], req) - - next unless enforce - - keys_by_type.each do |other_type,keys| - next if type == other_type || !keys.any? { |option| @options[option] } - - enforce = enforce_ssl_for?(keys, req) - - break unless enforce - end - - break if enforce + def enforce_ssl? + CONSTRAINTS_BY_TYPE.inject(true) do |memo, (type, keys)| + memo && enforce_ssl_for?(keys) end - - enforce end def replace_scheme(req, scheme) diff --git a/lib/rack/ssl-enforcer/constraint.rb b/lib/rack/ssl-enforcer/constraint.rb new file mode 100644 index 0000000..3842020 --- /dev/null +++ b/lib/rack/ssl-enforcer/constraint.rb @@ -0,0 +1,45 @@ +class SslEnforcerConstraint + def initialize(name, rule, request) + @name = name + @rule = rule + @request = request + end + + def matches? + if @rule.is_a?(String) && [:only, :except].include?(@name) + tested_string[0, @rule.size].send(operator, @rule) + else + tested_string.send(operator, @rule) + end + end + +private + + def operator + "#{operator_prefix}#{operator_suffix}" + end + + def operator_prefix + case @name + when /only/ + "=" + when /except/ + "!" + end + end + + def operator_suffix + @rule.is_a?(Regexp) ? "~" : "=" + end + + def tested_string + case @name + when /hosts/ + @request.host + when /methods/ + @request.request_method + else + @request.path + end + end +end diff --git a/rack-ssl-enforcer.gemspec b/rack-ssl-enforcer.gemspec index 6ed3cf9..926d294 100644 --- a/rack-ssl-enforcer.gemspec +++ b/rack-ssl-enforcer.gemspec @@ -21,6 +21,6 @@ Gem::Specification.new do |s| s.add_development_dependency "rack", "~> 1.2.0" s.add_development_dependency "rack-test", "~> 0.5.4" - s.files = Dir.glob("{lib}/**/*") + %w[LICENSE README.rdoc] + s.files = Dir.glob("{lib}/**/*") + %w[LICENSE README.md] s.require_path = 'lib' end diff --git a/test/rack-ssl-enforcer_test.rb b/test/rack-ssl-enforcer_test.rb index a908f6c..13738aa 100644 --- a/test/rack-ssl-enforcer_test.rb +++ b/test/rack-ssl-enforcer_test.rb @@ -1,42 +1,36 @@ require 'helper' class TestRackSslEnforcer < Test::Unit::TestCase - context 'that has no :redirect_to set' do - setup { mock_app } - should 'respond with a ssl redirect to plain-text requests' do - get 'http://www.example.org/' - assert_equal 301, last_response.status - assert_equal 'https://www.example.org/', last_response.location - end + context 'no options' do + setup { mock_app } - should 'respond with a ssl redirect to plain-text requests and keep params' do + should 'redirect to HTTPS and keep params' do get 'http://www.example.org/admin?token=33' assert_equal 301, last_response.status assert_equal 'https://www.example.org/admin?token=33', last_response.location end - #heroku / etc do proxied SSL - #http://github.com/pivotal/refraction/issues/issue/2 + # heroku / etc do proxied SSL should 'respect X-Forwarded-Proto header for proxied SSL' do get 'http://www.example.org/', {}, { 'HTTP_X_FORWARDED_PROTO' => 'http', 'rack.url_scheme' => 'http' } assert_equal 301, last_response.status assert_equal 'https://www.example.org/', last_response.location end - should 'respond not redirect ssl requests' do + should 'not redirect SSL requests' do get 'https://www.example.org/' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond not redirect ssl requests and respect X-Forwarded-Proto header for proxied SSL' do + should 'not redirect SSL requests and respect X-Forwarded-Proto header for proxied SSL' do get 'http://www.example.org/', {}, { 'HTTP_X_FORWARDED_PROTO' => 'https', 'rack.url_scheme' => 'http' } assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'use default https port when redirecting non-standard http port to ssl' do + should 'use default HTTPS port (443) when redirecting non-standard HTTP port to HTTPS' do get 'http://example.org:81/', {}, { 'rack.url_scheme' => 'http' } assert_equal 301, last_response.status assert_equal 'https://example.org/', last_response.location @@ -47,12 +41,12 @@ class TestRackSslEnforcer < Test::Unit::TestCase assert_equal ["id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") end - should 'not set default hsts headers to all ssl requests' do + should 'not set default HSTS headers to SSL requests' do get 'https://www.example.org/' assert !last_response.headers["Strict-Transport-Security"] end - should 'not set hsts headers to non ssl requests' do + should 'not set hsts headers to non-SSL requests' do get 'http://www.example.org/' assert !last_response.headers["Strict-Transport-Security"] end @@ -73,48 +67,42 @@ class TestRackSslEnforcer < Test::Unit::TestCase @app = builder.to_app end - should 'secure multiple cookies' do + should 'secure cookies' do get 'https://www.example.org/' assert_equal ["id=1; path=/; secure", "token=abc; path=/; HttpOnly; secure"], last_response.headers['Set-Cookie'].split("\n") end end - context 'that has :ssl_port set' do - setup { mock_app :https_port => 9443 } + context ':http_port' do + setup { mock_app :http_port => 8080, :only => [], :strict => true } - should 'respond with a ssl redirect to plain-text requests and redirect to a custom port' do - get 'http://www.example.org/' + should 'redirect to HTTP with custom port' do + get 'https://www.example.org/' assert_equal 301, last_response.status - assert_equal 'https://www.example.org:9443/', last_response.location + assert_equal 'http://www.example.org:8080/', last_response.location end end - context 'that has a default :ssl_port set' do - setup { mock_app :https_port => 443 } + context ':https_port' do + setup { mock_app :https_port => 9443 } - should 'respond with a ssl redirect to plain-text requests and redirect without a port identifier' do + should 'redirect to HTTPS with custom port' do get 'http://www.example.org/' assert_equal 301, last_response.status - assert_equal 'https://www.example.org/', last_response.location + assert_equal 'https://www.example.org:9443/', last_response.location end end - context 'that has :redirect_to set' do + context ':redirect_to' do setup { mock_app :redirect_to => 'https://www.google.com' } - should 'respond with a ssl redirect to plain-text requests and redirect to :redirect_to' do - get 'http://www.example.org/' - assert_equal 301, last_response.status - assert_equal 'https://www.google.com/', last_response.location - end - - should 'respond with a ssl redirect to plain-text requests and redirect to :redirect_to and keep params' do + should 'redirect to HTTPS and keep params' do get 'http://www.example.org/admin?token=33' assert_equal 301, last_response.status assert_equal 'https://www.google.com/admin?token=33', last_response.location end - should 'redirect to :redirect_to when host without scheme given' do + should 'redirect to HTTPS and append scheme automatically' do mock_app :redirect_to => 'www.google.com' get 'http://www.example.org/' @@ -122,628 +110,549 @@ class TestRackSslEnforcer < Test::Unit::TestCase assert_equal 'https://www.google.com/', last_response.location end - should 'respond not redirect ssl requests' do + should 'not redirect SSL requests' do get 'https://www.example.org/' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - context 'that has a regex pattern as :only option' do + context ':only (Regex)' do setup { mock_app :only => /^\/admin/ } - should 'respond with a ssl redirect for /admin path' do - get 'http://www.example.org/admin' + should 'redirect to HTTPS for /admin' do + get 'http://www.example.org/admin/account' assert_equal 301, last_response.status - assert_equal 'https://www.example.org/admin', last_response.location + assert_equal 'https://www.example.org/admin/account', last_response.location end - should 'respond not redirect ssl requests' do + should 'not redirect for other paths' do get 'http://www.example.org/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - - should 'secure cookies' do - get 'https://www.example.org/' - assert_equal ["id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") - end end - context 'that has a string path as :only option' do - setup { mock_app :only => "/login" } + context ':only (String)' do + setup { mock_app :only => "/account" } - should 'respond with a ssl redirect for /login path' do - get 'http://www.example.org/login' + should 'redirect to HTTPS for /account' do + get 'http://www.example.org/account' assert_equal 301, last_response.status - assert_equal 'https://www.example.org/login', last_response.location + assert_equal 'https://www.example.org/account', last_response.location end - should 'respond not redirect ssl requests' do - get 'http://www.example.org/foo/' + should 'redirect to HTTPS for /account/public' do + get 'http://www.example.org/account/public' + assert_equal 301, last_response.status + assert_equal 'https://www.example.org/account/public', last_response.location + end + + should 'not redirect SSL requests for /account' do + get 'https://www.example.org/account' + assert_equal 200, last_response.status + assert_equal 'Hello world!', last_response.body + end + + should 'not redirect for /foo' do + get 'http://www.example.org/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - context 'that has an array of regex patterns & string paths as :only option' do + context ':only (Array)' do setup { mock_app :only => [/\.xml$/, "/login"] } - should 'respond with a ssl redirect for /login path' do + should 'redirect to HTTPS for /login' do get 'http://www.example.org/login' assert_equal 301, last_response.status assert_equal 'https://www.example.org/login', last_response.location end - should 'respond with a ssl redirect for /admin path' do + should 'redirect to HTTPS for /admin path' do get 'http://www.example.org/users.xml' assert_equal 301, last_response.status assert_equal 'https://www.example.org/users.xml', last_response.location end - should 'respond not redirect ssl requests' do + should 'not redirect for /foo' do get 'http://www.example.org/foo/' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - context 'that has an array of regex patterns & string paths as :only option with :strict = true' do + context ':only (Array) & :strict == true' do setup { mock_app :only => [/\.xml$/, "/login"], :strict => true } - should 'respond with a http redirect from non-allowed https url' do - get 'https://www.example.org/foo/' + should 'redirect to HTTP for /foo' do + get 'https://www.example.org/foo' assert_equal 301, last_response.status - assert_equal 'http://www.example.org/foo/', last_response.location + assert_equal 'http://www.example.org/foo', last_response.location end - should 'respond from allowed https url' do + should 'not redirect for /login' do get 'https://www.example.org/login' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - - should 'use default https port when redirecting non-standard ssl port to http' do - get 'https://example.org:81/', {}, { 'rack.url_scheme' => 'https' } - assert_equal 301, last_response.status - assert_equal 'http://example.org/', last_response.location - end - - should 'secure cookies' do - get 'https://www.example.org/login' - assert_equal ["id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") - end - - should 'not secure cookies' do - get 'http://www.example.org/' - assert_equal ["id=1; path=/", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") - end end - context 'that has a regex pattern as :except option' do + context ':except (Regex)' do setup { mock_app :except => /^\/foo/ } - should 'respond with a ssl redirect for /admin path' do + should 'redirect to HTTPS for /admin' do get 'http://www.example.org/admin' assert_equal 301, last_response.status assert_equal 'https://www.example.org/admin', last_response.location end - should 'respond not redirect ssl requests' do + should 'not redirect for /foo' do get 'http://www.example.org/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - - should 'secure cookies' do - get 'https://www.example.org/' - assert_equal ["id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") - end end - context 'that has a string path as :except option' do + context ':except (String)' do setup { mock_app :except => "/foo" } - should 'respond with a ssl redirect for /login path' do + should 'redirect to HTTPS for /login' do get 'http://www.example.org/login' assert_equal 301, last_response.status assert_equal 'https://www.example.org/login', last_response.location end - should 'respond not redirect ssl requests' do + should 'not redirect for /foo' do get 'http://www.example.org/foo/' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - context 'that has an array of regex patterns & string paths as :except option' do + context ':except (Array)' do setup { mock_app :except => [/^\/foo/, "/bar"] } - should 'respond with a ssl redirect for /admin path' do + should 'redirect to HTTPS for /admin' do get 'http://www.example.org/admin' assert_equal 301, last_response.status assert_equal 'https://www.example.org/admin', last_response.location end - should 'not redirect ssl requests for /foo path' do + should 'not redirect for /foo' do get 'http://www.example.org/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'not redirect ssl requests for /bar path' do + should 'not redirect for /bar' do get 'http://www.example.org/bar' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - end - context 'that has a string path as :except option with :strict = true' do + context ':except & :strict == true' do setup { mock_app :except => "/foo", :strict => true } - should 'respond with a http redirect from non-allowed https url' do + should 'redirect to HTTP for /foo' do get 'https://www.example.org/foo/' assert_equal 301, last_response.status assert_equal 'http://www.example.org/foo/', last_response.location end - should 'respond from allowed https url' do + should 'not redirect for /login' do get 'https://www.example.org/login' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end + end + + context ':only_hosts (Regex)' do + setup { mock_app :only_hosts => /[www|api]\.example\.co\.uk$/ } - should 'use default https port when redirecting non-standard ssl port to http' do - get 'https://example.org:81/foo', {}, { 'rack.url_scheme' => 'https' } + should 'redirect to HTTPS for www.example.co.uk' do + get 'http://www.example.co.uk' assert_equal 301, last_response.status - assert_equal 'http://example.org/foo', last_response.location + assert_equal 'https://www.example.co.uk/', last_response.location end - should 'secure cookies' do - get 'https://www.example.org/' - assert_equal ["id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") + should 'redirect to HTTPS for api.example.co.uk' do + get 'http://api.example.co.uk' + assert_equal 301, last_response.status + assert_equal 'https://api.example.co.uk/', last_response.location end - should 'not secure cookies' do - get 'http://www.example.org/foo' - assert_equal ["id=1; path=/", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") + should 'not redirect for goo.example.co.uk' do + get 'http://goo.example.co.uk' + assert_equal 200, last_response.status + assert_equal 'Hello world!', last_response.body + end + + should 'not redirect for goo.example.co.uk for goo.example.co.uk' do + get 'http://teambox.example.co.uk' + assert_equal 200, last_response.status + assert_equal 'Hello world!', last_response.body end end - context 'that has a string domain as :only_hosts option' do + context ':only_hosts (String)' do setup { mock_app :only_hosts => "example.org" } - should 'respond with a ssl redirect for example.org' do + should 'redirect to HTTPS for example.org' do get 'http://example.org' assert_equal 301, last_response.status assert_equal 'https://example.org/', last_response.location end - should 'respond not redirect ssl requests for *.example.org' do + should 'not redirect for www.example.org' do get 'http://www.example.org' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond not redirect ssl requests' do + should 'not redirect for example.com' do get 'http://www.example.com' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - context 'that has a string domain as :only_hosts option & a string path as :only option' do - setup { mock_app :only_hosts => "example.org", :only => '/foo' } + context ':only_hosts (Array)' do + setup { mock_app :only_hosts => [/[www|api]\.example\.org$/, "example.com"] } - should 'respond with a ssl redirect for example.org/foo' do - get 'http://example.org/foo' + should 'redirect to HTTPS for www.example.org' do + get 'http://www.example.org' assert_equal 301, last_response.status - assert_equal 'https://example.org/foo', last_response.location + assert_equal 'https://www.example.org/', last_response.location end - should 'respond not redirect ssl requests for example.org/bar' do - get 'http://example.org/bar' - assert_equal 200, last_response.status - assert_equal 'Hello world!', last_response.body + should 'redirect to HTTPS for api.example.org' do + get 'http://api.example.org' + assert_equal 301, last_response.status + assert_equal 'https://api.example.org/', last_response.location end - should 'respond not redirect ssl requests for www.example.org' do - get 'http://www.example.org' + should 'not redirect for goo.example.com' do + get 'http://goo.example.com' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond not redirect ssl requests for www.example.org/foo' do - get 'http://www.example.org/foo' + should 'not redirect for example.org' do + get 'http://example.org' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond not redirect ssl requests for www.example.org/bar' do - get 'http://www.example.org/bar' + should 'not redirect for goo.example.org' do + get 'http://goo.example.org' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - context 'that has a string domain as :only_hosts option & a string path as :except option' do - setup { mock_app :only_hosts => "example.org", :except => '/foo' } + context ':except_hosts (Regex)' do + setup { mock_app :except_hosts => /[www|api]\.example\.co\.uk$/ } - should 'respond with a ssl redirect for example.org/bar' do - get 'http://example.org/bar' + should 'redirect to HTTPS for goo.example.co.uk' do + get 'http://goo.example.co.uk' assert_equal 301, last_response.status - assert_equal 'https://example.org/bar', last_response.location + assert_equal 'https://goo.example.co.uk/', last_response.location end - should 'respond not redirect ssl requests for example.org/foo' do - get 'http://example.org/foo' + should 'not redirect for www.example.co.uk' do + get 'http://api.example.co.uk' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond not redirect ssl requests for www.example.org' do - get 'http://www.example.org' + should 'not redirect for api.example.co.uk' do + get 'http://api.example.co.uk' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end + end - should 'respond not redirect ssl requests for www.example.org/bar' do - get 'http://www.example.org/bar' - assert_equal 200, last_response.status - assert_equal 'Hello world!', last_response.body + context ':except_hosts (String)' do + setup { mock_app :except_hosts => "www.example.org" } + + should 'redirect to HTTPS for api.example.org' do + get 'http://api.example.org' + assert_equal 301, last_response.status + assert_equal 'https://api.example.org/', last_response.location end - should 'respond not redirect ssl requests for www.example.org/foo' do - get 'http://www.example.org/foo' + should 'not redirect for www.example.org' do + get 'http://www.example.org' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - context 'that has a string domain as :except_hosts option & a string path as :only option' do - setup { mock_app :except_hosts => "example.org", :only => '/foo' } + context ':except_hosts (Array)' do + setup { mock_app :except_hosts => ["www.example.com", "example.com"] } - should 'respond with a ssl redirect for example.com/foo' do - get 'http://example.com/foo' + should 'redirect to HTTPS for *.example.org' do + get 'http://api.example.org' assert_equal 301, last_response.status - assert_equal 'https://example.com/foo', last_response.location - end - - should 'respond not redirect ssl requests for example.org/foo' do - get 'http://example.org/foo' - assert_equal 200, last_response.status - assert_equal 'Hello world!', last_response.body + assert_equal 'https://api.example.org/', last_response.location end - should 'respond not redirect ssl requests for example.com/bar' do - get 'http://example.com/bar' + should 'not redirect for www.example.com' do + get "http://www.example.com" assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond with a ssl redirect for www.example.org/foo' do - get 'http://www.example.org/foo' - assert_equal 301, last_response.status - assert_equal 'https://www.example.org/foo', last_response.location - end - - should 'respond not redirect ssl requests for www.example.org/bar' do - get 'http://www.example.org/bar' + should 'not redirect for example.com' do + get "http://example.com" assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - context 'that has a string domain as :except_hosts option & a string path as :except option' do - setup { mock_app :except_hosts => "example.org", :except => '/foo' } + context ':only_hosts & :only' do + setup { mock_app :only_hosts => "example.org", :only => '/foo' } - should 'respond with a ssl redirect for example.com/bar' do - get 'http://example.com/bar' + should 'redirect to HTTPS for example.org/foo' do + get 'http://example.org/foo' assert_equal 301, last_response.status - assert_equal 'https://example.com/bar', last_response.location + assert_equal 'https://example.org/foo', last_response.location end - should 'respond not redirect ssl requests for example.org/foo' do - get 'http://example.org/foo' + should 'not redirect for example.org/bar' do + get 'http://example.org/bar' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond not redirect ssl requests for example.org/bar' do - get 'http://example.org/bar' + should 'not redirect for www.example.org' do + get 'http://www.example.org' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond not redirect ssl requests for example.com/foo' do - get 'http://example.com/foo' + should 'not redirect for www.example.org/foo' do + get 'http://www.example.org/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond with a ssl redirect for www.example.org/bar' do + should 'not redirect for www.example.org/bar' do get 'http://www.example.org/bar' - assert_equal 301, last_response.status - assert_equal 'https://www.example.org/bar', last_response.location - end - - should 'respond not redirect ssl requests for www.example.org/foo' do - get 'http://www.example.org/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - context 'that has an array of regex patterns & string domains as :only_hosts option' do - setup { mock_app :only_hosts => [/[www|api]\.example\.org$/, "example.com"] } + context ':only_hosts & :except' do + setup { mock_app :only_hosts => "example.org", :except => '/foo' } - should 'respond with a ssl redirect for www.example.org' do - get 'http://www.example.org' + should 'redirect to HTTPS for example.org/bar' do + get 'http://example.org/bar' assert_equal 301, last_response.status - assert_equal 'https://www.example.org/', last_response.location + assert_equal 'https://example.org/bar', last_response.location end - should 'respond with a ssl redirect for api.example.org' do - get 'http://api.example.org' - assert_equal 301, last_response.status - assert_equal 'https://api.example.org/', last_response.location + should 'not redirect for example.org/foo' do + get 'http://example.org/foo' + assert_equal 200, last_response.status + assert_equal 'Hello world!', last_response.body end - should 'respond not redirect ssl requests for *.example.com' do - get 'http://goo.example.com' + should 'not redirect for www.example.org' do + get 'http://www.example.org' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond not redirect ssl requests for example.org' do - get 'http://example.org' + should 'not redirect for www.example.org/bar' do + get 'http://www.example.org/bar' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond not redirect ssl requests for goo.example.org' do - get 'http://goo.example.org' + should 'not redirect for www.example.org/foo' do + get 'http://www.example.org/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - context 'that has a regex pattern as :only_hosts option' do - setup { mock_app :only_hosts => /[www|api]\.example\.co\.uk$/ } - - should 'respond with a ssl redirect for www.example.co.uk' do - get 'http://www.example.co.uk' - assert_equal 301, last_response.status - assert_equal 'https://www.example.co.uk/', last_response.location - end + context ':except_hosts & :only' do + setup { mock_app :except_hosts => "example.org", :only => '/foo' } - should 'respond with a ssl redirect for api.example.co.uk' do - get 'http://api.example.co.uk' + should 'redirect to HTTPS for example.com/foo' do + get 'http://example.com/foo' assert_equal 301, last_response.status - assert_equal 'https://api.example.co.uk/', last_response.location + assert_equal 'https://example.com/foo', last_response.location end - should 'respond not redirect ssl requests for goo.example.co.uk' do - get 'http://goo.example.co.uk' + should 'not redirect for example.org/foo' do + get 'http://example.org/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond not redirect ssl requests for goo.example.co.uk for goo.example.co.uk' do - get 'http://teambox.example.co.uk' + should 'not redirect for example.com/bar' do + get 'http://example.com/bar' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - end - context 'that has an array of regex patterns & string domains as :only_hosts option with :strict = true' do - setup { mock_app :only_hosts => [/[www|api]\.example\.org$/, "example.com"], :strict => true } - - should 'respond with a http redirect from non-allowed https url' do - get 'https://abc.example.org/' + should 'redirect to HTTPS for www.example.org/foo' do + get 'http://www.example.org/foo' assert_equal 301, last_response.status - assert_equal 'http://abc.example.org/', last_response.location + assert_equal 'https://www.example.org/foo', last_response.location end - should 'respond from allowed https url' do - get 'https://www.example.org/' + should 'not redirect for www.example.org/bar' do + get 'http://www.example.org/bar' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - - should 'use default https port when redirecting non-standard ssl port to http' do - get 'https://goo.example.org:80/', {}, { 'rack.url_scheme' => 'https' } - assert_equal 301, last_response.status - assert_equal 'http://goo.example.org/', last_response.location - end - - should 'secure cookies' do - get 'https://www.example.org/' - assert_equal ["id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") - end - - should 'not secure cookies' do - get 'http://goo.example.org/' - assert_equal ["id=1; path=/", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") - end end - context 'that has a string domain as :except_hosts option' do - setup { mock_app :except_hosts => "www.example.org" } + context ':except_hosts & :except' do + setup { mock_app :except_hosts => "example.org", :except => '/foo' } - should 'respond with a ssl redirect for *.example.org' do - get 'http://api.example.org' + should 'redirect to HTTPS for example.com/bar' do + get 'http://example.com/bar' assert_equal 301, last_response.status - assert_equal 'https://api.example.org/', last_response.location + assert_equal 'https://example.com/bar', last_response.location end - should 'respond not redirect ssl requests' do - get 'http://www.example.org' + should 'not redirect for example.org/foo' do + get 'http://example.org/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - end - - context 'that has an array of domains as :except_hosts option' do - setup { mock_app :except_hosts => ["www.example.com", "example.com"] } - - should 'respond with a ssl redirect for *.example.org' do - get 'http://api.example.org' - assert_equal 301, last_response.status - assert_equal 'https://api.example.org/', last_response.location - end - should 'not redirect www.example.com' do - get "http://www.example.com" + should 'not redirect for example.org/bar' do + get 'http://example.org/bar' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'not redirect example.com' do - get "http://example.com" + should 'not redirect for example.com/foo' do + get 'http://example.com/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - end - context 'that has a regex pattern as :except_hosts option' do - setup { mock_app :except_hosts => /[www|api]\.example\.co\.uk$/ } - - should 'respond with a ssl redirect for goo.example.co.uk' do - get 'http://goo.example.co.uk' + should 'redirect to HTTPS for www.example.org/bar' do + get 'http://www.example.org/bar' assert_equal 301, last_response.status - assert_equal 'https://goo.example.co.uk/', last_response.location - end - - should 'respond not redirect ssl requests for www.example.co.uk' do - get 'http://api.example.co.uk' - assert_equal 200, last_response.status - assert_equal 'Hello world!', last_response.body + assert_equal 'https://www.example.org/bar', last_response.location end - should 'respond not redirect ssl requests for api.example.co.uk' do - get 'http://api.example.co.uk' + should 'not redirect for www.example.org/foo' do + get 'http://www.example.org/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - context 'that has a regex pattern as :except_hosts option with :hsts = true & :strict = true' do + context ':except_hosts & :hsts == true & :strict == true' do setup { mock_app :except_hosts => /[www|api]\.example\.org$/, :hsts => true, :strict => true } - should 'respond with a http redirect from non-allowed https url' do + should 'redirect to HTTP for www.example.org' do get 'https://www.example.org/' assert_equal 301, last_response.status assert_equal 'http://www.example.org/', last_response.location end - should 'respond from allowed https url' do + should 'not redirect for abc.example.org' do get 'https://abc.example.org/' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'use default https port when redirecting non-standard ssl port to http' do - get 'https://www.example.org:80/', {}, { 'rack.url_scheme' => 'https' } - assert_equal 301, last_response.status - assert_equal 'http://www.example.org/', last_response.location - end - - should 'secure cookies' do - get 'https://goo.example.org/' - assert_equal ["id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") - end - - should 'not secure cookies' do - get 'http://www.example.org/' - assert_equal ["id=1; path=/", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") - end - - should 'not set hsts from non-allowed http url' do + should 'not set hsts for www.example.org (HTTP)' do get 'http://www.example.org/' assert !last_response.headers["Strict-Transport-Security"] end - should 'not set hsts from non-allowed https url' do + should 'not set hsts for www.example.org (HTTPS)' do get 'https://www.example.org/' assert !last_response.headers["Strict-Transport-Security"] end - should 'not set hsts from allowed http url' do + should 'not set hsts for abc.example.org (HTTP)' do get 'http://abc.example.org/' assert !last_response.headers["Strict-Transport-Security"] end - should 'not set hsts from allowed https url' do + should 'not set hsts for abc.example.org (HTTPS)' do get 'https://abc.example.org/' assert !last_response.headers["Strict-Transport-Security"] end end - context 'that has an empty array as :only option & :strict = true' do + context ':only == [] & :strict == true' do setup { mock_app :only => [], :strict => true } - should 'respond with no redirect for /foo path' do + should 'not redirect for /foo (HTTP)' do get 'http://www.example.org/foo' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'respond with a non-ssl redirect for /users.xml path' do + should 'redirect to HTTP for /users.xml' do get 'https://www.example.org/users.xml' assert_equal 301, last_response.status assert_equal 'http://www.example.org/users.xml', last_response.location end end - context 'that has array of regex pattern & path as only option with strict option and post option' do - setup { mock_app :only => [/^\/users\/(.+)\/edit/], :mixed => true } + context ':only == nil & :strict = true' do + setup { mock_app :only => nil, :strict => true } - should 'respond with a http redirect from non-allowed https url' do - get 'https://www.example.org/foo/' + should 'redirect to HTTP for /users.xml' do + get 'http://www.example.org/foo' assert_equal 301, last_response.status - assert_equal 'http://www.example.org/foo/', last_response.location + assert_equal 'https://www.example.org/foo', last_response.location end - should 'respond from allowed https url' do - get 'https://www.example.org/users/123/edit' + should 'not redirect for /users.xml' do + get 'https://www.example.org/users.xml' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end + end + + context ':mixed' do + setup { mock_app :only => [/^\/users\/(.+)\/edit/], :mixed => true } - should 'use default https port when redirecting non-standard ssl port to http' do - get 'https://example.org:81/', {}, { 'rack.url_scheme' => 'https' } + should 'redirect to HTTPS for /foo' do + get 'https://www.example.org/foo/' assert_equal 301, last_response.status - assert_equal 'http://example.org/', last_response.location + assert_equal 'http://www.example.org/foo/', last_response.location end - should 'secure cookies' do + should 'not redirect for GET /users/123/edit' do get 'https://www.example.org/users/123/edit' - assert_equal ["id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") - end - - should 'not secure cookies' do - get 'http://www.example.org/' - assert_equal ["id=1; path=/", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") - end - - should 'not redirect if post' do - post 'https://www.example.org/users/' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end - should 'not redirect if put' do - put 'https://www.example.org/users/123' - assert_equal 200, last_response.status - assert_equal 'Hello world!', last_response.body + should 'redirect to HTTPS for POST /users/123/edit' do + post 'http://www.example.org/users/123/edit' + assert_equal 301, last_response.status + assert_equal 'https://www.example.org/users/123/edit', last_response.location + end + + should 'redirect to HTTPS for PUT /users/123/edit' do + put 'http://www.example.org/users/123/edit' + assert_equal 301, last_response.status + assert_equal 'https://www.example.org/users/123/edit', last_response.location end end - context 'that has hsts options set' do - setup { mock_app :hsts => {:expires => '500', :subdomains => false} } + context ':hsts' do + setup { mock_app :hsts => { :expires => '500', :subdomains => false } } should 'set expiry option' do get 'https://www.example.org/' @@ -756,7 +665,7 @@ class TestRackSslEnforcer < Test::Unit::TestCase end end - context 'that has force_secure_cookie option set to false' do + context ':force_secure_cookie == false' do setup { mock_app :force_secure_cookies => false } should 'not secure cookies but warn the user of the consequences' do @@ -765,35 +674,48 @@ class TestRackSslEnforcer < Test::Unit::TestCase end end - context 'that has a string method as only_methods option' do - setup { mock_app :only_methods => 'POST' } - - should 'respond with a ssl redirect for post method' do - post 'http://www.example.org/', 'param=value' + context ':only_methods' do + setup { mock_app :only_methods => 'POST' } + + should 'redirect to HTTPS for POST request' do + post 'http://www.example.org/' assert_equal 301, last_response.status - assert_equal 'https://www.example.org/', last_response.location + assert_equal 'https://www.example.org/', last_response.location + end + + should 'not redirect for PUT request' do + put 'http://www.example.org/' + assert_equal 200, last_response.status + assert_equal 'Hello world!', last_response.body end - - should 'respond not redirect ssl requests' do + + should 'not redirect for GET request' do get 'http://www.example.org/' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - - context 'that has a string method as except_methods option' do - setup { mock_app :except_methods => 'GET' } - should 'respond with a ssl redirect for post method' do - post 'http://www.example.org/', 'param=value' + context ':except_methods option' do + setup { mock_app :except_methods => 'GET' } + + should 'redirect to HTTPS for POST request' do + post 'http://www.example.org/' assert_equal 301, last_response.status - assert_equal 'https://www.example.org/', last_response.location + assert_equal 'https://www.example.org/', last_response.location end - - should 'respond not redirect ssl requests' do + + should 'redirect to HTTPS for PUT request' do + post 'http://www.example.org/' + assert_equal 301, last_response.status + assert_equal 'https://www.example.org/', last_response.location + end + + should 'not redirect for GET request' do get 'http://www.example.org/' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end + end