Class: HTTP::Cookie

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/http/cookie.rb,
lib/http/cookie/version.rb

Overview

This class is used to represent an HTTP Cookie.

Defined Under Namespace

Classes: Scanner

Constant Summary collapse

MAX_LENGTH =

Maximum number of bytes per cookie (RFC 6265 6.1 requires 4096 at least)

4096
MAX_COOKIES_PER_DOMAIN =

Maximum number of cookies per domain (RFC 6265 6.1 requires 50 at least)

50
MAX_COOKIES_TOTAL =

Maximum number of cookies total (RFC 6265 6.1 requires 3000 at least)

3000
UNIX_EPOCH =

:stopdoc:

Time.at(0)
PERSISTENT_PROPERTIES =
%w[
  name        value
  domain      for_domain  path
  secure      httponly
  expires     max_age
  created_at  accessed_at
]
VERSION =
"1.0.0"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Cookie

:call-seq:

new(name, value = nil)
new(name, value = nil, **attr_hash)
new(**attr_hash)

Creates a cookie object. For each key of ‘attr_hash`, the setter is called if defined and any error (typically ArgumentError or TypeError) that is raised will be passed through. Each key can be either a downcased symbol or a string that may be mixed case. Support for the latter may, however, be obsoleted in future when Ruby 2.0’s keyword syntax is adopted.

If ‘value` is omitted or it is nil, an expiration cookie is created unless `max_age` or `expires` (`expires_at`) is given.

e.g.

new("uid", "a12345")
new("uid", "a12345", :domain => 'example.org',
                     :for_domain => true, :expired => Time.now + 7*86400)
new("name" => "uid", "value" => "a12345", "Domain" => 'www.example.org')


130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/http/cookie.rb', line 130

def initialize(*args)
  @origin = @domain = @path =
    @expires = @max_age = nil
  @for_domain = @secure = @httponly = false
  @session = true
  @created_at = @accessed_at = Time.now
  case argc = args.size
  when 1
    if attr_hash = Hash.try_convert(args.last)
      args.pop
    else
      self.name, self.value = args # value is set to nil
      return
    end
  when 2..3
    if attr_hash = Hash.try_convert(args.last)
      args.pop
      self.name, value = args
    else
      argc == 2 or
        raise ArgumentError, "wrong number of arguments (#{argc} for 1-3)"
      self.name, self.value = args
      return
    end
  else
    raise ArgumentError, "wrong number of arguments (#{argc} for 1-3)"
  end
  for_domain = false
  domain = max_age = origin = nil
  attr_hash.each_pair { |okey, val|
    case key ||= okey
    when :name
      self.name = val
    when :value
      value = val
    when :domain
      domain = val
    when :path
      self.path = val
    when :origin
      origin = val
    when :for_domain, :for_domain?
      for_domain = val
    when :max_age
      # Let max_age take precedence over expires
      max_age = val
    when :expires, :expires_at
      self.expires = val
    when :httponly, :httponly?
      @httponly = val
    when :secure, :secure?
      @secure = val
    when Symbol
      setter = :"#{key}="
      if respond_to?(setter)
        __send__(setter, val)
      else
        warn "unknown attribute name: #{okey.inspect}" if $VERBOSE
        next
      end
    when String
      warn "use downcased symbol for keyword: #{okey.inspect}" if $VERBOSE
      key = key.downcase.to_sym
      redo
    else
      warn "invalid keyword ignored: #{okey.inspect}" if $VERBOSE
      next
    end
  }
  if @name.nil?
    raise ArgumentError, "name must be specified"
  end
  @for_domain = for_domain
  self.domain = domain if domain
  self.origin = origin if origin
  self.max_age = max_age if max_age
  self.value = value.nil? && (@expires || @max_age) ? '' : value
end

Instance Attribute Details

#accessed_atObject

The time this cookie was last accessed at.



538
539
540
# File 'lib/http/cookie.rb', line 538

def accessed_at
  @accessed_at
end

#created_atObject

The time this cookie was created at. This value is used as a base date for interpreting the Max-Age attribute value. See #expires.



535
536
537
# File 'lib/http/cookie.rb', line 535

def created_at
  @created_at
end

#domainObject

Returns the value of attribute domain.



