From a0422c10546b2e0dea252e68d1870f362095cdab Mon Sep 17 00:00:00 2001 From: Gibheer Date: Mon, 28 Oct 2013 14:37:24 +0100 Subject: [PATCH] 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. --- lib/zero/response.rb | 26 ++++- lib/zero/response/cookie.rb | 105 ++++++++++++++++++ .../zero/response/cookie/add_crumb_spec.rb | 54 +++++++++ .../zero/response/cookie/get_crumb_spec.rb | 21 ++++ .../zero/response/cookie/to_header_spec.rb | 29 +++++ spec/unit/zero/response/to_a_spec.rb | 7 ++ 6 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 lib/zero/response/cookie.rb create mode 100644 spec/unit/zero/response/cookie/add_crumb_spec.rb create mode 100644 spec/unit/zero/response/cookie/get_crumb_spec.rb create mode 100644 spec/unit/zero/response/cookie/to_header_spec.rb diff --git a/lib/zero/response.rb b/lib/zero/response.rb index ab4acde..cb58cfe 100644 --- a/lib/zero/response.rb +++ b/lib/zero/response.rb @@ -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 diff --git a/lib/zero/response/cookie.rb b/lib/zero/response/cookie.rb new file mode 100644 index 0000000..45ef65f --- /dev/null +++ b/lib/zero/response/cookie.rb @@ -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 diff --git a/spec/unit/zero/response/cookie/add_crumb_spec.rb b/spec/unit/zero/response/cookie/add_crumb_spec.rb new file mode 100644 index 0000000..3c8dbee --- /dev/null +++ b/spec/unit/zero/response/cookie/add_crumb_spec.rb @@ -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 diff --git a/spec/unit/zero/response/cookie/get_crumb_spec.rb b/spec/unit/zero/response/cookie/get_crumb_spec.rb new file mode 100644 index 0000000..3c315db --- /dev/null +++ b/spec/unit/zero/response/cookie/get_crumb_spec.rb @@ -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 diff --git a/spec/unit/zero/response/cookie/to_header_spec.rb b/spec/unit/zero/response/cookie/to_header_spec.rb new file mode 100644 index 0000000..7cdc278 --- /dev/null +++ b/spec/unit/zero/response/cookie/to_header_spec.rb @@ -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 diff --git a/spec/unit/zero/response/to_a_spec.rb b/spec/unit/zero/response/to_a_spec.rb index b016eb9..b547f98 100644 --- a/spec/unit/zero/response/to_a_spec.rb +++ b/spec/unit/zero/response/to_a_spec.rb @@ -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