Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Break up the cookies classes #1024

Merged
merged 3 commits into from
Jan 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 2 additions & 259 deletions src/amber/router/cookies.cr
Original file line number Diff line number Diff line change
@@ -1,270 +1,13 @@
require "http"
require "./cookies/*"
require "../support/*"

# Defines a better cookie store for the request
# The cookies being read are the ones received along with the request,
# the cookies being written will be sent out with the response.
# Reading a cookie does not get the cookie object itself back, just the value it holds.
module Amber::Router
class Cookies
module ChainedStore
def permanent
@permanent ||= PermanentStore.new(self)
end

def encrypted
@encrypted ||= EncryptedStore.new(self, @secret)
end

def signed
@signed ||= SignedStore.new(self, @secret)
end
end

module Cookies
# Cookies can typically store 4096 bytes.
MAX_COOKIE_SIZE = 4096

class Store
include Enumerable(String)
include ChainedStore

getter cookies
getter secret : String
property host : String?
property secure : Bool = false

def initialize(@host = nil, @secret = Random::Secure.urlsafe_base64(32), @secure = false)
@cookies = {} of String => String
@set_cookies = {} of String => HTTP::Cookie
@delete_cookies = {} of String => HTTP::Cookie
end

def self.from_headers(headers)
cookies = {} of String => HTTP::Cookie
if values = headers.get?("Cookie")
values.each do |header|
HTTP::Cookie::Parser.parse_cookies(header) do |cookie|
cookies[cookie.name] = cookie
end
end
end

if values = headers.get?("Set-Cookie")
values.each do |header|
HTTP::Cookie::Parser.parse_cookies(header) do |cookie|
cookies[cookie.name] = cookie
end
end
end

cookies
end

def self.build(request, secret)
headers = request.headers
host = request.host
secure = (headers["HTTPS"]? == "on")
new(host, secret, secure).tap do |store|
store.update(from_headers(headers))
end
end

def update(cookies)
cookies.each do |name, cookie|
@cookies[name] = cookie.value
end
end

def each(&block : T -> _)
@cookies.values.each do |cookie|
yield cookie
end
end

def each
@cookies.each_value
end

def [](name)
get(name)
end

def get(name)
@cookies[name]?
end

def set(name : String, value : String, path : String = "/",
expires : Time? = nil, domain : String? = nil,
secure : Bool = false, http_only : Bool = false,
extension : String? = nil)
if @cookies[name]? != value || expires
@cookies[name] = value
@set_cookies[name] = HTTP::Cookie.new(name, value, path, expires, domain, secure, http_only, extension)
@delete_cookies.delete(name) if @delete_cookies.has_key?(name)
end
end

def delete(name : String, path = "/", domain : String? = nil)
return unless @cookies.has_key?(name)

value = @cookies.delete(name)
@delete_cookies[name] = HTTP::Cookie.new(name, "", path, ::Time.unix(0), domain)
value
end

def deleted?(name)
@delete_cookies.has_key?(name)
end

def []=(name, value)
set(name, value)
end

def []=(name, cookie : HTTP::Cookie)
@cookies[name] = cookie.value
@set_cookies[name] = cookie
end

def write(headers)
cookies = [] of String
@set_cookies.each { |_, cookie| cookies << cookie.to_set_cookie_header if write_cookie?(cookie) }
@delete_cookies.each { |_, cookie| cookies << cookie.to_set_cookie_header }
headers.add("Set-Cookie", cookies)
end

def write_cookie?(cookie)
@secure || !cookie.secure
end
end

class JsonSerializer
def self.load(value)
JSON.parse(value)
end

def self.dump(value)
value.to_json
end
end

module SerializedStore
protected def serialize(value)
serializer.dump(value)
end

protected def deserialize(value)
serializer.load(value).to_s
end

protected def serializer
JsonSerializer
end

protected def digest
:sha256
end
end

class PermanentStore
include ChainedStore

getter store : Store

def initialize(@store)
end

def [](name)
get(name)
end

def get(name)
@store.get(name)
end

def []=(name, value)
set(name, value)
end

def set(name : String, value : String, path : String = "/", domain : String? = nil, secure : Bool = false, http_only : Bool = false, extension : String? = nil)
cookie = HTTP::Cookie.new(name, value, path, 20.years.from_now, domain, secure, http_only, extension)
@store[name] = cookie
end
end

class SignedStore
include ChainedStore

getter store : Store

def initialize(@store, secret)
@verifier = Support::MessageVerifier.new(secret)
end

def [](name)
get(name)
end

def []=(name, value)
set(name, value)
end

def get(name)
if value = @store.get(name)
verify(value)
end
end

