http_monkey-cookie 0.0.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,310 @@
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(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
+ if o.is_a? Hash
165
+ o = o['cookies']
166
+ end
167
+ cookies = o.inject [] do |result, cookie_json|
168
+ result << (Cookie.json_create cookie_json)
169
+ end
170
+ self.from_a cookies
171
+ end
172
+
173
+ # Create a new Jar from an array of Cookie objects. Expired cookies
174
+ # will still be added to the archive, and conflicting cookies will
175
+ # be overwritten by the last cookie in the array.
176
+ #
177
+ # @param [Array<Cookie>] cookies array of cookie objects
178
+ # @return [CookieJar] a new CookieJar instance
179
+ def self.from_a cookies
180
+ jar = new
181
+ cookies.each do |cookie|
182
+ jar.add_cookie cookie
183
+ end
184
+ jar
185
+ end
186
+
187
+ # Look through the jar for any cookies which have passed their expiration
188
+ # date, or session cookies from a previous session
189
+ #
190
+ # @param session [Boolean] whether session cookies should be expired,
191
+ # or just cookies past their expiration date.
192
+ def expire_cookies session = false
193
+ @domains.delete_if do |domain, paths|
194
+ paths.delete_if do |path, cookies|
195
+ cookies.delete_if do |cookie_name, cookie|
196
+ cookie.expired? || (session && cookie.session?)
197
+ end
198
+ cookies.empty?
199
+ end
200
+ paths.empty?
201
+ end
202
+ end
203
+
204
+ # Given a request URI, return a sorted list of Cookie objects. Cookies
205
+ # will be in order per RFC 2965 - sorted by longest path length, but
206
+ # otherwise unordered.
207
+ #
208
+ # @param [String, URI] request_uri the address the HTTP request will be
209
+ # sent to
210
+ # @param [Hash] opts options controlling returned cookies
211
+ # @option opts [Boolean] :script (false) Cookies marked HTTP-only will be ignored
212
+ # if true
213
+ # @return [Array<Cookie>] cookies which should be sent in the HTTP request
214
+ def get_cookies request_uri, opts = { }
215
+ uri = to_uri request_uri
216
+ hosts = Cookie.compute_search_domains uri
217
+
218
+ results = []
219
+ hosts.each do |host|
220
+ domain = find_domain host
221
+ domain.each do |path, cookies|
222
+ if uri.path.start_with? path
223
+ results += cookies.values.select do |cookie|
224
+ cookie.should_send? uri, opts[:script]
225
+ end
226
+ end
227
+ end
228
+ end
229
+ #Sort by path length, longest first
230
+ results.sort do |lhs, rhs|
231
+ rhs.path.length <=> lhs.path.length
232
+ end
233
+ end
234
+
235
+ # Given a request URI, return a string Cookie header.Cookies will be in
236
+ # order per RFC 2965 - sorted by longest path length, but otherwise
237
+ # unordered.
238
+ #
239
+ # @param [String, URI] request_uri the address the HTTP request will be
240
+ # sent to
241
+ # @param [Hash] opts options controlling returned cookies
242
+ # @option opts [Boolean] :script (false) Cookies marked HTTP-only will be ignored
243
+ # if true
244
+ # @return String value of the Cookie header which should be sent on the
245
+ # HTTP request
246
+ def get_cookie_header request_uri, opts = { }
247
+ cookies = get_cookies request_uri, opts
248
+ version = 0
249
+ ver = [[],[]]
250
+ cookies.each do |cookie|
251
+ ver[cookie.version] << cookie
252
+ end
253
+ if (ver[1].empty?)
254
+ # can do a netscape-style cookie header, relish the opportunity
255
+ cookies.map do |cookie|
256
+ cookie.to_s
257
+ end.join ";"
258
+ else
259
+ # build a RFC 2965-style cookie header. Split the cookies into
260
+ # version 0 and 1 groups so that we can reuse the '$Version' header
261
+ result = ''
262
+ unless ver[0].empty?
263
+ result << '$Version=0;'
264
+ result << ver[0].map do |cookie|
265
+ (cookie.to_s 1,false)
266
+ end.join(';')
267
+ # separate version 0 and 1 with a comma
268
+ result << ','
269
+ end
270
+ result << '$Version=1;'
271
+ ver[1].map do |cookie|
272
+ result << (cookie.to_s 1,false)
273
+ end
274
+ result
275
+ end
276
+ end
277
+
278
+ protected
279
+
280
+ def gather_header_values http_header_value, &block
281
+ result = []
282
+ http_header_value
283
+ if http_header_value.is_a? Array
284
+ http_header_value.each do |value|
285
+ result << block.call(value)
286
+ end
287
+ elsif http_header_value.is_a? String
288
+ result << block.call(http_header_value)
289
+ end
290
+ result.compact
291
+ end
292
+
293
+ def to_uri request_uri
294
+ (request_uri.is_a? URI)? request_uri : (URI.parse request_uri)
295
+ end
296
+
297
+ def find_domain host
298
+ @domains[host] || {}
299
+ end
300
+
301
+ def find_or_add_domain_for_cookie cookie
302
+ @domains[cookie.domain] ||= {}
303
+ end
304
+
305
+ def add_cookie_to_path paths, cookie
306
+ path_entry = (paths[cookie.path] ||= {})
307
+ path_entry[cookie.name] = cookie
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,36 @@
1
+ require "cookiejar"
2
+
3
+ require "http_monkey/cookie/version"
4
+ require "http_monkey/middlewares/cookie"
5
+
6
+ module HttpMonkey
7
+
8
+ module Cookie
9
+
10
+ class MemoryStore
11
+
12
+ def self.instance
13
+ @@me ||= self.new
14
+ end
15
+
16
+ def initialize
17
+ @store = Hash.new
18
+ end
19
+
20
+ def write(key, value)
21
+ @store[key] = value
22
+ end
23
+
24
+ def read(key)
25
+ @store[key]
26
+ end
27
+
28
+ def inspect
29
+ @store.inspect
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,5 @@
1
+ module HttpMonkey
2
+ module Cookie
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,58 @@
1
+ module HttpMonkey::Middlewares
2
+
3
+ class Cookie
4
+
5
+ def initialize(app, options = {})
6
+ @app = app
7
+ @store = options.fetch(:store, HttpMonkey::Cookie::MemoryStore.instance)
8
+ @debug = options.fetch(:debug, false)
9
+ end
10
+
11
+ def call(env)
12
+ log "-> Cookie Middleware"
13
+
14
+ retrieve_cookie(env)
15
+ response = @app.call(env)
16
+ store_cookie(env, response)
17
+
18
+ log "<- Cookie Middleware"
19
+ response
20
+ end
21
+
22
+ protected
23
+
24
+ # Chech for cookies stored to domain
25
+ def retrieve_cookie(env)
26
+ cookiejar = @store.read("cookiejar")
27
+ return if cookiejar.nil?
28
+
29
+ url = env.uri.to_s
30
+ cookies = cookiejar.get_cookie_header(url)
31
+ log "--> Cookies(#{url}): #{cookies.inspect}"
32
+ env["HTTP_COOKIE"] = cookies
33
+ end
34
+
35
+ def store_cookie(env, response)
36
+ headers = response[1]
37
+ set_cookie = headers["Set-Cookie"]
38
+ return if set_cookie.nil?
39
+
40
+ cookiejar = @store.read("cookiejar")
41
+ cookiejar = ::CookieJar::Jar.new unless cookiejar
42
+ url = env.uri.to_s
43
+ if cookiejar.get_cookies(url).empty?
44
+ Array(set_cookie).each do |header_cookie|
45
+ cookiejar.set_cookie(url, header_cookie)
46
+ end
47
+ @store.write("cookiejar", cookiejar)
48
+ log "<-- Store: #{@store.inspect}"
49
+ end
50
+ nil
51
+ end
52
+
53
+ def log(msg)
54
+ puts msg if @debug
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,50 @@
1
+ require "test_helper"
2
+
3
+ COOKIE_MAGIC = "token=magic1"
4
+ SET_COOKIE_MAGIC = "#{COOKIE_MAGIC};Version=1;Comment=;Domain=.cookies.com;Path=/;Max-Age=999999999;httpOnly"
5
+
6
+ CookieApp = Rack::Builder.new do
7
+
8
+
9
+ module Helper
10
+ def self.respond_with(headers = {})
11
+ [200, {"Content-Type" => "text/plain"}.merge(headers), ["body"]]
12
+ end
13
+ end
14
+
15
+ map "/" do
16
+ run lambda { |env|
17
+ Helper.respond_with("Set-Cookie" => SET_COOKIE_MAGIC)
18
+ }
19
+ end
20
+
21
+ map "/subhome" do
22
+ run lambda { |env|
23
+ Helper.respond_with("X-Request-Cookies" => env["HTTP_COOKIE"])
24
+ }
25
+ end
26
+ end
27
+
28
+ CookieMonkey = HttpMonkey.build do
29
+ middlewares.use HttpMonkey::Middlewares::Cookie
30
+ end
31
+
32
+ describe HttpMonkey::Middlewares::Cookie do
33
+
34
+ def self.before_suite
35
+ @server = MinionServer.new(CookieApp).start("local.cookies.com", 1234)
36
+ end
37
+
38
+ def self.after_suite
39
+ @server.shutdown
40
+ end
41
+
42
+ it "Magic cookie" do
43
+ response = CookieMonkey.at("http://local.cookies.com:1234").get
44
+ response.headers["Set-Cookie"].must_equal(SET_COOKIE_MAGIC)
45
+
46
+ response = CookieMonkey.at("http://local.cookies.com:1234/subhome").get
47
+ response.headers["X-Request-Cookies"].must_equal(COOKIE_MAGIC)
48
+ end
49
+
50
+ end