dwaite-cookiejar 0.1.3 → 0.2.0
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.
- data/lib/cookiejar.rb +0 -1
- data/lib/cookiejar/cookie.rb +52 -225
- data/lib/cookiejar/cookie_validation.rb +271 -0
- data/lib/cookiejar/jar.rb +120 -57
- data/test/cookie_test.rb +23 -223
- data/test/cookie_validation_test.rb +235 -0
- data/test/jar_test.rb +89 -4
- metadata +6 -4
- data/lib/cookiejar/cookie_common.rb +0 -14
data/lib/cookiejar.rb
CHANGED
data/lib/cookiejar/cookie.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'time'
|
2
2
|
require 'uri'
|
3
|
-
require 'cookiejar/
|
3
|
+
require 'cookiejar/cookie_validation'
|
4
4
|
|
5
5
|
module CookieJar
|
6
6
|
|
@@ -10,28 +10,6 @@ module CookieJar
|
|
10
10
|
# Specifically, the 'domain' and 'path' values may be set to defaults
|
11
11
|
# based on the requested resource that resulted in the cookie being set.
|
12
12
|
class Cookie
|
13
|
-
module PATTERN
|
14
|
-
include URI::REGEXP::PATTERN
|
15
|
-
|
16
|
-
TOKEN = '[^(),\/<>@;:\\\"\[\]?={}\s]*'
|
17
|
-
VALUE1 = "([^;]*)"
|
18
|
-
IPADDR = "#{IPV4ADDR}|#{IPV6ADDR}"
|
19
|
-
BASE_HOSTNAME = "(?:#{DOMLABEL}\\.)(?:((?:(?:#{DOMLABEL}\\.)+(?:#{TOPLABEL}\\.?))|local))"
|
20
|
-
|
21
|
-
# QUOTED_PAIR = "\\\\[\\x00-\\x7F]"
|
22
|
-
# LWS = "\\r\\n(?:[ \\t]+)"
|
23
|
-
# TEXT="[\\t\\x20-\\x7E\\x80-\\xFF]|(?:#{LWS})"
|
24
|
-
# QDTEXT="[\\t\\x20-\\x21\\x23-\\x7E\\x80-\\xFF]|(?:#{LWS})"
|
25
|
-
# QUOTED_TEXT = "\\\"((?:#{QDTEXT}|#{QUOTED_PAIR})*)\\\""
|
26
|
-
# VALUE2 = "(#{TOKEN})|#{QUOTED_TEXT}"
|
27
|
-
|
28
|
-
end
|
29
|
-
BASE_HOSTNAME = /#{PATTERN::BASE_HOSTNAME}/
|
30
|
-
BASE_PATH = /\A((?:[^\/?#]*\/)*)/
|
31
|
-
IPADDR = /\A#{PATTERN::IPADDR}\Z/
|
32
|
-
# HDN = /\A#{PATTERN::HOSTNAME}\Z/
|
33
|
-
# TOKEN = /\A#{PATTERN::TOKEN}\Z/
|
34
|
-
# TWO_DOT_DOMAINS = /\A\.(com|edu|net|mil|gov|int|org)\Z/
|
35
13
|
|
36
14
|
# The mandatory name and value of the cookie
|
37
15
|
attr_reader :name, :value
|
@@ -74,111 +52,29 @@ module CookieJar
|
|
74
52
|
end
|
75
53
|
end
|
76
54
|
|
77
|
-
def
|
55
|
+
def is_expired?
|
78
56
|
expires_at != nil && Time.now > expires_at
|
79
57
|
end
|
80
|
-
|
58
|
+
|
59
|
+
def is_session?
|
60
|
+
@expiry == nil
|
61
|
+
end
|
62
|
+
|
81
63
|
# Create a cookie based on an absolute URI and the string value of a
|
82
64
|
# 'Set-Cookie' header.
|
83
65
|
def self.from_set_cookie request_uri, set_cookie_value
|
84
|
-
args =
|
85
|
-
|
86
|
-
|
87
|
-
result = PARAM1.match param
|
88
|
-
if !result
|
89
|
-
raise InvalidCookieError.new "Invalid cookie parameter in cookie '#{set_cookie_value}'"
|
90
|
-
end
|
91
|
-
key = result[1].downcase.to_sym
|
92
|
-
keyvalue = result[2]
|
93
|
-
case key
|
94
|
-
when :expires
|
95
|
-
args[:expires_at] = Time.parse keyvalue
|
96
|
-
when :domain
|
97
|
-
args[:domain] = keyvalue
|
98
|
-
when :path
|
99
|
-
args[:path] = keyvalue
|
100
|
-
when :secure
|
101
|
-
args[:secure] = true
|
102
|
-
when :httponly
|
103
|
-
args[:http_only] = true
|
104
|
-
else
|
105
|
-
args[:name] = result[1]
|
106
|
-
args[:value] = keyvalue
|
107
|
-
end
|
108
|
-
end
|
109
|
-
args[:domain] = determine_cookie_domain request_uri, args[:domain]
|
110
|
-
args[:path] = determine_cookie_path request_uri, args[:path]
|
111
|
-
args[:version] = 0
|
66
|
+
args = CookieJar::CookieValidation.parse_set_cookie set_cookie_value
|
67
|
+
args[:domain] = CookieJar::CookieValidation.determine_cookie_domain request_uri, args[:domain]
|
68
|
+
args[:path] = CookieJar::CookieValidation.determine_cookie_path request_uri, args[:path]
|
112
69
|
cookie = Cookie.new args
|
113
|
-
validate_cookie request_uri, cookie
|
70
|
+
CookieJar::CookieValidation.validate_cookie request_uri, cookie
|
114
71
|
cookie
|
115
72
|
end
|
116
73
|
|
117
74
|
def to_s
|
118
|
-
|
75
|
+
"#{name}=#{value}"
|
119
76
|
end
|
120
77
|
|
121
|
-
# Check whether a cookie meets all of the rules to be created, based on
|
122
|
-
# its internal settings and the URI it came from.
|
123
|
-
#
|
124
|
-
# returns true on success, but will raise an InvalidCookieError on failure
|
125
|
-
# with an appropriate error message
|
126
|
-
def self.validate_cookie request_uri, cookie
|
127
|
-
uri = request_uri.is_a?(URI) ? request_uri : URI.parse(request_uri)
|
128
|
-
|
129
|
-
request_host = effective_host uri.host
|
130
|
-
request_path = uri.path
|
131
|
-
request_secure = (uri.scheme == 'https')
|
132
|
-
cookie_host = cookie.domain
|
133
|
-
cookie_path = cookie.path
|
134
|
-
|
135
|
-
errors = []
|
136
|
-
|
137
|
-
# From RFC 2965, Section 3.3.2 Rejecting Cookies
|
138
|
-
|
139
|
-
# A user agent rejects (SHALL NOT store its information) if the
|
140
|
-
# Version attribute is missing. Note that the legacy Set-Cookie
|
141
|
-
# directive will result in an implicit version 0.
|
142
|
-
unless cookie.version
|
143
|
-
errors << "Version missing"
|
144
|
-
end
|
145
|
-
|
146
|
-
# The value for the Path attribute is not a prefix of the request-URI
|
147
|
-
unless request_path.start_with? cookie_path
|
148
|
-
errors << "Path is not a prefix of the request uri path"
|
149
|
-
end
|
150
|
-
|
151
|
-
unless cookie_host =~ IPADDR || #is an IPv4 or IPv6 address
|
152
|
-
cookie_host =~ /.\../ || #contains an embedded dot
|
153
|
-
cookie_host == '.local' #is the domain cookie for local addresses
|
154
|
-
errors << "Domain format is illegal"
|
155
|
-
end
|
156
|
-
|
157
|
-
# The effective host name that derives from the request-host does
|
158
|
-
# not domain-match the Domain attribute.
|
159
|
-
#
|
160
|
-
# The request-host is a HDN (not IP address) and has the form HD,
|
161
|
-
# where D is the value of the Domain attribute, and H is a string
|
162
|
-
# that contains one or more dots.
|
163
|
-
effective_host = effective_host uri
|
164
|
-
unless domains_match effective_host, cookie_host
|
165
|
-
errors << "Domain is inappropriate based on request URI hostname"
|
166
|
-
end
|
167
|
-
|
168
|
-
# The Port attribute has a "port-list", and the request-port was
|
169
|
-
# not in the list.
|
170
|
-
unless cookie.ports.nil? || cookie.ports.length != 0
|
171
|
-
unless cookie.ports.find_index uri.port
|
172
|
-
errors << "Ports list does not contain request URI port"
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
raise InvalidCookieError.new errors unless errors.empty?
|
177
|
-
|
178
|
-
# Note: 'secure' is not explicitly defined as an SSL channel, and no
|
179
|
-
# test is defined around validity and the 'secure' attribute
|
180
|
-
true
|
181
|
-
end
|
182
78
|
# Return true if (given a URI, a cookie object and other options) a cookie
|
183
79
|
# should be sent to a host. Note that this currently ignores domain.
|
184
80
|
#
|
@@ -190,127 +86,58 @@ module CookieJar
|
|
190
86
|
path_match = uri.path.start_with? @path
|
191
87
|
secure_match = !(@secure && uri.scheme == 'http')
|
192
88
|
script_match = !(script && @http_only)
|
193
|
-
expiry_match = !
|
89
|
+
expiry_match = !is_expired?
|
194
90
|
path_match && secure_match && script_match && expiry_match
|
195
91
|
end
|
196
92
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
cookie_path
|
218
|
-
end
|
219
|
-
|
220
|
-
# Given a URI, compute the relevant search domains for pre-existing
|
221
|
-
# cookies. This includes all the valid dotted forms for a named or IP
|
222
|
-
# domains.
|
223
|
-
def self.compute_search_domains request_uri
|
224
|
-
uri = request_uri.is_a?(URI) ? request_uri : URI.parse(request_uri)
|
225
|
-
host = effective_host uri
|
226
|
-
result = [host]
|
227
|
-
if host !~ IPADDR
|
228
|
-
result << ".#{host}"
|
93
|
+
def to_json *a
|
94
|
+
result = {
|
95
|
+
:json_class => self.class.name,
|
96
|
+
:name => @name,
|
97
|
+
:value => @value,
|
98
|
+
:domain => @domain,
|
99
|
+
:path => @path,
|
100
|
+
:created_at => @created_at
|
101
|
+
}
|
102
|
+
{
|
103
|
+
:expiry => @expiry,
|
104
|
+
:secure => (true if @secure),
|
105
|
+
:http_only => (true if @http_only),
|
106
|
+
:version => (@version if version != 0),
|
107
|
+
:comment => @comment,
|
108
|
+
:comment_url => @comment_url,
|
109
|
+
:discard => (true if @discard),
|
110
|
+
:ports => @ports
|
111
|
+
}.each do |name, value|
|
112
|
+
result[name] = value if value
|
229
113
|
end
|
230
|
-
|
231
|
-
if base
|
232
|
-
result << ".#{base}"
|
233
|
-
end
|
234
|
-
result
|
114
|
+
result.to_json(*a)
|
235
115
|
end
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
# modified to '.foo.com', and as such will still apply to subdomains.
|
241
|
-
#
|
242
|
-
# Cookies without an explicit domain will have their domain value taken directly
|
243
|
-
# from the URL, and will _NOT_ have any leading dot applied. For example, a request
|
244
|
-
# to http://foo.com/ will cause an entry for 'foo.com' to be created - which applies
|
245
|
-
# to foo.com but no subdomain.
|
246
|
-
#
|
247
|
-
# Note that this will not attempt to detect a mismatch of the request uri domain
|
248
|
-
# and explicitly specified cookie domain
|
249
|
-
def self.determine_cookie_domain request_uri, cookie_domain
|
250
|
-
uri = request_uri.is_a?(URI) ? request_uri : URI.parse(request_uri)
|
251
|
-
domain = cookie_domain.is_a?(Cookie) ? cookie_domain.domain : cookie_domain
|
252
|
-
|
253
|
-
if domain == nil || domain.empty?
|
254
|
-
domain = effective_host uri.host
|
255
|
-
else
|
256
|
-
domain = domain.downcase
|
257
|
-
if domain =~ IPADDR || domain.start_with?('.')
|
258
|
-
domain
|
259
|
-
else
|
260
|
-
".#{domain}"
|
261
|
-
end
|
116
|
+
def self.json_create o
|
117
|
+
params = o.inject({}) do |hash, (key, value)|
|
118
|
+
hash[key.to_sym] = value
|
119
|
+
hash
|
262
120
|
end
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
# Has the added additional logic of searching for interior dots specifically, and
|
268
|
-
# matches colons to prevent .local being suffixed on IPv6 addresses
|
269
|
-
def self.effective_host host
|
270
|
-
hostname = host.is_a?(URI) ? host.host : host
|
271
|
-
hostname = hostname.downcase
|
272
|
-
|
273
|
-
if /.[\.:]./.match(hostname) || hostname == '.local'
|
274
|
-
hostname
|
121
|
+
params[:version] ||= 0
|
122
|
+
params[:created_at] = Time.parse params[:created_at]
|
123
|
+
if params[:expiry].is_a? String
|
124
|
+
params[:expires_at] = Time.parse params[:expiry]
|
275
125
|
else
|
276
|
-
|
126
|
+
params[:max_age] = params[:expiry]
|
277
127
|
end
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
def self.domains_match tested_domain,base_domain
|
282
|
-
return true if (tested_domain == base_domain || ".#{tested_domain}" == base_domain)
|
283
|
-
lhs = effective_host tested_domain
|
284
|
-
rhs = effective_host base_domain
|
285
|
-
lhs == rhs || ".#{lhs}" == rhs || hostname_reach(lhs) == rhs || ".#{hostname_reach lhs}" == rhs
|
128
|
+
params.delete :expiry
|
129
|
+
|
130
|
+
self.new params
|
286
131
|
end
|
287
|
-
|
288
|
-
|
289
|
-
def self.hostname_reach hostname
|
290
|
-
host = hostname.is_a?(URI) ? hostname.host : hostname
|
291
|
-
host = host.downcase
|
292
|
-
match = BASE_HOSTNAME.match host
|
293
|
-
if match
|
294
|
-
match[1]
|
295
|
-
end
|
132
|
+
def self.compute_search_domains request_uri
|
133
|
+
CookieValidation.compute_search_domains request_uri
|
296
134
|
end
|
297
135
|
protected
|
298
|
-
|
299
|
-
# PARAM2 = /\A(#{PATTERN::TOKEN})(?:=#{PATTERN::VALUE2})?\Z/
|
300
|
-
|
301
|
-
def initialize *params
|
302
|
-
case params.length
|
303
|
-
when 1
|
304
|
-
args = params[0]
|
305
|
-
when 2
|
306
|
-
args = {:name => params[0], :value => params[1], :version => 0}
|
307
|
-
else
|
308
|
-
raise ArgumentError.new "wrong number of arguments (expected 1 or 2)"
|
309
|
-
end
|
136
|
+
def initialize args
|
310
137
|
|
311
|
-
@created_at
|
138
|
+
@created_at = args[:created_at] || Time.now
|
312
139
|
@domain = args[:domain]
|
313
|
-
@expiry = args[:max_age] || args[:expires_at]
|
140
|
+
@expiry = args[:max_age] || args[:expires_at]
|
314
141
|
@path = args[:path]
|
315
142
|
@secure = args[:secure] || false
|
316
143
|
@http_only = args[:http_only] || false
|
@@ -327,4 +154,4 @@ module CookieJar
|
|
327
154
|
end
|
328
155
|
end
|
329
156
|
end
|
330
|
-
end
|
157
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
|
2
|
+
module CookieJar
|
3
|
+
# Represents all cookie validation errors
|
4
|
+
class InvalidCookieError < StandardError
|
5
|
+
attr_reader :messages
|
6
|
+
def initialize message
|
7
|
+
if message.is_a? Array
|
8
|
+
@messages = message
|
9
|
+
message = message.join ', '
|
10
|
+
end
|
11
|
+
super(message)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Contains logic to parse and validate cookie headers
|
16
|
+
module CookieValidation
|
17
|
+
module PATTERN
|
18
|
+
include URI::REGEXP::PATTERN
|
19
|
+
|
20
|
+
TOKEN = '[^(),\/<>@;:\\\"\[\]?={}\s]*'
|
21
|
+
VALUE1 = "([^;]*)"
|
22
|
+
IPADDR = "#{IPV4ADDR}|#{IPV6ADDR}"
|
23
|
+
BASE_HOSTNAME = "(?:#{DOMLABEL}\\.)(?:((?:(?:#{DOMLABEL}\\.)+(?:#{TOPLABEL}\\.?))|local))"
|
24
|
+
|
25
|
+
# QUOTED_PAIR = "\\\\[\\x00-\\x7F]"
|
26
|
+
# LWS = "\\r\\n(?:[ \\t]+)"
|
27
|
+
# TEXT="[\\t\\x20-\\x7E\\x80-\\xFF]|(?:#{LWS})"
|
28
|
+
# QDTEXT="[\\t\\x20-\\x21\\x23-\\x7E\\x80-\\xFF]|(?:#{LWS})"
|
29
|
+
# QUOTED_TEXT = "\\\"((?:#{QDTEXT}|#{QUOTED_PAIR})*)\\\""
|
30
|
+
# VALUE2 = "(#{TOKEN})|#{QUOTED_TEXT}"
|
31
|
+
|
32
|
+
end
|
33
|
+
BASE_HOSTNAME = /#{PATTERN::BASE_HOSTNAME}/
|
34
|
+
BASE_PATH = /\A((?:[^\/?#]*\/)*)/
|
35
|
+
IPADDR = /\A#{PATTERN::IPADDR}\Z/
|
36
|
+
HDN = /\A#{PATTERN::HOSTNAME}\Z/
|
37
|
+
TOKEN = /\A#{PATTERN::TOKEN}\Z/
|
38
|
+
PARAM1 = /\A(#{PATTERN::TOKEN})(?:=#{PATTERN::VALUE1})?\Z/
|
39
|
+
# PARAM2 = /\A(#{PATTERN::TOKEN})(?:=#{PATTERN::VALUE2})?\Z/
|
40
|
+
|
41
|
+
# TWO_DOT_DOMAINS = /\A\.(com|edu|net|mil|gov|int|org)\Z/
|
42
|
+
|
43
|
+
# Converts the input object to a URI (if not already a URI)
|
44
|
+
def self.to_uri request_uri
|
45
|
+
(request_uri.is_a? URI)? request_uri : (URI.parse request_uri)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Converts an input cookie or uri to a string representing the path.
|
49
|
+
# Assume strings are already paths
|
50
|
+
def self.to_path uri_or_path
|
51
|
+
if (uri_or_path.is_a? URI) || (uri_or_path.is_a? Cookie)
|
52
|
+
uri_or_path.path
|
53
|
+
else
|
54
|
+
uri_or_path
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Converts an input cookie or uri to a string representing the domain.
|
59
|
+
# Assume strings are already domains
|
60
|
+
def self.to_domain uri_or_domain
|
61
|
+
if uri_or_domain.is_a? URI
|
62
|
+
uri_or_domain.host
|
63
|
+
elsif uri_or_domain.is_a? Cookie
|
64
|
+
uri_or_domain.domain
|
65
|
+
else
|
66
|
+
uri_or_domain
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Compare a tested domain against the base domain to see if they match, or
|
71
|
+
# if the base domain is reachable.
|
72
|
+
#
|
73
|
+
# returns the effective_host on success, nil on failure
|
74
|
+
def self.domains_match tested_domain, base_domain
|
75
|
+
base = effective_host base_domain
|
76
|
+
search_domains = compute_search_domains_for_host base
|
77
|
+
result = search_domains.find do |domain|
|
78
|
+
domain == tested_domain
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Compute the reach of a hostname (RFC 2965, section 1)
|
83
|
+
# Determines the next highest superdomain, or nil if none valid
|
84
|
+
def self.hostname_reach hostname
|
85
|
+
host = to_domain hostname
|
86
|
+
host = host.downcase
|
87
|
+
match = BASE_HOSTNAME.match host
|
88
|
+
if match
|
89
|
+
match[1]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Compute the base of a path.
|
94
|
+
def self.cookie_base_path path
|
95
|
+
BASE_PATH.match(to_path path)[1]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Processes cookie path data using the following rules:
|
99
|
+
# Paths are separated by '/' characters, and accepted values are truncated
|
100
|
+
# to the last '/' character. If no path is specified in the cookie, a path
|
101
|
+
# value will be taken from the request URI which was used for the site.
|
102
|
+
#
|
103
|
+
# Note that this will not attempt to detect a mismatch of the request uri domain
|
104
|
+
# and explicitly specified cookie path
|
105
|
+
def self.determine_cookie_path request_uri, cookie_path
|
106
|
+
uri = to_uri request_uri
|
107
|
+
cookie_path = to_path cookie_path
|
108
|
+
|
109
|
+
if cookie_path == nil || cookie_path.empty?
|
110
|
+
cookie_path = cookie_base_path uri.path
|
111
|
+
end
|
112
|
+
cookie_path
|
113
|
+
end
|
114
|
+
|
115
|
+
# Given a URI, compute the relevant search domains for pre-existing
|
116
|
+
# cookies. This includes all the valid dotted forms for a named or IP
|
117
|
+
# domains.
|
118
|
+
def self.compute_search_domains request_uri
|
119
|
+
uri = to_uri request_uri
|
120
|
+
host = uri.host
|
121
|
+
compute_search_domains_for_host host
|
122
|
+
end
|
123
|
+
|
124
|
+
# Given a host, compute the relevant search domains for pre-existing
|
125
|
+
# cookies
|
126
|
+
def self.compute_search_domains_for_host host
|
127
|
+
host = effective_host host
|
128
|
+
result = [host]
|
129
|
+
unless host =~ IPADDR
|
130
|
+
result << ".#{host}"
|
131
|
+
base = hostname_reach host
|
132
|
+
if base
|
133
|
+
result << ".#{base}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
result
|
137
|
+
end
|
138
|
+
|
139
|
+
# Processes cookie domain data using the following rules:
|
140
|
+
# Domains strings of the form .foo.com match 'foo.com' and all immediate
|
141
|
+
# subdomains of 'foo.com'. Domain strings specified of the form 'foo.com' are
|
142
|
+
# modified to '.foo.com', and as such will still apply to subdomains.
|
143
|
+
#
|
144
|
+
# Cookies without an explicit domain will have their domain value taken directly
|
145
|
+
# from the URL, and will _NOT_ have any leading dot applied. For example, a request
|
146
|
+
# to http://foo.com/ will cause an entry for 'foo.com' to be created - which applies
|
147
|
+
# to foo.com but no subdomain.
|
148
|
+
#
|
149
|
+
# Note that this will not attempt to detect a mismatch of the request uri domain
|
150
|
+
# and explicitly specified cookie domain
|
151
|
+
def self.determine_cookie_domain request_uri, cookie_domain
|
152
|
+
uri = to_uri request_uri
|
153
|
+
domain = to_domain cookie_domain
|
154
|
+
|
155
|
+
if domain == nil || domain.empty?
|
156
|
+
domain = effective_host uri.host
|
157
|
+
else
|
158
|
+
domain = domain.downcase
|
159
|
+
if domain =~ IPADDR || domain.start_with?('.')
|
160
|
+
domain
|
161
|
+
else
|
162
|
+
".#{domain}"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Compute the effective host (RFC 2965, section 1)
|
168
|
+
# [host] a string or URI.
|
169
|
+
#
|
170
|
+
# Has the added additional logic of searching for interior dots specifically, and
|
171
|
+
# matches colons to prevent .local being suffixed on IPv6 addresses
|
172
|
+
def self.effective_host host_or_uri
|
173
|
+
hostname = to_domain host_or_uri
|
174
|
+
hostname = hostname.downcase
|
175
|
+
|
176
|
+
if /.[\.:]./.match(hostname) || hostname == '.local'
|
177
|
+
hostname
|
178
|
+
else
|
179
|
+
hostname + '.local'
|
180
|
+
end
|
181
|
+
end
|
182
|
+
# Check whether a cookie meets all of the rules to be created, based on
|
183
|
+
# its internal settings and the URI it came from.
|
184
|
+
#
|
185
|
+
# returns true on success, but will raise an InvalidCookieError on failure
|
186
|
+
# with an appropriate error message
|
187
|
+
def self.validate_cookie request_uri, cookie
|
188
|
+
uri = to_uri request_uri
|
189
|
+
request_host = effective_host uri.host
|
190
|
+
request_path = uri.path
|
191
|
+
request_secure = (uri.scheme == 'https')
|
192
|
+
cookie_host = cookie.domain
|
193
|
+
cookie_path = cookie.path
|
194
|
+
|
195
|
+
errors = []
|
196
|
+
|
197
|
+
# From RFC 2965, Section 3.3.2 Rejecting Cookies
|
198
|
+
|
199
|
+
# A user agent rejects (SHALL NOT store its information) if the
|
200
|
+
# Version attribute is missing. Note that the legacy Set-Cookie
|
201
|
+
# directive will result in an implicit version 0.
|
202
|
+
unless cookie.version
|
203
|
+
errors << "Version missing"
|
204
|
+
end
|
205
|
+
|
206
|
+
# The value for the Path attribute is not a prefix of the request-URI
|
207
|
+
unless request_path.start_with? cookie_path
|
208
|
+
errors << "Path is not a prefix of the request uri path"
|
209
|
+
end
|
210
|
+
|
211
|
+
unless cookie_host =~ IPADDR || #is an IPv4 or IPv6 address
|
212
|
+
cookie_host =~ /.\../ || #contains an embedded dot
|
213
|
+
cookie_host == '.local' #is the domain cookie for local addresses
|
214
|
+
errors << "Domain format is illegal"
|
215
|
+
end
|
216
|
+
|
217
|
+
# The effective host name that derives from the request-host does
|
218
|
+
# not domain-match the Domain attribute.
|
219
|
+
#
|
220
|
+
# The request-host is a HDN (not IP address) and has the form HD,
|
221
|
+
# where D is the value of the Domain attribute, and H is a string
|
222
|
+
# that contains one or more dots.
|
223
|
+
unless domains_match cookie_host, uri
|
224
|
+
errors << "Domain is inappropriate based on request URI hostname"
|
225
|
+
end
|
226
|
+
|
227
|
+
# The Port attribute has a "port-list", and the request-port was
|
228
|
+
# not in the list.
|
229
|
+
unless cookie.ports.nil? || cookie.ports.length != 0
|
230
|
+
unless cookie.ports.find_index uri.port
|
231
|
+
errors << "Ports list does not contain request URI port"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
raise InvalidCookieError.new errors unless errors.empty?
|
236
|
+
|
237
|
+
# Note: 'secure' is not explicitly defined as an SSL channel, and no
|
238
|
+
# test is defined around validity and the 'secure' attribute
|
239
|
+
true
|
240
|
+
end
|
241
|
+
def self.parse_set_cookie set_cookie_value
|
242
|
+
args = { }
|
243
|
+
params=set_cookie_value.split /;\s*/
|
244
|
+
params.each do |param|
|
245
|
+
result = PARAM1.match param
|
246
|
+
if !result
|
247
|
+
raise InvalidCookieError.new "Invalid cookie parameter in cookie '#{set_cookie_value}'"
|
248
|
+
end
|
249
|
+
key = result[1].downcase.to_sym
|
250
|
+
keyvalue = result[2]
|
251
|
+
case key
|
252
|
+
when :expires
|
253
|
+
args[:expires_at] = Time.parse keyvalue
|
254
|
+
when :domain
|
255
|
+
args[:domain] = keyvalue
|
256
|
+
when :path
|
257
|
+
args[:path] = keyvalue
|
258
|
+
when :secure
|
259
|
+
args[:secure] = true
|
260
|
+
when :httponly
|
261
|
+
args[:http_only] = true
|
262
|
+
else
|
263
|
+
args[:name] = result[1]
|
264
|
+
args[:value] = keyvalue
|
265
|
+
end
|
266
|
+
end
|
267
|
+
args[:version] = 0
|
268
|
+
args
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|