cookiejar2 0.3.5 → 0.3.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,410 +0,0 @@
1
- # frozen_string_literal: true
2
- require 'cgi'
3
- require 'uri'
4
-
5
- module CookieJar
6
- # Represents a set of cookie validation errors
7
- class InvalidCookieError < StandardError
8
- # [Array<String>] the specific validation issues encountered
9
- attr_reader :messages
10
-
11
- # Create a new instance
12
- # @param [String, Array<String>] the validation issue(s) encountered
13
- def initialize(message)
14
- if message.is_a? Array
15
- @messages = message
16
- message = message.join ', '
17
- else
18
- @messages = [message]
19
- end
20
- super message
21
- end
22
- end
23
-
24
- # Contains logic to parse and validate cookie headers
25
- module CookieValidation
26
- # REGEX cookie matching
27
- module PATTERN
28
- include URI::REGEXP::PATTERN
29
-
30
- TOKEN = '[^(),\/<>@;:\\\"\[\]?={}\s]+'.freeze
31
- VALUE1 = '([^;]*)'.freeze
32
- IPADDR = "#{IPV4ADDR}|#{IPV6ADDR}".freeze
33
- BASE_HOSTNAME = "(?:#{DOMLABEL}\\.)(?:((?:(?:#{DOMLABEL}\\.)+(?:#{TOPLABEL}\\.?))|local))".freeze
34
-
35
- QUOTED_PAIR = '\\\\[\\x00-\\x7F]'.freeze
36
- LWS = '\\r\\n(?:[ \\t]+)'.freeze
37
- # TEXT="[\\t\\x20-\\x7E\\x80-\\xFF]|(?:#{LWS})"
38
- QDTEXT = "[\\t\\x20-\\x21\\x23-\\x7E\\x80-\\xFF]|(?:#{LWS})".freeze
39
- QUOTED_TEXT = "\\\"(?:#{QDTEXT}|#{QUOTED_PAIR})*\\\"".freeze
40
- VALUE2 = "#{TOKEN}|#{QUOTED_TEXT}".freeze
41
- end
42
- BASE_HOSTNAME = /#{PATTERN::BASE_HOSTNAME}/
43
- BASE_PATH = %r{\A((?:[^/?#]*/)*)}
44
- IPADDR = /\A#{PATTERN::IPV4ADDR}\Z|\A#{PATTERN::IPV6ADDR}\Z/
45
- HDN = /\A#{PATTERN::HOSTNAME}\Z/
46
- TOKEN = /\A#{PATTERN::TOKEN}\Z/
47
- PARAM1 = /\A(#{PATTERN::TOKEN})(?:=#{PATTERN::VALUE1})?\Z/
48
- PARAM2 = Regexp.new "(#{PATTERN::TOKEN})(?:=(#{PATTERN::VALUE2}))?(?:\\Z|;)", '', 'n'
49
- # TWO_DOT_DOMAINS = /\A\.(com|edu|net|mil|gov|int|org)\Z/
50
-
51
- # Converts the input object to a URI (if not already a URI)
52
- #
53
- # @param [String, URI] request_uri URI we are normalizing
54
- # @param [URI] URI representation of input string, or original URI
55
- def self.to_uri(request_uri)
56
- (request_uri.is_a? URI) ? request_uri : (URI.parse request_uri)
57
- end
58
-
59
- # Converts an input cookie or uri to a string representing the path.
60
- # Assume strings are already paths
61
- #
62
- # @param [String, URI, Cookie] object containing the path
63
- # @return [String] path information
64
- def self.to_path(uri_or_path)
65
- if (uri_or_path.is_a? URI) || (uri_or_path.is_a? Cookie)
66
- uri_or_path.path
67
- else
68
- uri_or_path
69
- end
70
- end
71
-
72
- # Converts an input cookie or uri to a string representing the domain.
73
- # Assume strings are already domains. Value may not be an effective host.
74
- #
75
- # @param [String, URI, Cookie] object containing the domain
76
- # @return [String] domain information.
77
- def self.to_domain(uri_or_domain)
78
- if uri_or_domain.is_a? URI
79
- uri_or_domain.host
80
- elsif uri_or_domain.is_a? Cookie
81
- uri_or_domain.domain
82
- else
83
- uri_or_domain
84
- end
85
- end
86
-
87
- # Compare a tested domain against the base domain to see if they match, or
88
- # if the base domain is reachable.
89
- #
90
- # @param [String] tested_domain domain to be tested against
91
- # @param [String] base_domain new domain being tested
92
- # @return [String,nil] matching domain on success, nil on failure
93
- def self.domains_match(tested_domain, base_domain)
94
- base = effective_host base_domain
95
- search_domains = compute_search_domains_for_host base
96
- search_domains.find do |domain|
97
- domain == tested_domain
98
- end
99
- end
100
-
101
- # Compute the reach of a hostname (RFC 2965, section 1)
102
- # Determines the next highest superdomain
103
- #
104
- # @param [String,URI,Cookie] hostname hostname, or object holding hostname
105
- # @return [String,nil] next highest hostname, or nil if none
106
- def self.hostname_reach(hostname)
107
- host = to_domain hostname
108
- host = host.downcase
109
- match = BASE_HOSTNAME.match host
110
- match[1] if match
111
- end
112
-
113
- # Compute the base of a path, for default cookie path assignment
114
- #
115
- # @param [String, URI, Cookie] path, or object holding path
116
- # @return base path (all characters up to final '/')
117
- def self.cookie_base_path(path)
118
- BASE_PATH.match(to_path(path))[1]
119
- end
120
-
121
- # Processes cookie path data using the following rules:
122
- # Paths are separated by '/' characters, and accepted values are truncated
123
- # to the last '/' character. If no path is specified in the cookie, a path
124
- # value will be taken from the request URI which was used for the site.
125
- #
126
- # Note that this will not attempt to detect a mismatch of the request uri
127
- # domain and explicitly specified cookie path
128
- #
129
- # @param [String,URI] request URI yielding this cookie
130
- # @param [String] path on cookie
131
- def self.determine_cookie_path(request_uri, cookie_path)
132
- uri = to_uri request_uri
133
- cookie_path = to_path cookie_path
134
-
135
- if cookie_path.nil? || cookie_path.empty?
136
- cookie_path = cookie_base_path uri.path
137
- end
138
- cookie_path
139
- end
140
-
141
- # Given a URI, compute the relevant search domains for pre-existing
142
- # cookies. This includes all the valid dotted forms for a named or IP
143
- # domains.
144
- #
145
- # @param [String, URI] request_uri requested uri
146
- # @return [Array<String>] all cookie domain values which would match the
147
- # requested uri
148
- def self.compute_search_domains(request_uri)
149
- uri = to_uri request_uri
150
- return nil unless uri.is_a? URI::HTTP
151
- host = uri.host
152
- compute_search_domains_for_host host
153
- end
154
-
155
- # Given a host, compute the relevant search domains for pre-existing
156
- # cookies
157
- #
158
- # @param [String] host host being requested
159
- # @return [Array<String>] all cookie domain values which would match the
160
- # requested uri
161
- def self.compute_search_domains_for_host(host)
162
- host = effective_host host
163
- result = [host]
164
- unless host =~ IPADDR
165
- result << ".#{host}"
166
- base = hostname_reach host
167
- result << ".#{base}" if base
168
- end
169
- result
170
- end
171
-
172
- # Processes cookie domain data using the following rules:
173
- # Domains strings of the form .foo.com match 'foo.com' and all immediate
174
- # subdomains of 'foo.com'. Domain strings specified of the form 'foo.com'
175
- # are modified to '.foo.com', and as such will still apply to subdomains.
176
- #
177
- # Cookies without an explicit domain will have their domain value taken
178
- # directly from the URL, and will _NOT_ have any leading dot applied. For
179
- # example, a request to http://foo.com/ will cause an entry for 'foo.com'
180
- # to be created - which applies to foo.com but no subdomain.
181
- #
182
- # Note that this will not attempt to detect a mismatch of the request uri
183
- # domain and explicitly specified cookie domain
184
- #
185
- # @param [String, URI] request_uri originally requested URI
186
- # @param [String] cookie domain value
187
- # @return [String] effective host
188
- def self.determine_cookie_domain(request_uri, cookie_domain)
189
- uri = to_uri request_uri
190
- domain = to_domain cookie_domain
191
-
192
- return effective_host(uri.host) if domain.nil? || domain.empty?
193
- domain = domain.downcase
194
- if domain =~ IPADDR || domain.start_with?('.')
195
- domain
196
- else
197
- ".#{domain}"
198
- end
199
- end
200
-
201
- # Compute the effective host (RFC 2965, section 1)
202
- #
203
- # Has the added additional logic of searching for interior dots
204
- # specifically, and matches colons to prevent .local being suffixed on
205
- # IPv6 addresses
206
- #
207
- # @param [String, URI] host_or_uridomain name, or absolute URI
208
- # @return [String] effective host per RFC rules
209
- def self.effective_host(host_or_uri)
210
- hostname = to_domain host_or_uri
211
- hostname = hostname.downcase
212
-
213
- if /.[\.:]./.match(hostname) || hostname == '.local'
214
- hostname
215
- else
216
- hostname + '.local'
217
- end
218
- end
219
-
220
- # Check whether a cookie meets all of the rules to be created, based on
221
- # its internal settings and the URI it came from.
222
- #
223
- # @param [String,URI] request_uri originally requested URI
224
- # @param [Cookie] cookie object
225
- # @param [true] will always return true on success
226
- # @raise [InvalidCookieError] on failures, containing all validation errors
227
- def self.validate_cookie(request_uri, cookie)
228
- uri = to_uri request_uri
229
- request_path = uri.path
230
- cookie_host = cookie.domain
231
- cookie_path = cookie.path
232
-
233
- errors = []
234
-
235
- # From RFC 2965, Section 3.3.2 Rejecting Cookies
236
-
237
- # A user agent rejects (SHALL NOT store its information) if the
238
- # Version attribute is missing. Note that the legacy Set-Cookie
239
- # directive will result in an implicit version 0.
240
- errors << 'Version missing' unless cookie.version
241
-
242
- # The value for the Path attribute is not a prefix of the request-URI
243
-
244
- # If the initial request path is empty then this will always fail
245
- # so check if it is empty and if so then set it to /
246
- request_path = '/' if request_path == ''
247
-
248
- unless request_path.start_with? cookie_path
249
- errors << 'Path is not a prefix of the request uri path'
250
- end
251
-
252
- unless cookie_host =~ IPADDR || # is an IPv4 or IPv6 address
253
- cookie_host =~ /.\../ || # contains an embedded dot
254
- cookie_host == '.local' # is the domain cookie for local addresses
255
- errors << 'Domain format is illegal'
256
- end
257
-
258
- # The effective host name that derives from the request-host does
259
- # not domain-match the Domain attribute.
260
- #
261
- # The request-host is a HDN (not IP address) and has the form HD,
262
- # where D is the value of the Domain attribute, and H is a string
263
- # that contains one or more dots.
264
- unless domains_match cookie_host, uri
265
- errors << 'Domain is inappropriate based on request URI hostname'
266
- end
267
-
268
- # The Port attribute has a "port-list", and the request-port was
269
- # not in the list.
270
- unless cookie.ports.nil? || !cookie.ports.empty?
271
- unless cookie.ports.find_index uri.port
272
- errors << 'Ports list does not contain request URI port'
273
- end
274
- end
275
-
276
- fail InvalidCookieError, errors unless errors.empty?
277
-
278
- # Note: 'secure' is not explicitly defined as an SSL channel, and no
279
- # test is defined around validity and the 'secure' attribute
280
- true
281
- end
282
-
283
- # Break apart a traditional (non RFC 2965) cookie value into its core
284
- # components. This does not do any validation, or defaulting of values
285
- # based on requested URI
286
- #
287
- # @param [String] set_cookie_value a Set-Cookie header formatted cookie
288
- # definition
289
- # @return [Hash] Contains the parsed values of the cookie
290
- def self.parse_set_cookie(set_cookie_value)
291
- args = {}
292
- params = set_cookie_value.split(/;\s*/)
293
-
294
- first = true
295
- params.each do |param|
296
- result = PARAM1.match param
297
- unless result
298
- fail InvalidCookieError,
299
- "Invalid cookie parameter in cookie '#{set_cookie_value}'"
300
- end
301
- key = result[1].downcase.to_sym
302
- keyvalue = result[2]
303
- if first
304
- args[:name] = result[1]
305
- args[:value] = keyvalue
306
- first = false
307
- else
308
- case key
309
- when :expires
310
- begin
311
- args[:expires_at] = Time.parse keyvalue
312
- rescue ArgumentError
313
- raise unless $ERROR_INFO.message == 'time out of range'
314
- args[:expires_at] = Time.at(0x7FFFFFFF)
315
- end
316
- when :"max-age"
317
- args[:max_age] = keyvalue.to_i
318
- when :domain, :path
319
- args[key] = keyvalue
320
- when :secure
321
- args[:secure] = true
322
- when :httponly
323
- args[:http_only] = true
324
- when :samesite
325
- args[:samesite] = keyvalue.downcase
326
- else
327
- fail InvalidCookieError, "Unknown cookie parameter '#{key}'"
328
- end
329
- end
330
- end
331
- args[:version] = 0
332
- args
333
- end
334
-
335
- # Parse a RFC 2965 value and convert to a literal string
336
- def self.value_to_string(value)
337
- if /\A"(.*)"\Z/ =~ value
338
- value = Regexp.last_match(1)
339
- value.gsub(/\\(.)/, '\1')
340
- else
341
- value
342
- end
343
- end
344
-
345
- # Attempt to decipher a partially decoded version of text cookie values
346
- def self.decode_value(value)
347
- if /\A"(.*)"\Z/ =~ value
348
- value_to_string value
349
- else
350
- CGI.unescape value
351
- end
352
- end
353
-
354
- # Break apart a RFC 2965 cookie value into its core components.
355
- # This does not do any validation, or defaulting of values
356
- # based on requested URI
357
- #
358
- # @param [String] set_cookie_value a Set-Cookie2 header formatted cookie
359
- # definition
360
- # @return [Hash] Contains the parsed values of the cookie
361
- def self.parse_set_cookie2(set_cookie_value)
362
- args = {}
363
- first = true
364
- index = 0
365
- begin
366
- md = PARAM2.match set_cookie_value[index..-1]
367
- if md.nil? || md.offset(0).first != 0
368
- fail InvalidCookieError,
369
- "Invalid Set-Cookie2 header '#{set_cookie_value}'"
370
- end
371
- index += md.offset(0)[1]
372
-
373
- key = md[1].downcase.to_sym
374
- keyvalue = md[2] || md[3]
375
- if first
376
- args[:name] = md[1]
377
- args[:value] = keyvalue
378
- first = false
379
- else
380
- keyvalue = value_to_string keyvalue
381
- case key
382
- when :comment, :commenturl, :domain, :path
383
- args[key] = keyvalue
384
- when :discard, :secure
385
- args[key] = true
386
- when :httponly
387
- args[:http_only] = true
388
- when :"max-age"
389
- args[:max_age] = keyvalue.to_i
390
- when :version
391
- args[:version] = keyvalue.to_i
392
- when :port
393
- # must be in format '"port,port"'
394
- ports = keyvalue.split(/,\s*/)
395
- args[:ports] = ports.map(&:to_i)
396
- else
397
- fail InvalidCookieError, "Unknown cookie parameter '#{key}'"
398
- end
399
- end
400
- end until md.post_match.empty?
401
- # if our last match in the scan failed
402
- if args[:version] != 1
403
- fail InvalidCookieError,
404
- 'Set-Cookie2 declares a non RFC2965 version cookie'
405
- end
406
-
407
- args
408
- end
409
- end
410
- end