forked from assaf/zombie
/
cookies.coffee
201 lines (177 loc) · 7.53 KB
/
cookies.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# See [RFC 2109](http://tools.ietf.org/html/rfc2109.html) and
# [document.cookie](http://developer.mozilla.org/en/document.cookie)
URL = require("url")
core = require("jsdom").dom.level3.core
# Serialize cookie object into RFC2109 representation.
serialize = (browser, domain, path, name, cookie)->
str = "#{name}=#{cookie.value}; domain=#{domain}; path=#{path}"
str = str + "; max-age=#{cookie.expires - browser.clock}" if cookie.expires
str = str + "; secure" if cookie.secure
str
# Deserialize a cookie
deserialize = (serialized)->
fields = serialized.split(/;+/)
first = fields[0].trim()
[name, value] = first.split(/\=/, 2)
cookie = name: name, value: value
for field in fields
[key, val] = field.trim().split(/\=/, 2)
switch key.toLowerCase()
when "domain" then cookie.domain = dequote(val)
when "path" then cookie.path = dequote(val).replace(/%[^\/]*$/, "")
when "expires" then cookie.expires = new Date(dequote(val))
when "max-age" then cookie['max-age'] = parseInt(dequote(val), 10)
when "secure" then cookie.secure = true
return cookie
# Cookie header values are (supposed to be) quoted. This function strips
# double quotes aroud value, if it finds both quotes.
dequote = (value)-> value.replace(/^"(.*)"$/, "$1")
# Maintains cookies for a Browser instance. This is actually a domain/path
# specific scope around the global cookies collection.
class Cookies
constructor: (browser, cookies, hostname, pathname)->
pathname = "/" if !pathname || pathname == ""
domainMatch = (domain, hostname)->
return true if domain == hostname
return domain.charAt(0) == "." && domain.substring(1) == hostname.replace(/^[^.]+\./, "")
# Return all the cookies that match the given hostname/path, from most
# specific to least specific. Returns array of arrays, each item is
# [domain, path, name, cookie].
selected = ->
matching = []
for domain, in_domain of cookies
# Ignore cookies that don't match the exact hostname, or .domain.
continue unless domainMatch(domain, hostname)
# Ignore cookies that don't match the path.
for path, in_path of in_domain
continue unless pathname.indexOf(path) == 0
for name, cookie of in_path
# Delete expired cookies.
if typeof cookie.expires == "number" && cookie.expires <= browser.clock
delete in_path[name]
else
matching.push [domain, path, name, cookie]
# Sort from most specific to least specified. Only worry about path
# (longest is more specific)
matching.sort (a,b) -> a[1].length - b[1].length
#### cookies(host, path).get(name) => String
#
# Returns the value of a cookie.
#
# * name -- Cookie name
# * Returns cookie value if known
this.get = (name)->
for match in selected()
return match[3].value if match[2] == name
#### cookies(host, path).set(name, value, options?)
#
# Sets a cookie (deletes if expires/max-age is in the past).
#
# * name -- Cookie name
# * value -- Cookie value
# * options -- Options max-age, expires, secure, domain, path
this.set = (name, value, options = {})->
return if options.domain && !domainMatch(options.domain, hostname)
name = name
state = { value: value.toString() }
if options.expires
state.expires = options.expires.getTime()
else
maxage = options["max-age"]
state.expires = browser.clock + maxage if typeof maxage is "number"
state.secure = true if options.secure
if typeof state.expires is "number" && state.expires <= browser.clock
@remove(name, options)
else
path_without_resource = pathname.match(/.*\//) # everything but what trails the last /
in_domain = cookies[options.domain || hostname] ||= {}
in_path = in_domain[options.path || path_without_resource] ||= {}
in_path[name] = state
#### cookies(host, path).remove(name, options?)
#
# Deletes a cookie.
#
# * name -- Cookie name
# * options -- Options domain, path
this.remove = (name, options = {})->
if in_domain = cookies[options.domain || hostname]
if in_path = in_domain[options.path || pathname]
delete in_path[name]
#### cookies(host, path).clear()
#
# Clears all cookies.
this.clear = (options = {})->
if in_domain = cookies[hostname]
delete in_domain[pathname]
#### cookies(host, path).update(serialized)
#
# Update cookies from serialized form. This method works equally well for
# the Set-Cookie header and value passed to document.cookie setter.
#
# * serialized -- Serialized form
this.update = (serialized)->
return unless serialized
# Handle case where we get array of headers.
serialized = serialized.join(",") if serialized.constructor == Array
for cookie in serialized.split(/,(?=[^;,]*=)|,$/)
cookie = deserialize(cookie)
@set(cookie.name, cookie.value, cookie)
#### cookies(host, path).addHeader(headers)
#
# Adds Cookie header suitable for sending to the server.
this.addHeader = (headers)->
header = ("#{match[2]}=#{match[3].value}" for match in selected()).join("; ")
if header.length > 0
headers.cookie = header
#### cookies(host, path).pairs => String
#
# Returns key/value pairs of all cookies in this domain/path.
@__defineGetter__ "pairs", ->
("#{match[2]}=#{match[3].value}" for match in selected()).join("; ")
#### cookies(host, path).dump(separator?) => String
#
# The default separator is a line break, useful to output when
# debugging. If you need to save/load, use comma as the line
# separator and then call `cookies.update`.
this.dump = (separator = "\n")->
(@serialize(browser, match[0], match[1], match[2], match[3]) for match in selected()).join(separator)
# ### document.cookie => String
#
# Returns name=value pairs
core.HTMLDocument.prototype.__defineGetter__ "cookie", -> @parentWindow.cookies.pairs
# ### document.cookie = String
#
# Accepts serialized form (same as Set-Cookie header) and updates cookie from
# new values.
core.HTMLDocument.prototype.__defineSetter__ "cookie", (cookie)-> @parentWindow.cookies.update cookie
exports.use = (browser)->
cookies = {}
# Creates and returns cookie access scopes to given host/path.
access = (hostname, pathname)->
new Cookies(browser, cookies, hostname, pathname)
# Add cookies accessor to window: documents need this.
extend = (window)->
window.__defineGetter__ "cookies", -> access(@location.hostname, @location.pathname)
# Used to dump state to console (debuggin)
dump = ->
serialized = []
for domain, in_domain of cookies
for path, in_path of in_domain
for name, cookie of in_path
serialized.push serialize(browser, domain, path, name, cookie)
serialized
# browser.saveCookies uses this
save = ->
serialized = ["# Saved on #{new Date().toISOString()}"]
for domain, in_domain of cookies
for path, in_path of in_domain
for name, cookie of in_path
serialized.push serialize(browser, domain, path, name, cookie)
serialized.join("\n")
# browser.loadCookies uses this
load = (serialized)->
for cookie in serialized.split(/\n+/)
continue if cookie[0] == "#"
cookie = deserialize(cookie)
new Cookies(browser, cookies, cookie.domain, cookie.path).set(cookie.name, cookie.value, cookie)
return access: access, extend: extend, save: save, load: load, dump: dump