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

Modules: URIParser 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.8"

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')


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
208
209
210
# File 'lib/http/cookie.rb', line 133

def initialize(*args)
  @name = @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.



551
552
553
# File 'lib/http/cookie.rb', line 551

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.



548
549
550
# File 'lib/http/cookie.rb', line 548

def created_at
  @created_at
end

#domainObject

Returns the value of attribute domain.



386
387
388
# File 'lib/http/cookie.rb', line 386

def domain
  @domain
end

#domain_nameObject (readonly)

Returns the domain attribute value as a DomainName object.



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

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.



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

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.



479
480
481
# File 'lib/http/cookie.rb', line 479

def httponly
  @httponly
end

#max_ageObject

Returns the value of attribute max_age.



509
510
511
# File 'lib/http/cookie.rb', line 509

def max_age
  @max_age
end

#nameObject

Returns the value of attribute name.



350
351
352
# File 'lib/http/cookie.rb', line 350

def name
  @name
end

#originObject

Returns the value of attribute origin.



453
454
455
# File 'lib/http/cookie.rb', line 453

def origin
  @origin
end

#pathObject

Returns the value of attribute path.



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

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.



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

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.



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

def session
  @session
end

#valueObject

Returns the value of attribute value.



367
368
369
# File 'lib/http/cookie.rb', line 367

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



334
335
336
# File 'lib/http/cookie.rb', line 334

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.



341
342
343
344
345
346
347
# File 'lib/http/cookie.rb', line 341

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.



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
328
329
330
# File 'lib/http/cookie.rb', line 276

def parse(set_cookie, origin, options = nil, &block)
  if options
    logger = options[:logger]
    created_at = options[:created_at]
  end
  origin = HTTP::Cookie::URIParser.parse(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

Returns:

  • (Boolean)


232
233
234
235
236
237
238
239
240
241
# File 'lib/http/cookie.rb', line 232

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.



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

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)


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

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)


555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
# File 'lib/http/cookie.rb', line 555

def acceptable_from_uri?(uri)
  uri = HTTP::Cookie::URIParser.parse(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”`.



601
602
603
# File 'lib/http/cookie.rb', line 601

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

#dot_domainObject

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



429
430
431
# File 'lib/http/cookie.rb', line 429

def dot_domain
  @for_domain ? (+'.') << @domain : @domain
end

#encode_with(coder) ⇒ Object

YAML serialization helper for Psych.



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

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.



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

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)


531
532
533
534
535
536
537
# File 'lib/http/cookie.rb', line 531

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

#expiresObject Also known as: expires_at



488
489
490
# File 'lib/http/cookie.rb', line 488

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

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

See #expires.



493
494
495
496
497
498
499
500
501
502
503
504
# File 'lib/http/cookie.rb', line 493

def expires= t
  case t
  when nil, Time
  when DateTime
    t = t.to_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.



671
672
673
# File 'lib/http/cookie.rb', line 671

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

#inspectObject



640
641
642
643
644
# File 'lib/http/cookie.rb', line 640

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.



610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
# File 'lib/http/cookie.rb', line 610

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.



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

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)


589
590
591
592
593
594
595
596
597
# File 'lib/http/cookie.rb', line 589

def valid_for_uri?(uri)
  if @domain.nil?
    raise "cannot tell if this cookie is valid because the domain is unknown"
  end
  uri = HTTP::Cookie::URIParser.parse(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.



676
677
678
679
680
681
682
683
684
685
686
687
688
689
# File 'lib/http/cookie.rb', line 676

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