secobarbital-cookiejar 0.2.9.1

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