376
377
378
# File 'lib/http/cookie.rb', line 376

def domain
  @domain
end

#domain_nameObject (readonly)

Returns the domain attribute value as a DomainName object.



424
425
426
# File 'lib/http/cookie.rb', line 424

def domain_name
  @domain_name
end

#for_domainObject Also known as: for_domain?

The domain flag. (the opposite of host-only-flag)

If this flag is true, this cookie will be sent to any host in the #domain, including the host domain itself. If it is false, this cookie will be sent only to the host indicated by the #domain.



431
432
433
# File 'lib/http/cookie.rb', line 431

def for_domain
  @for_domain
end

#httponlyObject Also known as: httponly?

The HttpOnly flag. (http-only-flag)

A cookie with this flag on should be hidden from a client script.



468
469
470
# File 'lib/http/cookie.rb', line 468

def httponly
  @httponly
end

#max_ageObject

Returns the value of attribute max_age.



496
497
498
# File 'lib/http/cookie.rb', line 496

def max_age
  @max_age
end

#nameObject

Returns the value of attribute name.



340
341
342
# File 'lib/http/cookie.rb', line 340

def name
  @name
end

#originObject

Returns the value of attribute origin.



443
444
445
# File 'lib/http/cookie.rb', line 443

def origin
  @origin
end

#pathObject

Returns the value of attribute path.



434
435
436
# File 'lib/http/cookie.rb', line 434

def path
  @path
end

#secureObject Also known as: secure?

The secure flag. (secure-only-flag)

A cookie with this flag on should only be sent via a secure protocol like HTTPS.



462
463
464
# File 'lib/http/cookie.rb', line 462

def secure
  @secure
end

#sessionObject (readonly) Also known as: session?

The session flag. (the opposite of persistent-flag)

A cookie with this flag on should be hidden from a client script.



474
475
476
# File 'lib/http/cookie.rb', line 474

def session
  @session
end

#valueObject

Returns the value of attribute value.



357
358
359
# File 'lib/http/cookie.rb', line 357

def value
  @value
end

Class Method Details

Takes an array of cookies and returns a string for use in the Cookie header, like “name1=value2; name2=value2”.



324
325
326
# File 'lib/http/cookie.rb', line 324

def cookie_value(cookies)
  cookies.join('; ')
end

Parses a Cookie header value into a hash of name-value string pairs. The first appearance takes precedence if multiple pairs with the same name occur.



331
332
333
334
335
336
337
# File 'lib/http/cookie.rb', line 331

def cookie_value_to_hash(cookie_value)
  {}.tap { |hash|
    Scanner.new(cookie_value).scan_cookie { |name, value|
      hash[name] ||= value
    }
  }
end

.parse(set_cookie, origin, options = nil, &block) ⇒ Object

Parses a Set-Cookie header value ‘set_cookie` assuming that it is sent from a source URI/URL `origin`, and returns an array of Cookie objects. Parts (separated by commas) that are malformed or considered unacceptable are silently ignored.

If a block is given, each cookie object is passed to the block.

Available option keywords are below:

:created_at : The creation time of the cookies parsed.

:logger : Logger object useful for debugging

### Compatibility Note for Mechanize::Cookie users

  • Order of parameters changed in HTTP::Cookie.parse:

    Mechanize::Cookie.parse(uri, set_cookie[, log])
    
    HTTP::Cookie.parse(set_cookie, uri[, :logger => # log])
    
  • HTTP::Cookie.parse does not accept nil for ‘set_cookie`.

  • HTTP::Cookie.parse does not yield nil nor include nil in an returned array. It simply ignores unparsable parts.

  • HTTP::Cookie.parse is made to follow RFC 6265 to the extent not terribly breaking interoperability with broken implementations. In particular, it is capable of parsing cookie definitions containing double-quotes just as naturally expected.



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/http/cookie.rb', line 273

