http_monkey-cookie 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +54 -0
- data/Rakefile +11 -0
- data/http_monkey-cookie.gemspec +29 -0
- data/lib/cookiejar.rb +2 -0
- data/lib/cookiejar/cookie.rb +252 -0
- data/lib/cookiejar/cookie_validation.rb +390 -0
- data/lib/cookiejar/jar.rb +310 -0
- data/lib/http_monkey/cookie.rb +36 -0
- data/lib/http_monkey/cookie/version.rb +5 -0
- data/lib/http_monkey/middlewares/cookie.rb +58 -0
- data/test/middleware_test.rb +50 -0
- data/test/support/captain_hook.rb +38 -0
- data/test/test_helper.rb +21 -0
- metadata +169 -0
@@ -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,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
|