http_monkey-cookie 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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