def parse(set_cookie, origin, options = nil, &block)
  if options
    logger = options[:logger]
    created_at = options[:created_at]
  end
  origin = URI(origin)

  [].tap { |cookies|
    Scanner.new(set_cookie, logger).scan_set_cookie { |name, value, attrs|
      break if name.nil? || name.empty?

      cookie = new(name, value)
      cookie.created_at = created_at if created_at
      attrs.each { |aname, avalue|
        begin
          case aname
          when 'domain'
            cookie.for_domain = true
            cookie.domain = avalue # This may negate @for_domain
          when 'path'
            cookie.path = avalue
          when 'expires'
            # RFC 6265 4.1.2.2
            # The Max-Age attribute has precedence over the Expires
            # attribute.
            cookie.expires = avalue unless cookie.max_age
          when 'max-age'
            cookie.max_age = avalue
          when 'secure'
            cookie.secure = avalue
          when 'httponly'
            cookie.httponly = avalue
          end
        rescue => e
          logger.warn("Couldn't parse #{aname} '#{avalue}': #{e}") if logger
        end
      }

      cookie.origin = origin

      cookie.acceptable? or next

      yield cookie if block_given?

      cookies << cookie
    }
  }
end

.path_match?(base_path, target_path) ⇒ Boolean

Tests if target_path is under base_path as described in RFC 6265 5.1.4. base_path must be an absolute path. target_path may be empty, in which case it is treated as the root path.

e.g.

path_match?('/admin/', '/admin/index') == true
path_match?('/admin/', '/Admin/index') == false
path_match?('/admin/', '/admin/') == true
path_match?('/admin/', '/admin') == false

path_match?('/admin', '/admin') == true
path_match?('/admin', '/Admin') == false
path_match?('/admin', '/admins') == false
path_match?('/admin', '/admin/') == true
path_match?('/admin', '/admin/index') == true

Returns:

  • (Boolean)


229
230
231
232
233
234
235
236
237
238
# File 'lib/http/cookie.rb', line 229

def path_match?(base_path, target_path)
  base_path.start_with?('/') or return false
  # RFC 6265 5.1.4
  bsize = base_path.size
  tsize = target_path.size
  return bsize == 1 if tsize == 0 # treat empty target_path as "/"
  return false unless target_path.start_with?(base_path)
  return true if bsize == tsize || base_path.end_with?('/')
  target_path[bsize] == ?/
end

Instance Method Details

#<=>(other) ⇒ Object

Compares the cookie with another. When there are many cookies with the same name for a URL, the value of the smallest must be used.



635
636
637
638
639
640
641
642
# File 'lib/http/cookie.rb', line 635

def <=> other
  # RFC 6265 5.4
  # Precedence: 1. longer path  2. older creation
  (@name <=> other.name).nonzero? ||
    (other.path.length <=> @path.length).nonzero? ||
    (@created_at <=> other.created_at).nonzero? ||
    @value <=> other.value
end

#acceptable?Boolean

Tests if it is OK to accept this cookie considering its origin. If either domain or path is missing, raises ArgumentError. If origin is missing, returns true.

Returns:

  • (Boolean)


561
562
563
564
565
566
567
568
569
570
571
572
# File 'lib/http/cookie.rb', line 561

def acceptable?
  case
  when @domain.nil?
    raise "domain is missing"
  when @path.nil?
    raise "path is missing"
  when @origin.nil?
    true
  else
    acceptable_from_uri?(@origin)
  end
end

#acceptable_from_uri?(uri) ⇒ Boolean

Tests if it is OK to accept this cookie if it is sent from a given URI/URL, ‘uri`.

Returns:

  • (Boolean)


542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/http/cookie.rb', line 542

def acceptable_from_uri?(uri)
  uri = URI(uri)
  return false unless URI::HTTP === uri && uri.host
  host = DomainName.new(uri.host)

  # RFC 6265 5.3
  case
  when host.hostname == @domain
    true
  when @for_domain  # !host-only-flag
    host.cookie_domain?(@domain_name)
  else
    @domain.nil?
  end
end

Returns a string for use in the Cookie header, i.e. ‘name=value` or `name=“value”`.



588
589
590
# File 'lib/http/cookie.rb', line 588

def cookie_value
  "#{@name}=#{Scanner.quote(@value)}"
end

#dot_domainObject

Returns the domain, with a dot prefixed only if the domain flag is on.



419
420
421
# File 'lib/http/cookie.rb', line 419

def dot_domain
  @for_domain ? '.' << @domain : @domain
end

#encode_with(coder) ⇒ Object

YAML serialization helper for Psych.



