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:
parent
290165c440
commit
a0422c1054
|
@ -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
|
||||
|
|
|
@ -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 @@ 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
|
||||
|
|
Loading…
Reference in New Issue