cookiejar 0.2.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ require 'cookiejar/cookie'
2
+ require 'cookiejar/jar'
@@ -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::IPADDR}\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
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'
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
359
+ if md.nil? || md.offset(0).first != index
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