ssl.rb 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. require 'rack'
  2. require 'rack/request'
  3. module Rack
  4. class SSL
  5. YEAR = 31536000
  6. def self.default_hsts_options
  7. { :expires => YEAR, :subdomains => false }
  8. end
  9. def initialize(app, options = {})
  10. @app = app
  11. @hsts = options[:hsts]
  12. @hsts = {} if @hsts.nil? || @hsts == true
  13. @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts
  14. @exclude = options[:exclude]
  15. @host = options[:host]
  16. end
  17. def call(env)
  18. if @exclude && @exclude.call(env)
  19. @app.call(env)
  20. elsif scheme(env) == 'https'
  21. status, headers, body = @app.call(env)
  22. headers = hsts_headers.merge(headers)
  23. flag_cookies_as_secure!(headers)
  24. [status, headers, body]
  25. else
  26. redirect_to_https(env)
  27. end
  28. end
  29. private
  30. # Fixed in rack >= 1.3
  31. def scheme(env)
  32. if env['HTTPS'] == 'on'
  33. 'https'
  34. elsif env['HTTP_X_FORWARDED_PROTO']
  35. env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
  36. else
  37. env['rack.url_scheme']
  38. end
  39. end
  40. def redirect_to_https(env)
  41. req = Request.new(env)
  42. url = URI(req.url)
  43. url.scheme = "https"
  44. url.host = @host if @host
  45. headers = hsts_headers.merge('Content-Type' => 'text/html',
  46. 'Location' => url.to_s)
  47. [301, headers, []]
  48. end
  49. # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
  50. def hsts_headers
  51. if @hsts
  52. value = "max-age=#{@hsts[:expires]}"
  53. value += "; includeSubDomains" if @hsts[:subdomains]
  54. { 'Strict-Transport-Security' => value }
  55. else
  56. {}
  57. end
  58. end
  59. def flag_cookies_as_secure!(headers)
  60. if cookies = headers['Set-Cookie']
  61. # Rack 1.1's set_cookie_header! will sometimes wrap
  62. # Set-Cookie in an array
  63. unless cookies.respond_to?(:to_ary)
  64. cookies = cookies.split("\n")
  65. end
  66. headers['Set-Cookie'] = cookies.map { |cookie|
  67. if cookie !~ /; secure(;|$)/
  68. "#{cookie}; secure"
  69. else
  70. cookie
  71. end
  72. }.join("\n")
  73. end
  74. end
  75. end
  76. end