majoun 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ ---
2
+ threshold: 50.5
data/lib/cookie.rb ADDED
@@ -0,0 +1,89 @@
1
+ # encoding: utf-8
2
+
3
+ require 'time'
4
+ require 'base64'
5
+
6
+ require 'concord'
7
+ require 'adamantium'
8
+ require 'abstract_type'
9
+
10
+ # Models an HTTP Cookie
11
+ class Cookie
12
+
13
+ # An empty frozen array
14
+ EMPTY_ARRAY = [].freeze
15
+
16
+ # An empty frozen hash
17
+ EMPTY_HASH = {}.freeze
18
+
19
+ # An empty frozen string
20
+ EMPTY_STRING = ''.freeze
21
+
22
+ # Separates the cookie name from its value
23
+ NAME_VALUE_SEPARATOR = '='.freeze
24
+
25
+ # Separates cookies
26
+ COOKIE_SEPARATOR = '; '.freeze
27
+
28
+ # Separates ruby class names in a FQN
29
+ DOUBLE_COLON = '::'.freeze
30
+
31
+ # Helper for cookie deletion
32
+ class Empty < self
33
+ def initialize(name)
34
+ super(name, nil)
35
+ end
36
+ end
37
+
38
+ # Namespace for cookie encoders
39
+ module Encoder
40
+ Base64 = ->(string) { ::Base64.urlsafe_encode64(string) }
41
+ end
42
+
43
+ # Namespace for cookie decoders
44
+ module Decoder
45
+ Base64 = ->(string) { ::Base64.urlsafe_decode64(string) }
46
+ end
47
+
48
+ # Cookie error base class
49
+ Error = Class.new(StandardError)
50
+
51
+ include Concord::Public.new(:name, :value)
52
+ include Adamantium::Flat
53
+
54
+ def self.coerce(string)
55
+ new(*string.split(NAME_VALUE_SEPARATOR, 2))
56
+ end
57
+
58
+ def encode(encoder = Encoder::Base64)
59
+ new(encoder.call(value))
60
+ end
61
+
62
+ def decode(decoder = Decoder::Base64)
63
+ new(decoder.call(value))
64
+ end
65
+
66
+ def encrypt(box)
67
+ new(box.encrypt(value))
68
+ end
69
+
70
+ def decrypt(box)
71
+ new(box.decrypt(value))
72
+ end
73
+
74
+ def to_s
75
+ "#{name}=#{value}"
76
+ end
77
+ memoize :to_s
78
+
79
+ private
80
+
81
+ def new(new_value)
82
+ self.class.new(name, new_value)
83
+ end
84
+
85
+ end # class Cookie
86
+
87
+ require 'cookie/header'
88
+ require 'cookie/header/attribute'
89
+ require 'cookie/registry'
@@ -0,0 +1,70 @@
1
+ # encoding: utf-8
2
+
3
+ class Cookie
4
+
5
+ # Models a transient, new cookie on the server that can be serialized
6
+ # into an HTTP 'Set-Cookie' header
7
+ class Header
8
+
9
+ include Equalizer.new(:cookie, :attributes)
10
+ include Adamantium::Flat
11
+
12
+ attr_reader :cookie
13
+ protected :cookie
14
+
15
+ attr_reader :attributes
16
+ protected :attributes
17
+
18
+ def self.build(name, value, attributes)
19
+ new(Cookie.new(name, value), attributes)
20
+ end
21
+
22
+ def initialize(cookie, attributes = Attribute::Set::EMPTY)
23
+ @cookie, @attributes = cookie, attributes
24
+ end
25
+
26
+ def with_domain(domain)
27
+ with_attribute(Attribute::Domain.new(domain))
28
+ end
29
+
30
+ def with_path(path)
31
+ with_attribute(Attribute::Path.new(path))
32
+ end
33
+
34
+ def with_max_age(seconds)
35
+ with_attribute(Attribute::MaxAge.new(seconds))
36
+ end
37
+
38
+ def with_expires(time)
39
+ with_attribute(Attribute::Expires.new(time))
40
+ end
41
+
42
+ def secure
43
+ with_attribute(Attribute::Secure.instance)
44
+ end
45
+
46
+ def http_only
47
+ with_attribute(Attribute::HttpOnly.instance)
48
+ end
49
+
50
+ def delete
51
+ new(Empty.new(cookie.name), attributes.merge(Attribute::Expired))
52
+ end
53
+
54
+ def to_s
55
+ "#{cookie}#{attributes}"
56
+ end
57
+ memoize :to_s
58
+
59
+ private
60
+
61
+ def with_attribute(attribute)
62
+ new(cookie, attributes.merge(attribute))
63
+ end
64
+
65
+ def new(cookie, attributes)
66
+ self.class.new(cookie, attributes)
67
+ end
68
+
69
+ end # class Header
70
+ end # class Cookie
@@ -0,0 +1,172 @@
1
+ # encoding: utf-8
2
+
3
+ class Cookie
4
+ class Header
5
+
6
+ # Baseclass for cookie attributes
7
+ class Attribute
8
+
9
+ include Concord::Public.new(:name)
10
+ include Adamantium
11
+
12
+ REGISTRY = {}
13
+
14
+ def self.coerce(name, value)
15
+ REGISTRY.fetch(name.to_sym).build(value)
16
+ end
17
+
18
+ def self.register_as(name)
19
+ REGISTRY[name.to_sym] = self
20
+ end
21
+
22
+ def self.attribute_name
23
+ name.split(DOUBLE_COLON).last
24
+ end
25
+
26
+ def to_s
27
+ name
28
+ end
29
+
30
+ # Models a set of attributes used within a {Serializable} cookie
31
+ class Set
32
+
33
+ include Concord.new(:attributes)
34
+ include Enumerable
35
+ include Adamantium
36
+
37
+ def self.coerce(attributes)
38
+ new(attributes.each_with_object({}) { |(name, value), hash|
39
+ attribute = Attribute.coerce(name, value)
40
+ hash[attribute.name] = attribute if attribute
41
+ })
42
+ end
43
+
44
+ def each(&block)
45
+ return to_enum unless block
46
+ attributes.each_value(&block)
47
+ self
48
+ end
49
+
50
+ def merge(attribute)
51
+ Set.new(attributes.merge(attribute.name => attribute))
52
+ end
53
+
54
+ def to_s
55
+ "#{COOKIE_SEPARATOR}#{map(&:to_s).join(COOKIE_SEPARATOR)}"
56
+ end
57
+ memoize :to_s
58
+
59
+ # An empty {Set} to be serialized to {EMPTY_STRING}
60
+ class Empty < self
61
+
62
+ def initialize
63
+ super(EMPTY_HASH)
64
+ end
65
+
66
+ def to_s
67
+ EMPTY_STRING
68
+ end
69
+
70
+ end # class Empty
71
+
72
+ EMPTY = Empty.new
73
+
74
+ end # class Set
75
+
76
+ # Abstract baseclass for attributes that have no value
77
+ #
78
+ # @abstract
79
+ class Unary < self
80
+
81
+ include AbstractType
82
+
83
+ CACHE = {}
84
+
85
+ def self.build(value)
86
+ value ? instance : nil
87
+ end
88
+
89
+ def self.instance
90
+ CACHE.fetch(attribute_name) {
91
+ CACHE[attribute_name] = new(attribute_name)
92
+ }
93
+ end
94
+
95
+ end
96
+
97
+ # Abstract baseclass for attributes that consist of a name-value
98
+ # pair
99
+ #
100
+ # @abstract
101
+ class Binary < self
102
+
103
+ include AbstractType
104
+ include Equalizer.new(:name, :value)
105
+
106
+ attr_reader :value
107
+ protected :value
108
+
109
+ def self.build(value)
110
+ new(value)
111
+ end
112
+
113
+ def initialize(value)
114
+ super(self.class.attribute_name)
115
+ @value = value
116
+ end
117
+
118
+ def to_s
119
+ "#{name}=#{serialized_value}"
120
+ end
121
+ memoize :to_s
122
+
123
+ private
124
+
125
+ def serialized_value
126
+ value
127
+ end
128
+
129
+ end # class Binary
130
+
131
+ # The Domain attribute
132
+ class Domain < Binary
133
+ register_as :domain
134
+ end
135
+
136
+ # The Path attribute
137
+ class Path < Binary
138
+ register_as :path
139
+ end
140
+
141
+ # The Max-Age attribute
142
+ class MaxAge < Binary
143
+ register_as :max_age
144
+ end
145
+
146
+ # The Expires attribute
147
+ class Expires < Binary
148
+ register_as :expires
149
+
150
+ private
151
+
152
+ def serialized_value
153
+ super.dup.gmtime.rfc2822
154
+ end
155
+ end
156
+
157
+ # The Secure attribute
158
+ class Secure < Unary
159
+ register_as :secure
160
+ end
161
+
162
+ # The HttpOnly attribute
163
+ class HttpOnly < Unary
164
+ register_as :http_only
165
+ end
166
+
167
+ # Already expired {Expires} attribute useful for cookie deletion
168
+ Expired = Expires.new(Time.at(0))
169
+
170
+ end # class Attribute
171
+ end # class Header
172
+ end # class Cookie
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ class Cookie
4
+
5
+ # Models a registry of {Cookie} instances
6
+ class Registry
7
+
8
+ include Enumerable
9
+ include Equalizer.new(:entries)
10
+ include Adamantium::Flat
11
+
12
+ # Message for {UnknownCookieError}
13
+ UNKNOWN_COOKIE_MSG = 'No cookie named %s is registered'.freeze
14
+
15
+ # Raised when trying to {#fetch} an unknown {Cookie}
16
+ UnknownCookieError = Class.new(Cookie::Error)
17
+
18
+ def self.coerce(header)
19
+ new(cookie_hash(header))
20
+ end
21
+
22
+ def self.cookie_hash(header)
23
+ header.split(COOKIE_SEPARATOR).each_with_object({}) { |string, hash|
24
+ cookie = Cookie.coerce(string)
25
+ hash[cookie.name] = cookie
26
+ }
27
+ end
28
+
29
+ private_class_method :cookie_hash
30
+
31
+ attr_reader :entries
32
+ protected :entries
33
+
34
+ def initialize(entries = EMPTY_HASH)
35
+ @entries = entries
36
+ end
37
+
38
+ def each(&block)
39
+ return to_enum unless block
40
+ entries.each(&block)
41
+ self
42
+ end
43
+
44
+ def fetch(name)
45
+ get(name) or raise UnknownCookieError, UNKNOWN_COOKIE_MSG % name.inspect
46
+ end
47
+
48
+ def get(name)
49
+ @entries[name]
50
+ end
51
+
52
+ end # class Registry
53
+ end # class Cookie
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+
3
+ class Cookie
4
+
5
+ # Gem version
6
+ VERSION = '0.0.1'.freeze
7
+
8
+ end
data/majoun.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path('../lib/cookie/version', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "majoun"
7
+ gem.version = Cookie::VERSION.dup
8
+ gem.authors = [ "Martin Gamsjaeger (snusnu)" ]
9
+ gem.email = [ "gamsnjaga@gmail.com" ]
10
+ gem.description = "An HTTP cookie implemented in ruby"
11
+ gem.summary = "Support for sending and receiving HTTP cookies on ruby servers"
12
+ gem.homepage = "https://github.com/snusnu/cookie"
13
+
14
+ gem.require_paths = [ "lib" ]
15
+ gem.files = `git ls-files`.split("\n")
16
+ gem.test_files = `git ls-files -- {spec}/*`.split("\n")
17
+ gem.extra_rdoc_files = %w[LICENSE README.md TODO.md]
18
+ gem.license = 'MIT'
19
+
20
+ gem.add_dependency 'adamantium', '~> 0.2.0'
21
+ gem.add_dependency 'abstract_type', '~> 0.0.7'
22
+ gem.add_dependency 'concord', '~> 0.1.4'
23
+
24
+ gem.add_development_dependency 'bundler', '~> 1.6.1'
25
+ end
@@ -0,0 +1,186 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ module Spec
6
+ class CryptoBox
7
+ def encrypt(string)
8
+ string
9
+ end
10
+
11
+ def decrypt(string)
12
+ string
13
+ end
14
+ end
15
+
16
+ module Encoder
17
+ Noop = ->(string) { string }
18
+ end
19
+
20
+ module Decoder
21
+ Noop = ->(string) { string }
22
+ end
23
+ end
24
+
25
+ shared_context 'integration specs' do
26
+ let(:cookie) { Cookie.new(name, value) }
27
+ let(:name) { 'SID' }
28
+ let(:value) { '{"id": 11}' } # triggers padding char '=' when base64 encoded
29
+ end
30
+
31
+ describe Cookie do
32
+
33
+ include_context 'integration specs'
34
+
35
+ let(:encoder) { Spec::Encoder::Noop }
36
+ let(:decoder) { Spec::Decoder::Noop }
37
+ let(:box) { Spec::CryptoBox.new }
38
+
39
+ it 'supports coercion from string to a Cookie instance' do
40
+ c = cookie
41
+ expect(Cookie.coerce(c.to_s)).to eql(cookie)
42
+
43
+ c = cookie.encode
44
+ expect(Cookie.coerce(c.to_s)).to eql(cookie.encode)
45
+
46
+ c = c.decode
47
+ expect(Cookie.coerce(c.to_s)).to eql(cookie)
48
+ end
49
+
50
+ it 'supports #name and #value' do
51
+ expect(cookie.name).to be(name)
52
+ expect(cookie.value).to be(value)
53
+ end
54
+
55
+ it 'supports #to_s' do
56
+ expect(cookie.to_s).to eql("#{cookie.name}=#{cookie.value}")
57
+ end
58
+
59
+ context 'encoding and decoding' do
60
+ it 'defaults to base64' do
61
+ encoded = cookie.encode
62
+ expect(encoded).to eql(Cookie.new(name, Base64.urlsafe_encode64(cookie.value)))
63
+
64
+ decoded = encoded.decode
65
+ expect(decoded).to eql(Cookie.new(name, Base64.urlsafe_decode64(encoded.value)))
66
+ end
67
+
68
+ it 'supports custom encoders and decoders' do
69
+ encoded = cookie.encode(encoder)
70
+ expect(encoded).to eql(Cookie.new(name, encoder.call(cookie.value)))
71
+
72
+ decoded = encoded.decode(decoder)
73
+ expect(decoded).to eql(Cookie.new(name, decoder.call(encoded.value)))
74
+ end
75
+ end
76
+
77
+ context 'encryption and decryption' do
78
+ it 'supports crypto boxes which provide #encrypt(msg) and #decrypt(msg)' do
79
+ encrypted = cookie.encrypt(box)
80
+ expect(encrypted).to eql(Cookie.new(name, box.encrypt(cookie.value)))
81
+
82
+ decrypted = encrypted.decrypt(box)
83
+ expect(decrypted).to eql(Cookie.new(name, box.decrypt(encrypted.value)))
84
+ end
85
+ end
86
+ end
87
+
88
+ describe Cookie::Header do
89
+
90
+ include_context 'integration specs'
91
+
92
+ let(:definition) { Cookie::Header.new(cookie) }
93
+
94
+ it 'supports all cookie attributes' do
95
+ d = definition
96
+ expect(d.to_s).to eql('SID={"id": 11}')
97
+
98
+ d = definition.with_domain('.foo.bar')
99
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar')
100
+
101
+ d = d.with_path('/foo')
102
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo')
103
+
104
+ d = d.with_expires(Time.at(42))
105
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000')
106
+
107
+ d = d.with_max_age(42)
108
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000; MaxAge=42')
109
+
110
+ d = d.secure
111
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000; MaxAge=42; Secure')
112
+
113
+ d = d.http_only
114
+ expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000; MaxAge=42; Secure; HttpOnly')
115
+
116
+ d = definition.secure
117
+ expect(d.to_s).to eql('SID={"id": 11}; Secure')
118
+
119
+ d = d.http_only
120
+ expect(d.to_s).to eql('SID={"id": 11}; Secure; HttpOnly')
121
+
122
+ d = definition.http_only
123
+ expect(d.to_s).to eql('SID={"id": 11}; HttpOnly')
124
+
125
+ d = d.secure
126
+ expect(d.to_s).to eql('SID={"id": 11}; HttpOnly; Secure')
127
+ end
128
+
129
+ it 'overwrites previously defined attributes' do
130
+ d = definition.with_max_age(0).with_max_age(42)
131
+ expect(d.to_s).to eql('SID={"id": 11}; MaxAge=42')
132
+ end
133
+
134
+ it 'supports deleting cookies on the client' do
135
+ d = definition.delete
136
+ expect(d.to_s).to eql('SID=; Expires=Thu, 01 Jan 1970 00:00:00 -0000')
137
+
138
+ d = definition.with_domain('.foo.bar').with_path('/foo')
139
+ d = d.delete
140
+ expect(d.to_s).to eql('SID=; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:00 -0000')
141
+ end
142
+ end
143
+
144
+ describe Cookie::Registry do
145
+
146
+ include_context 'integration specs'
147
+
148
+ let(:registry) { Cookie::Registry.coerce(header) }
149
+ let(:header) { cookie.to_s }
150
+
151
+ it 'contains no entries after #initialize' do
152
+ expect(Cookie::Registry.new.count).to be(0)
153
+ end
154
+
155
+ it 'supports coercion from a Set-Cookie header' do
156
+ expect(registry.get(name)).to eql(cookie)
157
+ end
158
+
159
+ it 'supports #get(name)' do
160
+ expect(registry.get(name)).to eql(cookie)
161
+ expect(registry.get(:foo)).to be(nil)
162
+ end
163
+
164
+ it 'supports #fetch(name)' do
165
+ expect(registry.fetch(name)).to eql(cookie)
166
+ expect { registry.fetch(:foo) }.to raise_error(Cookie::Registry::UnknownCookieError)
167
+ end
168
+
169
+ it 'is an Enumerable' do
170
+ expect(registry).to be_kind_of(Enumerable)
171
+ end
172
+
173
+ context '#each' do
174
+ it 'returns self when a block is given' do
175
+ expect(registry.each { |_| }).to be(registry)
176
+ end
177
+
178
+ it 'returns an enumerator when no block is given' do
179
+ expect(registry.each).to be_instance_of(Enumerator)
180
+ end
181
+
182
+ it 'yields all cookies' do
183
+ expect { |block| registry.each(&block) }.to yield_successive_args([name, cookie])
184
+ end
185
+ end
186
+ end