diff --git a/README.rdoc b/README.rdoc index 30b0ff3..6f96f33 100644 --- a/README.rdoc +++ b/README.rdoc @@ -13,7 +13,7 @@ Cookies as secure and enables HSTS by default. require 'rack/ssl-enforcer' use Rack::SslEnforcer - + Or, if you are using Bundler, just add this to your Gemfile: gem 'rack-ssl-enforcer', :require => 'rack/ssl-enforcer' @@ -31,9 +31,9 @@ If all you want is SSL for your whole application, you are done! However, you ca 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 to redirect. - + config.middleware.use Rack::SslEnforcer, :only => /^\/admin\// config.middleware.use Rack::SslEnforcer, :only => "/login" config.middleware.use Rack::SslEnforcer, :only => ["/login", /\.xml$/] @@ -43,7 +43,7 @@ You can also define specific regex patterns or paths or hosts to redirect. config.middleware.use Rack::SslEnforcer, :except_hosts => /[help|blog]\.example\.com$/ 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 @@ -75,13 +75,13 @@ Flagging cookies as secure functionality and HSTS support is greatly inspired by == 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, + (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. diff --git a/Rakefile b/Rakefile index aec320d..884b101 100644 --- a/Rakefile +++ b/Rakefile @@ -27,7 +27,7 @@ require 'rake/rdoctask' require 'rack/ssl-enforcer/version' Rake::RDocTask.new do |rdoc| version = Rack::SslEnforcer::VERSION - + rdoc.rdoc_dir = 'rdoc' rdoc.title = "rack-ssl-enforcer #{version}" rdoc.rdoc_files.include('README*') diff --git a/lib/rack-ssl-enforcer.rb b/lib/rack-ssl-enforcer.rb index cd12ae4..7e1b82a 100644 --- a/lib/rack-ssl-enforcer.rb +++ b/lib/rack-ssl-enforcer.rb @@ -1 +1 @@ -require 'rack/ssl-enforcer' \ No newline at end of file +require 'rack/ssl-enforcer' diff --git a/lib/rack/ssl-enforcer.rb b/lib/rack/ssl-enforcer.rb index 472082f..fba68ca 100644 --- a/lib/rack/ssl-enforcer.rb +++ b/lib/rack/ssl-enforcer.rb @@ -1,10 +1,10 @@ module Rack class SslEnforcer - + def initialize(app, options = {}) @app, @options = app, options end - + def call(env) @req = Rack::Request.new(env) if enforce_ssl?(@req) @@ -26,14 +26,13 @@ module Rack @app.call(env) end end - - + private - + def ssl_request?(env) scheme(env) == 'https' end - + # Fixed in rack >= 1.3 def scheme(env) if env['HTTPS'] == 'on' @@ -44,7 +43,7 @@ module Rack env['rack.url_scheme'] end end - + def matches?(key, pattern, req) if pattern.is_a?(Regexp) case key @@ -103,7 +102,7 @@ module Rack true end end - + def replace_scheme(req, scheme) Rack::Request.new(req.env.merge( 'rack.url_scheme' => scheme, @@ -112,7 +111,7 @@ module Rack 'SERVER_PORT' => port_for(scheme).to_s )) end - + def port_for(scheme) scheme == 'https' ? 443 : 80 end @@ -129,7 +128,7 @@ module Rack }.join("\n") end end - + # see http://en.wikipedia.org/wiki/Strict_Transport_Security def set_hsts_headers!(headers) opts = { :expires => 31536000, :subdomains => true }.merge(@options[:hsts] || {}) @@ -137,6 +136,6 @@ module Rack value += "; includeSubDomains" if opts[:subdomains] headers.merge!({ 'Strict-Transport-Security' => value }) end - + end end diff --git a/lib/rack/ssl-enforcer/version.rb b/lib/rack/ssl-enforcer/version.rb index e6fb5bf..193fdc0 100644 --- a/lib/rack/ssl-enforcer/version.rb +++ b/lib/rack/ssl-enforcer/version.rb @@ -2,4 +2,4 @@ module Rack class SslEnforcer VERSION = "0.2.0" end -end \ No newline at end of file +end diff --git a/rack-ssl-enforcer.gemspec b/rack-ssl-enforcer.gemspec index dc7373d..b302c7b 100644 --- a/rack-ssl-enforcer.gemspec +++ b/rack-ssl-enforcer.gemspec @@ -25,4 +25,4 @@ Gem::Specification.new do |s| s.files = Dir.glob("{lib}/**/*") + %w[LICENSE README.rdoc] s.require_path = 'lib' -end \ No newline at end of file +end diff --git a/test/helper.rb b/test/helper.rb index 96c96b5..e4ac826 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -10,9 +10,9 @@ require 'rack/ssl-enforcer' class Test::Unit::TestCase include Rack::Test::Methods - + def app; Rack::Lint.new(@app); end - + def mock_app(options = {}) main_app = lambda { |env| request = Rack::Request.new(env) @@ -20,7 +20,7 @@ class Test::Unit::TestCase headers['Set-Cookie'] = "id=1; path=/\ntoken=abc; path=/; secure; HttpOnly" [200, headers, ['Hello world!']] } - + builder = Rack::Builder.new builder.use Rack::SslEnforcer, options builder.run main_app diff --git a/test/rack-ssl-enforcer_test.rb b/test/rack-ssl-enforcer_test.rb index 27bae20..832a5a7 100644 --- a/test/rack-ssl-enforcer_test.rb +++ b/test/rack-ssl-enforcer_test.rb @@ -1,22 +1,22 @@ 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 - + should 'respond with a ssl redirect to plain-text requests 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 should 'respect X-Forwarded-Proto header for proxied SSL' do @@ -24,13 +24,13 @@ class TestRackSslEnforcer < Test::Unit::TestCase assert_equal 301, last_response.status assert_equal 'https://www.example.org/', last_response.location end - + should 'respond 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 get 'http://www.example.org/', {}, { 'HTTP_X_FORWARDED_PROTO' => 'https', 'rack.url_scheme' => 'http' } assert_equal 200, last_response.status @@ -41,114 +41,114 @@ class TestRackSslEnforcer < Test::Unit::TestCase get 'http://example.org:81/', {}, { 'rack.url_scheme' => 'http' } assert_equal 301, last_response.status assert_equal 'https://example.org/', last_response.location - end + 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 set default hsts headers to all ssl requests' do get 'https://www.example.org/' assert last_response.headers["Strict-Transport-Security"].nil? end - + should 'not set hsts headers to non ssl requests' do get 'http://www.example.org/' - assert last_response.headers["Strict-Transport-Security"].nil? + assert last_response.headers["Strict-Transport-Security"].nil? end end - + context 'that has :redirect_to set' 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 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 regex pattern as only option' do setup { mock_app :only => /^\/admin/ } - + should 'respond with a ssl redirect for /admin path' 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 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 path as only option' do setup { mock_app :only => "/login" } - + should 'respond with a ssl redirect for /login path' 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 get 'http://www.example.org/foo/' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - + context 'that has array of regex pattern & path as only option' do setup { mock_app :only => [/\.xml$/, "/login"] } - + should 'respond with a ssl redirect for /login path' 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 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 get 'http://www.example.org/foo/' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - + context 'that has array of regex pattern & path as only option with strict option' 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/' assert_equal 301, last_response.status assert_equal 'http://www.example.org/foo/', last_response.location end - + should 'respond from allowed https url' 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 @@ -159,65 +159,65 @@ class TestRackSslEnforcer < Test::Unit::TestCase 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 regex pattern as except option' do setup { mock_app :except => /^\/foo/ } - + should 'respond with a ssl redirect for /admin path' 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 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 path as except option' do setup { mock_app :except => "/foo" } - + should 'respond with a ssl redirect for /login path' 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 get 'http://www.example.org/foo/' assert_equal 200, last_response.status assert_equal 'Hello world!', last_response.body end end - + context 'that has path as except option with strict option' do setup { mock_app :except => "/foo", :strict => true } - + should 'respond with a http redirect from non-allowed https url' 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 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/foo', {}, { 'rack.url_scheme' => 'https' } assert_equal 301, last_response.status @@ -228,7 +228,7 @@ class TestRackSslEnforcer < Test::Unit::TestCase 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://www.example.org/foo' assert_equal ["id=1; path=/", "token=abc; path=/; secure; HttpOnly"], last_response.headers['Set-Cookie'].split("\n") @@ -463,19 +463,19 @@ class TestRackSslEnforcer < Test::Unit::TestCase context 'that has array of regex pattern & domain as only_hosts option with strict option' 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/' assert_equal 301, last_response.status assert_equal 'http://abc.example.org/', last_response.location end - + should 'respond from allowed https url' do get 'https://www.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://goo.example.org:80/', {}, { 'rack.url_scheme' => 'https' } assert_equal 301, last_response.status @@ -486,7 +486,7 @@ class TestRackSslEnforcer < Test::Unit::TestCase 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") @@ -533,19 +533,19 @@ class TestRackSslEnforcer < Test::Unit::TestCase context 'that has regex pattern as except_hosts option with strict option' do setup { mock_app :except_hosts => /[www|api]\.example\.org$/, :strict => true } - + should 'respond with a http redirect from non-allowed https url' 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 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 @@ -556,27 +556,25 @@ class TestRackSslEnforcer < Test::Unit::TestCase 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 end - - context 'that has hsts options set' do setup { mock_app :hsts => {:expires => '500', :subdomains => false} } - + should 'set expiry option' do get 'https://www.example.org/' - assert_equal "max-age=500", last_response.headers["Strict-Transport-Security"] + assert_equal "max-age=500", last_response.headers["Strict-Transport-Security"] end - + should 'not include subdomains' do get 'https://www.example.org/' assert !last_response.headers["Strict-Transport-Security"].include?("includeSubDomains") end end - + end