http-cookie 0.1.5 → 1.0.0.pre1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c45ee093b1364bc1ce8c5c4120a4f4fae298a5e6
4
- data.tar.gz: d007f3979523ad48a557281b99292d660a5d781b
3
+ metadata.gz: 9f5ccaec04a2b731f1120d21079f05fda32dc05f
4
+ data.tar.gz: aea993fb843b5218522f7a12adf189112dd8a477
5
5
  SHA512:
6
- metadata.gz: 62257900f2b0e4ffad506bff326f11b724ebcb0f3495cdc156338188efc8db7c4bfb49d155ae43ea50889f5e0acb4059586f016a0c5fd84f659e58605b512d49
7
- data.tar.gz: c3f9f8a5deafe3335063e8cf3b09d054da55d01729b47e7b55171e06585b8e270da021bcea1804132b0fabe855b90d3d21c2cc333913b7283c318167f799496b
6
+ metadata.gz: f02c9afbb827b9a35e2417e4104488d3cfc237ac9ec8007c6b77b96b61c2b5c1f3a74f10660211ff1e4e2ee5000f28ddb1052148ce3cba5ccc1d2d3d49e135a8
7
+ data.tar.gz: 8316457b65faeb9d19449bf49e11b920d53d9c5c8e37edf8d99d02b32fcf30350dff5c270cad654a8e0d4dcc45950894d59496450a7fde2f14e339a1b5b0d077
@@ -1,12 +1,14 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - 1.8.7
4
+ - ree
4
5
  - 1.9.2
5
6
  - 1.9.3
6
7
  - 2.0.0
7
- - ree
8
+ - ruby-head
8
9
  - jruby-18mode
9
10
  - jruby-19mode
11
+ - jruby-head
10
12
  - rbx-18mode
11
13
  - rbx-19mode
12
14
  matrix:
@@ -1,3 +1,4 @@
1
+ # :markup: markdown
1
2
  require 'http/cookie/version'
2
3
  require 'time'
3
4
  require 'uri'
@@ -7,11 +8,12 @@ module HTTP
7
8
  autoload :CookieJar, 'http/cookie_jar'
8
9
  end
9
10
 
10
- # In Ruby < 1.9.3 URI() does not accept an URI object.
11
+ # In Ruby < 1.9.3 URI() does not accept a URI object.
11
12
  if RUBY_VERSION < "1.9.3"
12
13
  begin
13
14
  URI(URI(''))
14
15
  rescue
16
+ # :nodoc:
15
17
  def URI(url)
16
18
  url.is_a?(URI) ? url : URI.parse(url)
17
19
  end
@@ -20,13 +22,17 @@ end
20
22
 
21
23
  # This class is used to represent an HTTP Cookie.
22
24
  class HTTP::Cookie
23
- # Maximum number of bytes per cookie (RFC 6265 6.1 requires 4096 at least)
25
+ # Maximum number of bytes per cookie (RFC 6265 6.1 requires 4096 at
26
+ # least)
24
27
  MAX_LENGTH = 4096
25
- # Maximum number of cookies per domain (RFC 6265 6.1 requires 50 at least)
28
+ # Maximum number of cookies per domain (RFC 6265 6.1 requires 50 at
29
+ # least)
26
30
  MAX_COOKIES_PER_DOMAIN = 50
27
- # Maximum number of cookies total (RFC 6265 6.1 requires 3000 at least)
31
+ # Maximum number of cookies total (RFC 6265 6.1 requires 3000 at
32
+ # least)
28
33
  MAX_COOKIES_TOTAL = 3000
29
34
 
35
+ # :stopdoc:
30
36
  UNIX_EPOCH = Time.at(0)
31
37
 