651
652
653
654
655
# File 'lib/http/cookie.rb', line 651

def encode_with(coder)
  PERSISTENT_PROPERTIES.each { |key|
    coder[key.to_s] = instance_variable_get(:"@#{key}")
  }
end

#expire!Object

Expires this cookie by setting the expires attribute value to a past date.



528
529
530
531
# File 'lib/http/cookie.rb', line 528

def expire!
  self.expires = UNIX_EPOCH
  self
end

#expired?(time = Time.now) ⇒ Boolean

Tests if this cookie is expired by now, or by a given time.

Returns:

  • (Boolean)


518
519
520
521
522
523
524
# File 'lib/http/cookie.rb', line 518

def expired?(time = Time.now)
  if expires = self.expires
    expires <= time
  else
    false
  end
end

#expiresObject Also known as: expires_at



477
478
479
# File 'lib/http/cookie.rb', line 477

def expires
  @expires or @created_at && @max_age ? @created_at + @max_age : nil
end

#expires=(t) ⇒ Object Also known as: expires_at=

See #expires.



482
483
484
485
486
487
488
489
490
491
# File 'lib/http/cookie.rb', line 482

def expires= t
  case t
  when nil, Time
  else
    t = Time.parse(t)
  end
  @max_age = nil
  @session = t.nil?
  @expires = t
end

#init_with(coder) ⇒ Object

YAML deserialization helper for Syck.



658
659
660
# File 'lib/http/cookie.rb', line 658

def init_with(coder)
  yaml_initialize(coder.tag, coder.map)
end

#inspectObject



627
628
629
630
631
# File 'lib/http/cookie.rb', line 627

def inspect
  '#<%s:' % self.class << PERSISTENT_PROPERTIES.map { |key|
    '%s=%s' % [key, instance_variable_get(:"@#{key}").inspect]
  }.join(', ') << ' origin=%s>' % [@origin ? @origin.to_s : 'nil']
end

Returns a string for use in the Set-Cookie header. If necessary information like a path or domain (when ‘for_domain` is set) is missing, RuntimeError is raised. It is always the best to set an origin before calling this method.



597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
# File 'lib/http/cookie.rb', line 597

def set_cookie_value
  string = cookie_value
  if @for_domain
    if @domain
      string << "; Domain=#{@domain}"
    else
      raise "for_domain is specified but domain is known"
    end
  end
  if @path
    if !@origin || (@origin + './').path != @path
      string << "; Path=#{@path}"
    end
  else
    raise "path is known"
  end
  if @max_age
    string << "; Max-Age=#{@max_age}"
  elsif @expires
    string << "; Expires=#{@expires.httpdate}"
  end
  if @httponly
    string << "; HttpOnly"
  end
  if @secure
    string << "; Secure"
  end
  string
end

#to_yaml_propertiesObject

YAML serialization helper for Syck.



646
647
648
# File 'lib/http/cookie.rb', line 646

def to_yaml_properties
  PERSISTENT_PROPERTIES.map { |name| "@#{name}" }
end

#valid_for_uri?(uri) ⇒ Boolean

Tests if it is OK to send this cookie to a given ‘uri`. A RuntimeError is raised if the cookie’s domain is unknown.

Returns:

  • (Boolean)


576
577
578
579
580
581
582
583
584
# File 'lib/http/cookie.rb', line 576

def valid_for_uri?(uri)
  if @domain.nil?
    raise "cannot tell if this cookie is valid because the domain is unknown"
  end
  uri = URI(uri)
  # RFC 6265 5.4
  return false if secure? && !(URI::HTTPS === uri)
  acceptable_from_uri?(uri) && HTTP::Cookie.path_match?(@path, uri.path)
end

#yaml_initialize(tag, map) ⇒ Object

YAML deserialization helper for Psych.



663
664
665
666
667
668
669
670
671
672
673
674
675
676
# File 'lib/http/cookie.rb', line 663

def yaml_initialize(tag, map)
  expires = nil
  @origin = nil
  map.each { |key, value|
    case key
    when 'expires'
      # avoid clobbering max_age
      expires = value
    when *PERSISTENT_PROPERTIES
      __send__(:"#{key}=", value)
    end
  }
  self.expires = expires if self.max_age.nil?
end