Permalink
Browse files

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.
  • Loading branch information...
1 parent 290165c commit a0422c10546b2e0dea252e68d1870f362095cdab @Gibheer Gibheer committed Oct 28, 2013
View
@@ -1,3 +1,5 @@
+require 'zero/response/cookie'
+
module Zero
# This is the representation of a response
class Response
@@ -10,18 +12,17 @@ class Response
# 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 @@ def body=(content)
# 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 @@ def to_a
# 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,18 +81,32 @@ def content_length
# 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
# 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
View
@@ -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
@@ -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
@@ -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
@@ -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
@@ -69,5 +69,12 @@
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

0 comments on commit a0422c1

Please sign in to comment.