def set(name : String, value : String, path : String = "/", expires : Time? = nil, domain : String? = nil, secure : Bool = false, http_only : Bool = false, extension : String? = nil)
cookie = HTTP::Cookie.new(name, @verifier.generate(value), path, expires, domain, secure, http_only, extension)
raise Exceptions::CookieOverflow.new if cookie.value.bytesize > MAX_COOKIE_SIZE
@store[name] = cookie
end

private def verify(message)
@verifier.verify(message)
rescue e # TODO: This should probably actually raise the exception instead of rescuing from it.
""
end
end

class EncryptedStore
include ChainedStore
include SerializedStore

getter store : Store

def initialize(@store, secret)
@encryptor = Support::MessageEncryptor.new(secret, digest: digest)
end

def [](name)
get(name)
end

def []=(name, value)
set(name, value)
end

def get(name)
if value = @store.get(name)
verify_and_decrypt(value)
end
end

def set(name : String, value : String, path : String = "/", expires : Time? = nil,
domain : String? = nil, secure : Bool = false,
http_only : Bool = false, extension : String? = nil)
cookie = HTTP::Cookie.new(name, Base64.strict_encode(@encryptor.encrypt(value, sign: true)),
path, expires, domain, secure, http_only, extension)
raise Exceptions::CookieOverflow.new if cookie.value.bytesize > MAX_COOKIE_SIZE
@store[name] = cookie
end

private def verify_and_decrypt(encrypted_message)
String.new(@encryptor.verify_and_decrypt(Base64.decode(encrypted_message)))
rescue e # TODO: This should probably actually raise the exception instead of rescuing from it.
""
end
end
end
end
20 changes: 20 additions & 0 deletions src/amber/router/cookies/abstract_store.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Amber::Router::Cookies
abstract class AbstractStore
getter store : Store

def initialize(@store)
end

def [](name)
get(name)
end

def []=(name, value)
set(name, value)
end

abstract def get(name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use this opportunity to change it to get?. This way we have a free #[]? and a cheaper way to handle non-existent keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, I'd rather tackle that sort of improvement in another PR. The focus of this one was to break up the classes, remove dead code and have it all still work.


abstract def set(name : String, value : String, path : String = "/", expires : Time? = nil, domain : String? = nil, secure : Bool = false, http_only : Bool = false, extension : String? = nil)
end
end
31 changes: 31 additions & 0 deletions src/amber/router/cookies/encrypted_store.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require "http"
require "../../support/*"

module Amber::Router::Cookies
class EncryptedStore < AbstractStore
def initialize(@store, secret)
@encryptor = Support::MessageEncryptor.new(secret, digest: :sha256)
end

def get(name)
if value = @store.get(name)
verify_and_decrypt(value)
end
end

def set(name : String, value : String, path : String = "/", expires : Time? = nil,
domain : String? = nil, secure : Bool = false,
http_only : Bool = false, extension : String? = nil)
cookie = HTTP::Cookie.new(name, Base64.strict_encode(@encryptor.encrypt(value, sign: true)),
path, expires, domain, secure, http_only, extension)
raise Exceptions::CookieOverflow.new if cookie.value.bytesize > MAX_COOKIE_SIZE
@store[name] = cookie
end

private def verify_and_decrypt(encrypted_message)
String.new(@encryptor.verify_and_decrypt(Base64.decode(encrypted_message)))
rescue e # TODO: This should probably actually raise the exception instead of rescuing from it.
""
end
end
end
15 changes: 15 additions & 0 deletions src/amber/router/cookies/permanent_store.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require "http"
require "./store"

module Amber::Router::Cookies
class PermanentStore < AbstractStore
def get(name)
@store.get(name)
end

def set(name : String, value : String, path : String = "/", expires : Time? = nil, domain : String? = nil, secure : Bool = false, http_only : Bool = false, extension : String? = nil)
cookie = HTTP::Cookie.new(name, value, path, 20.years.from_now, domain, secure, http_only, extension)
@store[name] = cookie
end
end
end
29 changes: 29 additions & 0 deletions src/amber/router/cookies/signed_store.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require "./*"
require "http"
require "../../support/*"

module Amber::Router::Cookies
class SignedStore < AbstractStore
def initialize(@store, secret)
@verifier = Support::MessageVerifier.new(secret)
end

def get(name)
if value = @store.get(name)
verify(value)
end
end

def set(name : String, value : String, path : String = "/", expires : Time? = nil, domain : String? = nil, secure : Bool = false, http_only : Bool = false, extension : String? = nil)
cookie = HTTP::Cookie.new(name, @verifier.generate(value), path, expires, domain, secure, http_only, extension)
raise Exceptions::CookieOverflow.new if cookie.value.bytesize > MAX_COOKIE_SIZE
@store[name] = cookie
end

private def verify(message)
@verifier.verify(message)
rescue e # TODO: This should probably actually raise the exception instead of rescuing from it.
""
end
end
end
Loading