0
0
Fork 0

add cookie support to response

This commit adds support for response cookies. Response now has a method
cookie to fetch the current cookie. One cookie has multiple crumbs which
represent a key value pair. For each crumb multiple options can be set
according to the specs.
This commit is contained in:
Gibheer 2013-10-28 14:37:24 +01:00
parent 290165c440
commit a0422c1054
6 changed files with 236 additions and 6 deletions

View File

@ -1,3 +1,5 @@
require 'zero/response/cookie'
module Zero
# This is the representation of a response
class Response
@ -10,18 +12,17 @@ module Zero
# Constructor
# Sets default status code to 200.
#
def initialize
@status = 200
@header = {}
@body = []
@cookies = {}
end
# Sets the status.
# Also converts every input directly to an integer.
#
# @param [Integer] status The status code
#
def status=(status)
@status = status.to_i
end
@ -53,8 +54,8 @@ module Zero
# Removes Content-Type, Content-Length and body on status code 204 and 304.
#
# @return [Array] Usable by webservers
#
def to_a
add_cookie_headers
# Remove content length and body, on status 204 and 304
if status == 204 or status == 304
header.delete('Content-Length')
@ -72,7 +73,6 @@ module Zero
# Sets the content length header to the current length of the body
# Also creates one, if it does not exists
#
def content_length
self.header['Content-Length'] = body.join.bytesize.to_s
end
@ -81,7 +81,6 @@ module Zero
# Also creates it, if it does not exists
#
# @param [String] value Content-Type tp set
#
def content_type=(value)
self.header['Content-Type'] = value
end
@ -89,10 +88,25 @@ module Zero
# Sets the Location header to the given URL and the status code to 302.
#
# @param [String] location Redirect URL
#
def redirect(location, status = 302)
self.status = status
self.header['Location'] = location
end
# get the cookie for the response
#
# This returns the cookie holding all crumbs for the response.
# @response [Cookie] the cookie with crumbs holding the information
def cookie
@cookie ||= Cookie.new
end
private
# merge the cookie header into the other headers
def add_cookie_headers
return unless @cookie
header.merge!(cookie.to_header)
end
end
end

105
lib/zero/response/cookie.rb Normal file
View File

@ -0,0 +1,105 @@
module Zero
class Response
class Cookie
# initialize an empty cookie
def initialize
@crumbs = {}
end
# add a new crumb
#
# This adds a new crumb to the cookie specified through the key.
# @param key [String] the identifier for the crumb
# @param value [String] the value for the crumb
# @param options [Hash] hash with further options for the crumb
# @option options [Time] :expire the time when the crumb should expire
# @option options [String] :domain the domain the crumb should be sent to
# @option options [String] :path path when the crumb should be sent
# @option options [Array] :flags set flags for :secure or :http_only
def add_crumb(key, value, options = {:flags => []})
@crumbs[key] = Crumb.new(key, value, options)
end
# get the crumb for the key
#
# This method returns the crumb for the specified key. The crumb holds all
# information, like the expire time and domain and so on.
# @param key [String] the key to return
# @returns [Cookie::Crumb] a cookie crumb or nil when the key
# does not exist
def get_crumb(key)
@crumbs[key]
end
# merge all crumbs to one header line
#
# This merges all crumbs together to a header line, where each cookie is
# separated by the `Set-Cookie` header.
# @returns [Hash] a key value pair to merge with the headers
def to_header
{'Set-Cookie' => @crumbs.map{|key, crumb| crumb}.join("\nSet-Cookie: ")}
end
private
class Crumb
attr_reader :key, :secure, :http_only
attr_accessor :domain, :path, :expire, :value
def initialize(key, value, options = {})
options[:flags] ||= []
@key = key
@value = value
@domain = options[:domain]
@expire = options[:expire]
@path = options[:path]
@secure = options[:flags].include?(:secure)
@http_only = options[:flags].include?(:http_only)
end
# set the `http_only` flag
#
# This method sets the flag to only allow modifications from the
# server and makes the browser not allow modifications through
# javascript.
def deny_client_side_modification!
@http_only = true
end
# remove the `http_only` flag
#
# This removes the `http_only` flag to allow modifications of the
# crumb through javascript.
def allow_client_side_modification!
@http_only = false
end
# set the `secure` flag
#
# This sets the `secure` flag on the crumb which tells the browser to
# only send it through secure channels, like https.
# Keep in mind, that this does not encrypt the content of the Crumb!
def secure!
@secure = true
end
# unset the `secure` flag
#
# This unsets the `secure` flag which tells the browser, that it can
# send the crumb over unsecure channel too, like plain http.
def unsecure!
@secure = false
end
def to_s
"#{@key}=#{@value}" +
(@expire ? "; Expires=#{@expire.rfc2822}" : '') +
(@path ? "; Path=#{@path}" : '') +
(@domain ? "; Domain=#{domain}" : '') +
(@http_only ? '; HttpOnly' : '') +
(@secure ? '; Secure' : '')
end
end
end
end
end

View File

@ -0,0 +1,54 @@
require 'spec_helper'
describe Zero::Response::Cookie, '#add_crumb' do
let(:cookie) { Zero::Response::Cookie.new }
subject { cookie.add_crumb(key, value, options) }
let(:options) { {} }
let(:key) { 'key' }
let(:value) { 'value' }
before :each do
subject
end
context 'with no argument' do
it 'adds a new crumb' do
expect(cookie.get_crumb(key).key).to be(key)
end
end
context 'with flags' do
let(:options) { {:flags => [:secure, :http_only]} }
it 'adds a crumb with secure header' do
expect(cookie.get_crumb(key).secure).to be(true)
end
it 'adds a crumb with http_only header' do
expect(cookie.get_crumb(key).http_only).to be(true)
end
end
context 'with expire' do
let(:time) { Time.now }
let(:options) { {:expire => time} }
it 'adds a crumb with expire header' do
expect(cookie.get_crumb(key).expire).to be(time)
end
end
context 'with domain and path' do
let(:domain) { 'libzero.org' }
let(:path) { '/admin' }
let(:options) { {:domain => domain, :path => path} }
it 'adds a crumb with domain header' do
expect(cookie.get_crumb(key).domain).to be(domain)
end
it 'adds a crumb with path header' do
expect(cookie.get_crumb(key).path).to be(path)
end
end
end

View File

@ -0,0 +1,21 @@
require 'spec_helper'
describe Zero::Response::Cookie, '#add_crumb' do
let(:cookie) { Zero::Response::Cookie.new }
subject { cookie.add_crumb(key, value, options) }
let(:options) { {} }
let(:key) { 'key' }
let(:value) { 'value' }
before :each do
subject
end
it 'returns the crumb when the crumb exists' do
expect(cookie.get_crumb(key).key).to be(key)
end
it 'returns nil for the wrong key' do
expect(cookie.get_crumb('wrong key')).to be(nil)
end
end

View File

@ -0,0 +1,29 @@
require 'spec_helper'
describe Zero::Response::Cookie, '#add_crumb' do
let(:cookie) { Zero::Response::Cookie.new }
subject { cookie.add_crumb(key, value, options) }
let(:options) { {
:domain => domain,
:path => path,
:expire => time,
:flags => flags
} }
let(:key) { 'key' }
let(:value) { 'value' }
let(:time) { Time.new }
let(:domain) { 'libzero.org' }
let(:path) { '/admin' }
let(:flags) { [:secure, :http_only] }
before :each do
subject
end
it 'returns the header line' do
expect(cookie.to_header).to eq(
{'Set-Cookie' => "#{key}=#{value}; Expires=#{time.rfc2822};" +
" Path=#{path}; Domain=#{domain}; HttpOnly; Secure"}
)
end
end

View File

@ -69,5 +69,12 @@ describe Zero::Response do
value[1].should eq({}) # Headers
value[2].should eq([]) # Body
end
it "adds the cookie to the headers" do
key = 'key'
value = 'value'
subject.cookie.add_crumb(key, value)
expect(subject.to_a[1]['Set-Cookie']).to eq("#{key}=#{value}")
end
end
end