cookiejar2 0.3.4

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.
@@ -0,0 +1,314 @@
1
+ require 'cookiejar/cookie'
2
+
3
+ module CookieJar
4
+ # A cookie store for client side usage.
5
+ # - Enforces cookie validity rules
6
+ # - Returns just the cookies valid for a given URI
7
+ # - Handles expiration of cookies
8
+ # - Allows for persistence of cookie data (with or without session)
9
+ #
10
+ #--
11
+ #
12
+ # Internal format:
13
+ #
14
+ # Internally, the data structure is a set of nested hashes.
15
+ # Domain Level:
16
+ # At the domain level, the hashes are of individual domains,
17
+ # down-cased and without any leading period. For instance, imagine cookies
18
+ # for .foo.com, .bar.com, and .auth.bar.com:
19
+ #
20
+ # {
21
+ # "foo.com" : (host data),
22
+ # "bar.com" : (host data),
23
+ # "auth.bar.com" : (host data)
24
+ # }
25
+ #
26
+ # Lookups are done both for the matching entry, and for an entry without
27
+ # the first segment up to the dot, ie. for /^\.?[^\.]+\.(.*)$/.
28
+ # A lookup of auth.bar.com would match both bar.com and
29
+ # auth.bar.com, but not entries for com or www.auth.bar.com.
30
+ #
31
+ # Host Level:
32
+ # Entries are in an hash, with keys of the path and values of a hash of
33
+ # cookie names to cookie object
34
+ #
35
+ # {
36
+ # "/" : {"session" : (Cookie object), "cart_id" : (Cookie object)}
37
+ # "/protected" : {"authentication" : (Cookie Object)}
38
+ # }
39
+ #
40
+ # Paths are given a straight prefix string comparison to match.
41
+ # Further filters <secure, http only, ports> are not represented in this
42
+ # hierarchy.
43
+ #
44
+ # Cookies returned are ordered solely by specificity (length) of the
45
+ # path.
46
+ class Jar
47
+ # Create a new empty Jar
48
+ def initialize
49
+ @domains = {}
50
+ end
51
+
52
+ # Given a request URI and a literal Set-Cookie header value, attempt to
53
+ # add the cookie(s) to the cookie store.
54
+ #
55
+ # @param [String, URI] request_uri the resource returning the header
56
+ # @param [String] cookie_header_value the contents of the Set-Cookie
57
+ # @return [Cookie] which was created and stored
58
+ # @raise [InvalidCookieError] if the cookie header did not validate
59
+ def set_cookie(request_uri, cookie_header_values)
60
+ cookie_header_values.split(/, (?=[\w]+=)/).each do |cookie_header_value|
61
+ cookie = Cookie.from_set_cookie request_uri, cookie_header_value
62
+ add_cookie cookie
63
+ end
64
+ end
65
+
66
+ # Given a request URI and a literal Set-Cookie2 header value, attempt to
67
+ # add the cookie to the cookie store.
68
+ #
69
+ # @param [String, URI] request_uri the resource returning the header
70
+ # @param [String] cookie_header_value the contents of the Set-Cookie2
71
+ # @return [Cookie] which was created and stored
72
+ # @raise [InvalidCookieError] if the cookie header did not validate
73
+ def set_cookie2(request_uri, cookie_header_value)
74
+ cookie = Cookie.from_set_cookie2 request_uri, cookie_header_value
75
+ add_cookie cookie
76
+ end
77
+
78
+ # Given a request URI and some HTTP headers, attempt to add the cookie(s)
79
+ # (from Set-Cookie or Set-Cookie2 headers) to the cookie store. If a
80
+ # cookie is defined (by equivalent name, domain, and path) via Set-Cookie
81
+ # and Set-Cookie2, the Set-Cookie version is ignored.
82
+ #
83
+ # @param [String, URI] request_uri the resource returning the header
84
+ # @param [Hash<String,[String,Array<String>]>] http_headers a Hash
85
+ # which may have a key of "Set-Cookie" or "Set-Cookie2", and values of
86
+ # either strings or arrays of strings
87
+ # @return [Array<Cookie>,nil] the cookies created, or nil if none found.
88
+ # @raise [InvalidCookieError] if one of the cookie headers contained
89
+ # invalid formatting or data
90
+ def set_cookies_from_headers(request_uri, http_headers)
91
+ set_cookie_key = http_headers.keys.detect { |k| /\ASet-Cookie\Z/i.match k }
92
+ cookies = gather_header_values http_headers[set_cookie_key] do |value|
93
+ begin
94
+ Cookie.from_set_cookie request_uri, value
95
+ rescue InvalidCookieError
96
+ end
97
+ end
98
+
99
+ set_cookie2_key = http_headers.keys.detect { |k| /\ASet-Cookie2\Z/i.match k }
100
+ cookies += gather_header_values(http_headers[set_cookie2_key]) do |value|
101
+ begin
102
+ Cookie.from_set_cookie2 request_uri, value
103
+ rescue InvalidCookieError
104
+ end
105
+ end
106
+
107
+ # build the list of cookies, using a Jar. Since Set-Cookie2 values
108
+ # come second, they will replace the Set-Cookie versions.
109
+ jar = Jar.new
110
+ cookies.each do |cookie|
111
+ jar.add_cookie cookie
112
+ end
113
+ cookies = jar.to_a
114
+
115
+ # now add them all to our own store.
116
+ cookies.each do |cookie|
117
+ add_cookie cookie
118
+ end
119
+ cookies
120
+ end
121
+
122
+ # Add a pre-existing cookie object to the jar.
123
+ #
124
+ # @param [Cookie] cookie a pre-existing cookie object
125
+ # @return [Cookie] the cookie added to the store
126
+ def add_cookie(cookie)
127
+ domain_paths = find_or_add_domain_for_cookie cookie
128
+ add_cookie_to_path domain_paths, cookie
129
+ cookie
130
+ end
131
+
132
+ # Return an array of all cookie objects in the jar
133
+ #
134
+ # @return [Array<Cookie>] all cookies. Includes any expired cookies
135
+ # which have not yet been removed with expire_cookies
136
+ def to_a
137
+ result = []
138
+ @domains.values.each do |paths|
139
+ paths.values.each do |cookies|
140
+ cookies.values.inject result, :<<
141
+ end
142
+ end
143
+ result
144
+ end
145
+
146
+ # Return a JSON 'object' for the various data values. Allows for
147
+ # persistence of the cookie information
148
+ #
149
+ # @param [Array] a options controlling output JSON text
150
+ # (usually a State and a depth)
151
+ # @return [String] JSON representation of object data
152
+ def to_json(*a)
153
+ {
154
+ 'json_class' => self.class.name,
155
+ 'cookies' => to_a.to_json(*a)
156
+ }.to_json(*a)
157
+ end
158
+
159
+ # Create a new Jar from a JSON-backed hash
160
+ #
161
+ # @param o [Hash] the expanded JSON object
162
+ # @return [CookieJar] a new CookieJar instance
163
+ def self.json_create(o)
164
+ o = JSON.parse(o) if o.is_a? String
165
+ o = o['cookies'] if o.is_a? Hash
166
+ cookies = o.inject([]) do |result, cookie_json|
167
+ result << (Cookie.json_create cookie_json)
168
+ end
169
+ from_a cookies
170
+ end
171
+
172
+ # Create a new Jar from an array of Cookie objects. Expired cookies
173
+ # will still be added to the archive, and conflicting cookies will
174
+ # be overwritten by the last cookie in the array.
175
+ #
176
+ # @param [Array<Cookie>] cookies array of cookie objects
177
+ # @return [CookieJar] a new CookieJar instance
178
+ def self.from_a(cookies)
179
+ jar = new
180
+ cookies.each do |cookie|
181
+ jar.add_cookie cookie
182
+ end
183
+ jar
184
+ end
185
+
186
+ # Look through the jar for any cookies which have passed their expiration
187
+ # date, or session cookies from a previous session
188
+ #
189
+ # @param session [Boolean] whether session cookies should be expired,
190
+ # or just cookies past their expiration date.
191
+ def expire_cookies(session = false)
192
+ @domains.delete_if do |_domain, paths|
193
+ paths.delete_if do |_path, cookies|
194
+ cookies.delete_if do |_cookie_name, cookie|
195
+ cookie.expired? || (session && cookie.session?)
196
+ end
197
+ cookies.empty?
198
+ end
199
+ paths.empty?
200
+ end
201
+ end
202
+
203
+ # Given a request URI, return a sorted list of Cookie objects. Cookies
204
+ # will be in order per RFC 2965 - sorted by longest path length, but
205
+ # otherwise unordered.
206
+ #
207
+ # @param [String, URI] request_uri the address the HTTP request will be
208
+ # sent to. This must be a full URI, i.e. must include the protocol,
209
+ # if you pass digi.ninja it will fail to find the domain, you must pass
210
+ # http://digi.ninja
211
+ # @param [Hash] opts options controlling returned cookies
212
+ # @option opts [Boolean] :script (false) Cookies marked HTTP-only will be
213
+ # ignored if true
214
+ # @return [Array<Cookie>] cookies which should be sent in the HTTP request
215
+ def get_cookies(request_uri, opts = {})
216
+ uri = to_uri request_uri
217
+ hosts = Cookie.compute_search_domains uri
218
+
219
+ return [] if hosts.nil?
220
+
221
+ path = if uri.path == ''
222
+ '/'
223
+ else
224
+ uri.path
225
+ end
226
+
227
+ results = []
228
+ hosts.each do |host|
229
+ domain = find_domain host
230
+ domain.each do |apath, cookies|
231
+ next unless path.start_with? apath
232
+ results += cookies.values.select do |cookie|
233
+ cookie.should_send? uri, opts[:script]
234
+ end
235
+ end
236
+ end
237
+ # Sort by path length, longest first
238
+ results.sort do |lhs, rhs|
239
+ rhs.path.length <=> lhs.path.length
240
+ end
241
+ end
242
+
243
+ # Given a request URI, return a string Cookie header.Cookies will be in
244
+ # order per RFC 2965 - sorted by longest path length, but otherwise
245
+ # unordered.
246
+ #
247
+ # @param [String, URI] request_uri the address the HTTP request will be
248
+ # sent to
249
+ # @param [Hash] opts options controlling returned cookies
250
+ # @option opts [Boolean] :script (false) Cookies marked HTTP-only will be
251
+ # ignored if true
252
+ # @return String value of the Cookie header which should be sent on the
253
+ # HTTP request
254
+ def get_cookie_header(request_uri, opts = {})
255
+ cookies = get_cookies request_uri, opts
256
+ ver = [[], []]
257
+ cookies.each do |cookie|
258
+ ver[cookie.version] << cookie
259
+ end
260
+ if ver[1].empty?
261
+ # can do a netscape-style cookie header, relish the opportunity
262
+ cookies.map(&:to_s).join ';'
263
+ else
264
+ # build a RFC 2965-style cookie header. Split the cookies into
265
+ # version 0 and 1 groups so that we can reuse the '$Version' header
266
+ result = ''
267
+ unless ver[0].empty?
268
+ result << '$Version=0;'
269
+ result << ver[0].map do |cookie|
270
+ (cookie.to_s 1, false)
271
+ end.join(';')
272
+ # separate version 0 and 1 with a comma
273
+ result << ','
274
+ end
275
+ result << '$Version=1;'
276
+ ver[1].map do |cookie|
277
+ result << (cookie.to_s 1, false)
278
+ end
279
+ result
280
+ end
281
+ end
282
+
283
+ protected
284
+
285
+ def gather_header_values(http_header_value, &_block)
286
+ result = []
287
+ if http_header_value.is_a? Array
288
+ http_header_value.each do |value|
289
+ result << yield(value)
290
+ end
291
+ elsif http_header_value.is_a? String
292
+ result << yield(http_header_value)
293
+ end
294
+ result.compact
295
+ end
296
+
297
+ def to_uri(request_uri)
298
+ (request_uri.is_a? URI) ? request_uri : (URI.parse request_uri)
299
+ end
300
+
301
+ def find_domain(host)
302
+ @domains[host] || {}
303
+ end
304
+
305
+ def find_or_add_domain_for_cookie(cookie)
306
+ @domains[cookie.domain] ||= {}
307
+ end
308
+
309
+ def add_cookie_to_path(paths, cookie)
310
+ path_entry = (paths[cookie.path] ||= {})
311
+ path_entry[cookie.name] = cookie
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module CookieJar
3
+ VERSION = '0.3.4'.freeze
4
+ end
data/lib/cookiejar.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'cookiejar/cookie'
2
+ require 'cookiejar/jar'
3
+ require 'cookiejar/version'
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ include CookieJar
5
+
6
+ FOO_URL = 'http://localhost/foo'.freeze
7
+ AMMO_URL = 'http://localhost/ammo'.freeze
8
+ NETSCAPE_SPEC_SET_COOKIE_HEADERS =
9
+ [['CUSTOMER=WILE_E_COYOTE; path=/; expires=Wednesday, 09-Nov-99 23:12:40 GMT',
10
+ FOO_URL],
11
+ ['PART_NUMBER=ROCKET_LAUNCHER_0001; path=/',
12
+ FOO_URL],
13
+ ['SHIPPING=FEDEX; path=/foo',
14
+ FOO_URL],
15
+ ['PART_NUMBER=ROCKET_LAUNCHER_0001; path=/',
16
+ FOO_URL],
17
+ ['PART_NUMBER=RIDING_ROCKET_0023; path=/ammo',
18
+ AMMO_URL]].freeze
19
+
20
+ describe Cookie do
21
+ describe '#from_set_cookie' do
22
+ it 'should handle cookies from the netscape spec' do
23
+ NETSCAPE_SPEC_SET_COOKIE_HEADERS.each do |value|
24
+ header, url = *value
25
+ Cookie.from_set_cookie url, header
26
+ end
27
+ end
28
+ it 'should give back the input names and values' do
29
+ cookie = Cookie.from_set_cookie 'http://localhost/', 'foo=bar'
30
+ expect(cookie.name).to eq 'foo'
31
+ expect(cookie.value).to eq 'bar'
32
+ end
33
+ it 'should normalize domain names' do
34
+ cookie = Cookie.from_set_cookie 'http://localhost/', 'foo=Bar;domain=LoCaLHoSt.local'
35
+ expect(cookie.domain).to eq '.localhost.local'
36
+ end
37
+ it 'should accept non-normalized .local' do
38
+ cookie = Cookie.from_set_cookie 'http://localhost/', 'foo=bar;domain=.local'
39
+ expect(cookie.domain).to eq '.local'
40
+ end
41
+ it 'should accept secure cookies' do
42
+ cookie = Cookie.from_set_cookie 'https://www.google.com/a/blah', 'GALX=RgmSftjnbPM;Path=/a/;Secure'
43
+ expect(cookie.name).to eq 'GALX'
44
+ expect(cookie.secure).to be_truthy
45
+ end
46
+ end
47
+ describe '#from_set_cookie2' do
48
+ it 'should give back the input names and values' do
49
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'foo=bar;Version=1'
50
+ expect(cookie.name).to eq 'foo'
51
+ expect(cookie.value).to eq 'bar'
52
+ end
53
+ it 'should normalize domain names' do
54
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'foo=Bar;domain=LoCaLHoSt.local;Version=1'
55
+ expect(cookie.domain).to eq '.localhost.local'
56
+ end
57
+ it 'should accept non-normalized .local' do
58
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'foo=bar;domain=.local;Version=1'
59
+ expect(cookie.domain).to eq '.local'
60
+ end
61
+ it 'should accept secure cookies' do
62
+ cookie = Cookie.from_set_cookie2 'https://www.google.com/a/blah', 'GALX=RgmSftjnbPM;Path="/a/";Secure;Version=1'
63
+ expect(cookie.name).to eq 'GALX'
64
+ expect(cookie.path).to eq '/a/'
65
+ expect(cookie.secure).to be_truthy
66
+ end
67
+ it 'should fail on unquoted paths' do
68
+ expect {
69
+ Cookie.from_set_cookie2 'https://www.google.com/a/blah',
70
+ 'GALX=RgmSftjnbPM;Path=/a/;Secure;Version=1'
71
+ }.to raise_error InvalidCookieError
72
+ end
73
+ it 'should accept quoted values' do
74
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'foo="bar";Version=1'
75
+ expect(cookie.name).to eq 'foo'
76
+ expect(cookie.value).to eq '"bar"'
77
+ end
78
+ it 'should accept poorly chosen names' do
79
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'Version=mine;Version=1'
80
+ expect(cookie.name).to eq 'Version'
81
+ expect(cookie.value).to eq 'mine'
82
+ end
83
+ it 'should accept quoted parameter values' do
84
+ Cookie.from_set_cookie2 'http://localhost/', 'foo=bar;Version="1"'
85
+ end
86
+ it 'should honor the discard and max-age parameters' do
87
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f=b;max-age=100;discard;Version=1'
88
+ expect(cookie).to be_session
89
+ expect(cookie).to_not be_expired
90
+
91
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f=b;max-age=100;Version=1'
92
+ expect(cookie).to_not be_session
93
+
94
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f=b;Version=1'
95
+ expect(cookie).to be_session
96
+ end
97
+ it 'should handle quotable quotes' do
98
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f="\"";Version=1'
99
+ expect(cookie.value).to eq '"\""'
100
+ end
101
+ it 'should handle quotable apostrophes' do
102
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f="\;";Version=1'
103
+ expect(cookie.value).to eq '"\;"'
104
+ end
105
+ end
106
+ describe '#decoded_value' do
107
+ it 'should leave normal values alone' do
108
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f=b;Version=1'
109
+ expect(cookie.decoded_value).to eq 'b'
110
+ end
111
+ it 'should attempt to unencode quoted values' do
112
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f="\"b";Version=1'
113
+ expect(cookie.value).to eq '"\"b"'
114
+ expect(cookie.decoded_value).to eq '"b'
115
+ end
116
+ end
117
+ describe '#to_s' do
118
+ it 'should handle a simple cookie' do
119
+ cookie = Cookie.from_set_cookie 'http://localhost/', 'f=b'
120
+ expect(cookie.to_s).to eq 'f=b'
121
+ expect(cookie.to_s(1)).to eq '$Version=0;f=b;$Path="/"'
122
+ end
123
+ it 'should report an explicit domain' do
124
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f=b;Version=1;Domain=.local'
125
+ expect(cookie.to_s(1)).to eq '$Version=1;f=b;$Path="/";$Domain=.local'
126
+ end
127
+ it 'should return specified ports' do
128
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f=b;Version=1;Port="80,443"'
129
+ expect(cookie.to_s(1)).to eq '$Version=1;f=b;$Path="/";$Port="80,443"'
130
+ end
131
+ it 'should handle specified paths' do
132
+ cookie = Cookie.from_set_cookie 'http://localhost/bar/', 'f=b;path=/bar/'
133
+ expect(cookie.to_s).to eq 'f=b'
134
+ expect(cookie.to_s(1)).to eq '$Version=0;f=b;$Path="/bar/"'
135
+ end
136
+ it 'should omit $Version header when asked' do
137
+ cookie = Cookie.from_set_cookie 'http://localhost/', 'f=b'
138
+ expect(cookie.to_s(1, false)).to eq 'f=b;$Path="/"'
139
+ end
140
+ end
141
+ describe '#should_send?' do
142
+ it 'should not send if ports do not match' do
143
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f=b;Version=1;Port="80"'
144
+ expect(cookie.should_send?('http://localhost/', false)).to be_truthy
145
+ expect(cookie.should_send?('https://localhost/', false)).to be_falsey
146
+ end
147
+ end
148
+ begin
149
+ require 'json'
150
+ describe '.to_json' do
151
+ it 'should serialize a cookie to JSON' do
152
+ c = Cookie.from_set_cookie 'https://localhost/', 'foo=bar;secure;expires=Fri, September 11 2009 18:10:00 -0700'
153
+ json = c.to_json
154
+ expect(json).to be_a String
155
+ end
156
+ end
157
+ describe '.json_create' do
158
+ it 'should deserialize JSON to a cookie' do
159
+ json = '{"name":"foo","value":"bar","domain":"localhost.local","path":"\\/","created_at":"2009-09-11 12:51:03 -0600","expiry":"2009-09-11 19:10:00 -0600","secure":true}'
160
+ hash = JSON.parse json
161
+ c = Cookie.json_create hash
162
+ CookieValidation.validate_cookie 'https://localhost/', c
163
+ end
164
+ it 'should automatically deserialize to a cookie' do
165
+ json = '{"json_class":"CookieJar::Cookie","name":"foo","value":"bar","domain":"localhost.local","path":"\\/","created_at":"2009-09-11 12:51:03 -0600","expiry":"2009-09-11 19:10:00 -0600","secure":true}'
166
+ c = JSON.parse json, create_additions: true
167
+ expect(c).to be_a Cookie
168
+ CookieValidation.validate_cookie 'https://localhost/', c
169
+ end
170
+ end
171
+ rescue LoadError
172
+ it 'does not appear the JSON library is installed' do
173
+ raise 'please install the JSON library'
174
+ end
175
+ end
176
+ end