cookie_store 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 03f6d983ab27d0ae52b8af6468da9a131e815e4c
4
+ data.tar.gz: b235fd0ecfba2b57d10d121cca4bace173e56b08
5
+ SHA512:
6
+ metadata.gz: c74560238d8e3ec19ccf3d987bea2c8dcb1f516fa45ec58eaeddacb43553557736f54e2c92f6e2e87daf0234d1ec3acee2346b291e63f583718432d7bf735774
7
+ data.tar.gz: 3a4f46c5276009ca3203ea6bdf1b6597fa872f1e3ad4ab731cae18e719fc15bb03f721439ec5f3a177c065f65b7b10a93c7cc3ab74f7a1c2dc7b17f05264c13f
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Jonathan Bracy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ cookie_store
2
+ ============
3
+
4
+ A Ruby library to handle client-side HTTP cookies
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require 'bundler/setup'
2
+ require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+ require 'rdoc/task'
5
+
6
+ task :console do
7
+ exec 'irb -Ilib -r cookie_store.rb'
8
+ end
9
+ task :c => :console
10
+
11
+
12
+ Rake::TestTask.new do |t|
13
+ t.libs << 'lib' << 'test'
14
+ t.test_files = FileList['test/**/*_test.rb']
15
+ # t.warning = true
16
+ # t.verbose = true
17
+ end
18
+
19
+ desc "Run tests"
20
+ task :default => :test
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "cookie_store"
6
+ s.version = '0.1.0'
7
+ s.licenses = ['MIT']
8
+ s.authors = ["Jon Bracy"]
9
+ s.email = ["jonbracy@gmail.com"]
10
+ s.homepage = "https://github.com/malomalo/cookie_store"
11
+ s.summary = %q{A Ruby library to handle client-side HTTP cookies}
12
+ s.description = %q{A Ruby library to handle and store client-side HTTP cookies}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.require_paths = ["lib"]
17
+
18
+ # Developoment
19
+ s.add_development_dependency 'rake'
20
+ # s.add_development_dependency 'rdoc'
21
+ # s.add_development_dependency 'sdoc'
22
+ s.add_development_dependency 'minitest'
23
+ s.add_development_dependency 'minitest-reporters'
24
+ s.add_development_dependency 'mocha'
25
+ end
@@ -0,0 +1,67 @@
1
+ require 'uri'
2
+
3
+ class CookieStore
4
+
5
+ # Maximum number of bytes per cookie (RFC 6265 6.1 requires at least 4096)
6
+ MAX_COOKIE_LENGTH = 4096
7
+
8
+ # Maximum number of cookies per domain (RFC 6265 6.1 requires 50 at least)
9
+ MAX_COOKIES_PER_DOMAIN = 50
10
+
11
+ # Maximum number of cookies total (RFC 6265 6.1 requires 3000 at least)
12
+ MAX_COOKIES_TOTAL = 3000
13
+
14
+ # Read and set the cookie from the Set-Cookie header
15
+ def set_cookie(request_uri, set_cookie_value)
16
+ request_uri = URI.parse(request_uri)
17
+ cookie = CookieStore::Cookie.parse(request_uri, set_cookie_value)
18
+
19
+ # reject as per RFC2965 Section 3.3.2
20
+ return if !cookie.request_match?(request_uri) || !(cookie.domain =~ /.+\..+/)
21
+
22
+ # reject cookies over the max-bytes
23
+ return if cookie.to_s.size > MAX_COOKIE_LENGTH
24
+
25
+ add(cookie)
26
+ end
27
+
28
+ def cookie_header_for(request_uri)
29
+ cookies_for(request_uri).map(&:to_s).join('; ')
30
+ end
31
+
32
+ # (RFC 2965, section 1)
33
+ def search_domains_for(domain)
34
+ domain.downcase!
35
+ serach_domains = []
36
+
37
+ if domain =~ CookieStore::Cookie::IPADDR
38
+ serach_domains << domain
39
+ else
40
+ domain = domain + '.local' if !(domain =~ /.\../)
41
+ serach_domains << domain
42
+ serach_domains << ".#{domain}"
43
+
44
+ # H is the host domain name of a host; and,
45
+ # H has the form A.B; and
46
+ if domain =~ /[^\.]+(\..+)/
47
+ reach = $1
48
+ # B has at least one embedded dot
49
+ if reach =~ /.[\.:]./
50
+ # B has at least one embedded dot, or B is the string "local".
51
+ # then the reach of H is .B.
52
+ serach_domains << reach
53
+ end
54
+ end
55
+ end
56
+
57
+ serach_domains
58
+ end
59
+
60
+ def close_session
61
+ gc(true)
62
+ end
63
+
64
+ end
65
+
66
+ require 'cookie_store/cookie'
67
+ require 'cookie_store/hash_store'
@@ -0,0 +1,238 @@
1
+ class CookieStore::Cookie
2
+
3
+ QUOTED_PAIR = "\\\\[\\x00-\\x7F]"
4
+ LWS = "\\r\\n(?:[ \\t]+)"
5
+ QDTEXT = "[\\t\\x20-\\x21\\x23-\\x7E\\x80-\\xFF]|(?:#{LWS})"
6
+ QUOTED_TEXT = "(?:#{QUOTED_PAIR}|#{QDTEXT})*"
7
+ IPADDR = /\A#{URI::REGEXP::PATTERN::IPV4ADDR}\Z|\A#{URI::REGEXP::PATTERN::IPV6ADDR}\Z/
8
+
9
+ TOKEN = '[^(),\/<>@;:\\\"\[\]?={}\s]+'
10
+ VALUE = "(?:#{TOKEN}|#{IPADDR}|#{QUOTED_TEXT})"
11
+ EXPIRES_AT_VALUE = '[A-Za-z]{3},\ \d{2}[-\ ][A-Za-z]{3}[-\ ]\d{4}\ \d{2}:\d{2}:\d{2}\ [A-Z]{3}'
12
+
13
+ COOKIE = /(?<name>#{TOKEN})=(?:"(?<quoted_value>#{QUOTED_TEXT})"|(?<value>#{VALUE}))(?<attributes>.*)/n
14
+ COOKIE_AV = %r{
15
+ ;\s+
16
+ (?<key>#{TOKEN})
17
+ (?:
18
+ =
19
+ (?:
20
+ "(?<quoted_value>#{QUOTED_TEXT})"
21
+ |
22
+ (?<value>#{EXPIRES_AT_VALUE}|#{VALUE})
23
+ )
24
+ ){0,1}
25
+ }nx
26
+
27
+ # [String] The name of the cookie.
28
+ attr_reader :name
29
+
30
+ # [String] The value of the cookie, without any attempts at decoding.
31
+ attr_reader :value
32
+
33
+ # [String] The domain scope of the cookie. Follows the RFC 2965
34
+ # 'effective host' rules. A 'dot' prefix indicates that it applies both
35
+ # to the non-dotted domain and child domains, while no prefix indicates
36
+ # that only exact matches of the domain are in scope.
37
+ attr_reader :domain
38
+
39
+ # [String] The path scope of the cookie. The cookie applies to URI paths
40
+ # that prefix match this value.
41
+ attr_reader :path
42
+
43
+ # [Boolean] The secure flag is set to indicate that the cookie should
44
+ # only be sent securely. Nearly all HTTP User Agent implementations assume
45
+ # this to mean that the cookie should only be sent over a
46
+ # SSL/TLS-protected connection
47
+ attr_reader :secure
48
+
49
+ # [Boolean] Popular browser extension to mark a cookie as invisible
50
+ # to code running within the browser, such as JavaScript
51
+ attr_reader :http_only
52
+
53
+ # [Fixnum] Version indicator, currently either
54
+ # * 0 for netscape cookies
55
+ # * 1 for RFC 2965 cookies
56
+ attr_reader :version
57
+
58
+ # [String, nil] RFC 2965 field for indicating comment (or a location)
59
+ # describing the cookie to a usesr agent.
60
+ attr_reader :comment, :comment_url
61
+
62
+ # [Boolean] RFC 2965 field for indicating session lifetime for a cookie
63
+ attr_reader :discard
64
+
65
+ # [Array<FixNum>, nil] RFC 2965 port scope for the cookie. If not nil,
66
+ # indicates specific ports on the HTTP server which should receive this
67
+ # cookie if contacted.
68
+ attr_reader :ports
69
+
70
+ # [DateTime] The Expires directive tells the browser when to delete the cookie.
71
+ # Derived from the format used in RFC 1123
72
+ attr_reader :expires
73
+
74
+ # [Fixnum] RFC 6265 allows the use of the Max-Age attribute to set the
75
+ # cookie’s expiration as an interval of seconds in the future, relative
76
+ # to the time the browser received the cookie.
77
+ attr_reader :max_age
78
+
79
+ # [Time] Time when this cookie was first evaluated and created.
80
+ attr_reader :created_at
81
+
82
+ def initialize(name, value, options={})
83
+ @name = name
84
+ @value = value
85
+ @secure = false
86
+ @http_only = false
87
+ @version = 1
88
+ @discard = false
89
+ @created_at = Time.now
90
+
91
+ options.each do |attr_name, attr_value|
92
+ self.instance_variable_set(:"@#{attr_name}", attr_value)
93
+ end
94
+ end
95
+
96
+ # Evaluate when this cookie will expire. Uses the original cookie fields
97
+ # for a max-age or expires
98
+ #
99
+ # @return [Time, nil] Time of expiry, if this cookie has an expiry set
100
+ def expires_at
101
+ if max_age
102
+ created_at + max_age
103
+ else
104
+ expires
105
+ end
106
+ end
107
+
108
+ # Indicates whether the cookie is currently considered valid
109
+ #
110
+ # @return [Boolean]
111
+ def expired?
112
+ expires_at && Time.now > expires_at
113
+ end
114
+
115
+ # Indicates whether the cookie will be considered invalid after the end
116
+ # of the current user session
117
+ # @return [Boolean]
118
+ def session?
119
+ !expires_at || discard
120
+ end
121
+
122
+ def to_s
123
+ if value.include?('"')
124
+ "#{name}=\"#{value.gsub('"', '\\"')}\""
125
+ else
126
+ "#{name}=#{value}"
127
+ end
128
+ end
129
+
130
+ # Returns a true if the request_uri is a domain-match, a path-match, and a
131
+ # port-match
132
+ def request_match?(request_uri)
133
+ uri = request_uri.is_a?(URI) ? request_uri : URI.parse(request_uri)
134
+ domain_match(uri.host) && path_match(uri.path) && port_match(uri.port)
135
+ end
136
+
137
+ # From RFC2965 Section 1.
138
+ #
139
+ # For two strings that represent paths, P1 and P2, P1 path-matches P2
140
+ # if P2 is a prefix of P1 (including the case where P1 and P2 string-
141
+ # compare equal). Thus, the string /tec/waldo path-matches /tec.
142
+ def path_match(request_path)
143
+ request_path.start_with?(path)
144
+ end
145
+
146
+ # From RFC2965 Section 1.
147
+ #
148
+ # Host A's name domain-matches host B's if
149
+ #
150
+ # * their host name strings string-compare equal; or
151
+ #
152
+ # * A is a HDN string and has the form NB, where N is a non-empty
153
+ # name string, B has the form .B', and B' is a HDN string. (So,
154
+ # x.y.com domain-matches .Y.com but not Y.com.)
155
+ def domain_match(request_domain)
156
+ request_domain = request_domain.downcase
157
+
158
+ return true if domain == request_domain
159
+
160
+ return false if request_domain =~ IPADDR
161
+
162
+ return true if domain == ".#{request_domain}"
163
+
164
+ return false if !domain.include?('.') && domain != 'local'
165
+
166
+ return false if !request_domain.end_with?(domain)
167
+
168
+ return !(request_domain[0...-domain.length].count('.') > (request_domain[-domain.length-1] == '.' ? 1 : 0))
169
+ end
170
+
171
+ # From RFC2965 Section 3.3
172
+ #
173
+ # The default behavior is that a cookie MAY be returned to any request-port.
174
+ #
175
+ # If the port attribute is set the port must be in the port-list.
176
+ def port_match(request_port)
177
+ return true unless ports
178
+ ports.include?(request_port)
179
+ end
180
+
181
+ def self.parse(request_uri, set_cookie_value)
182
+ uri = request_uri.is_a?(URI) ? request_uri : URI.parse(request_uri)
183
+ data = COOKIE.match(set_cookie_value)
184
+ options = {}
185
+
186
+ if !data
187
+ raise Net::HTTPHeaderSyntaxError.new("Invalid Set-Cookie header format")
188
+ end
189
+
190
+ if data[:attributes]
191
+ data[:attributes].scan(COOKIE_AV) do |key, quoted_value, value|
192
+ value = quoted_value.gsub(/\\(.)/, '\1') if !value && quoted_value
193
+
194
+ # RFC 2109 4.1, Attributes (names) are case-insensitive
195
+ case key.downcase
196
+ when 'comment'
197
+ options[:comment] = value
198
+ when 'commenturl'
199
+ options[:comment_url] = value
200
+ when 'discard'
201
+ options[:discard] = true
202
+ when 'domain'
203
+ if value =~ IPADDR
204
+ options[:domain] = value
205
+ else
206
+ # As per RFC2965 if a host name contains no dots, the effective host name is
207
+ # that name with the string .local appended to it.
208
+ value = "#{value}.local" if !value.include?('.')
209
+ options[:domain] = (value.start_with?('.') ? value : ".#{value}").downcase
210
+ end
211
+ when 'expires'
212
+ if value.include?('-')
213
+ options[:expires] = DateTime.strptime(value, '%a, %d-%b-%Y %H:%M:%S %Z')
214
+ else
215
+ options[:expires] = DateTime.strptime(value, '%a, %d %b %Y %H:%M:%S %Z')
216
+ end
217
+ when 'max-age'
218
+ options[:max_age] = value.to_i
219
+ when 'path'
220
+ options[:path] = value
221
+ when 'port'
222
+ options[:ports] = value.split(',').map(&:to_i)
223
+ when 'secure'
224
+ options[:secure] = true
225
+ when 'httponly'
226
+ options[:http_only] = true
227
+ when 'version'
228
+ options[:version] = value.to_i
229
+ end
230
+ end
231
+ end
232
+ options[:domain] ||= uri.host.downcase
233
+ options[:path] ||= uri.path
234
+
235
+ CookieStore::Cookie.new(data[:name], data[:value] || data[:quoted_value].gsub(/\\(.)/, '\1'), options)
236
+ end
237
+
238
+ end
@@ -0,0 +1,52 @@
1
+ class CookieStore::HashStore < CookieStore
2
+
3
+ def initialize
4
+ @domains = {}
5
+ end
6
+
7
+ def add(cookie)
8
+ #TODO: check for MAX_COOKIES_PER_DOMAIN && MAX_COOKIES_TOTAL, think remove the MAX_COOKIES_TOTAL tho
9
+ @domains[cookie.domain] ||= {}
10
+ @domains[cookie.domain][cookie.path] ||= {}
11
+ @domains[cookie.domain][cookie.path][cookie.name] = cookie
12
+ end
13
+
14
+ def cookies_for(request_uri)
15
+ request_uri = URI.parse(request_uri)
16
+ trigger_gc = false
17
+ request_cookies = []
18
+
19
+ search_domains_for(request_uri.host).each do |domain|
20
+ next unless @domains[domain]
21
+
22
+ @domains[domain].each do |path, cookies|
23
+ if request_uri.path.start_with?(path)
24
+ cookies.each do |name, cookie|
25
+ if cookie.expired?
26
+ trigger_gc = true
27
+ elsif cookie.port_match(request_uri.port) && (!cookie.secure || (cookie.secure && request_uri.scheme == 'https'))
28
+ request_cookies << cookie
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ gc if trigger_gc
36
+
37
+ request_cookies
38
+ end
39
+
40
+ def gc(close_session=false)
41
+ @domains.delete_if do |domain, paths|
42
+ paths.delete_if do |path, cookies|
43
+ cookies.delete_if do |cookie_name, cookie|
44
+ cookie.expired? || (close_session && cookie.session?)
45
+ end
46
+ cookies.empty?
47
+ end
48
+ paths.empty?
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,313 @@
1
+ require 'test_helper'
2
+
3
+ class CookieStore::CookieTest < Minitest::Test
4
+
5
+ # CookieStore::Cookie.new ================================================================
6
+
7
+ test "::new(name, value)" do
8
+ cookie = CookieStore::Cookie.new('foo', 'bar')
9
+
10
+ assert_equal 'foo', cookie.name
11
+ assert_equal 'bar', cookie.value
12
+ end
13
+
14
+ test "::new(name, value, options)" do
15
+ #TODO: test all options are set
16
+ cookie = CookieStore::Cookie.new('foo', 'bar', :domain => 'test.com')
17
+
18
+ assert_equal 'test.com', cookie.domain
19
+ end
20
+
21
+ # CookieStore::Cookie.domain_match =======================================================
22
+
23
+ test "::domain_match(request_domain)" do
24
+ {
25
+ 'a.com' => 'a.com',
26
+ 'test.com' => '.test.com',
27
+ '123.456.57.21' => '123.456.57.21'
28
+ #TODO: not sure how ipv6 works '[E3D7::51F4:9BC8:C0A8:6420]' => '[E3D7::51F4:9BC8:C0A8:6420]'
29
+ }.each do |host, cookie_host|
30
+ cookie = CookieStore::Cookie.new('key', 'value', :domain => cookie_host)
31
+ assert_equal true, cookie.domain_match(host)
32
+ end
33
+
34
+ {
35
+ 'a.com' => 'b.com',
36
+ 'test.com' => '.com',
37
+ 'test.com' => '.com.',
38
+ 'test.com' => 'com',
39
+ 'test.com' => 'com.',
40
+ 'y.x.foo.com' => '.foo.com',
41
+ 'y.x.foo.com' => 'foo.com',
42
+ '123.456.57.21' => '123.456.57.22',
43
+ '123.456.57.21' => '.123.456.57.21'
44
+ #TODO: not sure how ipv6 works '[E3D7::51F4:9BC8:C0A8:6420]' => '[E3D7::51F4:9BC8:C0A8:6421]'
45
+ }.each do |host, cookie_host|
46
+ cookie = CookieStore::Cookie.new('key', 'value', :domain => cookie_host)
47
+ assert_equal false, cookie.domain_match(host)
48
+ end
49
+ end
50
+
51
+ # CookieStore::Cookie.path_match =========================================================
52
+
53
+ test "::path_match(request_path)" do
54
+ {
55
+ '/test' => '/',
56
+ '/this/is/my/url' => '/this/is'
57
+ }.each do |path, cookie_path|
58
+ cookie = CookieStore::Cookie.new('key', 'value', :path => cookie_path)
59
+ assert_equal true, cookie.path_match(path)
60
+ end
61
+
62
+ {
63
+ '/test' => '/rest',
64
+ '/' => '/test'
65
+ }.each do |path, cookie_path|
66
+ cookie = CookieStore::Cookie.new('key', 'value', :path => cookie_path)
67
+ assert_equal false, cookie.path_match(path)
68
+ end
69
+ end
70
+
71
+ # CookieStore::Cookie.port_match =========================================================
72
+
73
+ test "::port_match(request_port) without ports attribute set" do
74
+ cookie = CookieStore::Cookie.new('key', 'value')
75
+ assert_equal true, cookie.port_match(158)
76
+ end
77
+
78
+ test "::port_match(request_port) with ports attribute set" do
79
+ cookie = CookieStore::Cookie.new('key', 'value', :ports => [80, 8700])
80
+ assert_equal true, cookie.port_match(8700)
81
+ assert_equal false, cookie.port_match(87)
82
+ end
83
+
84
+ # CookieStore::Cookie.expires_at =========================================================
85
+
86
+ test "#expires_at based on max-age" do
87
+ travel_to Time.new(2013, 12, 13, 8, 26, 12, 0) do
88
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Max-Age=3600')
89
+ assert_equal Time.new(2013, 12, 13, 9, 26, 12, 0), cookie.expires_at
90
+ end
91
+ end
92
+
93
+ test "#expires_at based on expires attribute" do
94
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Expires="Wed, 13 Jan 2021 22:23:01 GMT"')
95
+ assert_equal DateTime.new(2021, 1, 13, 22, 23, 1, 0), cookie.expires_at
96
+ end
97
+
98
+ test "#expires_at perfers max-age to expires" do
99
+ travel_to Time.new(2013, 12, 13, 8, 26, 12, 0) do
100
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Max-Age=3600 Expires="Wed, 13 Jan 2021 22:23:01 GMT"')
101
+ assert_equal Time.new(2013, 12, 13, 9, 26, 12, 0), cookie.expires_at
102
+ end
103
+ end
104
+
105
+ test "#expires_at returns nil if no max-age or expires attribute" do
106
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar')
107
+ assert_equal nil, cookie.expires_at
108
+ end
109
+
110
+ # CookieStore::Cookie.expired? =========================================================
111
+
112
+ test "#expired?" do
113
+ cookie = travel_to Time.new(2013, 12, 13, 8, 26, 12, 0) do
114
+ CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Max-Age=3600')
115
+ end
116
+
117
+ assert_equal true, cookie.expired?
118
+
119
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Max-Age=3600')
120
+ assert_equal false, cookie.expired?
121
+ end
122
+
123
+ # CookieStore::Cookie.session? ===========================================================
124
+
125
+ test "#session? true by default" do
126
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar')
127
+ assert_equal true, cookie.session?
128
+ end
129
+
130
+ test "#session? false if on expiration" do
131
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Max-Age=3600')
132
+ assert_equal false, cookie.session?
133
+ end
134
+
135
+ test "#session? true if discard attribute is present" do
136
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Max-Age=3600; Discard')
137
+ assert_equal true, cookie.session?
138
+
139
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Discard')
140
+ assert_equal true, cookie.session?
141
+ end
142
+
143
+ # CookieStore::Cookie.to_s ===============================================================
144
+
145
+ test "#to_s" do
146
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar')
147
+ assert_equal "foo=bar", cookie.to_s
148
+ end
149
+
150
+ test "#to_s with a \" in the value" do
151
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo="ba\"r"')
152
+ assert_equal "foo=\"ba\\\"r\"", cookie.to_s
153
+ end
154
+
155
+ #TODO: # CookieStore::Cookie.to_h ===============================================================
156
+ #
157
+ # test "#to_h" do
158
+ # cookie = travel_to Time.new(2013, 12, 13, 8, 26, 12, 0) do
159
+ # CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Max-Age=3600; Discard')
160
+ # end
161
+ #
162
+ # assert_equal({
163
+ # :name => 'foo',
164
+ # :value => 'bar',
165
+ # :domain => 'google.com',
166
+ # :path => '/test/this',
167
+ # :secure => false,
168
+ # :http_only => false,
169
+ # :version => 1,
170
+ # :comment => nil,
171
+ # :comment_url => nil,
172
+ # :discard => true,
173
+ # :ports => nil,
174
+ # :expires => nil,
175
+ # :max_age => 3600,
176
+ # :created_at => Time.new(2013, 12, 13, 8, 26, 12, 0)
177
+ # }, cookie.to_h)
178
+ # end
179
+
180
+ # CookieStore::Cookie.parse ==============================================================
181
+
182
+ test "::parse a simple cookie" do
183
+ cookie = CookieStore::Cookie.parse('http://google.com/test', "foo=bar")
184
+
185
+ assert_equal 'foo', cookie.name
186
+ assert_equal 'bar', cookie.value
187
+ assert_equal 'google.com', cookie.domain
188
+ assert_equal '/test', cookie.path
189
+ assert_equal false, cookie.secure
190
+ assert_equal false, cookie.http_only
191
+ assert_equal nil, cookie.comment
192
+ assert_equal nil, cookie.comment_url
193
+ assert_equal 1, cookie.version
194
+ assert_equal false, cookie.discard
195
+ assert_equal nil, cookie.ports
196
+ assert_equal nil, cookie.expires
197
+ assert_equal nil, cookie.max_age
198
+ end
199
+
200
+ test "::parse normalizes the request domain" do
201
+ cookie = CookieStore::Cookie.parse('http://GoOGlE.com/test', "foo=bar")
202
+ assert_equal 'google.com', cookie.domain
203
+ end
204
+
205
+ test "::parse parth with a ? at the end" do
206
+ cookie = CookieStore::Cookie.parse('http://GoOGlE.com/test?key=value', "foo=bar")
207
+ assert_equal '/test', cookie.path
208
+ end
209
+
210
+
211
+ test "::parse parth with a # at the end" do
212
+ cookie = CookieStore::Cookie.parse('http://GoOGlE.com/test#anchor', "foo=bar")
213
+ assert_equal '/test', cookie.path
214
+ end
215
+
216
+ test "::parse a simple quoted cookie" do
217
+ cookie = CookieStore::Cookie.parse('http://google.com/test', 'foo="b\"ar"')
218
+
219
+ assert_equal 'google.com', cookie.domain
220
+ assert_equal 'foo', cookie.name
221
+ assert_equal 'b"ar', cookie.value
222
+ end
223
+
224
+ test "::parse domain attribute without leading ." do
225
+ cookie = CookieStore::Cookie.parse('http://google.com/test', "foo=bar; Domain=google.com")
226
+ assert_equal '.google.com', cookie.domain
227
+ end
228
+
229
+ test "::parse domain attribute with leading ." do
230
+ cookie = CookieStore::Cookie.parse('http://google.com/test', "foo=bar; Domain=.google.com")
231
+ assert_equal '.google.com', cookie.domain
232
+ end
233
+
234
+ test "::parse domain attribute that is the superdomain" do
235
+ cookie = CookieStore::Cookie.parse('http://site.google.com/test', "foo=bar; Domain=google.com")
236
+ assert_equal '.google.com', cookie.domain
237
+ end
238
+
239
+ test "::parse domain attribute as ip" do
240
+ cookie = CookieStore::Cookie.parse('http://123.456.57.21/test', "foo=bar; Domain=123.456.57.21")
241
+
242
+ assert_equal '123.456.57.21', cookie.domain
243
+ end
244
+
245
+ test "::parse path attribute" do
246
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Path="/"')
247
+
248
+ assert_equal '/', cookie.path
249
+ end
250
+
251
+ test "::parse secure attribute" do
252
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Secure')
253
+ assert_equal true, cookie.secure
254
+ end
255
+
256
+ test "::parse http_only attribute" do
257
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; HttpOnly')
258
+ assert_equal true, cookie.http_only
259
+ end
260
+
261
+ test "::parse comment attribute" do
262
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Comment="the c\"omment"')
263
+ assert_equal 'the c"omment', cookie.comment
264
+ end
265
+
266
+ test "::parse coment_url attribute" do
267
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; CommentURL="http://google.com/url"')
268
+ assert_equal "http://google.com/url", cookie.comment_url
269
+ end
270
+
271
+ test "::parse version attribute" do
272
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Version=0')
273
+ assert_equal 0, cookie.version
274
+ end
275
+
276
+ test "::parse discard attribute" do
277
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Discard')
278
+ assert_equal true, cookie.discard
279
+ end
280
+
281
+ test "::parse port attribute" do
282
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Port="80"')
283
+ assert_equal [80], cookie.ports
284
+
285
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Port="80,8080"')
286
+ assert_equal [80, 8080], cookie.ports
287
+ end
288
+
289
+ test "::parse expires attribute" do
290
+ # Wed, 13 Jan 2021 22:23:01 GMT format
291
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Expires=Wed, 13 Jan 2021 22:23:01 GMT')
292
+ assert_equal DateTime.new(2021, 1, 13, 22, 23, 1, 0), cookie.expires
293
+
294
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Expires="Wed, 13 Jan 2021 22:23:01 GMT"')
295
+ assert_equal DateTime.new(2021, 1, 13, 22, 23, 1, 0), cookie.expires
296
+
297
+ # Wed, 13-Jan-2021 22:23:01 GMT format
298
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Expires=Wed, 13-Jan-2021 22:23:01 GMT')
299
+ assert_equal DateTime.new(2021, 1, 13, 22, 23, 1, 0), cookie.expires
300
+
301
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Expires="Wed, 13-Jan-2021 22:23:01 GMT"')
302
+ assert_equal DateTime.new(2021, 1, 13, 22, 23, 1, 0), cookie.expires
303
+ end
304
+
305
+ test "::parse max_age attribute" do
306
+ cookie = CookieStore::Cookie.parse('http://google.com/test/this', 'foo=bar; Max-Age=3660')
307
+ assert_equal 3660, cookie.max_age
308
+ end
309
+
310
+ # TODO: test expires_at, based on expires attribute
311
+ # TODO: test expires_at, based on max-age attribute
312
+
313
+ end
@@ -0,0 +1,78 @@
1
+ require 'test_helper'
2
+
3
+ class CookieStore::HashStoreTest < Minitest::Test
4
+
5
+ test "#cookies_for" do
6
+ store = CookieStore::HashStore.new
7
+
8
+ store.add(CookieStore::Cookie.new('a','value', :domain => 'google.com', :path => '/'))
9
+ store.add(CookieStore::Cookie.new('b','value', :domain => '.google.com', :path => '/'))
10
+ store.add(CookieStore::Cookie.new('c','value', :domain => 'www.google.com', :path => '/'))
11
+ store.add(CookieStore::Cookie.new('d','value', :domain => 'random.com', :path => '/'))
12
+
13
+ store.add(CookieStore::Cookie.new('e','value', :domain => '.google.com', :path => '/'))
14
+ store.add(CookieStore::Cookie.new('f','value', :domain => '.google.com', :path => '/test'))
15
+
16
+ store.add(CookieStore::Cookie.new('g','value', :domain => '.google.com', :path => '/', :max_age => 3660))
17
+ store.add(CookieStore::Cookie.new('h','value', :domain => '.google.com', :path => '/', :max_age => 0))
18
+
19
+ store.add(CookieStore::Cookie.new('i','value', :domain => '.google.com', :path => '/', :ports => [80,8080]))
20
+ store.add(CookieStore::Cookie.new('j','value', :domain => '.google.com', :path => '/', :ports => [80,8080], :secure => true))
21
+
22
+ store.add(CookieStore::Cookie.new('k','value', :domain => '.google.com', :path => '/', :secure => true))
23
+
24
+ assert_equal %w(a b e g i), store.cookies_for('http://google.com/').map(&:name).sort
25
+ assert_equal %w(b e g i), store.cookies_for('http://test.google.com/').map(&:name).sort
26
+ assert_equal %w(b c e g i), store.cookies_for('http://www.google.com/').map(&:name).sort
27
+
28
+ assert_equal %w(b c e g i), store.cookies_for('http://www.google.com/rest').map(&:name).sort
29
+ assert_equal %w(b c e f g i), store.cookies_for('http://www.google.com/test').map(&:name).sort
30
+
31
+ assert_equal %w(b c e f g k), store.cookies_for('https://www.google.com/test').map(&:name).sort
32
+ end
33
+
34
+ test '#cookies_for removes expired cookies while iterating' do
35
+ store = CookieStore::HashStore.new
36
+ store.expects(:gc).once
37
+
38
+ store.add(CookieStore::Cookie.new('h','value', :domain => '.google.com', :path => '/', :max_age => 0))
39
+ store.cookies_for('http://google.com/')
40
+ end
41
+
42
+ test '#gc clears out expired cookies' do
43
+ store = CookieStore::HashStore.new
44
+
45
+ store.add(CookieStore::Cookie.new('h','value', :domain => '.google.com', :path => '/', :max_age => 0))
46
+ store.gc
47
+ assert_equal({}, store.instance_variable_get(:@domains))
48
+
49
+ store.add(CookieStore::Cookie.new('h','value', :domain => '.google.com', :path => '/', :max_age => 0))
50
+ store.add(CookieStore::Cookie.new('h','value', :domain => '.google.com', :path => '/'))
51
+ store.gc
52
+ assert_equal 1, store.instance_variable_get(:@domains).size
53
+ assert_equal 1, store.instance_variable_get(:@domains)['.google.com'].size
54
+ assert_equal 1, store.instance_variable_get(:@domains)['.google.com']['/'].size
55
+ end
56
+
57
+ test "#gc doesn't clears out session cookies when not out the session" do
58
+ store = CookieStore::HashStore.new
59
+
60
+ store.add(CookieStore::Cookie.new('h','value', :domain => '.google.com', :path => '/', :max_age => 0))
61
+ store.add(CookieStore::Cookie.new('h','value', :domain => '.google.com', :path => '/', :discard => true))
62
+
63
+ store.gc
64
+ assert_equal 1, store.instance_variable_get(:@domains).size
65
+ assert_equal 1, store.instance_variable_get(:@domains)['.google.com'].size
66
+ assert_equal 1, store.instance_variable_get(:@domains)['.google.com']['/'].size
67
+ end
68
+
69
+ test '#gc clears out session cookies when closing out the session' do
70
+ store = CookieStore::HashStore.new
71
+
72
+ store.add(CookieStore::Cookie.new('h','value', :domain => '.google.com', :path => '/', :discard => true))
73
+
74
+ store.gc(true)
75
+ assert_equal({}, store.instance_variable_get(:@domains))
76
+ end
77
+
78
+ end
@@ -0,0 +1,88 @@
1
+ require 'test_helper'
2
+
3
+ class CookieStoreTest < Minitest::Test
4
+
5
+ # Cookie.set_cookie =========================================================
6
+
7
+ test "#set_cookie" do
8
+ store = CookieStore.new
9
+ store.expects(:add)
10
+
11
+ store.set_cookie('http://google.com/test/this', 'foo=bar; Max-Age=3600')
12
+ end
13
+
14
+ test "#set_cookie rejects cookies where the value for the Domain attribute contains no embedded dots" do
15
+ store = CookieStore.new
16
+ store.expects(:add).never
17
+
18
+ store.set_cookie('http://google.com/test', 'foo=bar; Domain=com')
19
+ store.set_cookie('http://google.com/test', 'foo=bar; Domain=.com')
20
+ store.set_cookie('http://google.com/test', 'foo=bar; Domain=com.')
21
+ store.set_cookie('http://google.com/test', 'foo=bar; Domain=.com.')
22
+ end
23
+
24
+ test "#set_cookie rejects cookies that do not domain-match" do
25
+ store = CookieStore.new
26
+ store.expects(:add).never
27
+
28
+ store.set_cookie('http://google.com/test', 'foo=bar; Domain=gobble.com')
29
+ store.set_cookie('http://y.x.foo.com/test', 'foo=bar; Domain=.foo.com')
30
+ store.set_cookie('http://y.x.foo.com/test', 'foo=bar; Domain=foo.com')
31
+ store.set_cookie('http://123.456.57.21/test', 'foo=bar; Domain=123.456.57.22')
32
+ store.set_cookie('http://123.456.57.21/test', 'foo=bar; Domain=.123.456.57.21')
33
+ #TODO: not sure how ipv6 works '' => '[E3D7::51F4:9BC8:C0A8:6421]'
34
+ end
35
+
36
+ test "#set_cookie rejects cookies that do not path-match" do
37
+ store = CookieStore.new
38
+ store.expects(:add).never
39
+
40
+ store.set_cookie('http://google.com/test', 'foo=bar; Path=/text')
41
+ store.set_cookie('http://google.com/test', 'foo=bar; Path=/test/mykey')
42
+ end
43
+
44
+ test "#set_cookie rejects cookies that do not port-match" do
45
+ store = CookieStore.new
46
+ store.expects(:add).never
47
+
48
+ store.set_cookie('http://google.com:97/test', 'foo=bar; Port="80,8080"')
49
+ end
50
+
51
+ test "#set_cookie rejects cookies that are over the byte limit" do
52
+ store = CookieStore.new
53
+ store.expects(:add).never
54
+
55
+ store.set_cookie('http://google.com/test', "foo=#{'k'*(CookieStore::MAX_COOKIE_LENGTH-3)}; Max-Age=3600")
56
+ end
57
+
58
+ test "#search_domains_for" do
59
+ store = CookieStore.new
60
+
61
+ assert_equal ['google.com', '.google.com'], store.search_domains_for('google.com')
62
+ assert_equal ["www.google.com", ".www.google.com", ".google.com"], store.search_domains_for('www.google.com')
63
+ assert_equal ["com.local", ".com.local"], store.search_domains_for('com')
64
+ assert_equal ["123.456.57.22"], store.search_domains_for('123.456.57.22')
65
+ #TODO: not sure about ipv6 assert_equal ["com.local", ".com.local"], store.search_domains_for('[E3D7::51F4:9BC8:C0A8:6420]')
66
+ end
67
+
68
+ test "#close_session calls gc(true)" do
69
+ store = CookieStore.new
70
+ store.expects(:gc).with(true).once
71
+
72
+ store.close_session
73
+ end
74
+
75
+ test "#cookie_header_for" do
76
+ store = CookieStore.new
77
+
78
+ store.expects(:cookies_for).with('url').returns([CookieStore::Cookie.new('key', 'value')])
79
+ assert_equal 'key=value', store.cookie_header_for('url')
80
+
81
+ store.expects(:cookies_for).with('url').returns([CookieStore::Cookie.new('key', 'value'), CookieStore::Cookie.new('foo', 'bar')])
82
+ assert_equal 'key=value; foo=bar', store.cookie_header_for('url')
83
+
84
+ store.expects(:cookies_for).with('url').returns([CookieStore::Cookie.new('key', 'v"alue')])
85
+ assert_equal 'key="v\"alue"', store.cookie_header_for('url')
86
+ end
87
+
88
+ end
@@ -0,0 +1,38 @@
1
+ # To make testing/debugging easier, test within this source tree versus an
2
+ # installed gem
3
+ dir = File.dirname(__FILE__)
4
+ root = File.expand_path(File.join(dir, '..'))
5
+ lib = File.expand_path(File.join(root, 'lib'))
6
+
7
+ $LOAD_PATH << lib
8
+
9
+ require 'cookie_store'
10
+ require "minitest/autorun"
11
+ require 'minitest/unit'
12
+ require 'minitest/reporters'
13
+ require 'faker'
14
+ require 'webmock/minitest'
15
+ require "mocha"
16
+ require "mocha/mini_test"
17
+ require 'active_support/testing/time_helpers'
18
+
19
+ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
20
+
21
+ # File 'lib/active_support/testing/declarative.rb', somewhere in rails....
22
+ class Minitest::Test
23
+ include ActiveSupport::Testing::TimeHelpers
24
+
25
+ def self.test(name, &block)
26
+ test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
27
+ defined = instance_method(test_name) rescue false
28
+ raise "#{test_name} is already defined in #{self}" if defined
29
+ if block_given?
30
+ define_method(test_name, &block)
31
+ else
32
+ define_method(test_name) do
33
+ flunk "No implementation provided for #{name}"
34
+ end
35
+ end
36
+ end
37
+
38
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cookie_store
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jon Bracy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest-reporters
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mocha
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: A Ruby library to handle and store client-side HTTP cookies
70
+ email:
71
+ - jonbracy@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - LICENSE
77
+ - README.md
78
+ - Rakefile
79
+ - cookie_store.gemspec
80
+ - lib/cookie_store.rb
81
+ - lib/cookie_store/cookie.rb
82
+ - lib/cookie_store/hash_store.rb
83
+ - test/cookie_store/cookie_test.rb
84
+ - test/cookie_store/hash_store_test.rb
85
+ - test/cookie_store_test.rb
86
+ - test/test_helper.rb
87
+ homepage: https://github.com/malomalo/cookie_store
88
+ licenses:
89
+ - MIT
90
+ metadata: {}
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubyforge_project:
107
+ rubygems_version: 2.2.2
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: A Ruby library to handle client-side HTTP cookies
111
+ test_files:
112
+ - test/cookie_store/cookie_test.rb
113
+ - test/cookie_store/hash_store_test.rb
114
+ - test/cookie_store_test.rb
115
+ - test/test_helper.rb
116
+ has_rdoc: