cookiejar 0.2.9

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,300 @@
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
+ # heirarchy.
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 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_value
60
+ cookie = Cookie.from_set_cookie request_uri, cookie_header_value
61
+ add_cookie cookie
62
+ end
63
+
64
+ # Given a request URI and a literal Set-Cookie2 header value, attempt to
65
+ # add the cookie to the cookie store.
66
+ #
67
+ # @param [String, URI] request_uri the resource returning the header
68
+ # @param [String] cookie_header_value the contents of the Set-Cookie2
69
+ # @return [Cookie] which was created and stored
70
+ # @raise [InvalidCookieError] if the cookie header did not validate
71
+ def set_cookie2 request_uri, cookie_header_value
72
+ cookie = Cookie.from_set_cookie2 request_uri, cookie_header_value
73
+ add_cookie cookie
74
+ end
75
+
76
+ # Given a request URI and some HTTP headers, attempt to add the cookie(s)
77
+ # (from Set-Cookie or Set-Cookie2 headers) to the cookie store. If a
78
+ # cookie is defined (by equivalent name, domain, and path) via Set-Cookie
79
+ # and Set-Cookie2, the Set-Cookie version is ignored.
80
+ #
81
+ # @param [String, URI] request_uri the resource returning the header
82
+ # @param [Hash<String,[String,Array<String>]>] http_headers a Hash
83
+ # which may have a key of "Set-Cookie" or "Set-Cookie2", and values of
84
+ # either strings or arrays of strings
85
+ # @return [Array<Cookie>,nil] the cookies created, or nil if none found.
86
+ # @raise [InvalidCookieError] if one of the cookie headers contained
87
+ # invalid formatting or data
88
+ def set_cookies_from_headers request_uri, http_headers
89
+ cookies = gather_header_values http_headers['Set-Cookie'] do |value|
90
+ Cookie.from_set_cookie request_uri, value
91
+ end
92
+
93
+ cookies += gather_header_values(http_headers['Set-Cookie2']) do |value|
94
+ Cookie.from_set_cookie2 request_uri, value
95
+ end
96
+
97
+ # build the list of cookies, using a Jar. Since Set-Cookie2 values
98
+ # come second, they will replace the Set-Cookie versions.
99
+ jar = Jar.new
100
+ cookies.each do |cookie|
101
+ jar.add_cookie cookie
102
+ end
103
+ cookies = jar.to_a
104
+
105
+ # now add them all to our own store.
106
+ cookies.each do |cookie|
107
+ add_cookie cookie
108
+ end
109
+ cookies
110
+ end
111
+
112
+ # Add a pre-existing cookie object to the jar.
113
+ #
114
+ # @param [Cookie] cookie a pre-existing cookie object
115
+ # @return [Cookie] the cookie added to the store
116
+ def add_cookie cookie
117
+ domain_paths = find_or_add_domain_for_cookie cookie
118
+ add_cookie_to_path domain_paths, cookie
119
+ cookie
120
+ end
121
+
122
+ # Return an array of all cookie objects in the jar
123
+ #
124
+ # @return [Array<Cookie>] all cookies. Includes any expired cookies
125
+ # which have not yet been removed with expire_cookies
126
+ def to_a
127
+ result = []
128
+ @domains.values.each do |paths|
129
+ paths.values.each do |cookies|
130
+ cookies.values.inject result, :<<
131
+ end
132
+ end
133
+ result
134
+ end
135
+
136
+ # Return a JSON 'object' for the various data values. Allows for
137
+ # persistence of the cookie information
138
+ #
139
+ # @param [Array] a options controlling output JSON text
140
+ # (usually a State and a depth)
141
+ # @return [String] JSON representation of object data
142
+ def to_json *a
143
+ {
144
+ 'json_class' => self.class.name,
145
+ 'cookies' => (to_a.to_json *a)
146
+ }.to_json *a
147
+ end
148
+
149
+ # Create a new Jar from a JSON-backed hash
150
+ #
151
+ # @param o [Hash] the expanded JSON object
152
+ # @return [CookieJar] a new CookieJar instance
153
+ def self.json_create o
154
+ if o.is_a? Hash
155
+ o = o['cookies']
156
+ end
157
+ cookies = o.inject [] do |result, cookie_json|
158
+ result << (Cookie.json_create cookie_json)
159
+ end
160
+ self.from_a cookies
161
+ end
162
+
163
+ # Create a new Jar from an array of Cookie objects. Expired cookies
164
+ # will still be added to the archive, and conflicting cookies will
165
+ # be overwritten by the last cookie in the array.
166
+ #
167
+ # @param [Array<Cookie>] cookies array of cookie objects
168
+ # @return [CookieJar] a new CookieJar instance
169
+ def self.from_a cookies
170
+ jar = new
171
+ cookies.each do |cookie|
172
+ jar.add_cookie cookie
173
+ end
174
+ jar
175
+ end
176
+
177
+ # Look through the jar for any cookies which have passed their expiration
178
+ # date, or session cookies from a previous session
179
+ #
180
+ # @param session [Boolean] whether session cookies should be expired,
181
+ # or just cookies past their expiration date.
182
+ def expire_cookies session = false
183
+ @domains.delete_if do |domain, paths|
184
+ paths.delete_if do |path, cookies|
185
+ cookies.delete_if do |cookie_name, cookie|
186
+ cookie.expired? || (session && cookie.session?)
187
+ end
188
+ cookies.empty?
189
+ end
190
+ paths.empty?
191
+ end
192
+ end
193
+
194
+ # Given a request URI, return a sorted list of Cookie objects. Cookies
195
+ # will be in order per RFC 2965 - sorted by longest path length, but
196
+ # otherwise unordered.
197
+ #
198
+ # @param [String, URI] request_uri the address the HTTP request will be
199
+ # sent to
200
+ # @param [Hash] opts options controlling returned cookies
201
+ # @option opts [Boolean] :script (false) Cookies marked HTTP-only will be ignored
202
+ # if true
203
+ # @return [Array<Cookie>] cookies which should be sent in the HTTP request
204
+ def get_cookies request_uri, opts = { }
205
+ uri = to_uri request_uri
206
+ hosts = Cookie.compute_search_domains uri
207
+
208
+ results = []
209
+ hosts.each do |host|
210
+ domain = find_domain host
211
+ domain.each do |path, cookies|
212
+ if uri.path.start_with? path
213
+ results += cookies.values.select do |cookie|
214
+ cookie.should_send? uri, opts[:script]
215
+ end
216
+ end
217
+ end
218
+ end
219
+ #Sort by path length, longest first
220
+ results.sort do |lhs, rhs|
221
+ rhs.path.length <=> lhs.path.length
222
+ end
223
+ end
224
+
225
+ # Given a request URI, return a string Cookie header.Cookies will be in
226
+ # order per RFC 2965 - sorted by longest path length, but otherwise
227
+ # unordered.
228
+ #
229
+ # @param [String, URI] request_uri the address the HTTP request will be
230
+ # sent to
231
+ # @param [Hash] opts options controlling returned cookies
232
+ # @option opts [Boolean] :script (false) Cookies marked HTTP-only will be ignored
233
+ # if true
234
+ # @return String value of the Cookie header which should be sent on the
235
+ # HTTP request
236
+ def get_cookie_header request_uri, opts = { }
237
+ cookies = get_cookies request_uri, opts
238
+ version = 0
239
+ ver = [[],[]]
240
+ cookies.each do |cookie|
241
+ ver[cookie.version] << cookie
242
+ end
243
+ if (ver[1].empty?)
244
+ # can do a netscape-style cookie header, relish the opportunity
245
+ cookies.map do |cookie|
246
+ cookie.to_s
247
+ end.join ";"
248
+ else
249
+ # build a RFC 2965-style cookie header. Split the cookies into
250
+ # version 0 and 1 groups so that we can reuse the '$Version' header
251
+ result = ''
252
+ unless ver[0].empty?
253
+ result << '$Version=0;'
254
+ result << ver[0].map do |cookie|
255
+ (cookie.to_s 1,false)
256
+ end.join(';')
257
+ # separate version 0 and 1 with a comma
258
+ result << ','
259
+ end
260
+ result << '$Version=1;'
261
+ ver[1].map do |cookie|
262
+ result << (cookie.to_s 1,false)
263
+ end
264
+ result
265
+ end
266
+ end
267
+
268
+ protected
269
+
270
+ def gather_header_values http_header_value, &block
271
+ result = []
272
+ http_header_value
273
+ if http_header_value.is_a? Array
274
+ http_header_value.each do |value|
275
+ result << block.call(value)
276
+ end
277
+ elsif http_header_value.is_a? String
278
+ result << block.call(http_header_value)
279
+ end
280
+ result
281
+ end
282
+
283
+ def to_uri request_uri
284
+ (request_uri.is_a? URI)? request_uri : (URI.parse request_uri)
285
+ end
286
+
287
+ def find_domain host
288
+ @domains[host] || {}
289
+ end
290
+
291
+ def find_or_add_domain_for_cookie cookie
292
+ @domains[cookie.domain] ||= {}
293
+ end
294
+
295
+ def add_cookie_to_path paths, cookie
296
+ path_entry = (paths[cookie.path] ||= {})
297
+ path_entry[cookie.name] = cookie
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,176 @@
1
+ require 'cookiejar'
2
+ require 'rubygems'
3
+
4
+ include CookieJar
5
+
6
+ FOO_URL = 'http://localhost/foo'
7
+ AMMO_URL = 'http://localhost/ammo'
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]]
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 = 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
+ cookie.name.should == 'foo'
31
+ cookie.value.should == 'bar'
32
+ end
33
+ it "should normalize domain names" do
34
+ cookie = Cookie.from_set_cookie 'http://localhost/', 'foo=Bar;domain=LoCaLHoSt.local'
35
+ cookie.domain.should == '.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
+ cookie.domain.should == '.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
+ cookie.name.should == 'GALX'
44
+ cookie.secure.should be_true
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
+ cookie.name.should == 'foo'
51
+ cookie.value.should == '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
+ cookie.domain.should == '.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
+ cookie.domain.should == '.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
+ cookie.name.should == 'GALX'
64
+ cookie.path.should == '/a/'
65
+ cookie.secure.should be_true
66
+ end
67
+ it "should fail on unquoted paths" do
68
+ lambda do
69
+ Cookie.from_set_cookie2 'https://www.google.com/a/blah',
70
+ 'GALX=RgmSftjnbPM;Path=/a/;Secure;Version=1'
71
+ end.should 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
+ cookie.name.should == 'foo'
76
+ cookie.value.should == '"bar"'
77
+ end
78
+ it "should accept poorly chosen names" do
79
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'Version=mine;Version=1'
80
+ cookie.name.should == 'Version'
81
+ cookie.value.should == 'mine'
82
+ end
83
+ it "should accept quoted parameter values" do
84
+ cookie = 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
+ cookie.should be_session
89
+ cookie.should_not be_expired
90
+
91
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f=b;max-age=100;Version=1'
92
+ cookie.should_not be_session
93
+
94
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f=b;Version=1'
95
+ cookie.should be_session
96
+ end
97
+ it "should handle quotable quotes" do
98
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f="\"";Version=1'
99
+ cookie.value.should eql '"\""'
100
+ end
101
+ it "should handle quotable apostrophes" do
102
+ cookie = Cookie.from_set_cookie2 'http://localhost/', 'f="\;";Version=1'
103
+ cookie.value.should eql '"\;"'
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
+ cookie.decoded_value.should eql '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
+ cookie.value.should eql '"\"b"'
114
+ cookie.decoded_value.should eql '"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
+ cookie.to_s.should == 'f=b'
121
+ cookie.to_s(1).should == '$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
+ cookie.to_s(1).should == '$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
+ cookie.to_s(1).should == '$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
+ cookie.to_s.should == 'f=b'
134
+ cookie.to_s(1).should == '$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
+ cookie.to_s(1,false).should == '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
+ cookie.should_send?("http://localhost/", false).should be_true
145
+ cookie.should_send?("https://localhost/", false).should be_false
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
+ json.should 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
167
+ c.should 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