cookie_store 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: