lib/safe_cookies.rb in safe_cookies-0.1.4 vs lib/safe_cookies.rb in safe_cookies-0.1.5

- old
+ new

@@ -1,25 +1,33 @@ # -*- encoding: utf-8 -*- require "safe_cookies/configuration" require "safe_cookies/cookie_path_fix" +require "safe_cookies/helpers" require "safe_cookies/util" require "safe_cookies/version" require "rack" +# Naming: +# - application_cookies: cookies received from the application. The 'Set-Cookie' header is a string +# - request_cookies: cookies received from the client. Rack::Request#cookies returns a Hash of { 'name' => 'value' } +# - response_cookies: cookies to be sent to the client +# (= application_cookies + any cookies set in the middleware) + module SafeCookies UnknownCookieError = Class.new(StandardError) - CACHE_COOKIE_NAME = '_safe_cookies__known_cookies' + STORE_COOKIE_NAME = '_safe_cookies__known_cookies' SECURED_COOKIE_NAME = 'secured_old_cookies' HELPER_COOKIES_LIFETIME = 10 * 365 * 24 * 60 * 60 # 10 years class Middleware include CookiePathFix + include Helpers - KNOWN_COOKIES_DIVIDER = '|' + COOKIE_NAME_REGEX = /(?=^|\n)[^\n;,=]+/i def initialize(app) @app = app @configuration = SafeCookies.configuration or raise "Don't know what to do without configuration" @@ -27,82 +35,52 @@ def call(env) reset_instance_variables @request = Rack::Request.new(env) - ensure_no_unknown_cookies! + ensure_no_unknown_cookies_in_request! status, @headers, body = @app.call(env) - - fix_cookie_paths if fix_cookie_paths? - rewrite_request_cookies unless cookies_have_been_rewritten_before - cache_application_cookies + cache_application_cookies_string + + remove_application_cookies_from_request_cookies rewrite_application_cookies + store_application_cookie_names + fix_cookie_paths if fix_cookie_paths? + rewrite_request_cookies unless cookies_have_been_rewritten_before? [ status, @headers, body ] end private def reset_instance_variables - @request, @headers = nil + @request, @headers, @application_cookies = nil end - def secure(cookie) - # Regexp from https://github.com/tobmatth/rack-ssl-enforcer/ - if should_be_secure?(cookie) and cookie !~ /(^|;\s)secure($|;)/ - "#{cookie}; secure" - else - cookie + def ensure_no_unknown_cookies_in_request! + request_cookie_names = request_cookies.keys.map(&:to_s) + unknown_cookie_names = request_cookie_names - known_cookie_names + + if unknown_cookie_names.any? + handle_unknown_cookies(unknown_cookie_names) end end - def http_only(cookie) - if should_be_http_only?(cookie) and cookie !~ /(^|;\s)HttpOnly($|;)/ - "#{cookie}; HttpOnly" - else - cookie - end - end - - # This method takes all cookies sent with the request and rewrites them, - # making them both secure and http-only (unless specified otherwise in - # the configuration). - # With the SECURED_COOKIE_NAME cookie we remember the exact time that we - # rewrote the cookies. - def rewrite_request_cookies - if @request.cookies.any? - registered_cookies_in_request.each do |registered_cookie, options| - value = @request.cookies[registered_cookie] - - set_cookie!(registered_cookie, value, options) + def remove_application_cookies_from_request_cookies + if @application_cookies + application_cookie_names = @application_cookies.scan(COOKIE_NAME_REGEX) + application_cookie_names.each do |cookie| + request_cookies.delete(cookie) end - - formatted_now = Rack::Utils.rfc2822(Time.now.gmtime) - set_cookie!(SECURED_COOKIE_NAME, formatted_now, :expire_after => HELPER_COOKIES_LIFETIME) end end - def set_cookie!(name, value, options) - options = options.dup - expire_after = options.delete(:expire_after) - - options[:expires] = Time.now + expire_after if expire_after - options[:path] = '/' unless options.has_key?(:path) # allow setting path = nil - options[:value] = value - options[:secure] = should_be_secure?(name) - options[:httponly] = should_be_http_only?(name) - - Rack::Utils.set_cookie_header!(@headers, name, options) - end - def rewrite_application_cookies - cookies = @headers['Set-Cookie'] - if cookies - # Rails 2.3 / Rack 1.1 offers an array which is actually nice. - cookies = cookies.split("\n") unless cookies.is_a?(Array) - + if @application_cookies + cookies = @application_cookies.split("\n") + # On Rack 1.1, cookie values sometimes contain trailing newlines. # Example => ["foo=1; path=/\n", "bar=2; path=/"] # Note that they also mess up browsers, when this array is merged # again and the "Set-Cookie" header then contains double newlines. cookies = cookies. @@ -116,72 +94,39 @@ # browser's request contained, so a `Rack::Request` can't parse it for # us. A `Rack::Response` doesn't offer a way either. @headers['Set-Cookie'] = cookies.join("\n") end end - - def should_be_secure?(cookie) - cookie_name = cookie.split('=').first.strip - ssl? and not @configuration.insecure_cookie?(cookie_name) - end - - def ssl? - if @request.respond_to?(:ssl?) - @request.ssl? - else - # older Rack versions - @request.scheme == 'https' + + def store_application_cookie_names + if @application_cookies + application_cookie_names = stored_application_cookie_names + @application_cookies.scan(COOKIE_NAME_REGEX) + application_cookies_string = application_cookie_names.uniq.join(KNOWN_COOKIES_DIVIDER) + + set_cookie!(STORE_COOKIE_NAME, application_cookies_string, :expire_after => HELPER_COOKIES_LIFETIME) end end - def should_be_http_only?(cookie) - cookie_name = cookie.split('=').first.strip - not @configuration.scriptable_cookie?(cookie_name) - end - - def ensure_no_unknown_cookies! - request_cookies = @request.cookies.keys.map(&:to_s) - unknown_cookies = request_cookies - known_cookies + # This method takes all cookies sent with the request and rewrites them, + # making them both secure and http-only (unless specified otherwise in + # the configuration). + # With the SECURED_COOKIE_NAME cookie we remember the exact time that we + # rewrote the cookies. + def rewrite_request_cookies + if request_cookies.any? + registered_cookies_in_request.each do |cookie_name, options| + value = request_cookies[cookie_name] + + set_cookie!(cookie_name, value, options) + end - if unknown_cookies.any? - handle_unknown_cookies(unknown_cookies) + formatted_now = Rack::Utils.rfc2822(Time.now.gmtime) + set_cookie!(SECURED_COOKIE_NAME, formatted_now, :expire_after => HELPER_COOKIES_LIFETIME) end end - def handle_unknown_cookies(cookies) - raise SafeCookies::UnknownCookieError.new("Request for '#{@request.url}' had unknown cookies: #{cookies.join(', ')}") - end - - def cache_application_cookies - new_application_cookies = @headers['Set-Cookie'] - - if new_application_cookies - new_application_cookies = new_application_cookies.join("\n") if new_application_cookies.is_a?(Array) - application_cookies = cached_application_cookies + new_application_cookies.scan(/(?=^|\n)[^\n;,=]+/i) - application_cookies_string = application_cookies.uniq.join(KNOWN_COOKIES_DIVIDER) - - set_cookie!(CACHE_COOKIE_NAME, application_cookies_string, :expire_after => HELPER_COOKIES_LIFETIME) - end - end - - def cached_application_cookies - cache_cookie = @request.cookies[CACHE_COOKIE_NAME] || "" - cache_cookie.split(KNOWN_COOKIES_DIVIDER) - end - - def known_cookies - known = [CACHE_COOKIE_NAME, SECURED_COOKIE_NAME] - known += cached_application_cookies - known += @configuration.registered_cookies.keys - end - - def cookies_have_been_rewritten_before - @request.cookies.has_key? SECURED_COOKIE_NAME - end - - # returns those of the registered cookies that appear in the request - def registered_cookies_in_request - Util.slice(@configuration.registered_cookies, *@request.cookies.keys) + def handle_unknown_cookies(cookie_names) + raise SafeCookies::UnknownCookieError.new("Request for '#{@request.url}' had unknown cookies: #{cookie_names.join(', ')}") end end end