secobarbital-cookiejar 0.2.9.1

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,308 @@
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
+ set_cookie_key = http_headers.keys.detect { |k| /\ASet-Cookie\Z/i.match k }
90
+ cookies = gather_header_values http_headers[set_cookie_key] do |value|
91
+ begin
92
+ Cookie.from_set_cookie request_uri, value
93
+ rescue InvalidCookieError
94
+ end
95
+ end
96
+
97
+ set_cookie2_key = http_headers.keys.detect { |k| /\ASet-Cookie2\Z/i.match k }
98
+ cookies += gather_header_values(http_headers[set_cookie2_key]) do |value|
99
+ begin
100
+ Cookie.from_set_cookie2 request_uri, value
101
+ rescue InvalidCookieError
102
+ end
103
+ end
104
+
105
+ # build the list of cookies, using a Jar. Since Set-Cookie2 values
106
+ # come second, they will replace the Set-Cookie versions.
107
+ jar = Jar.new
108
+ cookies.each do |cookie|
109
+ jar.add_cookie cookie
110
+ end
111
+ cookies = jar.to_a
112
+
113
+ # now add them all to our own store.
114
+ cookies.each do |cookie|
115
+ add_cookie cookie
116
+ end
117
+ cookies
118
+ end
119
+
120
+ # Add a pre-existing cookie object to the jar.
121
+ #
122
+ # @param [Cookie] cookie a pre-existing cookie object
123
+ # @return [Cookie] the cookie added to the store
124
+ def add_cookie cookie
125
+ domain_paths = find_or_add_domain_for_cookie cookie
126
+ add_cookie_to_path domain_paths, cookie
127
+ cookie
128
+ end
129
+
130
+ # Return an array of all cookie objects in the jar
131
+ #
132
+ # @return [Array<Cookie>] all cookies. Includes any expired cookies
133
+ # which have not yet been removed with expire_cookies
134
+ def to_a
135
+ result = []
136
+ @domains.values.each do |paths|
137
+ paths.values.each do |cookies|
138
+ cookies.values.inject result, :<<
139
+ end
140
+ end
141
+ result
142
+ end
143
+
144
+ # Return a JSON 'object' for the various data values. Allows for
145
+ # persistence of the cookie information
146
+ #
147
+ # @param [Array] a options controlling output JSON text
148
+ # (usually a State and a depth)
149
+ # @return [String] JSON representation of object data
150
+ def to_json *a
151
+ {
152
+ 'json_class' => self.class.name,
153
+ 'cookies' => (to_a.to_json *a)
154
+ }.to_json *a
155
+ end
156
+
157
+ # Create a new Jar from a JSON-backed hash
158
+ #
159
+ # @param o [Hash] the expanded JSON object
160
+ # @return [CookieJar] a new CookieJar instance
161
+ def self.json_create o
162
+ if o.is_a? Hash
163
+ o = o['cookies']
164
+ end
165
+ cookies = o.inject [] do |result, cookie_json|
166
+ result << (Cookie.json_create cookie_json)
167
+ end
168
+ self.from_a cookies
169
+ end
170
+
171
+ # Create a new Jar from an array of Cookie objects. Expired cookies
172
+ # will still be added to the archive, and conflicting cookies will
173
+ # be overwritten by the last cookie in the array.
174
+ #
175
+ # @param [Array<Cookie>] cookies array of cookie objects
176
+ # @return [CookieJar] a new CookieJar instance
177
+ def self.from_a cookies
178
+ jar = new
179
+ cookies.each do |cookie|
180
+ jar.add_cookie cookie
181
+ end
182
+ jar
183
+ end
184
+
185
+ # Look through the jar for any cookies which have passed their expiration
186
+ # date, or session cookies from a previous session
187
+ #
188
+ # @param session [Boolean] whether session cookies should be expired,
189
+ # or just cookies past their expiration date.
190
+ def expire_cookies session = false
191
+ @domains.delete_if do |domain, paths|
192
+ paths.delete_if do |path, cookies|
193
+ cookies.delete_if do |cookie_name, cookie|
194
+ cookie.expired? || (session && cookie.session?)
195
+ end
196
+ cookies.empty?
197
+ end
198
+ paths.empty?
199
+ end
200
+ end
201
+
202
+ # Given a request URI, return a sorted list of Cookie objects. Cookies
203
+ # will be in order per RFC 2965 - sorted by longest path length, but
204
+ # otherwise unordered.
205
+ #
206
+ # @param [String, URI] request_uri the address the HTTP request will be
207
+ # sent to
208
+ # @param [Hash] opts options controlling returned cookies
209
+ # @option opts [Boolean] :script (false) Cookies marked HTTP-only will be ignored
210
+ # if true
211
+ # @return [Array<Cookie>] cookies which should be sent in the HTTP request
212
+ def get_cookies request_uri, opts = { }
213
+ uri = to_uri request_uri
214
+ hosts = Cookie.compute_search_domains uri
215
+
216
+ results = []
217
+ hosts.each do |host|
218
+ domain = find_domain host
219
+ domain.each do |path, cookies|
220
+ if uri.path.start_with? path
221
+ results += cookies.values.select do |cookie|
222
+ cookie.should_send? uri, opts[:script]
223
+ end
224
+ end
225
+ end
226
+ end
227
+ #Sort by path length, longest first
228
+ results.sort do |lhs, rhs|
229
+ rhs.path.length <=> lhs.path.length
230
+ end
231
+ end
232
+
233
+ # Given a request URI, return a string Cookie header.Cookies will be in
234
+ # order per RFC 2965 - sorted by longest path length, but otherwise
235
+ # unordered.
236
+ #
237
+ # @param [String, URI] request_uri the address the HTTP request will be
238
+ # sent to
239
+ # @param [Hash] opts options controlling returned cookies
240
+ # @option opts [Boolean] :script (false) Cookies marked HTTP-only will be ignored
241
+ # if true
242
+ # @return String value of the Cookie header which should be sent on the
243
+ # HTTP request
244
+ def get_cookie_header request_uri, opts = { }
245
+ cookies = get_cookies request_uri, opts
246
+ version = 0
247
+ ver = [[],[]]
248
+ cookies.each do |cookie|
249
+ ver[cookie.version] << cookie
250
+ end
251
+ if (ver[1].empty?)
252
+ # can do a netscape-style cookie header, relish the opportunity
253
+ cookies.map do |cookie|
254
+ cookie.to_s
255
+ end.join ";"
256
+ else
257
+ # build a RFC 2965-style cookie header. Split the cookies into
258
+ # version 0 and 1 groups so that we can reuse the '$Version' header
259
+ result = ''
260
+ unless ver[0].empty?
261
+ result << '$Version=0;'
262
+ result << ver[0].map do |cookie|
263
+ (cookie.to_s 1,false)
264
+ end.join(';')
265
+ # separate version 0 and 1 with a comma
266
+ result << ','
267
+ end
268
+ result << '$Version=1;'
269
+ ver[1].map do |cookie|
270
+ result << (cookie.to_s 1,false)
271
+ end
272
+ result
273
+ end
274
+ end
275
+
276
+ protected
277
+
278
+ def gather_header_values http_header_value, &block
279
+ result = []
280
+ http_header_value
281
+ if http_header_value.is_a? Array
282
+ http_header_value.each do |value|
283
+ result << block.call(value)
284
+ end
285
+ elsif http_header_value.is_a? String
286
+ result << block.call(http_header_value)
287
+ end
288
+ result.compact
289
+ end
290
+
291
+ def to_uri request_uri
292
+ (request_uri.is_a? URI)? request_uri : (URI.parse request_uri)
293
+ end
294
+
295
+ def find_domain host
296
+ @domains[host] || {}
297
+ end
298
+
299
+ def find_or_add_domain_for_cookie cookie
300
+ @domains[cookie.domain] ||= {}
301
+ end
302
+
303
+ def add_cookie_to_path paths, cookie
304
+ path_entry = (paths[cookie.path] ||= {})
305
+ path_entry[cookie.name] = cookie
306
+ end
307
+ end
308
+ end
data/lib/cookiejar.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'cookiejar/cookie'
2
+ require 'cookiejar/jar'
@@ -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