32
38
  PERSISTENT_PROPERTIES = %w[
@@ -52,31 +58,91 @@ class HTTP::Cookie
52
58
  end
53
59
  private :check_string_type
54
60
  end
61
+ # :startdoc:
55
62
 
56
- attr_reader :name, :domain, :path, :origin
57
- attr_accessor :secure, :httponly, :value, :version
58
- attr_reader :domain_name, :expires
59
- attr_accessor :comment, :max_age
63
+ # The cookie name. It may not be nil or empty.
64
+ #
65
+ # Trying to set a value with the normal setter method will raise
66
+ # ArgumentError only when it contains any of these characters:
67
+ # control characters (\x00-\x1F and \x7F), space and separators
68
+ # `,;\"=`.
69
+ #
70
+ # Note that RFC 6265 4.1.1 lists more characters disallowed for use
71
+ # in a cookie name, which are these: `<>@:/[]?{}`. Using these
72
+ # characters will reduce interoperability.
73
+ #
74
+ # :attr_accessor: name
75
+
76
+ # The cookie value.
77
+ #
78
+ # Trying to set a value with the normal setter method will raise an
79
+ # ArgumentError only when it contains any of these characters:
80
+ # control characters (\x00-\x1F and \x7F).
81
+ #
82
+ # Note that RFC 6265 4.1.1 lists more characters disallowed for use
83
+ # in a cookie value, which are these: ` ",;\`. Using these
84
+ # characters will reduce interoperability.
85
+ #
86
+ # :attr_accessor: value
60
87
 
61
- attr_accessor :session
88
+ # The cookie domain.
89
+ #
90
+ # Setting a domain with a leading dot implies that the #for_domain
91
+ # flag should be turned on. The setter accepts a `DomainName`
92
+ # object as well as a string-like.
93
+ #
94
+ # :attr_accessor: domain
62
95
 
63
- attr_accessor :created_at
64
- attr_accessor :accessed_at
96
+ # The path attribute value.
97
+ #
98
+ # The setter treats an empty path ("") as the root path ("/").
99
+ #
100
+ # :attr_accessor: path
101
+
102
+ # The origin of the cookie.
103
+ #
104
+ # Setting this will initialize the #domain and #path attribute
105
+ # values if unknown yet.
106
+ #
107
+ # :attr_accessor: origin
108
+
109
+ # The Expires attribute value as a Time object.
110
+ #
111
+ # The setter method accepts a Time object, a string representation
112
+ # of date/time, or `nil`.
113
+ #
114
+ # Note that #max_age and #expires are mutually exclusive. Setting
115
+ # \#max_age resets #expires to nil, and vice versa.
116
+ #
117
+ # :attr_accessor: expires
118
+
119
+ # The Max-Age attribute value as an integer, the number of seconds
120
+ # before expiration.
121
+ #
122
+ # The setter method accepts an integer, or a string-like that
123
+ # represents an integer which will be stringified and then
124
+ # integerized using #to_i.
125
+ #
126
+ # Note that #max_age and #expires are mutually exclusive. Setting
127
+ # \#max_age resets #expires to nil, and vice versa.
128
+ #
129
+ # :attr_accessor: max_age
65
130
 
66
131
  # :call-seq:
67
132
  # new(name, value)
68
133
  # new(name, value, attr_hash)
69
134
  # new(attr_hash)
70
135
  #
71
- # Creates a cookie object. For each key of +attr_hash+, the setter
136
+ # Creates a cookie object. For each key of `attr_hash`, the setter
72
137
  # is called if defined. Each key can be either a symbol or a
73
138
  # string, downcased or not.
74
139
  #
75
140
  # This methods accepts any attribute name for which a setter method
76
- # is defined. Beware, however, any error (typically ArgumentError)
77
- # a setter method raises will be passed through.
141
+ # is defined. Beware, however, any error (typically
142
+ # `ArgumentError`) a setter method raises will be passed through.
78
143
  #
79
144
  # e.g.
145
+ #
80
146
  # new("uid", "a12345")
81
147
  # new("uid", "a12345", :domain => 'example.org',
82
148
  # :for_domain => true, :expired => Time.now + 7*86400)
@@ -86,11 +152,12 @@ class HTTP::Cookie
86
152
  @version = 0 # Netscape Cookie
87
153
 
88
154
  @origin = @domain = @path =
89
- @secure = @httponly =
90
155
  @expires = @max_age =
91
156
  @comment = nil
92
-
157
+ @secure = @httponly = false
158
+ @session = true
93
159
  @created_at = @accessed_at = Time.now
160
+
94
161
  case args.size
95
162
  when 2
96
163
  self.name, self.value = *args
@@ -129,45 +196,43 @@ class HTTP::Cookie
129
196
  end
130
197
  end
131
198
 
132
- # If this flag is true, this cookie will be sent to any host in the
133
- # +domain+. If it is false, this cookie will be sent only to the
134
- # host indicated by the +domain+.
135
- attr_accessor :for_domain
136
- alias for_domain? for_domain
199
+ autoload :Scanner, 'http/cookie/scanner'
137
200
 
138
201
  class << self
139
- include URIFix if defined?(URIFix)
140
-
141
- # Normalizes a given path. If it is empty, the root path '/' is
142
- # returned. If a URI object is given, returns a new URI object
143
- # with the path part normalized.
144
- def normalize_path(uri)
145
- # Currently does not replace // to /
146
- case uri
147
- when URI
148
- uri.path.empty? ? uri + '/' : uri
149
- else
150
- uri.empty? ? '/' : uri
151
- end
202
+ # Normalizes a given path. If it is empty or it is a relative
203
+ # path, the root path '/' is returned.
204
+ #
205
+ # If a URI object is given, returns a new URI object with the path
206
+ # part normalized.
207
+ def normalize_path(path)
208
+ return path + normalize_path(path.path) if URI === path
209
+ # RFC 6265 5.1.4
210
+ path.start_with?('/') ? path : '/'
152
211
  end
153
212
 
154
- # Parses a Set-Cookie header value +set_cookie+ into an array of
213
+ # Parses a Set-Cookie header value `set_cookie` into an array of
155
214
  # Cookie objects. Parts (separated by commas) that are malformed
156
215
  # or invalid are silently ignored. For example, a cookie that a
157
216
  # given origin is not allowed to issue is not included in the
158
217
  # resulted array.
159
218
  #
219
+ # Any Max-Age attribute value found is converted to an expires
220
+ # value computing from the current time so that expiration check
221
+ # (#expired?) can be performed.
222
+ #
160
223
  # If a block is given, each cookie object is passed to the block.
161
224
  #
162
225
  # Available option keywords are below:
163
226
  #
164
- # * +origin+
165
- # The cookie's origin URI/URL
166
- # * +date+
167
- # The base date used for interpreting Max-Age attribute values
227
+ # `origin`
228
+ # : The cookie's origin URI/URL
229
+ #
230
+ # `date`
231
+ # : The base date used for interpreting Max-Age attribute values
168
232
  # instead of the current time
169
- # * +logger+
170
- # Logger object useful for debugging
233
+ #
234
+ # `logger`
235
+ # : Logger object useful for debugging
171
236
  def parse(set_cookie, options = nil, *_, &block)
172
237
  _.empty? && !options.is_a?(String) or
173
238
  raise ArgumentError, 'HTTP::Cookie equivalent for Mechanize::Cookie.parse(uri, set_cookie[, log]) is HTTP::Cookie.parse(set_cookie, :origin => uri[, :logger => log]).'
@@ -180,83 +245,44 @@ class HTTP::Cookie
180
245
  date ||= Time.now
181
246
 
182
247
  [].tap { |cookies|
183
- # The expires attribute may include a comma in the value.
184
- set_cookie.split(/,(?=[^;,]*=|\s*\z)/).each { |c|
185
- if c.bytesize > MAX_LENGTH
186
- logger.warn("Cookie definition too long: #{c}") if logger
187
- next
188
- end
189
-
190
- first_elem, *cookie_elem = c.split(/;+/)
191
- first_elem.strip!
192
- key, value = first_elem.split(/\=/, 2)
248
+ s = Scanner.new(set_cookie, logger)
249
+ until s.eos?
250
+ name, value, attrs = s.scan_cookie
251
+ break if name.nil? || name.empty?
193
252
 
194
- begin
195
- cookie = new(key, value.dup)
196
- rescue
197
- logger.warn("Couldn't parse key/value: #{first_elem}") if logger
198
- next
199
- end
200
-
201
- cookie_elem.each do |pair|
202
- pair.strip!
203
- key, value = pair.split(/=/, 2) #/)
204
- next unless key
205
- case value # may be nil
206
- when /\A"(.*)"\z/
207
- value = $1.gsub(/\\(.)/, "\\1")
208
- end
209
-
210
- case key.downcase
211
- when 'domain'
212
- next unless value && !value.empty?
213
- begin
214
- cookie.domain = value
253
+ cookie = new(name, value)
254
+ attrs.each { |aname, avalue|
255
+ begin
256
+ case aname
257
+ when 'domain'
258
+ cookie.domain = avalue
215
259
  cookie.for_domain = true
216
- rescue
217
- logger.warn("Couldn't parse domain: #{value}") if logger
218
- end
219
- when 'path'
220
- next unless value && !value.empty?
221
- cookie.path = value
222
- when 'expires'
223
- next unless value && !value.empty?
224
- begin
225
- cookie.expires = Time.parse(value)
226
- rescue
227
- logger.warn("Couldn't parse expires: #{value}") if logger
228
- end
229
- when 'max-age'
230
- next unless value && !value.empty?
231
- begin
232
- cookie.max_age = Integer(value)
233
- rescue
234
- logger.warn("Couldn't parse max age '#{value}'") if logger
235
- end
236
- when 'comment'
237
- next unless value
238
- cookie.comment = value
239
- when 'version'
240
- next unless value
241
- begin
242
- cookie.version = Integer(value)
243
- rescue
244
- logger.warn("Couldn't parse version '#{value}'") if logger
245
- cookie.version = nil
260
+ when 'path'
261
+ cookie.path = avalue
262
+ when 'expires'
263
+ # RFC 6265 4.1.2.2
264
+ # The Max-Age attribute has precedence over the Expires
265
+ # attribute.
266
+ cookie.expires = avalue unless cookie.max_age
267
+ when 'max-age'
268
+ cookie.max_age = avalue
269
+ when 'comment'
270
+ cookie.comment = avalue
271
+ when 'version'
272
+ cookie.version = avalue
273
+ when 'secure'
274
+ cookie.secure = avalue
275
+ when 'httponly'
276
+ cookie.httponly = avalue
246
277
  end
247
- when 'secure'
248
- cookie.secure = true
249
- when 'httponly'
250
- cookie.httponly = true
278
+ rescue => e
279
+ logger.warn("Couldn't parse #{aname} '#{avalue}': #{e}") if logger
251
280
  end
252
- end
253
-
254
- cookie.secure ||= false
255
- cookie.httponly ||= false
281
+ }
256
282
 
257
- # RFC 6265 4.1.2.2
258
- cookie.expires = date + cookie.max_age if cookie.max_age
259
- cookie.session = !cookie.expires
283
+ # Have `expires` set instead of `max_age`, so that
284
+ # expiration check (`expired?`) can be performed.
285
+ cookie.expires = date + cookie.max_age if cookie.max_age
260
286
 
261
287
  if origin
262
288
  begin
@@ -270,22 +296,46 @@ class HTTP::Cookie
270
296
  yield cookie if block_given?
271
297
 
272
298
  cookies << cookie
273
- }
299
+ end
274
300
  }
275
301
  end
276
302
  end
277
303
 
304
+ attr_reader :name
305
+
306
+ # See #name.
278
307
  def name=(name)
279
- if name.nil? || name.empty?
308
+ name = check_string_type(name) or
309
+ raise TypeError, "#{name.class} is not a String"
310
+ if name.empty?
280
311
  raise ArgumentError, "cookie name cannot be empty"
281
- elsif name.match(/[\x00-\x1F=\x7F]/)
282
- raise ArgumentError, "cookie name cannot contain a control character or an equal sign"
312
+ elsif name.match(/[\x00-\x20\x7F,;\\"=]/)
313
+ raise ArgumentError, "invalid cookie name"
283
314
  end
315
+ # RFC 6265 4.1.1
316
+ # cookie-name may not match:
317
+ # /[\x00-\x20\x7F()<>@,;:\\"\/\[\]?={}]/
284
318
  @name = name
285
319
  end
286
320
 
287
- # Sets the domain attribute. A leading dot in +domain+ implies
288
- # turning the +for_domain?+ flag on.
321
+ attr_reader :value
322
+
323
+ # See #value.
324
+ def value=(value)
325
+ value = check_string_type(value) or
326
+ raise TypeError, "#{value.class} is not a String"
327
+ if value.match(/[\x00-\x1F\x7F]/)
328
+ raise ArgumentError, "invalid cookie value"
329
+ end
330
+ # RFC 6265 4.1.1
331
+ # cookie-name may not match:
332
+ # /[^\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/
333
+ @value = value
334
+ end
335
+
336
+ attr_reader :domain
337
+
338
+ # See #domain.
289
339
  def domain=(domain)
290
340
  if DomainName === domain
291
341
  @domain_name = domain
@@ -310,13 +360,29 @@ class HTTP::Cookie
310
360
  raise NoMethodError, 'HTTP::Cookie equivalent for Mechanize::CookieJar#set_domain() is #domain=().'
311
361
  end
312
362
 
313
- # Sets the path attribute.
363
+ # Returns the domain attribute value as a DomainName object.
364
+ attr_reader :domain_name
365
+
366
+ # The domain flag.
367
+ #
368
+ # If this flag is true, this cookie will be sent to any host in the
369
+ # \#domain, including the host domain itself. If it is false, this
370
+ # cookie will be sent only to the host indicated by the #domain.
371
+ attr_accessor :for_domain
372
+ alias for_domain? for_domain
373
+
374
+ attr_reader :path
375
+
376
+ # See #path.
314
377
  def path=(path)
378
+ path = check_string_type(path) or
379
+ raise TypeError, "#{path.class} is not a String"
315
380
  @path = HTTP::Cookie.normalize_path(path)
316
381
  end
317
382
 
318
- # Sets the origin of the cookie. This initializes the `domain` and
319
- # `path` attribute values if unknown yet.
383
+ attr_reader :origin
384
+
385
+ # See #origin.
320
386
  def origin=(origin)
321
387
  @origin.nil? or
322
388
  raise ArgumentError, "origin cannot be changed once it is set"
@@ -328,14 +394,58 @@ class HTTP::Cookie
328
394
  @origin = origin
329
395
  end
330
396
 
331
- # Sets the expires attribute. A `Time` object, a string
332
- # representation of date/time, and `nil` are good values to set.
397
+ # The secure flag.
398
+ #
399
+ # A cookie with this flag on should only be sent via a secure
400
+ # protocol like HTTPS.
401
+ attr_accessor :secure
402
+ alias secure? secure
403
+
404
+ # The HttpOnly flag.
405
+ #
406
+ # A cookie with this flag on should be hidden from a client script.
407
+ attr_accessor :httponly
408
+ alias httponly? httponly
409
+
410
+ # The session flag.
411
+ #
412
+ # A cookie with this flag on should be hidden from a client script.
413
+ attr_reader :session
414
+ alias session? session
415
+
416
+ attr_reader :expires
417
+
418
+ # See #expires.
333
419
  def expires=(t)
334
420
  case t
335
421
  when nil, Time
336
- @expires = t
337
422
  else
338
- @expires = Time.parse(t)
423
+ t = Time.parse(t)
424
+ end
425
+ @max_age = nil
426
+ @session = t.nil?
427
+ @expires = t
428
+ end
429
+
430
+ alias expires_at expires
431
+ alias expires_at= expires=
432
+
433
+ attr_reader :max_age
434
+
435
+ # See #max_age.
436
+ def max_age=(sec)
437
+ @expires = nil
438
+ case sec
439
+ when Integer, nil
440
+ else
441
+ str = check_string_type(sec) or
442
+ raise TypeError, "#{sec.class} is not an Integer or String"
443
+ sec = str.to_i
444
+ end
445
+ if @session = sec.nil?
446
+ @max_age = nil
447
+ else
448
+ @max_age = sec
339
449
  end
340
450
  end
341
451
 
@@ -347,17 +457,26 @@ class HTTP::Cookie
347
457
 
348
458
  # Expires this cookie by setting the expires attribute value to a
349
459
  # past date.
350
- def expire
351
- @expires = UNIX_EPOCH
460
+ def expire!
461
+ self.expires = UNIX_EPOCH
352
462
  self
353
463
  end
354
464
 
355
- alias secure? secure
356
- alias httponly? httponly
357
- alias session? session
465
+ # The version attribute. The only known version of the cookie
466
+ # format is 0.
467
+ attr_accessor :version
468
+
469
+ # The comment attribute.
470
+ attr_accessor :comment
471
+
472
+ # The time this cookie was created at.
473
+ attr_accessor :created_at
474
+
475
+ # The time this cookie was last accessed at.
476
+ attr_accessor :accessed_at
358
477
 
359
478
  # Tests if it is OK to accept this cookie if it is sent from a given
360
- # +uri.
479
+ # `uri`.
361
480
  def acceptable_from_uri?(uri)
362
481
  uri = URI(uri)
363
482
  return false unless URI::HTTP === uri && uri.host
@@ -377,7 +496,7 @@ class HTTP::Cookie
377
496
  end
378
497
  end
379
498
 
380
- # Tests if it is OK to send this cookie to a given +uri+, A runtime
499
+ # Tests if it is OK to send this cookie to a given `uri`. A runtime
381
500
  # error is raised if the cookie's domain is unknown.
382
501
  def valid_for_uri?(uri)
383
502
  if @domain.nil?
@@ -405,28 +524,37 @@ class HTTP::Cookie
405
524
  origin = origin ? URI(origin) : @origin or
406
525
  raise "origin must be specified to produce a value for Set-Cookie"
407
526
 
408
- string = cookie_value
527
+ string = "#{@name}=#{Scanner.quote(@value)}"
409
528
  if @for_domain || @domain != DomainName.new(origin.host).hostname
410
- string << "; domain=#{@domain}"
529
+ string << "; Domain=#{@domain}"
411
530
  end
412
531
  if (HTTP::Cookie.normalize_path(origin) + './').path != @path
413
- string << "; path=#{@path}"
532
+ string << "; Path=#{@path}"
414
533
  end
415
- if @expires
416
- string << "; expires=#{@expires.httpdate}"
534
+ if @max_age
535
+ string << "; Max-Age=#{@max_age}"
536
+ elsif @expires
537
+ string << "; Expires=#{@expires.httpdate}"
417
538
  end
418
539
  if @comment
419
- string << "; comment=#{@comment}"
540
+ string << "; Comment=#{Scanner.quote(@comment)}"
420
541
  end
421
542
  if @httponly
422
543
  string << "; HttpOnly"
423
544
  end
424
545
  if @secure
425
- string << "; secure"
546
+ string << "; Secure"
426
547
  end
427
548
  string
428
549
  end
429
550
 
551
+ def inspect
552
+ '#<%s:' % self.class << PERSISTENT_PROPERTIES.map { |key|
553
+ '%s=%s' % [key, instance_variable_get(:"@#{key}").inspect]
554
+ }.join(', ') << ' origin=%s>' % [@origin ? @origin.to_s : 'nil']
555
+
556
+ end
557
+
430
558
  # Compares the cookie with another. When there are many cookies with
431
559
  # the same name for a URL, the value of the smallest must be used.
432
560
  def <=>(other)
@@ -461,7 +589,7 @@ class HTTP::Cookie
461
589
  map.each { |key, value|
462
590
  case key
463
591
  when *PERSISTENT_PROPERTIES
464
- send(:"#{key}=", value)
592
+ __send__(:"#{key}=", value)
465
593
  end
466
594
  }
467
595
  end
@@ -0,0 +1,215 @@
1
+ require 'http/cookie'
2
+ require 'strscan'
3
+ require 'time'
4
+
5
+ class HTTP::Cookie::Scanner < StringScanner
6
+ # Whitespace.
7
+ RE_WSP = /[ \t]+/
8
+
9
+ # A pattern that matches a cookie name or attribute name which may
10
+ # be empty, capturing trailing whitespace.
11
+ RE_NAME = /(?!#{RE_WSP})[^,;\\"=]*/
12
+
13
+ RE_BAD_CHAR = /([\x00-\x20\x7F",;\\])/
14
+
15
+ # A pattern that matches the comma in a (typically date) value.
16
+ RE_COOKIE_COMMA = /,(?=#{RE_WSP}?#{RE_NAME}=)/
17
+
18
+ def initialize(string, logger = nil)
19
+ @logger = logger
20
+ super(string)
21
+ end
22
+
23
+ class << self
24
+ def quote(s)
25
+ return s unless s.match(RE_BAD_CHAR)
26
+ '"' << s.gsub(RE_BAD_CHAR, "\\\\\\1") << '"'
27
+ end
28
+ end
29
+
30
+ def skip_wsp
31
+ skip(RE_WSP)
32
+ end
33
+
34
+ def scan_dquoted
35
+ ''.tap { |s|
36
+ case
37
+ when skip(/"/)
38
+ break
39
+ when skip(/\\/)
40
+ s << getch
41
+ when scan(/[^"\\]+/)
42
+ s << matched
43
+ end until eos?
44
+ }
45
+ end
46
+
47
+ def scan_name
48
+ scan(RE_NAME).tap { |s|
49
+ s.rstrip! if s
50
+ }
51
+ end
52
+
53
+ def scan_value
54
+ ''.tap { |s|
55
+ case
56
+ when scan(/[^,;"]+/)
57
+ s << matched
58
+ when skip(/"/)
59
+ # RFC 6265 2.2
60
+ # A cookie-value may be DQUOTE'd.
61
+ s << scan_dquoted
62
+ when check(/;|#{RE_COOKIE_COMMA}/o)
63
+ break
64
+ else
65
+ s << getch
66
+ end until eos?
67
+ s.rstrip!
68
+ }
69
+ end
70
+
71
+ def scan_name_value
72
+ name = scan_name
73
+ if skip(/\=/)
74
+ value = scan_value
75
+ else
76
+ scan_value
77
+ value = nil
78
+ end
79
+ [name, value]
80
+ end
81
+
82
+ if Time.respond_to?(:strptime)
83
+ def tuple_to_time(day_of_month, month, year, time)
84
+ Time.strptime(
85
+ '%02d %s %04d %02d:%02d:%02d UTC' % [day_of_month, month, year, *time],
86
+ '%d %b %Y %T %Z'
87
+ ).tap { |date|
88
+ date.day == day_of_month or return nil
89
+ }
90
+ end
91
+ else
92
+ def tuple_to_time(day_of_month, month, year, time)
93
+ Time.parse(
94
+ '%02d %s %04d %02d:%02d:%02d UTC' % [day_of_month, month, year, *time]
95
+ ).tap { |date|
96
+ date.day == day_of_month or return nil
97
+ }
98
+ end
99
+ end
100
+ private :tuple_to_time
101
+
102
+ def parse_cookie_date(s)
103
+ # RFC 6265 5.1.1
104
+ time = day_of_month = month = year = nil
105
+
106
+ s.split(/[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]+/).each { |token|
107
+ case
108
+ when time.nil? && token.match(/\A(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?=\D|\z)/)
109
+ sec =
110
+ if $3
111
+ $3.to_i
112
+ else
113
+ # violation of the RFC
114
+ @logger.warn("Time lacks the second part: #{token}") if @logger
115
+ 0
116
+ end
117
+ time = [$1.to_i, $2.to_i, sec]
118
+ when day_of_month.nil? && token.match(/\A(\d{1,2})(?=\D|\z)/)
119
+ day_of_month = $1.to_i
120
+ when month.nil? && token.match(/\A(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i)
121
+ month = $1.capitalize
122
+ when year.nil? && token.match(/\A(\d{2,4})(?=\D|\z)/)
123
+ year = $1.to_i
124
+ end
125
+ }
126
+
127
+ if day_of_month.nil? || month.nil? || year.nil? || time.nil?
128
+ return nil
129
+ end
130
+
131
+ case day_of_month
132
+ when 1..31
133
+ else
134
+ return nil
135
+ end
136
+
137
+ case year
138
+ when 100..1600
139
+ return nil
140
+ when 70..99
141
+ year += 1900
142
+ when 0..69
143
+ year += 2000
144
+ end
145
+
146
+ if (time <=> [23,59,59]) > 0
147
+ return nil
148
+ end
149
+
150
+ tuple_to_time(day_of_month, month, year, time)
151
+ end
152
+
153
+ def scan_cookie
154
+ # cf. RFC 6265 5.2
155
+ until eos?
156
+ start = pos
157
+ len = nil
158
+
159
+ skip_wsp
160
+
161
+ name, value = scan_name_value
162
+ if name.nil?
163
+ break
164
+ elsif value.nil?
165
+ @logger.warn("Cookie definition lacks a name-value pair.") if @logger
166
+ elsif name.empty?
167
+ @logger.warn("Cookie definition has an empty name.") if @logger
168
+ value = nil
169
+ end
170
+ attrs = {}
171
+
172
+ case
173
+ when skip(/,/)
174
+ len = (pos - 1) - start
175
+ break
176
+ when skip(/;/)
177
+ skip_wsp
178
+ aname, avalue = scan_name_value
179
+ break if aname.nil?
180
+ next if aname.empty? || value.nil?
181
+ aname.downcase!
182
+ case aname
183
+ when 'expires'
184
+ # RFC 6265 5.2.1
185
+ avalue &&= parse_cookie_date(avalue) or next
186
+ when 'max-age'
187
+ # RFC 6265 5.2.2
188
+ next unless /\A-?\d+\z/.match(avalue)
189
+ when 'domain'
190
+ # RFC 6265 5.2.3
191
+ # An empty value SHOULD be ignored.
192
+ next if avalue.nil? || avalue.empty?
193
+ when 'path'
194
+ # RFC 6265 5.2.4
195
+ # A relative path must be ignored rather than normalizing it
196
+ # to "/".
197
+ next unless /\A\//.match(avalue)
198
+ when 'secure', 'httponly'
199
+ # RFC 6265 5.2.5, 5.2.6
200
+ avalue = true
201
+ end
202
+ attrs[aname] = avalue
203
+ end until eos?
204
+
205
+ len ||= pos - start
206
+
207
+ if len > HTTP::Cookie::MAX_LENGTH
208
+ @logger.warn("Cookie definition too long: #{name}") if @logger
209
+ next
210
+ end
211
+
212
+ return [name, value, attrs] if value
213
+ end
214
+ end
215
+ end
@@ -1,5 +1,5 @@
1
1
  module HTTP
2
2
  class Cookie
3
- VERSION = "0.1.5"
3
+ VERSION = "1.0.0.pre1"
4
4
  end
5
5
  end
@@ -224,7 +224,7 @@ class HTTP::CookieJar
224
224
  end
225
225
 
226
226
  # Used to exist in Mechanize::CookieJar. Use #clear().
227
- def clear!(*args)
227
+ def clear!
228
228
  raise NoMethodError, 'HTTP::Cookie equivalent for Mechanize::CookieJar#clear!() is #clear().'
229
229
  end
230
230
 
@@ -114,7 +114,7 @@ class HTTP::CookieJar
114
114
  if (debt = domain_cookies.size - HTTP::Cookie::MAX_COOKIES_PER_DOMAIN) > 0
115
115
  domain_cookies.sort_by!(&:created_at)
116
116
  domain_cookies.slice!(0, debt).each { |cookie|
117
- add(cookie.expire)
117
+ add(cookie.expire!)
118
118
  }
119
119
  end
120
120
 
@@ -124,7 +124,7 @@ class HTTP::CookieJar
124
124
  if (debt = all_cookies.size - HTTP::Cookie::MAX_COOKIES_TOTAL) > 0
125
125
  all_cookies.sort_by!(&:created_at)
126
126
  all_cookies.slice!(0, debt).each { |cookie|
127
- add(cookie.expire)
127
+ add(cookie.expire!)
128
128
  }
129
129
  end
130
130
 
@@ -33,7 +33,7 @@ class TestHTTPCookie < Test::Unit::TestCase
33
33
  "Fri, 17 Mar 89 4:01:33",
34
34
  "Fri, 17 Mar 89 4:01 GMT",
35
35
  "Mon Jan 16 16:12 PDT 1989",
36
- "Mon Jan 16 16:12 +0130 1989",
36
+ #"Mon Jan 16 16:12 +0130 1989",
37
37
  "6 May 1992 16:41-JST (Wednesday)",
38
38
  #"22-AUG-1993 10:59:12.82",
39
39
  "22-AUG-1993 10:59pm",
@@ -42,7 +42,7 @@ class TestHTTPCookie < Test::Unit::TestCase
42
42
  #"Friday, August 04, 1995 3:54 PM",
43
43
  #"06/21/95 04:24:34 PM",
44
44
  #"20/06/95 21:07",
45
- "95-06-08 19:32:48 EDT",
45
+ #"95-06-08 19:32:48 EDT",
46
46
  ]
47
47
 
48
48
  dates.each do |date|
@@ -83,7 +83,7 @@ class TestHTTPCookie < Test::Unit::TestCase
83
83
  def test_parse_too_long_cookie
84
84
  uri = URI.parse 'http://example'
85
85
 
86
- cookie_str = "foo=#{'クッキー' * 340}; path=/ab/"
86
+ cookie_str = "foo=#{'Cookie' * 680}; path=/ab/"
87
87
  assert_equal(HTTP::Cookie::MAX_LENGTH - 1, cookie_str.bytesize)
88
88
 
89
89
  assert_equal 1, HTTP::Cookie.parse(cookie_str, :origin => uri).size
@@ -101,7 +101,7 @@ class TestHTTPCookie < Test::Unit::TestCase
101
101
 
102
102
  assert_equal 1, HTTP::Cookie.parse(cookie_str, :origin => uri) { |cookie|
103
103
  assert_equal 'quoted', cookie.name
104
- assert_equal '"value"', cookie.value
104
+ assert_equal 'value', cookie.value
105
105
  assert_equal 'comment is "comment"', cookie.comment
106
106
  }.size
107
107
  end
@@ -248,12 +248,14 @@ class TestHTTPCookie < Test::Unit::TestCase
248
248
  "no_path1=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT, no_expires=nope; Path=/, " \
249
249
  "no_path2=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path, " \
250
250
  "no_path3=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path=, " \
251
+ "rel_path1=rel_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path=foo/bar, " \
252
+ "rel_path1=rel_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path=foo, " \
251
253
  "no_domain1=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope, " \
252
254
  "no_domain2=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope; Domain, " \
253
255
  "no_domain3=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope; Domain="
254
256
 
255
257
  cookies = HTTP::Cookie.parse cookie_str, :origin => url
256
- assert_equal 13, cookies.length
258
+ assert_equal 15, cookies.length
257
259
 
258
260
  name = cookies.find { |c| c.name == 'name' }
259
261
  assert_equal "Aaron", name.value
@@ -277,6 +279,13 @@ class TestHTTPCookie < Test::Unit::TestCase
277
279
  assert_equal Time.at(1320539392), c.expires, c.name
278
280
  }
279
281
 
282
+ rel_path_cookies = cookies.select { |c| c.value == 'rel_path' }
283
+ assert_equal 2, rel_path_cookies.size
284
+ rel_path_cookies.each { |c|
285
+ assert_equal "/", c.path, c.name
286
+ assert_equal Time.at(1320539392), c.expires, c.name
287
+ }
288
+
280
289
  no_domain_cookies = cookies.select { |c| c.value == 'no_domain' }
281
290
  assert_equal 3, no_domain_cookies.size
282
291
  no_domain_cookies.each { |c|
@@ -364,23 +373,32 @@ class TestHTTPCookie < Test::Unit::TestCase
364
373
 
365
374
  def test_set_cookie_value
366
375
  url = URI.parse('http://rubyforge.org/')
367
- cookie_params = @cookie_params.merge('secure' => 'secure')
368
- cookie_value = 'foo=bar'
369
376
 
370
- cookie_params.keys.combine.each do |keys|
371
- cookie_text = [cookie_value, *keys.map { |key| cookie_params[key] }].join('; ')
372
- cookie, = HTTP::Cookie.parse(cookie_text, :origin => url)
373
- cookie2, = HTTP::Cookie.parse(cookie.set_cookie_value, :origin => url)
374
-
375
- assert_equal(cookie.name, cookie2.name)
376
- assert_equal(cookie.value, cookie2.value)
377
- assert_equal(cookie.domain, cookie2.domain)
378
- assert_equal(cookie.for_domain?, cookie2.for_domain?)
379
- assert_equal(cookie.path, cookie2.path)
380
- assert_equal(cookie.expires, cookie2.expires)
381
- assert_equal(cookie.secure?, cookie2.secure?)
382
- assert_equal(cookie.httponly?, cookie2.httponly?)
383
- end
377
+ ['foo=bar', 'foo="bar"', 'foo="ba\"r baz"'].each { |cookie_value|
378
+ cookie_params = @cookie_params.merge('secure' => 'secure', 'max-age' => 'Max-Age=1000')
379
+ date = Time.at(Time.now.to_i)
380
+ cookie_params.keys.combine.each do |keys|
381
+ cookie_text = [cookie_value, *keys.map { |key| cookie_params[key] }].join('; ')
382
+ cookie, = HTTP::Cookie.parse(cookie_text, :origin => url, :date => date)
383
+ cookie2, = HTTP::Cookie.parse(cookie.set_cookie_value, :origin => url, :date => date)
384
+
385
+ assert_equal(cookie.name, cookie2.name)
386
+ assert_equal(cookie.value, cookie2.value)
387
+ assert_equal(cookie.domain, cookie2.domain)
388
+ assert_equal(cookie.for_domain?, cookie2.for_domain?)
389
+ assert_equal(cookie.path, cookie2.path)
390
+ assert_equal(cookie.expires, cookie2.expires)
391
+ if keys.include?('max-age')
392
+ assert_equal(date + 1000, cookie2.expires)
393
+ elsif keys.include?('expires')
394
+ assert_equal(@expires, cookie2.expires)
395
+ else
396
+ assert_equal(nil, cookie2.expires)
397
+ end
398
+ assert_equal(cookie.secure?, cookie2.secure?)
399
+ assert_equal(cookie.httponly?, cookie2.httponly?)
400
+ end
401
+ }
384
402
  end
385
403
 
386
404
  def test_parse_cookie_no_spaces
@@ -445,6 +463,37 @@ class TestHTTPCookie < Test::Unit::TestCase
445
463
  }.merge(options)
446
464
  end
447
465
 
466
+ def test_bad_name
467
+ [
468
+ "a\tb", "a\vb", "a\rb", "a\nb", 'a b',
469
+ "a\\b", 'a"b', # 'a:b', 'a/b', 'a[b]',
470
+ 'a=b', 'a,b', 'a;b',
471
+ ].each { |name|
472
+ assert_raises(ArgumentError) {
473
+ HTTP::Cookie.new(cookie_values(:name => name))
474
+ }
475
+ cookie = HTTP::Cookie.new(cookie_values)
476
+ assert_raises(ArgumentError) {
477
+ cookie.name = name
478
+ }
479
+ }
480
+ end
481
+
482
+ def test_bad_value
483
+ [
484
+ "a\tb", "a\vb", "a\rb", "a\nb",
485
+ "a\\b", 'a"b', # 'a:b', 'a/b', 'a[b]',
486
+ ].each { |name|
487
+ assert_raises(ArgumentError) {
488
+ HTTP::Cookie.new(cookie_values(:name => name))
489
+ }
490
+ cookie = HTTP::Cookie.new(cookie_values)
491
+ assert_raises(ArgumentError) {
492
+ cookie.name = name
493
+ }
494
+ }
495
+ end
496
+
448
497
  def test_compare
449
498
  time = Time.now
450
499
  cookies = [
@@ -466,10 +515,33 @@ class TestHTTPCookie < Test::Unit::TestCase
466
515
  assert_equal false, cookie.expired?
467
516
  assert_equal true, cookie.expired?(cookie.expires + 1)
468
517
  assert_equal false, cookie.expired?(cookie.expires - 1)
469
- cookie.expire
518
+ cookie.expire!
470
519
  assert_equal true, cookie.expired?
471
520
  end
472
521
 
522
+ def test_session
523
+ cookie = HTTP::Cookie.new(cookie_values)
524
+
525
+ assert_equal false, cookie.session?
526
+ assert_equal nil, cookie.max_age
527
+
528
+ cookie.expires = nil
529
+ assert_equal true, cookie.session?
530
+ assert_equal nil, cookie.max_age
531
+
532
+ cookie.expires = Time.now + 3600
533
+ assert_equal false, cookie.session?
534
+ assert_equal nil, cookie.max_age
535
+
536
+ cookie.max_age = 3600
537
+ assert_equal false, cookie.session?
538
+ assert_equal nil, cookie.expires
539
+
540
+ cookie.max_age = nil
541
+ assert_equal true, cookie.session?
542
+ assert_equal nil, cookie.expires
543
+ end
544
+
473
545
  def test_equal
474
546
  assert_not_equal(HTTP::Cookie.new(cookie_values),
475
547
  HTTP::Cookie.new(cookie_values(:value => 'bar')))
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http-cookie
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 1.0.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Akinori MUSHA
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2013-03-17 00:00:00.000000000 Z
14
+ date: 2013-03-21 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: domain_name
@@ -116,6 +116,7 @@ files:
116
116
  - http-cookie.gemspec
117
117
  - lib/http-cookie.rb
118
118
  - lib/http/cookie.rb
119
+ - lib/http/cookie/scanner.rb
119
120
  - lib/http/cookie/version.rb
120
121
  - lib/http/cookie_jar.rb
121
122
  - lib/http/cookie_jar/abstract_saver.rb
@@ -141,9 +142,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
141
142
  version: '0'
142
143
  required_rubygems_version: !ruby/object:Gem::Requirement
143
144
  requirements:
144
- - - '>='
145
+ - - '>'
145
146
  - !ruby/object:Gem::Version
146
- version: '0'
147
+ version: 1.3.1
147
148
  requirements: []
148
149
  rubyforge_project:
149
150
  rubygems_version: 2.0.3