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.2"

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 unless max_age
    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.


546
547
548
# File 'lib/http/cookie.rb', line 546

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.


543
544
545
# File 'lib/http/cookie.rb', line 543

def created_at
  @created_at
end

#domainObject

Returns the value of attribute domain


383
384
385
# File 'lib/http/cookie.rb', line 383

def domain
  @domain
end

#domain_nameObject (readonly)

Returns the domain attribute value as a DomainName object.


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

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.


438
439
440
# File 'lib/http/cookie.rb', line 438

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.


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

def httponly
  @httponly
end

#max_ageObject

Returns the value of attribute max_age


504
505
506
# File 'lib/http/cookie.rb', line 504

def max_age
  @max_age
end

#nameObject

Returns the value of attribute name


347
348
349
# File 'lib/http/cookie.rb', line 347

def name
  @name
end

#originObject

Returns the value of attribute origin


450
451
452
# File 'lib/http/cookie.rb', line 450

def origin
  @origin
end

#pathObject

Returns the value of attribute path


441
442
443
# File 'lib/http/cookie.rb', line 441

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.


470
471
472
# File 'lib/http/cookie.rb', line 470

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.


482
483
484
# File 'lib/http/cookie.rb', line 482

def session
  @session
end

#valueObject

Returns the value of attribute value


364
365
366
# File 'lib/http/cookie.rb', line 364

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”.


331
332
333
# File 'lib/http/cookie.rb', line 331

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.


338
339
340
341
342
343
344
# File 'lib/http/cookie.rb', line 338

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
321
322
323
324
325
326
327
# 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?

      begin
        cookie = new(name, value)
      rescue => e
        logger.warn("Invalid name or value: #{e}") if logger
        next
      end
      cookie.created_at = created_at if created_at
      attrs.each { |aname, avalue|
        begin
          case aname
          when 'domain'
            cookie.for_domain = true
            # The following may negate @for_domain if the value is
            # an eTLD or IP address, hence this order.
            cookie.domain = avalue
          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

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.


643
644
645
646
647
648
649
650
# File 'lib/http/cookie.rb', line 643

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.


569
570
571
572
573
574
575
576
577
578
579
580
# File 'lib/http/cookie.rb', line 569

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`.


550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
# File 'lib/http/cookie.rb', line 550

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”`.


596
597
598
# File 'lib/http/cookie.rb', line 596

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

#dot_domainObject

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


426
427
428
# File 'lib/http/cookie.rb', line 426

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

#encode_with(coder) ⇒ Object

YAML serialization helper for Psych.


659
660
661
662
663
# File 'lib/http/cookie.rb', line 659

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.


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

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.


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

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

#expiresObject Also known as: expires_at


485
486
487
# File 'lib/http/cookie.rb', line 485

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

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

See #expires.


490
491
492
493
494
495
496
497
498
499
# File 'lib/http/cookie.rb', line 490

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.


666
667
668
# File 'lib/http/cookie.rb', line 666

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

#inspectObject


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

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.


605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
# File 'lib/http/cookie.rb', line 605

def set_cookie_value
  string = cookie_value
  if @for_domain
    if @domain
      string << "; Domain=#{@domain}"
    else
      raise "for_domain is specified but domain is unknown"
    end
  end
  if @path
    if !@origin || (@origin + './').path != @path
      string << "; Path=#{@path}"
    end
  else
    raise "path is unknown"
  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.


654
655
656
# File 'lib/http/cookie.rb', line 654

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.


584
585
586
587
588
589
590
591
592
# File 'lib/http/cookie.rb', line 584

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.


671
672
673
674
675
676
677
678
679
680
681
682
683
684
# File 'lib/http/cookie.rb', line 671

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