pax_global_header00006660000000000000000000000064134136450200014510gustar00rootroot0000000000000052 comment=2daa161d7eed62a61c7270fc8e547e56acb0ea18 ruby-http-cookie-1.0.3/000077500000000000000000000000001341364502000147165ustar00rootroot00000000000000ruby-http-cookie-1.0.3/.gitignore000066400000000000000000000002321341364502000167030ustar00rootroot00000000000000*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp ruby-http-cookie-1.0.3/.travis.yml000066400000000000000000000004101341364502000170220ustar00rootroot00000000000000sudo: false language: ruby cache: bundler rvm: - 1.8.7 - ree - 1.9.3 - 2.0.0 - 2.1 - 2.2 - 2.3.0 - ruby-head - jruby-1.7 - jruby-9 - rbx-2 matrix: allow_failures: - rvm: ruby-head - rvm: rbx-2 before_install: - gem update bundler ruby-http-cookie-1.0.3/CHANGELOG.md000066400000000000000000000011711341364502000165270ustar00rootroot00000000000000## 1.0.3 (2016-09-30) - Treat comma as normal character in HTTP::Cookie.cookie_value_to_hash instead of key-value pair separator. This should fix the problem described in CVE-2016-7401. ## 1.0.2 (2013-09-10) - Fix HTTP::Cookie.parse so that it does not raise ArgumentError when it finds a bad name or value that is parsable but considered invalid. ## 1.0.1 (2013-04-21) - Minor error handling improvements and documentation updates. - Argument error regarding specifying store/saver classes no longer raises IndexError, but either ArgumentError or TypeError. ## 1.0.0 (2013-04-17) - Initial Release. ruby-http-cookie-1.0.3/Gemfile000066400000000000000000000001401341364502000162040ustar00rootroot00000000000000source 'https://rubygems.org' # Specify your gem's dependencies in http-cookie.gemspec gemspec ruby-http-cookie-1.0.3/LICENSE.txt000066400000000000000000000022271341364502000165440ustar00rootroot00000000000000Copyright (c) 2013 Akinori MUSHA Copyright (c) 2011-2012 Akinori MUSHA, Eric Hodel Copyright (c) 2006-2011 Aaron Patterson, Mike Dalessio MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ruby-http-cookie-1.0.3/README.md000066400000000000000000000156041341364502000162030ustar00rootroot00000000000000# HTTP::Cookie HTTP::Cookie is a ruby library to handle HTTP cookies in a way both compliant with RFCs and compatible with today's major browsers. It was originally a part of the [Mechanize](https://github.com/sparklemotion/mechanize) library, separated as an independent library in the hope of serving as a common component that is reusable from any HTTP related piece of software. The following is an incomplete list of its features: * Its behavior is highly compatible with that of today's major web browsers. * It is based on and conforms to RFC 6265 (the latest standard for the HTTP cookie mechanism) to a high extent, with real world conventions deeply in mind. * It takes eTLD (effective TLD, also known as "Public Suffix") into account just as major browsers do, to reject cookies with an eTLD domain like "org", "co.jp", or "appspot.com". This feature is brought to you by the domain_name gem. * The number of cookies and the size are properly capped so that a cookie store does not get flooded. * It supports the legacy Netscape cookies.txt format for serialization, maximizing the interoperability with other implementations. * It supports the cookies.sqlite format adopted by Mozilla Firefox for backend store database which can be shared among multiple program instances. * It is relatively easy to add a new serialization format or a backend store because of its modular API. ## Installation Add this line to your application's `Gemfile`: gem 'http-cookie' And then execute: $ bundle Or install it yourself as: $ gem install http-cookie ## Usage ######################## # Client side example 1 ######################## # Initialize a cookie jar jar = HTTP::CookieJar.new # Load from a file jar.load(filename) if File.exist?(filename) # Store received cookies, where uri is the origin of this header header["Set-Cookie"].each { |value| jar.parse(value, uri) } # ... # Set the Cookie header value, where uri is the destination URI header["Cookie"] = HTTP::Cookie.cookie_value(jar.cookies(uri)) # Save to a file jar.save(filename) ######################## # Client side example 2 ######################## # Initialize a cookie jar using a Mozilla compatible SQLite3 backend jar = HTTP::CookieJar.new(store: :mozilla, filename: 'cookies.sqlite') # There is no need for load & save in this backend. # Store received cookies, where uri is the origin of this header header["Set-Cookie"].each { |value| jar.parse(value, uri) } # ... # Set the Cookie header value, where uri is the destination URI header["Cookie"] = HTTP::Cookie.cookie_value(jar.cookies(uri)) ######################## # Server side example ######################## # Generate a domain cookie cookie1 = HTTP::Cookie.new("uid", "u12345", domain: 'example.org', for_domain: true, path: '/', max_age: 7*86400) # Add it to the Set-Cookie response header header['Set-Cookie'] = cookie1.set_cookie_value # Generate a host-only cookie cookie2 = HTTP::Cookie.new("aid", "a12345", origin: my_url, path: '/', max_age: 7*86400) # Add it to the Set-Cookie response header header['Set-Cookie'] = cookie2.set_cookie_value ## Incompatibilities with Mechanize::Cookie/CookieJar There are several incompatibilities between Mechanize::Cookie/CookieJar and HTTP::Cookie/CookieJar. Below is how to rewrite existing code written for Mechanize::Cookie with equivalent using HTTP::Cookie: - Mechanize::Cookie.parse The parameter order changed in HTTP::Cookie.parse. # before cookies1 = Mechanize::Cookie.parse(uri, set_cookie1) cookies2 = Mechanize::Cookie.parse(uri, set_cookie2, log) # after cookies1 = HTTP::Cookie.parse(set_cookie1, uri_or_url) cookies2 = HTTP::Cookie.parse(set_cookie2, uri_or_url, logger: log) # or you can directly store parsed cookies in your jar jar.parse(set_cookie1, uri_or_url) jar.parse(set_cookie1, uri_or_url, logger: log) - Mechanize::Cookie#version, #version= There is no longer a sense of version in the HTTP cookie specification. The only version number ever defined was zero, and there will be no other version defined since the version attribute has been removed in RFC 6265. - Mechanize::Cookie#comment, #comment= Ditto. The comment attribute has been removed in RFC 6265. - Mechanize::Cookie#set_domain This method was unintentionally made public. Simply use HTTP::Cookie#domain=. # before cookie.set_domain(domain) # after cookie.domain = domain - Mechanize::CookieJar#add, #add! Always use HTTP::CookieJar#add. # before jar.add!(cookie1) jar.add(uri, cookie2) # after jar.add(cookie1) cookie2.origin = uri; jar.add(cookie2) # or specify origin in parse() or new() - Mechanize::CookieJar#clear! Use HTTP::Cookiejar#clear. # before jar.clear! # after jar.clear - Mechanize::CookieJar#save_as Use HTTP::CookieJar#save. # before jar.save_as(file) # after jar.save(file) - Mechanize::CookieJar#jar There is no direct access to the internal hash in HTTP::CookieJar since it has introduced an abstract store layer. If you want to tweak the internals of the hash store, try creating a new store class referring to the default store class HTTP::CookieJar::HashStore. If you desperately need it you can access it by `jar.store.instance_variable_get(:@jar)`, but there is no guarantee that it will remain available in the future. HTTP::Cookie/CookieJar raise runtime errors to help migration, so after replacing the class names, try running your test code once to find out how to fix your code base. ### File formats The YAML serialization format has changed, and HTTP::CookieJar#load cannot import what is written in a YAML file saved by Mechanize::CookieJar#save_as. HTTP::CookieJar#load will not raise an exception if an incompatible YAML file is given, but the content is silently ignored. Note that there is (obviously) no forward compatibillity with this. Trying to load a YAML file saved by HTTP::CookieJar with Mechanize::CookieJar will fail in runtime error. On the other hand, there has been (and will ever be) no change in the cookies.txt format, so use it instead if compatibility is significant. ## Contributing 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request ruby-http-cookie-1.0.3/Rakefile000066400000000000000000000010051341364502000163570ustar00rootroot00000000000000require 'bundler/gem_tasks' require 'rake/testtask' Rake::TestTask.new(:test) do |test| test.ruby_opts << '-r./test/simplecov_start.rb' if RUBY_VERSION >= '1.9' test.pattern = 'test/**/test_*.rb' test.verbose = true end task :default => :test require 'rdoc/task' Rake::RDocTask.new do |rdoc| version = HTTP::Cookie::VERSION rdoc.rdoc_dir = 'rdoc' rdoc.title = "http-cookie #{version}" rdoc.rdoc_files.include('lib/**/*.rb') rdoc.rdoc_files.include(Bundler::GemHelper.gemspec.extra_rdoc_files) end ruby-http-cookie-1.0.3/http-cookie.gemspec000066400000000000000000000035461341364502000205210ustar00rootroot00000000000000# -*- encoding: utf-8 -*- lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'http/cookie/version' Gem::Specification.new do |gem| gem.name = "http-cookie" gem.version = HTTP::Cookie::VERSION gem.authors, gem.email = { 'Akinori MUSHA' => 'knu@idaemons.org', 'Aaron Patterson' => 'aaronp@rubyforge.org', 'Eric Hodel' => 'drbrain@segment7.net', 'Mike Dalessio' => 'mike.dalessio@gmail.com', }.instance_eval { [keys, values] } gem.description = %q{HTTP::Cookie is a Ruby library to handle HTTP Cookies based on RFC 6265. It has with security, standards compliance and compatibility in mind, to behave just the same as today's major web browsers. It has builtin support for the legacy cookies.txt and the latest cookies.sqlite formats of Mozilla Firefox, and its modular API makes it easy to add support for a new backend store.} gem.summary = %q{A Ruby library to handle HTTP Cookies based on RFC 6265} gem.homepage = "https://github.com/sparklemotion/http-cookie" gem.license = "MIT" gem.files = `git ls-files`.split($/) gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.extra_rdoc_files = ['README.md', 'LICENSE.txt'] gem.add_runtime_dependency("domain_name", ["~> 0.5"]) gem.add_development_dependency("sqlite3", ["~> 1.3.3"]) unless defined?(JRUBY_VERSION) gem.add_development_dependency("bundler", [">= 1.2.0"]) gem.add_development_dependency("test-unit", [">= 2.4.3", *("< 3" if RUBY_VERSION < "1.9")]) gem.add_development_dependency("rake", [">= 0.9.2.2", *("< 11" if RUBY_VERSION < "1.9")]) gem.add_development_dependency("rdoc", ["> 2.4.2"]) gem.add_development_dependency("simplecov", [">= 0"]) end ruby-http-cookie-1.0.3/lib/000077500000000000000000000000001341364502000154645ustar00rootroot00000000000000ruby-http-cookie-1.0.3/lib/http-cookie.rb000066400000000000000000000000261341364502000202350ustar00rootroot00000000000000require 'http/cookie' ruby-http-cookie-1.0.3/lib/http/000077500000000000000000000000001341364502000164435ustar00rootroot00000000000000ruby-http-cookie-1.0.3/lib/http/cookie.rb000066400000000000000000000464651341364502000202600ustar00rootroot00000000000000# :markup: markdown require 'http/cookie/version' require 'time' require 'uri' require 'domain_name' require 'http/cookie/ruby_compat' module HTTP autoload :CookieJar, 'http/cookie_jar' end # This class is used to represent an HTTP Cookie. class HTTP::Cookie # Maximum number of bytes per cookie (RFC 6265 6.1 requires 4096 at # least) MAX_LENGTH = 4096 # Maximum number of cookies per domain (RFC 6265 6.1 requires 50 at # least) MAX_COOKIES_PER_DOMAIN = 50 # Maximum number of cookies total (RFC 6265 6.1 requires 3000 at # least) MAX_COOKIES_TOTAL = 3000 # :stopdoc: UNIX_EPOCH = Time.at(0) PERSISTENT_PROPERTIES = %w[ name value domain for_domain path secure httponly expires max_age created_at accessed_at ] # :startdoc: # The cookie name. It may not be nil or empty. # # Assign a string containing any of the following characters will # raise ArgumentError: control characters (`\x00-\x1F` and `\x7F`), # space and separators `,;\"=`. # # Note that RFC 6265 4.1.1 lists more characters disallowed for use # in a cookie name, which are these: `<>@:/[]?{}`. Using these # characters will reduce interoperability. # # :attr_accessor: name # The cookie value. # # Assign a string containing a control character (`\x00-\x1F` and # `\x7F`) will raise ArgumentError. # # Assigning nil sets the value to an empty string and the expiration # date to the Unix epoch. This is a handy way to make a cookie for # expiration. # # Note that RFC 6265 4.1.1 lists more characters disallowed for use # in a cookie value, which are these: ` ",;\`. Using these # characters will reduce interoperability. # # :attr_accessor: value # The cookie domain. # # Setting a domain with a leading dot implies that the #for_domain # flag should be turned on. The setter accepts a DomainName object # as well as a string-like. # # :attr_accessor: domain # The path attribute value. # # The setter treats an empty path ("") as the root path ("/"). # # :attr_accessor: path # The origin of the cookie. # # Setting this will initialize the #domain and #path attribute # values if unknown yet. If the cookie already has a domain value # set, it is checked against the origin URL to see if the origin is # allowed to issue a cookie of the domain, and ArgumentError is # raised if the check fails. # # :attr_accessor: origin # The Expires attribute value as a Time object. # # The setter method accepts a Time object, a string representation # of date/time that Time.parse can understand, or `nil`. # # Setting this value resets #max_age to nil. When #max_age is # non-nil, #expires returns `created_at + max_age`. # # :attr_accessor: expires # The Max-Age attribute value as an integer, the number of seconds # before expiration. # # The setter method accepts an integer, or a string-like that # represents an integer which will be stringified and then # integerized using #to_i. # # This value is reset to nil when #expires= is called. # # :attr_accessor: max_age # :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') # 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 autoload :Scanner, 'http/cookie/scanner' class << self # 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 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 # 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. 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 # Takes an array of cookies and returns a string for use in the # Cookie header, like "name1=value2; name2=value2". 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. def cookie_value_to_hash(cookie_value) {}.tap { |hash| Scanner.new(cookie_value).scan_cookie { |name, value| hash[name] ||= value } } end end attr_reader :name # See #name. def name= name name = (String.try_convert(name) or raise TypeError, "#{name.class} is not a String") if name.empty? raise ArgumentError, "cookie name cannot be empty" elsif name.match(/[\x00-\x20\x7F,;\\"=]/) raise ArgumentError, "invalid cookie name" end # RFC 6265 4.1.1 # cookie-name may not match: # /[\x00-\x20\x7F()<>@,;:\\"\/\[\]?={}]/ @name = name end attr_reader :value # See #value. def value= value if value.nil? self.expires = UNIX_EPOCH return @value = '' end value = (String.try_convert(value) or raise TypeError, "#{value.class} is not a String") if value.match(/[\x00-\x1F\x7F]/) raise ArgumentError, "invalid cookie value" end # RFC 6265 4.1.1 # cookie-name may not match: # /[^\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/ @value = value end attr_reader :domain # See #domain. def domain= domain case domain when nil @for_domain = false if @origin @domain_name = DomainName.new(@origin.host) @domain = @domain_name.hostname else @domain_name = @domain = nil end return nil when DomainName @domain_name = domain else domain = (String.try_convert(domain) or raise TypeError, "#{domain.class} is not a String") if domain.start_with?('.') for_domain = true domain = domain[1..-1] end if domain.empty? return self.domain = nil end # Do we really need to support this? if domain.match(/\A([^:]+):[0-9]+\z/) domain = $1 end @domain_name = DomainName.new(domain) end # RFC 6265 5.3 5. if domain_name.domain.nil? # a public suffix or IP address @for_domain = false else @for_domain = for_domain unless for_domain.nil? end @domain = @domain_name.hostname end # Returns the domain, with a dot prefixed only if the domain flag is # on. def dot_domain @for_domain ? '.' << @domain : @domain end # Returns the domain attribute value as a DomainName object. attr_reader :domain_name # 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. attr_accessor :for_domain alias for_domain? for_domain attr_reader :path # See #path. def path= path path = (String.try_convert(path) or raise TypeError, "#{path.class} is not a String") @path = path.start_with?('/') ? path : '/' end attr_reader :origin # See #origin. def origin= origin return origin if origin == @origin @origin.nil? or raise ArgumentError, "origin cannot be changed once it is set" # Delay setting @origin because #domain= or #path= may fail origin = URI(origin) if URI::HTTP === origin self.domain ||= origin.host self.path ||= (origin + './').path end @origin = origin end # The secure flag. (secure-only-flag) # # A cookie with this flag on should only be sent via a secure # protocol like HTTPS. attr_accessor :secure alias secure? secure # The HttpOnly flag. (http-only-flag) # # A cookie with this flag on should be hidden from a client script. attr_accessor :httponly alias httponly? httponly # The session flag. (the opposite of persistent-flag) # # A cookie with this flag on should be hidden from a client script. attr_reader :session alias session? session def expires @expires or @created_at && @max_age ? @created_at + @max_age : nil end # See #expires. def expires= t case t when nil, Time else t = Time.parse(t) end @max_age = nil @session = t.nil? @expires = t end alias expires_at expires alias expires_at= expires= attr_reader :max_age # See #max_age. def max_age= sec case sec when Integer, nil else str = String.try_convert(sec) or raise TypeError, "#{sec.class} is not an Integer or String" /\A-?\d+\z/.match(str) or raise ArgumentError, "invalid Max-Age: #{sec.inspect}" sec = str.to_i end @expires = nil if @session = sec.nil? @max_age = nil else @max_age = sec end end # Tests if this cookie is expired by now, or by a given time. def expired?(time = Time.now) if expires = self.expires expires <= time else false end end # Expires this cookie by setting the expires attribute value to a # past date. def expire! self.expires = UNIX_EPOCH self end # The time this cookie was created at. This value is used as a base # date for interpreting the Max-Age attribute value. See #expires. attr_accessor :created_at # The time this cookie was last accessed at. attr_accessor :accessed_at # Tests if it is OK to accept this cookie if it is sent from a given # URI/URL, `uri`. 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 # 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. 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 # Tests if it is OK to send this cookie to a given `uri`. A # RuntimeError is raised if the cookie's domain is unknown. 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 # Returns a string for use in the Cookie header, i.e. `name=value` # or `name="value"`. def cookie_value "#{@name}=#{Scanner.quote(@value)}" end alias to_s cookie_value # 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. 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 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 # 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. 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 include Comparable # YAML serialization helper for Syck. def to_yaml_properties PERSISTENT_PROPERTIES.map { |name| "@#{name}" } end # YAML serialization helper for Psych. def encode_with(coder) PERSISTENT_PROPERTIES.each { |key| coder[key.to_s] = instance_variable_get(:"@#{key}") } end # YAML deserialization helper for Syck. def init_with(coder) yaml_initialize(coder.tag, coder.map) end # YAML deserialization helper for Psych. 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 end ruby-http-cookie-1.0.3/lib/http/cookie/000077500000000000000000000000001341364502000177145ustar00rootroot00000000000000ruby-http-cookie-1.0.3/lib/http/cookie/ruby_compat.rb000066400000000000000000000023551341364502000225720ustar00rootroot00000000000000class Array def select! # :yield: x i = 0 each_with_index { |x, j| yield x or next self[i] = x if i != j i += 1 } return nil if i == size self[i..-1] = [] self end unless method_defined?(:select!) def sort_by!(&block) # :yield: x replace(sort_by(&block)) end unless method_defined?(:sort_by!) end class Hash class << self def try_convert(object) if object.is_a?(Hash) || (object.respond_to?(:to_hash) && (object = object.to_hash).is_a?(Hash)) object else nil end end unless method_defined?(:try_convert) end end class String class << self def try_convert(object) if object.is_a?(String) || (object.respond_to?(:to_str) && (object = object.to_str).is_a?(String)) object else nil end end unless method_defined?(:try_convert) end end # In Ruby < 1.9.3 URI() does not accept a URI object. if RUBY_VERSION < "1.9.3" require 'uri' begin URI(URI('')) rescue def URI(url) # :nodoc: case url when URI url when String URI.parse(url) else raise ArgumentError, 'bad argument (expected URI object or URI string)' end end end end ruby-http-cookie-1.0.3/lib/http/cookie/scanner.rb000066400000000000000000000124251341364502000216760ustar00rootroot00000000000000require 'http/cookie' require 'strscan' require 'time' class HTTP::Cookie::Scanner < StringScanner # Whitespace. RE_WSP = /[ \t]+/ # A pattern that matches a cookie name or attribute name which may # be empty, capturing trailing whitespace. RE_NAME = /(?!#{RE_WSP})[^,;\\"=]*/ RE_BAD_CHAR = /([\x00-\x20\x7F",;\\])/ # A pattern that matches the comma in a (typically date) value. RE_COOKIE_COMMA = /,(?=#{RE_WSP}?#{RE_NAME}=)/ def initialize(string, logger = nil) @logger = logger super(string) end class << self def quote(s) return s unless s.match(RE_BAD_CHAR) '"' << s.gsub(/([\\"])/, "\\\\\\1") << '"' end end def skip_wsp skip(RE_WSP) end def scan_dquoted ''.tap { |s| case when skip(/"/) break when skip(/\\/) s << getch when scan(/[^"\\]+/) s << matched end until eos? } end def scan_name scan(RE_NAME).tap { |s| s.rstrip! if s } end def scan_value(comma_as_separator = false) ''.tap { |s| case when scan(/[^,;"]+/) s << matched when skip(/"/) # RFC 6265 2.2 # A cookie-value may be DQUOTE'd. s << scan_dquoted when check(/;/) break when comma_as_separator && check(RE_COOKIE_COMMA) break else s << getch end until eos? s.rstrip! } end def scan_name_value(comma_as_separator = false) name = scan_name if skip(/\=/) value = scan_value(comma_as_separator) else scan_value(comma_as_separator) value = nil end [name, value] end if Time.respond_to?(:strptime) def tuple_to_time(day_of_month, month, year, time) Time.strptime( '%02d %s %04d %02d:%02d:%02d UTC' % [day_of_month, month, year, *time], '%d %b %Y %T %Z' ).tap { |date| date.day == day_of_month or return nil } end else def tuple_to_time(day_of_month, month, year, time) Time.parse( '%02d %s %04d %02d:%02d:%02d UTC' % [day_of_month, month, year, *time] ).tap { |date| date.day == day_of_month or return nil } end end private :tuple_to_time def parse_cookie_date(s) # RFC 6265 5.1.1 time = day_of_month = month = year = nil s.split(/[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]+/).each { |token| case when time.nil? && token.match(/\A(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?=\D|\z)/) sec = if $3 $3.to_i else # violation of the RFC @logger.warn("Time lacks the second part: #{token}") if @logger 0 end time = [$1.to_i, $2.to_i, sec] when day_of_month.nil? && token.match(/\A(\d{1,2})(?=\D|\z)/) day_of_month = $1.to_i when month.nil? && token.match(/\A(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i) month = $1.capitalize when year.nil? && token.match(/\A(\d{2,4})(?=\D|\z)/) year = $1.to_i end } if day_of_month.nil? || month.nil? || year.nil? || time.nil? return nil end case day_of_month when 1..31 else return nil end case year when 100..1600 return nil when 70..99 year += 1900 when 0..69 year += 2000 end hh, mm, ss = time if hh > 23 || mm > 59 || ss > 59 return nil end tuple_to_time(day_of_month, month, year, time) end def scan_set_cookie # RFC 6265 4.1.1 & 5.2 until eos? start = pos len = nil skip_wsp name, value = scan_name_value(true) if value.nil? @logger.warn("Cookie definition lacks a name-value pair.") if @logger elsif name.empty? @logger.warn("Cookie definition has an empty name.") if @logger value = nil end attrs = {} case when skip(/,/) # The comma is used as separator for concatenating multiple # values of a header. len = (pos - 1) - start break when skip(/;/) skip_wsp aname, avalue = scan_name_value(true) next if aname.empty? || value.nil? aname.downcase! case aname when 'expires' # RFC 6265 5.2.1 avalue &&= parse_cookie_date(avalue) or next when 'max-age' # RFC 6265 5.2.2 next unless /\A-?\d+\z/.match(avalue) when 'domain' # RFC 6265 5.2.3 # An empty value SHOULD be ignored. next if avalue.nil? || avalue.empty? when 'path' # RFC 6265 5.2.4 # A relative path must be ignored rather than normalizing it # to "/". next unless /\A\//.match(avalue) when 'secure', 'httponly' # RFC 6265 5.2.5, 5.2.6 avalue = true end attrs[aname] = avalue end until eos? len ||= pos - start if len > HTTP::Cookie::MAX_LENGTH @logger.warn("Cookie definition too long: #{name}") if @logger next end yield name, value, attrs if value end end def scan_cookie # RFC 6265 4.1.1 & 5.4 until eos? skip_wsp # Do not treat comma in a Cookie header value as separator; see CVE-2016-7401 name, value = scan_name_value(false) yield name, value if value skip(/;/) end end end ruby-http-cookie-1.0.3/lib/http/cookie/version.rb000066400000000000000000000000731341364502000217260ustar00rootroot00000000000000module HTTP class Cookie VERSION = "1.0.3" end end ruby-http-cookie-1.0.3/lib/http/cookie_jar.rb000066400000000000000000000231231341364502000210760ustar00rootroot00000000000000# :markup: markdown require 'http/cookie' ## # This class is used to manage the Cookies that have been returned from # any particular website. class HTTP::CookieJar class << self def const_missing(name) case name.to_s when /\A([A-Za-z]+)Store\z/ file = 'http/cookie_jar/%s_store' % $1.downcase when /\A([A-Za-z]+)Saver\z/ file = 'http/cookie_jar/%s_saver' % $1.downcase end begin require file rescue LoadError raise NameError, 'can\'t resolve constant %s; failed to load %s' % [name, file] end if const_defined?(name) const_get(name) else raise NameError, 'can\'t resolve constant %s after loading %s' % [name, file] end end end attr_reader :store def get_impl(base, value, *args) case value when base value when Symbol begin base.implementation(value).new(*args) rescue IndexError => e raise ArgumentError, e.message end when Class if base >= value value.new(*args) else raise TypeError, 'not a subclass of %s: %s' % [base, value] end else raise TypeError, 'invalid object: %s' % value.inspect end end private :get_impl # Generates a new cookie jar. # # Available option keywords are as below: # # :store # : The store class that backs this jar. (default: `:hash`) # A symbol addressing a store class, a store class, or an instance # of a store class is accepted. Symbols are mapped to store # classes, like `:hash` to HTTP::CookieJar::HashStore and `:mozilla` # to HTTP::CookieJar::MozillaStore. # # Any options given are passed through to the initializer of the # specified store class. For example, the `:mozilla` # (HTTP::CookieJar::MozillaStore) store class requires a `:filename` # option. See individual store classes for details. def initialize(options = nil) opthash = { :store => :hash, } opthash.update(options) if options @store = get_impl(AbstractStore, opthash[:store], opthash) end # The copy constructor. Not all backend store classes support cloning. def initialize_copy(other) @store = other.instance_eval { @store.dup } end # Adds a cookie to the jar if it is acceptable, and returns self in # any case. A given cookie must have domain and path attributes # set, or ArgumentError is raised. # # Whether a cookie with the `for_domain` flag on overwrites another # with the flag off or vice versa depends on the store used. See # individual store classes for that matter. # # ### Compatibility Note for Mechanize::Cookie users # # In HTTP::Cookie, each cookie object can store its origin URI # (cf. #origin). While the origin URI of a cookie can be set # manually by #origin=, one is typically given in its generation. # To be more specific, HTTP::Cookie.new takes an `:origin` option # and HTTP::Cookie.parse takes one via the second argument. # # # Mechanize::Cookie # jar.add(origin, cookie) # jar.add!(cookie) # no acceptance check is performed # # # HTTP::Cookie # jar.origin = origin # jar.add(cookie) # acceptance check is performed def add(cookie) @store.add(cookie) if begin cookie.acceptable? rescue RuntimeError => e raise ArgumentError, e.message end self end alias << add # Deletes a cookie that has the same name, domain and path as a # given cookie from the jar and returns self. # # How the `for_domain` flag value affects the set of deleted cookies # depends on the store used. See individual store classes for that # matter. def delete(cookie) @store.delete(cookie) self end # Gets an array of cookies sorted by the path and creation time. If # `url` is given, only ones that should be sent to the URL/URI are # selected, with the access time of each of them updated. def cookies(url = nil) each(url).sort end # Tests if the jar is empty. If `url` is given, tests if there is # no cookie for the URL. def empty?(url = nil) if url each(url) { return false } return true else @store.empty? end end # Iterates over all cookies that are not expired in no particular # order. # # An optional argument `uri` specifies a URI/URL indicating the # destination of the cookies being selected. Every cookie yielded # should be good to send to the given URI, # i.e. cookie.valid_for_uri?(uri) evaluates to true. # # If (and only if) the `uri` option is given, last access time of # each cookie is updated to the current time. def each(uri = nil, &block) # :yield: cookie block_given? or return enum_for(__method__, uri) if uri uri = URI(uri) return self unless URI::HTTP === uri && uri.host end @store.each(uri, &block) self end include Enumerable # Parses a Set-Cookie field value `set_cookie` assuming that it is # sent from a source URL/URI `origin`, and adds the cookies parsed # as valid and considered acceptable to the jar. Returns an array # of cookies that have been added. # # If a block is given, it is called for each cookie and the cookie # is added only if the block returns a true value. # # `jar.parse(set_cookie, origin)` is a shorthand for this: # # HTTP::Cookie.parse(set_cookie, origin) { |cookie| # jar.add(cookie) # } # # See HTTP::Cookie.parse for available options. def parse(set_cookie, origin, options = nil) # :yield: cookie if block_given? HTTP::Cookie.parse(set_cookie, origin, options).tap { |cookies| cookies.select! { |cookie| yield(cookie) && add(cookie) } } else HTTP::Cookie.parse(set_cookie, origin, options) { |cookie| add(cookie) } end end # call-seq: # jar.save(filename_or_io, **options) # jar.save(filename_or_io, format = :yaml, **options) # # Saves the cookie jar into a file or an IO in the format specified # and returns self. If a given object responds to #write it is # taken as an IO, or taken as a filename otherwise. # # Available option keywords are below: # # * `:format` # # Specifies the format for saving. A saver class, a symbol # addressing a saver class, or a pre-generated instance of a # saver class is accepted. # #
#
:yaml
#
YAML structure (default)
#
:cookiestxt
#
Mozilla's cookies.txt format
#
# # * `:session` # #
#
true
#
Save session cookies as well.
#
false
#
Do not save session cookies. (default)
#
# # All options given are passed through to the underlying cookie # saver module's constructor. def save(writable, *options) opthash = { :format => :yaml, :session => false, } case options.size when 0 when 1 case options = options.first when Symbol opthash[:format] = options else if hash = Hash.try_convert(options) opthash.update(hash) end end when 2 opthash[:format], options = options if hash = Hash.try_convert(options) opthash.update(hash) end else raise ArgumentError, 'wrong number of arguments (%d for 1-3)' % (1 + options.size) end saver = get_impl(AbstractSaver, opthash[:format], opthash) if writable.respond_to?(:write) saver.save(writable, self) else File.open(writable, 'w') { |io| saver.save(io, self) } end self end # call-seq: # jar.load(filename_or_io, **options) # jar.load(filename_or_io, format = :yaml, **options) # # Loads cookies recorded in a file or an IO in the format specified # into the jar and returns self. If a given object responds to # \#read it is taken as an IO, or taken as a filename otherwise. # # Available option keywords are below: # # * `:format` # # Specifies the format for loading. A saver class, a symbol # addressing a saver class, or a pre-generated instance of a # saver class is accepted. # #
#
:yaml
#
YAML structure (default)
#
:cookiestxt
#
Mozilla's cookies.txt format
#
# # All options given are passed through to the underlying cookie # saver module's constructor. def load(readable, *options) opthash = { :format => :yaml, :session => false, } case options.size when 0 when 1 case options = options.first when Symbol opthash[:format] = options else if hash = Hash.try_convert(options) opthash.update(hash) end end when 2 opthash[:format], options = options if hash = Hash.try_convert(options) opthash.update(hash) end else raise ArgumentError, 'wrong number of arguments (%d for 1-3)' % (1 + options.size) end saver = get_impl(AbstractSaver, opthash[:format], opthash) if readable.respond_to?(:write) saver.load(readable, self) else File.open(readable, 'r') { |io| saver.load(io, self) } end self end # Clears the cookie jar and returns self. def clear @store.clear self end # Removes expired cookies and returns self. If `session` is true, # all session cookies are removed as well. def cleanup(session = false) @store.cleanup session self end end ruby-http-cookie-1.0.3/lib/http/cookie_jar/000077500000000000000000000000001341364502000205505ustar00rootroot00000000000000ruby-http-cookie-1.0.3/lib/http/cookie_jar/abstract_saver.rb000066400000000000000000000032071341364502000241020ustar00rootroot00000000000000# :markup: markdown # An abstract superclass for all saver classes. class HTTP::CookieJar::AbstractSaver class << self @@class_map = {} # Gets an implementation class by the name, optionally trying to # load "http/cookie_jar/*_saver" if not found. If loading fails, # IndexError is raised. def implementation(symbol) @@class_map.fetch(symbol) rescue IndexError begin require 'http/cookie_jar/%s_saver' % symbol @@class_map.fetch(symbol) rescue LoadError, IndexError raise IndexError, 'cookie saver unavailable: %s' % symbol.inspect end end def inherited(subclass) # :nodoc: @@class_map[class_to_symbol(subclass)] = subclass end def class_to_symbol(klass) # :nodoc: klass.name[/[^:]+?(?=Saver$|$)/].downcase.to_sym end end # Defines options and their default values. def default_options # {} end private :default_options # :call-seq: # new(**options) # # Called by the constructor of each subclass using super(). def initialize(options = nil) options ||= {} @logger = options[:logger] @session = options[:session] # Initializes each instance variable of the same name as option # keyword. default_options.each_pair { |key, default| instance_variable_set("@#{key}", options.fetch(key, default)) } end # Implements HTTP::CookieJar#save(). # # This is an abstract method that each subclass must override. def save(io, jar) # self end # Implements HTTP::CookieJar#load(). # # This is an abstract method that each subclass must override. def load(io, jar) # self end end ruby-http-cookie-1.0.3/lib/http/cookie_jar/abstract_store.rb000066400000000000000000000060521341364502000241170ustar00rootroot00000000000000# :markup: markdown require 'monitor' # An abstract superclass for all store classes. class HTTP::CookieJar::AbstractStore include MonitorMixin class << self @@class_map = {} # Gets an implementation class by the name, optionally trying to # load "http/cookie_jar/*_store" if not found. If loading fails, # IndexError is raised. def implementation(symbol) @@class_map.fetch(symbol) rescue IndexError begin require 'http/cookie_jar/%s_store' % symbol @@class_map.fetch(symbol) rescue LoadError, IndexError raise IndexError, 'cookie store unavailable: %s' % symbol.inspect end end def inherited(subclass) # :nodoc: @@class_map[class_to_symbol(subclass)] = subclass end def class_to_symbol(klass) # :nodoc: klass.name[/[^:]+?(?=Store$|$)/].downcase.to_sym end end # Defines options and their default values. def default_options # {} end private :default_options # :call-seq: # new(**options) # # Called by the constructor of each subclass using super(). def initialize(options = nil) super() # MonitorMixin options ||= {} @logger = options[:logger] # Initializes each instance variable of the same name as option # keyword. default_options.each_pair { |key, default| instance_variable_set("@#{key}", options.fetch(key, default)) } end # This is an abstract method that each subclass must override. def initialize_copy(other) # self end # Implements HTTP::CookieJar#add(). # # This is an abstract method that each subclass must override. def add(cookie) # self end # Implements HTTP::CookieJar#delete(). # # This is an abstract method that each subclass must override. def delete(cookie) # self end # Iterates over all cookies that are not expired. # # An optional argument +uri+ specifies a URI object indicating the # destination of the cookies being selected. Every cookie yielded # should be good to send to the given URI, # i.e. cookie.valid_for_uri?(uri) evaluates to true. # # If (and only if) the +uri+ option is given, last access time of # each cookie is updated to the current time. # # This is an abstract method that each subclass must override. def each(uri = nil, &block) # :yield: cookie # if uri # ... # else # synchronize { # ... # } # end # self end include Enumerable # Implements HTTP::CookieJar#empty?(). def empty? each { return false } true end # Implements HTTP::CookieJar#clear(). # # This is an abstract method that each subclass must override. def clear # self end # Implements HTTP::CookieJar#cleanup(). # # This is an abstract method that each subclass must override. def cleanup(session = false) # if session # select { |cookie| cookie.session? || cookie.expired? } # else # select(&:expired?) # end.each { |cookie| # delete(cookie) # } # # subclasses can optionally remove over-the-limit cookies. # self end end ruby-http-cookie-1.0.3/lib/http/cookie_jar/cookiestxt_saver.rb000066400000000000000000000046501341364502000244760ustar00rootroot00000000000000# :markup: markdown require 'http/cookie_jar' # CookiestxtSaver saves and loads cookies in the cookies.txt format. class HTTP::CookieJar::CookiestxtSaver < HTTP::CookieJar::AbstractSaver # :singleton-method: new # :call-seq: # new(**options) # # Available option keywords are below: # # * `:header` # # Specifies the header line not including a line feed, which is # only used by #save(). None is output if nil is # given. (default: `"# HTTP Cookie File"`) # # * `:linefeed` # # Specifies the line separator (default: `"\n"`). ## def save(io, jar) io.puts @header if @header jar.each { |cookie| next if !@session && cookie.session? io.print cookie_to_record(cookie) } end def load(io, jar) io.each_line { |line| cookie = parse_record(line) and jar.add(cookie) } end private def default_options { :header => "# HTTP Cookie File", :linefeed => "\n", } end # :stopdoc: True = "TRUE" False = "FALSE" HTTPONLY_PREFIX = '#HttpOnly_' RE_HTTPONLY_PREFIX = /\A#{HTTPONLY_PREFIX}/ # :startdoc: # Serializes the cookie into a cookies.txt line. def cookie_to_record(cookie) cookie.instance_eval { [ @httponly ? HTTPONLY_PREFIX + dot_domain : dot_domain, @for_domain ? True : False, @path, @secure ? True : False, expires.to_i, @name, @value ] }.join("\t") << @linefeed end # Parses a line from cookies.txt and returns a cookie object if the # line represents a cookie record or returns nil otherwise. def parse_record(line) case line when RE_HTTPONLY_PREFIX httponly = true line = $' when /\A#/ return nil else httponly = false end domain, s_for_domain, # Whether this cookie is for domain path, # Path for which the cookie is relevant s_secure, # Requires a secure connection s_expires, # Time the cookie expires (Unix epoch time) name, value = line.split("\t", 7) return nil if value.nil? value.chomp! if (expires_seconds = s_expires.to_i).nonzero? expires = Time.at(expires_seconds) return nil if expires < Time.now end HTTP::Cookie.new(name, value, :domain => domain, :for_domain => s_for_domain == True, :path => path, :secure => s_secure == True, :httponly => httponly, :expires => expires) end end ruby-http-cookie-1.0.3/lib/http/cookie_jar/hash_store.rb000066400000000000000000000101711341364502000232340ustar00rootroot00000000000000# :markup: markdown require 'http/cookie_jar' class HTTP::CookieJar # A store class that uses a hash-based cookie store. # # In this store, cookies that share the same name, domain and path # will overwrite each other regardless of the `for_domain` flag # value. This store is built after the storage model described in # RFC 6265 5.3 where there is no mention of how the host-only-flag # affects in storing cookies. On the other hand, in MozillaStore # two cookies with the same name, domain and path coexist as long as # they differ in the `for_domain` flag value, which means they need # to be expired individually. class HashStore < AbstractStore def default_options { :gc_threshold => HTTP::Cookie::MAX_COOKIES_TOTAL / 20 } end # :call-seq: # new(**options) # # Generates a hash based cookie store. # # Available option keywords are as below: # # :gc_threshold # : GC threshold; A GC happens when this many times cookies have # been stored (default: `HTTP::Cookie::MAX_COOKIES_TOTAL / 20`) def initialize(options = nil) super @jar = { # hostname => { # path => { # name => cookie, # ... # }, # ... # }, # ... } @gc_index = 0 end # The copy constructor. This store class supports cloning. def initialize_copy(other) @jar = Marshal.load(Marshal.dump(other.instance_variable_get(:@jar))) end def add(cookie) path_cookies = ((@jar[cookie.domain] ||= {})[cookie.path] ||= {}) path_cookies[cookie.name] = cookie cleanup if (@gc_index += 1) >= @gc_threshold self end def delete(cookie) path_cookies = ((@jar[cookie.domain] ||= {})[cookie.path] ||= {}) path_cookies.delete(cookie.name) self end def each(uri = nil) # :yield: cookie now = Time.now if uri thost = DomainName.new(uri.host) tpath = uri.path @jar.each { |domain, paths| next unless thost.cookie_domain?(domain) paths.each { |path, hash| next unless HTTP::Cookie.path_match?(path, tpath) hash.delete_if { |name, cookie| if cookie.expired?(now) true else if cookie.valid_for_uri?(uri) cookie.accessed_at = now yield cookie end false end } } } else synchronize { @jar.each { |domain, paths| paths.each { |path, hash| hash.delete_if { |name, cookie| if cookie.expired?(now) true else yield cookie false end } } } } end self end def clear @jar.clear self end def cleanup(session = false) now = Time.now all_cookies = [] synchronize { break if @gc_index == 0 @jar.each { |domain, paths| domain_cookies = [] paths.each { |path, hash| hash.delete_if { |name, cookie| if cookie.expired?(now) || (session && cookie.session?) true else domain_cookies << cookie false end } } if (debt = domain_cookies.size - HTTP::Cookie::MAX_COOKIES_PER_DOMAIN) > 0 domain_cookies.sort_by!(&:created_at) domain_cookies.slice!(0, debt).each { |cookie| delete(cookie) } end all_cookies.concat(domain_cookies) } if (debt = all_cookies.size - HTTP::Cookie::MAX_COOKIES_TOTAL) > 0 all_cookies.sort_by!(&:created_at) all_cookies.slice!(0, debt).each { |cookie| delete(cookie) } end @jar.delete_if { |domain, paths| paths.delete_if { |path, hash| hash.empty? } paths.empty? } @gc_index = 0 } self end end end ruby-http-cookie-1.0.3/lib/http/cookie_jar/mozilla_store.rb000066400000000000000000000322361341364502000237660ustar00rootroot00000000000000# :markup: markdown require 'http/cookie_jar' require 'sqlite3' class HTTP::CookieJar # A store class that uses Mozilla compatible SQLite3 database as # backing store. # # Session cookies are stored separately on memory and will not be # stored persistently in the SQLite3 database. class MozillaStore < AbstractStore # :stopdoc: SCHEMA_VERSION = 5 def default_options { :gc_threshold => HTTP::Cookie::MAX_COOKIES_TOTAL / 20, :app_id => 0, :in_browser_element => false, } end ALL_COLUMNS = %w[ baseDomain appId inBrowserElement name value host path expiry creationTime lastAccessed isSecure isHttpOnly ] UK_COLUMNS = %w[ name host path appId inBrowserElement ] SQL = {} Callable = proc { |obj, meth, *args| proc { obj.__send__(meth, *args) } } class Database < SQLite3::Database def initialize(file, options = {}) @stmts = [] options = { :results_as_hash => true, }.update(options) super end def prepare(sql) case st = super when SQLite3::Statement @stmts << st end st end def close return self if closed? @stmts.reject! { |st| st.closed? || st.close } super end end # :startdoc: # :call-seq: # new(**options) # # Generates a Mozilla cookie store. If the file does not exist, # it is created. If it does and its schema is old, it is # automatically upgraded with a new schema keeping the existing # data. # # Available option keywords are as below: # # :filename # : A file name of the SQLite3 database to open. This option is # mandatory. # # :gc_threshold # : GC threshold; A GC happens when this many times cookies have # been stored (default: `HTTP::Cookie::MAX_COOKIES_TOTAL / 20`) # # :app_id # : application ID (default: `0`) to have per application jar. # # :in_browser_element # : a flag to tell if cookies are stored in an in browser # element. (default: `false`) def initialize(options = nil) super @filename = options[:filename] or raise ArgumentError, ':filename option is missing' @sjar = HTTP::CookieJar::HashStore.new @db = Database.new(@filename) @stmt = Hash.new { |st, key| st[key] = @db.prepare(SQL[key]) } ObjectSpace.define_finalizer(self, Callable[@db, :close]) upgrade_database @gc_index = 0 end # Raises TypeError. Cloning is inhibited in this store class. def initialize_copy(other) raise TypeError, 'can\'t clone %s' % self.class end # The file name of the SQLite3 database given in initialization. attr_reader :filename # Closes the SQLite3 database. After closing, any operation may # raise an error. def close @db.closed? || @db.close self end # Tests if the SQLite3 database is closed. def closed? @db.closed? end # Returns the schema version of the database. def schema_version @schema_version ||= @db.execute("PRAGMA user_version").first[0] rescue SQLite3::SQLException @logger.warn "couldn't get schema version!" if @logger return nil end protected def schema_version= version @db.execute("PRAGMA user_version = %d" % version) @schema_version = version end def create_table self.schema_version = SCHEMA_VERSION @db.execute("DROP TABLE IF EXISTS moz_cookies") @db.execute(<<-'SQL') CREATE TABLE moz_cookies ( id INTEGER PRIMARY KEY, baseDomain TEXT, appId INTEGER DEFAULT 0, inBrowserElement INTEGER DEFAULT 0, name TEXT, value TEXT, host TEXT, path TEXT, expiry INTEGER, lastAccessed INTEGER, creationTime INTEGER, isSecure INTEGER, isHttpOnly INTEGER, CONSTRAINT moz_uniqueid UNIQUE (name, host, path, appId, inBrowserElement) ) SQL @db.execute(<<-'SQL') CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, appId, inBrowserElement); SQL end def db_prepare(sql) st = @db.prepare(sql) yield st ensure st.close if st end def upgrade_database loop { case schema_version when nil, 0 self.schema_version = SCHEMA_VERSION break when 1 @db.execute("ALTER TABLE moz_cookies ADD lastAccessed INTEGER") self.schema_version += 1 when 2 @db.execute("ALTER TABLE moz_cookies ADD baseDomain TEXT") db_prepare("UPDATE moz_cookies SET baseDomain = :baseDomain WHERE id = :id") { |st_update| @db.execute("SELECT id, host FROM moz_cookies") { |row| domain_name = DomainName.new(row['host'][/\A\.?(.*)/, 1]) domain = domain_name.domain || domain_name.hostname st_update.execute(:baseDomain => domain, :id => row['id']) } } @db.execute("CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)") self.schema_version += 1 when 3 db_prepare("DELETE FROM moz_cookies WHERE id = :id") { |st_delete| prev_row = nil @db.execute(<<-'SQL') { |row| SELECT id, name, host, path FROM moz_cookies ORDER BY name ASC, host ASC, path ASC, expiry ASC SQL if %w[name host path].all? { |col| prev_row and row[col] == prev_row[col] } st_delete.execute(prev_row['id']) end prev_row = row } } @db.execute("ALTER TABLE moz_cookies ADD creationTime INTEGER") @db.execute("UPDATE moz_cookies SET creationTime = (SELECT id WHERE id = moz_cookies.id)") @db.execute("CREATE UNIQUE INDEX moz_uniqueid ON moz_cookies (name, host, path)") self.schema_version += 1 when 4 @db.execute("ALTER TABLE moz_cookies RENAME TO moz_cookies_old") @db.execute("DROP INDEX moz_basedomain") create_table @db.execute(<<-'SQL') INSERT INTO moz_cookies (baseDomain, appId, inBrowserElement, name, value, host, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly) SELECT baseDomain, 0, 0, name, value, host, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly FROM moz_cookies_old SQL @db.execute("DROP TABLE moz_cookies_old") @logger.info("Upgraded database to schema version %d" % schema_version) if @logger else break end } begin @db.execute("SELECT %s from moz_cookies limit 1" % ALL_COLUMNS.join(', ')) rescue SQLite3::SQLException create_table end end SQL[:add] = <<-'SQL' % [ INSERT OR REPLACE INTO moz_cookies (%s) VALUES (%s) SQL ALL_COLUMNS.join(', '), ALL_COLUMNS.map { |col| ":#{col}" }.join(', ') ] def db_add(cookie) @stmt[:add].execute({ :baseDomain => cookie.domain_name.domain || cookie.domain, :appId => @app_id, :inBrowserElement => @in_browser_element ? 1 : 0, :name => cookie.name, :value => cookie.value, :host => cookie.dot_domain, :path => cookie.path, :expiry => cookie.expires_at.to_i, :creationTime => cookie.created_at.to_i, :lastAccessed => cookie.accessed_at.to_i, :isSecure => cookie.secure? ? 1 : 0, :isHttpOnly => cookie.httponly? ? 1 : 0, }) cleanup if (@gc_index += 1) >= @gc_threshold self end SQL[:delete] = <<-'SQL' DELETE FROM moz_cookies WHERE appId = :appId AND inBrowserElement = :inBrowserElement AND name = :name AND host = :host AND path = :path SQL def db_delete(cookie) @stmt[:delete].execute({ :appId => @app_id, :inBrowserElement => @in_browser_element ? 1 : 0, :name => cookie.name, :host => cookie.dot_domain, :path => cookie.path, }) self end public def add(cookie) if cookie.session? @sjar.add(cookie) db_delete(cookie) else @sjar.delete(cookie) db_add(cookie) end end def delete(cookie) @sjar.delete(cookie) db_delete(cookie) end SQL[:cookies_for_domain] = <<-'SQL' SELECT * FROM moz_cookies WHERE baseDomain = :baseDomain AND appId = :appId AND inBrowserElement = :inBrowserElement AND expiry >= :expiry SQL SQL[:update_lastaccessed] = <<-'SQL' UPDATE moz_cookies SET lastAccessed = :lastAccessed WHERE id = :id SQL SQL[:all_cookies] = <<-'SQL' SELECT * FROM moz_cookies WHERE appId = :appId AND inBrowserElement = :inBrowserElement AND expiry >= :expiry SQL def each(uri = nil, &block) # :yield: cookie now = Time.now if uri thost = DomainName.new(uri.host) tpath = uri.path @stmt[:cookies_for_domain].execute({ :baseDomain => thost.domain || thost.hostname, :appId => @app_id, :inBrowserElement => @in_browser_element ? 1 : 0, :expiry => now.to_i, }).each { |row| if secure = row['isSecure'] != 0 next unless URI::HTTPS === uri end cookie = HTTP::Cookie.new({}.tap { |attrs| attrs[:name] = row['name'] attrs[:value] = row['value'] attrs[:domain] = row['host'] attrs[:path] = row['path'] attrs[:expires_at] = Time.at(row['expiry']) attrs[:accessed_at] = Time.at(row['lastAccessed'] || 0) attrs[:created_at] = Time.at(row['creationTime'] || 0) attrs[:secure] = secure attrs[:httponly] = row['isHttpOnly'] != 0 }) if cookie.valid_for_uri?(uri) cookie.accessed_at = now @stmt[:update_lastaccessed].execute({ 'lastAccessed' => now.to_i, 'id' => row['id'], }) yield cookie end } @sjar.each(uri, &block) else @stmt[:all_cookies].execute({ :appId => @app_id, :inBrowserElement => @in_browser_element ? 1 : 0, :expiry => now.to_i, }).each { |row| cookie = HTTP::Cookie.new({}.tap { |attrs| attrs[:name] = row['name'] attrs[:value] = row['value'] attrs[:domain] = row['host'] attrs[:path] = row['path'] attrs[:expires_at] = Time.at(row['expiry']) attrs[:accessed_at] = Time.at(row['lastAccessed'] || 0) attrs[:created_at] = Time.at(row['creationTime'] || 0) attrs[:secure] = row['isSecure'] != 0 attrs[:httponly] = row['isHttpOnly'] != 0 }) yield cookie } @sjar.each(&block) end self end def clear @db.execute("DELETE FROM moz_cookies") @sjar.clear self end SQL[:delete_expired] = <<-'SQL' DELETE FROM moz_cookies WHERE expiry < :expiry SQL SQL[:overusing_domains] = <<-'SQL' SELECT LTRIM(host, '.') domain, COUNT(*) count FROM moz_cookies GROUP BY domain HAVING count > :count SQL SQL[:delete_per_domain_overuse] = <<-'SQL' DELETE FROM moz_cookies WHERE id IN ( SELECT id FROM moz_cookies WHERE LTRIM(host, '.') = :domain ORDER BY creationtime LIMIT :limit) SQL SQL[:delete_total_overuse] = <<-'SQL' DELETE FROM moz_cookies WHERE id IN ( SELECT id FROM moz_cookies ORDER BY creationTime ASC LIMIT :limit ) SQL def cleanup(session = false) synchronize { break if @gc_index == 0 @stmt[:delete_expired].execute({ 'expiry' => Time.now.to_i }) @stmt[:overusing_domains].execute({ 'count' => HTTP::Cookie::MAX_COOKIES_PER_DOMAIN }).each { |row| domain, count = row['domain'], row['count'] @stmt[:delete_per_domain_overuse].execute({ 'domain' => domain, 'limit' => count - HTTP::Cookie::MAX_COOKIES_PER_DOMAIN, }) } overrun = count - HTTP::Cookie::MAX_COOKIES_TOTAL if overrun > 0 @stmt[:delete_total_overuse].execute({ 'limit' => overrun }) end @gc_index = 0 } self end end end ruby-http-cookie-1.0.3/lib/http/cookie_jar/yaml_saver.rb000066400000000000000000000037361341364502000232500ustar00rootroot00000000000000# :markup: markdown require 'http/cookie_jar' require 'psych' if !defined?(YAML) && RUBY_VERSION == "1.9.2" require 'yaml' # YAMLSaver saves and loads cookies in the YAML format. It can load a # YAML file saved by Mechanize, but the saving format is not # compatible with older versions of Mechanize (< 2.7). class HTTP::CookieJar::YAMLSaver < HTTP::CookieJar::AbstractSaver # :singleton-method: new # :call-seq: # new(**options) # # There is no option keyword supported at the moment. ## def save(io, jar) YAML.dump(@session ? jar.to_a : jar.reject(&:session?), io) end def load(io, jar) begin data = YAML.load(io) rescue ArgumentError => e case e.message when %r{\Aundefined class/module Mechanize::} # backward compatibility with Mechanize::Cookie begin io.rewind # hopefully yaml = io.read # a gross hack yaml.gsub!(%r{^( [^ ].*:) !ruby/object:Mechanize::Cookie$}, "\\1") data = YAML.load(yaml) rescue Errno::ESPIPE @logger.warn "could not rewind the stream for conversion" if @logger rescue ArgumentError end end end case data when Array data.each { |cookie| jar.add(cookie) } when Hash # backward compatibility with Mechanize::Cookie data.each { |domain, paths| paths.each { |path, names| names.each { |cookie_name, cookie_hash| if cookie_hash.respond_to?(:ivars) # YAML::Object of Syck cookie_hash = cookie_hash.ivars end cookie = HTTP::Cookie.new({}.tap { |hash| cookie_hash.each_pair { |key, value| hash[key.to_sym] = value } }) jar.add(cookie) } } } else @logger.warn "incompatible YAML cookie data discarded" if @logger return end end private def default_options {} end end ruby-http-cookie-1.0.3/test/000077500000000000000000000000001341364502000156755ustar00rootroot00000000000000ruby-http-cookie-1.0.3/test/helper.rb000066400000000000000000000021201341364502000174740ustar00rootroot00000000000000require 'rubygems' require 'test-unit' require 'uri' require 'http/cookie' module Test module Unit module Assertions def assert_warn(pattern, message = nil, &block) class << (output = "") alias write << end stderr, $stderr = $stderr, output yield assert_match(pattern, output, message) ensure $stderr = stderr end def assert_warning(pattern, message = nil, &block) verbose, $VERBOSE = $VERBOSE, true assert_warn(pattern, message, &block) ensure $VERBOSE = verbose end end end end module Enumerable def combine masks = inject([[], 1]){|(ar, m), e| [ar << m, m << 1 ] }[0] all = masks.inject(0){ |al, m| al|m } result = [] for i in 1..all do tmp = [] each_with_index do |e, idx| tmp << e unless (masks[idx] & i) == 0 end result << tmp end result end end def test_file(filename) File.expand_path(filename, File.dirname(__FILE__)) end def sleep_until(time) if (s = time - Time.now) > 0 sleep s end end ruby-http-cookie-1.0.3/test/mechanize.yml000066400000000000000000000055721341364502000203740ustar00rootroot00000000000000--- google.com: /: PREF: !ruby/object:Mechanize::Cookie version: 0 port: discard: comment_url: expires: Tue, 24 Mar 2065 08:20:15 GMT max_age: comment: secure: false path: / domain: google.com accessed_at: 2013-03-24 17:20:15.822619000 +09:00 created_at: 2013-03-24 17:20:15.822619000 +09:00 name: PREF value: ID=7571a59c059e09db:FF=0:TM=1364199615:LM=1364199615:S=BxUqnqPrchd2cVmC for_domain: true domain_name: !ruby/object:DomainName ipaddr: hostname: google.com uri_host: google.com tld: com canonical_tld_p: true domain: google.com session: false NID: !ruby/object:Mechanize::Cookie version: 0 port: discard: comment_url: expires: Sun, 23 Sep 2063 08:20:15 GMT max_age: comment: secure: false path: / domain: google.com accessed_at: 2013-03-24 17:20:15.828434000 +09:00 created_at: 2013-03-24 17:20:15.828434000 +09:00 name: NID value: 67=Kn2osS6wOzILpl7sCM1QIDmGg2VESBiwCyt6zx4vOVSWKOYDlwGIpgIGrpD8FpkbS9eqizo3QWFa5YkOygnCF6vRIQpbvlTxWB2Hq1Oo-qXWy0317yCqQ-B25eJLfUcC for_domain: true domain_name: !ruby/object:DomainName ipaddr: hostname: google.com uri_host: google.com tld: com canonical_tld_p: true domain: google.com session: false google.co.jp: /: PREF: !ruby/object:Mechanize::Cookie version: 0 port: discard: comment_url: expires: Tue, 24 Mar 2065 08:20:16 GMT max_age: comment: secure: false path: / domain: google.co.jp accessed_at: 2013-03-24 17:20:17.136581000 +09:00 created_at: 2013-03-24 17:20:17.136581000 +09:00 name: PREF value: ID=cb25dd1567d8b5c8:FF=0:TM=1364199616:LM=1364199616:S=c3PbhRq79Wo5T_vV for_domain: true domain_name: !ruby/object:DomainName ipaddr: hostname: google.co.jp uri_host: google.co.jp tld: jp canonical_tld_p: true domain: google.co.jp session: false NID: !ruby/object:Mechanize::Cookie version: 0 port: discard: comment_url: expires: Sun, 23 Sep 2063 08:20:16 GMT max_age: comment: secure: false path: / domain: google.co.jp accessed_at: 2013-03-24 17:20:17.139782000 +09:00 created_at: 2013-03-24 17:20:17.139782000 +09:00 name: NID value: 67=GS7P-68zgm_KRA0e0dpN_XbYpmw9uBDe56qUeoCGiSRTahsM7dtOBCKfCoIFRKlzSuOiwJQdIZNpwv3DSXQNHXDKltucgfv2qkHlGeoj8-5VlowPXLLesz2VIpLOLw-a for_domain: true domain_name: !ruby/object:DomainName ipaddr: hostname: google.co.jp uri_host: google.co.jp tld: jp canonical_tld_p: true domain: google.co.jp session: false ruby-http-cookie-1.0.3/test/simplecov_start.rb000066400000000000000000000000441341364502000214360ustar00rootroot00000000000000require 'simplecov' SimpleCov.start ruby-http-cookie-1.0.3/test/test_http_cookie.rb000066400000000000000000001107211341364502000215730ustar00rootroot00000000000000# -*- coding: utf-8 -*- require File.expand_path('helper', File.dirname(__FILE__)) class TestHTTPCookie < Test::Unit::TestCase def setup httpdate = 'Sun, 27-Sep-2037 00:00:00 GMT' @cookie_params = { 'expires' => 'expires=%s' % httpdate, 'path' => 'path=/', 'domain' => 'domain=.rubyforge.org', 'httponly' => 'HttpOnly', } @expires = Time.parse(httpdate) end def test_parse_dates url = URI.parse('http://localhost/') yesterday = Time.now - 86400 dates = [ "14 Apr 89 03:20:12", "14 Apr 89 03:20 GMT", "Fri, 17 Mar 89 4:01:33", "Fri, 17 Mar 89 4:01 GMT", "Mon Jan 16 16:12 PDT 1989", #"Mon Jan 16 16:12 +0130 1989", "6 May 1992 16:41-JST (Wednesday)", #"22-AUG-1993 10:59:12.82", "22-AUG-1993 10:59pm", "22-AUG-1993 12:59am", "22-AUG-1993 12:59 PM", #"Friday, August 04, 1995 3:54 PM", #"06/21/95 04:24:34 PM", #"20/06/95 21:07", #"95-06-08 19:32:48 EDT", ] dates.each do |date| cookie = "PREF=1; expires=#{date}" assert_equal 1, HTTP::Cookie.parse(cookie, url) { |c| assert c.expires, "Tried parsing: #{date}" assert_send [c.expires, :<, yesterday] }.size end [ ["PREF=1; expires=Wed, 01 Jan 100 12:34:56 GMT", nil], ["PREF=1; expires=Sat, 01 Jan 1600 12:34:56 GMT", nil], ["PREF=1; expires=Tue, 01 Jan 69 12:34:56 GMT", 2069], ["PREF=1; expires=Thu, 01 Jan 70 12:34:56 GMT", 1970], ["PREF=1; expires=Wed, 01 Jan 20 12:34:56 GMT", 2020], ["PREF=1; expires=Sat, 01 Jan 2020 12:34:60 GMT", nil], ["PREF=1; expires=Sat, 01 Jan 2020 12:60:56 GMT", nil], ["PREF=1; expires=Sat, 01 Jan 2020 24:00:00 GMT", nil], ["PREF=1; expires=Sat, 32 Jan 2020 12:34:56 GMT", nil], ].each { |set_cookie, year| cookie, = HTTP::Cookie.parse(set_cookie, url) if year assert_equal year, cookie.expires.year, "#{set_cookie}: expires in #{year}" else assert_equal nil, cookie.expires, "#{set_cookie}: invalid expiry date" end } end def test_parse_empty cookie_str = 'a=b; ; c=d' uri = URI.parse 'http://example' assert_equal 1, HTTP::Cookie.parse(cookie_str, uri) { |cookie| assert_equal 'a', cookie.name assert_equal 'b', cookie.value }.size end def test_parse_no_space cookie_str = "foo=bar;Expires=Sun, 06 Nov 2011 00:28:06 GMT;Path=/" uri = URI.parse 'http://example' assert_equal 1, HTTP::Cookie.parse(cookie_str, uri) { |cookie| assert_equal 'foo', cookie.name assert_equal 'bar', cookie.value assert_equal '/', cookie.path assert_equal Time.at(1320539286), cookie.expires }.size end def test_parse_too_long_cookie uri = URI.parse 'http://example' cookie_str = "foo=#{'Cookie' * 680}; path=/ab/" assert_equal(HTTP::Cookie::MAX_LENGTH - 1, cookie_str.bytesize) assert_equal 1, HTTP::Cookie.parse(cookie_str, uri).size assert_equal 1, HTTP::Cookie.parse(cookie_str.sub(';', 'x;'), uri).size assert_equal 0, HTTP::Cookie.parse(cookie_str.sub(';', 'xx;'), uri).size end def test_parse_quoted cookie_str = "quoted=\"value\"; Expires=Sun, 06 Nov 2011 00:11:18 GMT; Path=/; comment=\"comment is \\\"comment\\\"\"" uri = URI.parse 'http://example' assert_equal 1, HTTP::Cookie.parse(cookie_str, uri) { |cookie| assert_equal 'quoted', cookie.name assert_equal 'value', cookie.value }.size end def test_parse_no_nothing cookie = '; "", ;' url = URI.parse('http://www.example.com/') assert_equal 0, HTTP::Cookie.parse(cookie, url).size end def test_parse_no_name cookie = '=no-name; path=/' url = URI.parse('http://www.example.com/') assert_equal 0, HTTP::Cookie.parse(cookie, url).size end def test_parse_bad_name cookie = "a\001b=c" url = URI.parse('http://www.example.com/') assert_nothing_raised { assert_equal 0, HTTP::Cookie.parse(cookie, url).size } end def test_parse_bad_value cookie = "a=b\001c" url = URI.parse('http://www.example.com/') assert_nothing_raised { assert_equal 0, HTTP::Cookie.parse(cookie, url).size } end def test_parse_weird_cookie cookie = 'n/a, ASPSESSIONIDCSRRQDQR=FBLDGHPBNDJCPCGNCPAENELB; path=/' url = URI.parse('http://www.searchinnovation.com/') assert_equal 1, HTTP::Cookie.parse(cookie, url) { |c| assert_equal('ASPSESSIONIDCSRRQDQR', c.name) assert_equal('FBLDGHPBNDJCPCGNCPAENELB', c.value) }.size end def test_double_semicolon double_semi = 'WSIDC=WEST;; domain=.williams-sonoma.com; path=/' url = URI.parse('http://williams-sonoma.com/') assert_equal 1, HTTP::Cookie.parse(double_semi, url) { |cookie| assert_equal('WSIDC', cookie.name) assert_equal('WEST', cookie.value) }.size end def test_parse_bad_version bad_cookie = 'PRETANET=TGIAqbFXtt; Name=/PRETANET; Path=/; Version=1.2; Content-type=text/html; Domain=192.168.6.196; expires=Friday, 13-November-2026 23:01:46 GMT;' url = URI.parse('http://192.168.6.196/') # The version attribute is obsolete and simply ignored cookies = HTTP::Cookie.parse(bad_cookie, url) assert_equal 1, cookies.size end def test_parse_bad_max_age bad_cookie = 'PRETANET=TGIAqbFXtt; Name=/PRETANET; Path=/; Max-Age=forever; Content-type=text/html; Domain=192.168.6.196; expires=Friday, 13-November-2026 23:01:46 GMT;' url = URI.parse('http://192.168.6.196/') # A bad max-age is simply ignored cookies = HTTP::Cookie.parse(bad_cookie, url) assert_equal 1, cookies.size assert_equal nil, cookies.first.max_age end def test_parse_date_fail url = URI.parse('http://localhost/') dates = [ "20/06/95 21:07", ] dates.each { |date| cookie = "PREF=1; expires=#{date}" assert_equal 1, HTTP::Cookie.parse(cookie, url) { |c| assert_equal(true, c.expires.nil?) }.size } end def test_parse_domain_dot url = URI.parse('http://host.example.com/') cookie_str = 'a=b; domain=.example.com' cookie = HTTP::Cookie.parse(cookie_str, url).first assert_equal 'example.com', cookie.domain assert cookie.for_domain? assert_equal '.example.com', cookie.dot_domain end def test_parse_domain_no_dot url = URI.parse('http://host.example.com/') cookie_str = 'a=b; domain=example.com' cookie = HTTP::Cookie.parse(cookie_str, url).first assert_equal 'example.com', cookie.domain assert cookie.for_domain? assert_equal '.example.com', cookie.dot_domain end def test_parse_public_suffix cookie = HTTP::Cookie.new('a', 'b', :domain => 'com') assert_equal('com', cookie.domain) assert_equal(false, cookie.for_domain?) cookie.origin = 'http://com/' assert_equal('com', cookie.domain) assert_equal(false, cookie.for_domain?) assert_raises(ArgumentError) { cookie.origin = 'http://example.com/' } end def test_parse_domain_none url = URI.parse('http://example.com/') cookie_str = 'a=b;' cookie = HTTP::Cookie.parse(cookie_str, url).first assert_equal 'example.com', cookie.domain assert !cookie.for_domain? assert_equal 'example.com', cookie.dot_domain end def test_parse_max_age url = URI.parse('http://localhost/') epoch, date = 4485353164, 'Fri, 19 Feb 2112 19:26:04 GMT' base = Time.at(1363014000) cookie = HTTP::Cookie.parse("name=Akinori; expires=#{date}", url).first assert_equal Time.at(epoch), cookie.expires cookie = HTTP::Cookie.parse('name=Akinori; max-age=3600', url).first assert_in_delta Time.now + 3600, cookie.expires, 1 cookie = HTTP::Cookie.parse('name=Akinori; max-age=3600', url, :created_at => base).first assert_equal base + 3600, cookie.expires # Max-Age has precedence over Expires cookie = HTTP::Cookie.parse("name=Akinori; max-age=3600; expires=#{date}", url).first assert_in_delta Time.now + 3600, cookie.expires, 1 cookie = HTTP::Cookie.parse("name=Akinori; max-age=3600; expires=#{date}", url, :created_at => base).first assert_equal base + 3600, cookie.expires cookie = HTTP::Cookie.parse("name=Akinori; expires=#{date}; max-age=3600", url).first assert_in_delta Time.now + 3600, cookie.expires, 1 cookie = HTTP::Cookie.parse("name=Akinori; expires=#{date}; max-age=3600", url, :created_at => base).first assert_equal base + 3600, cookie.expires end def test_parse_expires_session url = URI.parse('http://localhost/') [ 'name=Akinori', 'name=Akinori; expires', 'name=Akinori; max-age', 'name=Akinori; expires=', 'name=Akinori; max-age=', ].each { |str| cookie = HTTP::Cookie.parse(str, url).first assert cookie.session?, str } [ 'name=Akinori; expires=Mon, 19 Feb 2012 19:26:04 GMT', 'name=Akinori; max-age=3600', ].each { |str| cookie = HTTP::Cookie.parse(str, url).first assert !cookie.session?, str } end def test_parse_many url = URI 'http://localhost/' cookie_str = "abc, " \ "name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/, " \ "name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/, " \ "name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/, " \ "name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/; HttpOnly, " \ "expired=doh; Expires=Fri, 04 Nov 2011 00:29:51 GMT; Path=/, " \ "a_path=some_path; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/some_path, " \ "no_path1=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT, no_expires=nope; Path=/, " \ "no_path2=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path, " \ "no_path3=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path=, " \ "rel_path1=rel_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path=foo/bar, " \ "rel_path1=rel_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path=foo, " \ "no_domain1=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope, " \ "no_domain2=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope; Domain, " \ "no_domain3=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope; Domain=" cookies = HTTP::Cookie.parse cookie_str, url assert_equal 15, cookies.length name = cookies.find { |c| c.name == 'name' } assert_equal "Aaron", name.value assert_equal "/", name.path assert_equal Time.at(1320539391), name.expires a_path = cookies.find { |c| c.name == 'a_path' } assert_equal "some_path", a_path.value assert_equal "/some_path", a_path.path assert_equal Time.at(1320539391), a_path.expires no_expires = cookies.find { |c| c.name == 'no_expires' } assert_equal "nope", no_expires.value assert_equal "/", no_expires.path assert_nil no_expires.expires no_path_cookies = cookies.select { |c| c.value == 'no_path' } assert_equal 3, no_path_cookies.size no_path_cookies.each { |c| assert_equal "/", c.path, c.name assert_equal Time.at(1320539392), c.expires, c.name } rel_path_cookies = cookies.select { |c| c.value == 'rel_path' } assert_equal 2, rel_path_cookies.size rel_path_cookies.each { |c| assert_equal "/", c.path, c.name assert_equal Time.at(1320539392), c.expires, c.name } no_domain_cookies = cookies.select { |c| c.value == 'no_domain' } assert_equal 3, no_domain_cookies.size no_domain_cookies.each { |c| assert !c.for_domain?, c.name assert_equal c.domain, url.host, c.name assert_equal Time.at(1320539393), c.expires, c.name } assert cookies.find { |c| c.name == 'expired' } end def test_parse_valid_cookie url = URI.parse('http://rubyforge.org/') cookie_params = @cookie_params cookie_value = '12345%7D=ASDFWEE345%3DASda' cookie_params.keys.combine.each do |keys| cookie_text = [cookie_value, *keys.map { |key| cookie_params[key] }].join('; ') cookie, = HTTP::Cookie.parse(cookie_text, url) assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) assert_equal('/', cookie.path) assert_equal(keys.include?('expires') ? @expires : nil, cookie.expires) assert_equal(keys.include?('httponly'), cookie.httponly?) end end def test_parse_valid_cookie_empty_value url = URI.parse('http://rubyforge.org/') cookie_params = @cookie_params cookie_value = '12345%7D=' cookie_params.keys.combine.each do |keys| cookie_text = [cookie_value, *keys.map { |key| cookie_params[key] }].join('; ') cookie, = HTTP::Cookie.parse(cookie_text, url) assert_equal('12345%7D=', cookie.to_s) assert_equal('', cookie.value) assert_equal('/', cookie.path) assert_equal(keys.include?('expires') ? @expires : nil, cookie.expires) assert_equal(keys.include?('httponly'), cookie.httponly?) end end # If no path was given, use the one from the URL def test_cookie_using_url_path url = URI.parse('http://rubyforge.org/login.php') cookie_params = @cookie_params cookie_value = '12345%7D=ASDFWEE345%3DASda' cookie_params.keys.combine.each do |keys| next if keys.include?('path') cookie_text = [cookie_value, *keys.map { |key| cookie_params[key] }].join('; ') cookie, = HTTP::Cookie.parse(cookie_text, url) assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) assert_equal('/', cookie.path) assert_equal(keys.include?('expires') ? @expires : nil, cookie.expires) assert_equal(keys.include?('httponly'), cookie.httponly?) end end # Test using secure cookies def test_cookie_with_secure url = URI.parse('http://rubyforge.org/') cookie_params = @cookie_params.merge('secure' => 'secure') cookie_value = '12345%7D=ASDFWEE345%3DASda' cookie_params.keys.combine.each do |keys| next unless keys.include?('secure') cookie_text = [cookie_value, *keys.map { |key| cookie_params[key] }].join('; ') cookie, = HTTP::Cookie.parse(cookie_text, url) assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) assert_equal('/', cookie.path) assert_equal(true, cookie.secure) assert_equal(keys.include?('expires') ? @expires : nil, cookie.expires) assert_equal(keys.include?('httponly'), cookie.httponly?) end end def test_cookie_value [ ['foo="bar baz"', 'bar baz'], ['foo="bar\"; \"baz"', 'bar"; "baz'], ].each { |cookie_value, value| cookie = HTTP::Cookie.new('foo', value) assert_equal(cookie_value, cookie.cookie_value) } pairs = [ ['Foo', 'value1'], ['Bar', 'value 2'], ['Baz', 'value3'], ['Bar', 'value"4'], ['Quux', 'x, value=5'], ] cookie_value = HTTP::Cookie.cookie_value(pairs.map { |name, value| HTTP::Cookie.new(:name => name, :value => value) }) assert_equal 'Foo=value1; Bar="value 2"; Baz=value3; Bar="value\\"4"; Quux="x, value=5"', cookie_value hash = HTTP::Cookie.cookie_value_to_hash(cookie_value) assert_equal pairs.map(&:first).uniq.size, hash.size hash.each_pair { |name, value| _, pvalue = pairs.assoc(name) assert_equal pvalue, value } # Do not treat comma in a Cookie header value as separator; see CVE-2016-7401 hash = HTTP::Cookie.cookie_value_to_hash('Quux=x, value=5; Foo=value1; Bar="value 2"; Baz=value3; Bar="value\\"4"') assert_equal pairs.map(&:first).uniq.size, hash.size hash.each_pair { |name, value| _, pvalue = pairs.assoc(name) assert_equal pvalue, value } end def test_set_cookie_value url = URI.parse('http://rubyforge.org/path/') [ HTTP::Cookie.new('a', 'b', :domain => 'rubyforge.org', :path => '/path/'), HTTP::Cookie.new('a', 'b', :origin => url), ].each { |cookie| cookie.set_cookie_value } [ HTTP::Cookie.new('a', 'b', :domain => 'rubyforge.org'), HTTP::Cookie.new('a', 'b', :for_domain => true, :path => '/path/'), ].each { |cookie| assert_raises(RuntimeError) { cookie.set_cookie_value } } ['foo=bar', 'foo="bar"', 'foo="ba\"r baz"'].each { |cookie_value| cookie_params = @cookie_params.merge('path' => '/path/', 'secure' => 'secure', 'max-age' => 'Max-Age=1000') date = Time.at(Time.now.to_i) cookie_params.keys.combine.each do |keys| cookie_text = [cookie_value, *keys.map { |key| cookie_params[key] }].join('; ') cookie, = HTTP::Cookie.parse(cookie_text, url, :created_at => date) cookie2, = HTTP::Cookie.parse(cookie.set_cookie_value, url, :created_at => date) assert_equal(cookie.name, cookie2.name) assert_equal(cookie.value, cookie2.value) assert_equal(cookie.domain, cookie2.domain) assert_equal(cookie.for_domain?, cookie2.for_domain?) assert_equal(cookie.path, cookie2.path) assert_equal(cookie.expires, cookie2.expires) if keys.include?('max-age') assert_equal(date + 1000, cookie2.expires) elsif keys.include?('expires') assert_equal(@expires, cookie2.expires) else assert_equal(nil, cookie2.expires) end assert_equal(cookie.secure?, cookie2.secure?) assert_equal(cookie.httponly?, cookie2.httponly?) end } end def test_parse_cookie_no_spaces url = URI.parse('http://rubyforge.org/') cookie_params = @cookie_params cookie_value = '12345%7D=ASDFWEE345%3DASda' cookie_params.keys.combine.each do |keys| cookie_text = [cookie_value, *keys.map { |key| cookie_params[key] }].join(';') cookie, = HTTP::Cookie.parse(cookie_text, url) assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) assert_equal('/', cookie.path) assert_equal(keys.include?('expires') ? @expires : nil, cookie.expires) assert_equal(keys.include?('httponly'), cookie.httponly?) end end def test_new cookie = HTTP::Cookie.new('key', 'value') assert_equal 'key', cookie.name assert_equal 'value', cookie.value assert_equal nil, cookie.expires assert_raises(RuntimeError) { cookie.acceptable? } # Minimum unit for the expires attribute is second expires = Time.at((Time.now + 3600).to_i) cookie = HTTP::Cookie.new('key', 'value', :expires => expires.dup) assert_equal 'key', cookie.name assert_equal 'value', cookie.value assert_equal expires, cookie.expires assert_raises(RuntimeError) { cookie.acceptable? } # various keywords [ ["Expires", /use downcased symbol/], ].each { |key, pattern| assert_warning(pattern, "warn of key: #{key.inspect}") { cookie = HTTP::Cookie.new(:value => 'value', :name => 'key', key => expires.dup) assert_equal 'key', cookie.name assert_equal 'value', cookie.value assert_equal expires, cookie.expires, "key: #{key.inspect}" } } [ [:Expires, /unknown attribute name/], [:expires?, /unknown attribute name/], [[:expires], /invalid keyword/], ].each { |key, pattern| assert_warning(pattern, "warn of key: #{key.inspect}") { cookie = HTTP::Cookie.new(:value => 'value', :name => 'key', key => expires.dup) assert_equal 'key', cookie.name assert_equal 'value', cookie.value assert_equal nil, cookie.expires, "key: #{key.inspect}" } } cookie = HTTP::Cookie.new(:value => 'value', :name => 'key', :expires => expires.dup) assert_equal 'key', cookie.name assert_equal 'value', cookie.value assert_equal expires, cookie.expires assert_equal false, cookie.for_domain? assert_raises(RuntimeError) { # domain and path are missing cookie.acceptable? } cookie = HTTP::Cookie.new(:value => 'value', :name => 'key', :expires => expires.dup, :domain => '.example.com') assert_equal 'key', cookie.name assert_equal 'value', cookie.value assert_equal expires, cookie.expires assert_equal true, cookie.for_domain? assert_raises(RuntimeError) { # path is missing cookie.acceptable? } cookie = HTTP::Cookie.new(:value => 'value', :name => 'key', :expires => expires.dup, :domain => 'example.com', :for_domain => false) assert_equal 'key', cookie.name assert_equal 'value', cookie.value assert_equal expires, cookie.expires assert_equal false, cookie.for_domain? assert_raises(RuntimeError) { # path is missing cookie.acceptable? } cookie = HTTP::Cookie.new(:value => 'value', :name => 'key', :expires => expires.dup, :domain => 'example.org', :for_domain? => true) assert_equal 'key', cookie.name assert_equal 'value', cookie.value assert_equal expires, cookie.expires assert_equal 'example.org', cookie.domain assert_equal true, cookie.for_domain? assert_raises(RuntimeError) { # path is missing cookie.acceptable? } assert_raises(ArgumentError) { HTTP::Cookie.new() } assert_raises(ArgumentError) { HTTP::Cookie.new(:value => 'value') } assert_raises(ArgumentError) { HTTP::Cookie.new('', 'value') } assert_raises(ArgumentError) { HTTP::Cookie.new('key=key', 'value') } assert_raises(ArgumentError) { HTTP::Cookie.new("key\tkey", 'value') } assert_raises(ArgumentError) { HTTP::Cookie.new('key', 'value', 'something') } assert_raises(ArgumentError) { HTTP::Cookie.new('key', 'value', {}, 'something') } [ HTTP::Cookie.new(:name => 'name'), HTTP::Cookie.new("key", nil, :for_domain => true), HTTP::Cookie.new("key", nil), HTTP::Cookie.new("key", :secure => true), HTTP::Cookie.new("key"), ].each { |cookie| assert_equal '', cookie.value assert_equal true, cookie.expired? } [ HTTP::Cookie.new(:name => 'name', :max_age => 3600), HTTP::Cookie.new("key", nil, :expires => Time.now + 3600), HTTP::Cookie.new("key", :expires => Time.now + 3600), HTTP::Cookie.new("key", :expires => Time.now + 3600, :value => nil), ].each { |cookie| assert_equal '', cookie.value assert_equal false, cookie.expired? } end def cookie_values(options = {}) { :name => 'Foo', :value => 'Bar', :path => '/', :expires => Time.now + (10 * 86400), :for_domain => true, :domain => 'rubyforge.org', :origin => 'http://rubyforge.org/' }.merge(options) end def test_bad_name [ "a\tb", "a\vb", "a\rb", "a\nb", 'a b', "a\\b", 'a"b', # 'a:b', 'a/b', 'a[b]', 'a=b', 'a,b', 'a;b', ].each { |name| assert_raises(ArgumentError) { HTTP::Cookie.new(cookie_values(:name => name)) } cookie = HTTP::Cookie.new(cookie_values) assert_raises(ArgumentError) { cookie.name = name } } end def test_bad_value [ "a\tb", "a\vb", "a\rb", "a\nb", "a\\b", 'a"b', # 'a:b', 'a/b', 'a[b]', ].each { |name| assert_raises(ArgumentError) { HTTP::Cookie.new(cookie_values(:name => name)) } cookie = HTTP::Cookie.new(cookie_values) assert_raises(ArgumentError) { cookie.name = name } } end def test_compare time = Time.now cookies = [ { :created_at => time + 1 }, { :created_at => time - 1 }, { :created_at => time }, { :created_at => time, :path => '/foo/bar/' }, { :created_at => time, :path => '/foo/' }, { :created_at => time, :path => '/foo' }, ].map { |attrs| HTTP::Cookie.new(cookie_values(attrs)) } assert_equal([3, 4, 5, 1, 2, 0], cookies.sort.map { |i| cookies.find_index { |j| j.equal?(i) } }) end def test_expiration cookie = HTTP::Cookie.new(cookie_values) assert_equal false, cookie.expired? assert_equal true, cookie.expired?(cookie.expires + 1) assert_equal false, cookie.expired?(cookie.expires - 1) cookie.expire! assert_equal true, cookie.expired? end def test_max_age= cookie = HTTP::Cookie.new(cookie_values) expires = cookie.expires assert_raises(ArgumentError) { cookie.max_age = "+1" } # make sure #expires is not destroyed assert_equal expires, cookie.expires assert_raises(ArgumentError) { cookie.max_age = "1.5" } # make sure #expires is not destroyed assert_equal expires, cookie.expires assert_raises(ArgumentError) { cookie.max_age = "1 day" } # make sure #expires is not destroyed assert_equal expires, cookie.expires assert_raises(TypeError) { cookie.max_age = [1] } # make sure #expires is not destroyed assert_equal expires, cookie.expires cookie.max_age = "12" assert_equal 12, cookie.max_age cookie.max_age = -3 assert_equal -3, cookie.max_age end def test_session cookie = HTTP::Cookie.new(cookie_values) assert_equal false, cookie.session? assert_equal nil, cookie.max_age cookie.expires = nil assert_equal true, cookie.session? assert_equal nil, cookie.max_age cookie.expires = Time.now + 3600 assert_equal false, cookie.session? assert_equal nil, cookie.max_age cookie.max_age = 3600 assert_equal false, cookie.session? assert_equal cookie.created_at + 3600, cookie.expires cookie.max_age = nil assert_equal true, cookie.session? assert_equal nil, cookie.expires end def test_equal assert_not_equal(HTTP::Cookie.new(cookie_values), HTTP::Cookie.new(cookie_values(:value => 'bar'))) end def test_new_tld_domain url = URI 'http://rubyforge.org/' tld_cookie1 = HTTP::Cookie.new(cookie_values(:domain => 'org', :origin => url)) assert_equal false, tld_cookie1.for_domain? assert_equal 'org', tld_cookie1.domain assert_equal false, tld_cookie1.acceptable? tld_cookie2 = HTTP::Cookie.new(cookie_values(:domain => '.org', :origin => url)) assert_equal false, tld_cookie1.for_domain? assert_equal 'org', tld_cookie2.domain assert_equal false, tld_cookie2.acceptable? end def test_new_tld_domain_from_tld url = URI 'http://org/' tld_cookie1 = HTTP::Cookie.new(cookie_values(:domain => 'org', :origin => url)) assert_equal false, tld_cookie1.for_domain? assert_equal 'org', tld_cookie1.domain assert_equal true, tld_cookie1.acceptable? tld_cookie2 = HTTP::Cookie.new(cookie_values(:domain => '.org', :origin => url)) assert_equal false, tld_cookie1.for_domain? assert_equal 'org', tld_cookie2.domain assert_equal true, tld_cookie2.acceptable? end def test_fall_back_rules_for_local_domains url = URI 'http://www.example.local' tld_cookie = HTTP::Cookie.new(cookie_values(:domain => '.local', :origin => url)) assert_equal false, tld_cookie.acceptable? sld_cookie = HTTP::Cookie.new(cookie_values(:domain => '.example.local', :origin => url)) assert_equal true, sld_cookie.acceptable? end def test_new_rejects_cookies_with_ipv4_address_subdomain url = URI 'http://192.168.0.1/' cookie = HTTP::Cookie.new(cookie_values(:domain => '.0.1', :origin => url)) assert_equal false, cookie.acceptable? end def test_value cookie = HTTP::Cookie.new('name', 'value') assert_equal 'value', cookie.value cookie.value = 'new value' assert_equal 'new value', cookie.value assert_raises(ArgumentError) { cookie.value = "a\tb" } assert_raises(ArgumentError) { cookie.value = "a\nb" } assert_equal false, cookie.expired? cookie.value = nil assert_equal '', cookie.value assert_equal true, cookie.expired? end def test_path uri = URI.parse('http://example.com/foo/bar') assert_equal '/foo/bar', uri.path cookie_str = 'a=b' cookie = HTTP::Cookie.parse(cookie_str, uri).first assert '/foo/', cookie.path cookie_str = 'a=b; path=/foo' cookie = HTTP::Cookie.parse(cookie_str, uri).first assert '/foo', cookie.path uri = URI.parse('http://example.com') assert_equal '', uri.path cookie_str = 'a=b' cookie = HTTP::Cookie.parse(cookie_str, uri).first assert '/', cookie.path cookie_str = 'a=b; path=/foo' cookie = HTTP::Cookie.parse(cookie_str, uri).first assert '/foo', cookie.path end def test_domain_nil cookie = HTTP::Cookie.new('a', 'b') assert_raises(RuntimeError) { cookie.valid_for_uri?('http://example.com/') } end def test_domain= url = URI.parse('http://host.dom.example.com:8080/') cookie_str = 'a=b; domain=Example.Com' cookie = HTTP::Cookie.parse(cookie_str, url).first assert 'example.com', cookie.domain cookie.domain = DomainName(url.host) assert 'host.dom.example.com', cookie.domain cookie.domain = 'Dom.example.com' assert 'dom.example.com', cookie.domain cookie.domain = Object.new.tap { |o| def o.to_str 'Example.com' end } assert 'example.com', cookie.domain url = URI 'http://rubyforge.org/' [nil, '', '.'].each { |d| cookie = HTTP::Cookie.new('Foo', 'Bar', :path => '/') cookie.domain = d assert_equal nil, cookie.domain, "domain=#{d.inspect}" assert_equal nil, cookie.domain_name, "domain=#{d.inspect}" assert_raises(RuntimeError) { cookie.acceptable? } cookie = HTTP::Cookie.new('Foo', 'Bar', :path => '/') cookie.origin = url cookie.domain = d assert_equal url.host, cookie.domain, "domain=#{d.inspect}" assert_equal true, cookie.acceptable?, "domain=#{d.inspect}" } end def test_origin= url = URI.parse('http://example.com/path/') cookie = HTTP::Cookie.new('a', 'b') assert_raises(ArgumentError) { cookie.origin = 123 } cookie.origin = url assert_equal '/path/', cookie.path assert_equal 'example.com', cookie.domain assert_equal false, cookie.for_domain assert_raises(ArgumentError) { # cannot change the origin once set cookie.origin = URI.parse('http://www.example.com/') } cookie = HTTP::Cookie.new('a', 'b', :domain => '.example.com', :path => '/') cookie.origin = url assert_equal '/', cookie.path assert_equal 'example.com', cookie.domain assert_equal true, cookie.for_domain assert_raises(ArgumentError) { # cannot change the origin once set cookie.origin = URI.parse('http://www.example.com/') } cookie = HTTP::Cookie.new('a', 'b', :domain => '.example.com') cookie.origin = URI.parse('http://example.org/') assert_equal false, cookie.acceptable? cookie = HTTP::Cookie.new('a', 'b', :domain => '.example.com') cookie.origin = 'file:///tmp/test.html' assert_equal nil, cookie.path cookie = HTTP::Cookie.new('a', 'b', :domain => '.example.com', :path => '/') cookie.origin = 'file:///tmp/test.html' assert_equal false, cookie.acceptable? end def test_acceptable_from_uri? cookie = HTTP::Cookie.new(cookie_values( :domain => 'uk', :for_domain => true, :origin => nil)) assert_equal false, cookie.for_domain? assert_equal true, cookie.acceptable_from_uri?('http://uk/') assert_equal false, cookie.acceptable_from_uri?('http://foo.uk/') end def test_valid_for_uri? { HTTP::Cookie.parse('a1=b', 'http://example.com/dir/file.html').first => { true => [ 'http://example.com/dir/', 'http://example.com/dir/test.html', 'https://example.com/dir/', 'https://example.com/dir/test.html', ], false => [ 'file:///dir/test.html', 'http://example.com/dir', 'http://example.com/dir2/test.html', 'http://www.example.com/dir/test.html', 'http://www.example.com/dir2/test.html', 'https://example.com/dir', 'https://example.com/dir2/test.html', 'https://www.example.com/dir/test.html', 'https://www.example.com/dir2/test.html', ] }, HTTP::Cookie.parse('a2=b; path=/dir2/', 'http://example.com/dir/file.html').first => { true => [ 'http://example.com/dir2/', 'http://example.com/dir2/test.html', 'https://example.com/dir2/', 'https://example.com/dir2/test.html', ], false => [ 'file:///dir/test.html', 'http://example.com/dir/test.html', 'http://www.example.com/dir/test.html', 'http://www.example.com/dir2', 'http://www.example.com/dir2/test.html', 'https://example.com/dir/test.html', 'https://www.example.com/dir/test.html', 'https://www.example.com/dir2', 'https://www.example.com/dir2/test.html', ] }, HTTP::Cookie.parse('a4=b; domain=example.com; path=/dir2/', URI('http://example.com/dir/file.html')).first => { true => [ 'https://example.com/dir2/test.html', 'http://example.com/dir2/test.html', 'https://www.example.com/dir2/test.html', 'http://www.example.com/dir2/test.html', ], false => [ 'https://example.com/dir/test.html', 'http://example.com/dir/test.html', 'https://www.example.com/dir/test.html', 'http://www.example.com/dir/test.html', 'file:///dir2/test.html', ] }, HTTP::Cookie.parse('a4=b; secure', URI('https://example.com/dir/file.html')).first => { true => [ 'https://example.com/dir/test.html', ], false => [ 'http://example.com/dir/test.html', 'https://example.com/dir2/test.html', 'http://example.com/dir2/test.html', 'file:///dir2/test.html', ] }, HTTP::Cookie.parse('a5=b', URI('https://example.com/')).first => { true => [ 'https://example.com', ], false => [ 'file:///', ] }, HTTP::Cookie.parse('a6=b; path=/dir', 'http://example.com/dir/file.html').first => { true => [ 'http://example.com/dir', 'http://example.com/dir/', 'http://example.com/dir/test.html', 'https://example.com/dir', 'https://example.com/dir/', 'https://example.com/dir/test.html', ], false => [ 'file:///dir/test.html', 'http://example.com/dir2', 'http://example.com/dir2/test.html', 'http://www.example.com/dir/test.html', 'http://www.example.com/dir2/test.html', 'https://example.com/dir2', 'https://example.com/dir2/test.html', 'https://www.example.com/dir/test.html', 'https://www.example.com/dir2/test.html', ] }, }.each { |cookie, hash| hash.each { |expected, urls| urls.each { |url| assert_equal expected, cookie.valid_for_uri?(url), '%s: %s' % [cookie.name, url] assert_equal expected, cookie.valid_for_uri?(URI(url)), "%s: URI(%s)" % [cookie.name, url] } } } end def test_yaml_expires require 'yaml' cookie = HTTP::Cookie.new(cookie_values) assert_equal false, cookie.session? assert_equal nil, cookie.max_age ycookie = YAML.load(cookie.to_yaml) assert_equal false, ycookie.session? assert_equal nil, ycookie.max_age assert_in_delta cookie.expires, ycookie.expires, 1 cookie.expires = nil ycookie = YAML.load(cookie.to_yaml) assert_equal true, ycookie.session? assert_equal nil, ycookie.max_age cookie.expires = Time.now + 3600 ycookie = YAML.load(cookie.to_yaml) assert_equal false, ycookie.session? assert_equal nil, ycookie.max_age assert_in_delta cookie.expires, ycookie.expires, 1 cookie.max_age = 3600 ycookie = YAML.load(cookie.to_yaml) assert_equal false, ycookie.session? assert_in_delta cookie.created_at + 3600, ycookie.expires, 1 cookie.max_age = nil ycookie = YAML.load(cookie.to_yaml) assert_equal true, ycookie.session? assert_equal nil, ycookie.expires end def test_s_path_match? assert_equal true, HTTP::Cookie.path_match?('/admin/', '/admin/index') assert_equal false, HTTP::Cookie.path_match?('/admin/', '/Admin/index') assert_equal true, HTTP::Cookie.path_match?('/admin/', '/admin/') assert_equal false, HTTP::Cookie.path_match?('/admin/', '/admin') assert_equal true, HTTP::Cookie.path_match?('/admin', '/admin') assert_equal false, HTTP::Cookie.path_match?('/admin', '/Admin') assert_equal false, HTTP::Cookie.path_match?('/admin', '/admins') assert_equal true, HTTP::Cookie.path_match?('/admin', '/admin/') assert_equal true, HTTP::Cookie.path_match?('/admin', '/admin/index') end end ruby-http-cookie-1.0.3/test/test_http_cookie_jar.rb000066400000000000000000000771341341364502000224410ustar00rootroot00000000000000require File.expand_path('helper', File.dirname(__FILE__)) require 'tmpdir' module TestHTTPCookieJar class TestAutoloading < Test::Unit::TestCase def test_nonexistent_store assert_raises(NameError) { HTTP::CookieJar::NonexistentStore } end def test_erroneous_store Dir.mktmpdir { |dir| Dir.mkdir(File.join(dir, 'http')) Dir.mkdir(File.join(dir, 'http', 'cookie_jar')) rb = File.join(dir, 'http', 'cookie_jar', 'erroneous_store.rb') File.open(rb, 'w').close $LOAD_PATH.unshift(dir) assert_raises(NameError) { HTTP::CookieJar::ErroneousStore } if RUBY_VERSION >= "1.9" assert_includes $LOADED_FEATURES, rb else assert_includes $LOADED_FEATURES, rb[(dir.size + 1)..-1] end } end def test_nonexistent_saver assert_raises(NameError) { HTTP::CookieJar::NonexistentSaver } end def test_erroneous_saver Dir.mktmpdir { |dir| Dir.mkdir(File.join(dir, 'http')) Dir.mkdir(File.join(dir, 'http', 'cookie_jar')) rb = File.join(dir, 'http', 'cookie_jar', 'erroneous_saver.rb') File.open(rb, 'w').close $LOAD_PATH.unshift(dir) assert_raises(NameError) { HTTP::CookieJar::ErroneousSaver } if RUBY_VERSION >= "1.9" assert_includes $LOADED_FEATURES, rb else assert_includes $LOADED_FEATURES, rb[(dir.size + 1)..-1] end } end end module CommonTests def setup(options = nil, options2 = nil) default_options = { :store => :hash, :gc_threshold => 1500, # increased by 10 for shorter test time } new_options = default_options.merge(options || {}) new_options2 = new_options.merge(options2 || {}) @store_type = new_options[:store] @gc_threshold = new_options[:gc_threshold] @jar = HTTP::CookieJar.new(new_options) @jar2 = HTTP::CookieJar.new(new_options2) end #def hash_store? # @store_type == :hash #end def mozilla_store? @store_type == :mozilla end def cookie_values(options = {}) { :name => 'Foo', :value => 'Bar', :path => '/', :expires => Time.at(Time.now.to_i + 10 * 86400), # to_i is important here :for_domain => true, :domain => 'rubyforge.org', :origin => 'http://rubyforge.org/' }.merge(options) end def test_empty? assert_equal true, @jar.empty? cookie = HTTP::Cookie.new(cookie_values) @jar.add(cookie) assert_equal false, @jar.empty? assert_equal false, @jar.empty?('http://rubyforge.org/') assert_equal true, @jar.empty?('http://example.local/') end def test_two_cookies_same_domain_and_name_different_paths url = URI 'http://rubyforge.org/' cookie = HTTP::Cookie.new(cookie_values) @jar.add(cookie) @jar.add(HTTP::Cookie.new(cookie_values(:path => '/onetwo'))) assert_equal(1, @jar.cookies(url).length) assert_equal 2, @jar.cookies(URI('http://rubyforge.org/onetwo')).length end def test_domain_case url = URI 'http://rubyforge.org/' # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(cookie_values) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) @jar.add(HTTP::Cookie.new(cookie_values(:domain => 'RuByForge.Org', :name => 'aaron'))) assert_equal(2, @jar.cookies(url).length) url2 = URI 'http://RuByFoRgE.oRg/' assert_equal(2, @jar.cookies(url2).length) end def test_host_only url = URI.parse('http://rubyforge.org/') @jar.add(HTTP::Cookie.new( cookie_values(:domain => 'rubyforge.org', :for_domain => false))) assert_equal(1, @jar.cookies(url).length) assert_equal(1, @jar.cookies(URI('http://RubyForge.org/')).length) assert_equal(1, @jar.cookies(URI('https://RubyForge.org/')).length) assert_equal(0, @jar.cookies(URI('http://www.rubyforge.org/')).length) end def test_empty_value url = URI 'http://rubyforge.org/' values = cookie_values(:value => "") # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(values) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) @jar.add HTTP::Cookie.new(values.merge(:domain => 'RuByForge.Org', :name => 'aaron')) assert_equal(2, @jar.cookies(url).length) url2 = URI 'http://RuByFoRgE.oRg/' assert_equal(2, @jar.cookies(url2).length) end def test_add_future_cookies url = URI 'http://rubyforge.org/' # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(cookie_values) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) # Add the same cookie, and we should still only have one @jar.add(HTTP::Cookie.new(cookie_values)) assert_equal(1, @jar.cookies(url).length) # Make sure we can get the cookie from different paths assert_equal(1, @jar.cookies(URI('http://rubyforge.org/login')).length) # Make sure we can't get the cookie from different domains assert_equal(0, @jar.cookies(URI('http://google.com/')).length) end def test_add_multiple_cookies url = URI 'http://rubyforge.org/' # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(cookie_values) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) # Add the same cookie, and we should still only have one @jar.add(HTTP::Cookie.new(cookie_values(:name => 'Baz'))) assert_equal(2, @jar.cookies(url).length) # Make sure we can get the cookie from different paths assert_equal(2, @jar.cookies(URI('http://rubyforge.org/login')).length) # Make sure we can't get the cookie from different domains assert_equal(0, @jar.cookies(URI('http://google.com/')).length) end def test_add_multiple_cookies_with_the_same_name now = Time.now cookies = [ { :value => 'a', :path => '/', }, { :value => 'b', :path => '/abc/def/', :created_at => now - 1 }, { :value => 'c', :path => '/abc/def/', :domain => 'www.rubyforge.org', :origin => 'http://www.rubyforge.org/abc/def/', :created_at => now }, { :value => 'd', :path => '/abc/' }, ].map { |attrs| HTTP::Cookie.new(cookie_values(attrs)) } url = URI 'http://www.rubyforge.org/abc/def/ghi' cookies.permutation(cookies.size) { |shuffled| @jar.clear shuffled.each { |cookie| @jar.add(cookie) } assert_equal %w[b c d a], @jar.cookies(url).map { |cookie| cookie.value } } end def test_fall_back_rules_for_local_domains url = URI 'http://www.example.local' sld_cookie = HTTP::Cookie.new(cookie_values(:domain => '.example.local', :origin => url)) @jar.add(sld_cookie) assert_equal(1, @jar.cookies(url).length) end def test_add_makes_exception_for_localhost url = URI 'http://localhost' tld_cookie = HTTP::Cookie.new(cookie_values(:domain => 'localhost', :origin => url)) @jar.add(tld_cookie) assert_equal(1, @jar.cookies(url).length) end def test_add_cookie_for_the_parent_domain url = URI 'http://x.foo.com' cookie = HTTP::Cookie.new(cookie_values(:domain => '.foo.com', :origin => url)) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) end def test_add_rejects_cookies_with_unknown_domain_or_path cookie = HTTP::Cookie.new(cookie_values.reject { |k,v| [:origin, :domain].include?(k) }) assert_raises(ArgumentError) { @jar.add(cookie) } cookie = HTTP::Cookie.new(cookie_values.reject { |k,v| [:origin, :path].include?(k) }) assert_raises(ArgumentError) { @jar.add(cookie) } end def test_add_does_not_reject_cookies_from_a_nested_subdomain url = URI 'http://y.x.foo.com' cookie = HTTP::Cookie.new(cookie_values(:domain => '.foo.com', :origin => url)) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) end def test_cookie_without_leading_dot_does_not_cause_substring_match url = URI 'http://arubyforge.org/' cookie = HTTP::Cookie.new(cookie_values(:domain => 'rubyforge.org')) @jar.add(cookie) assert_equal(0, @jar.cookies(url).length) end def test_cookie_without_leading_dot_matches_subdomains url = URI 'http://admin.rubyforge.org/' cookie = HTTP::Cookie.new(cookie_values(:domain => 'rubyforge.org', :origin => url)) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) end def test_cookies_with_leading_dot_match_subdomains url = URI 'http://admin.rubyforge.org/' @jar.add(HTTP::Cookie.new(cookie_values(:domain => '.rubyforge.org', :origin => url))) assert_equal(1, @jar.cookies(url).length) end def test_cookies_with_leading_dot_match_parent_domains url = URI 'http://rubyforge.org/' @jar.add(HTTP::Cookie.new(cookie_values(:domain => '.rubyforge.org', :origin => url))) assert_equal(1, @jar.cookies(url).length) end def test_cookies_with_leading_dot_match_parent_domains_exactly url = URI 'http://arubyforge.org/' @jar.add(HTTP::Cookie.new(cookie_values(:domain => '.rubyforge.org'))) assert_equal(0, @jar.cookies(url).length) end def test_cookie_for_ipv4_address_matches_the_exact_ipaddress url = URI 'http://192.168.0.1/' cookie = HTTP::Cookie.new(cookie_values(:domain => '192.168.0.1', :origin => url)) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) end def test_cookie_for_ipv6_address_matches_the_exact_ipaddress url = URI 'http://[fe80::0123:4567:89ab:cdef]/' cookie = HTTP::Cookie.new(cookie_values(:domain => '[fe80::0123:4567:89ab:cdef]', :origin => url)) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) end def test_cookies_dot url = URI 'http://www.host.example/' @jar.add(HTTP::Cookie.new(cookie_values(:domain => 'www.host.example', :origin => url))) url = URI 'http://wwwxhost.example/' assert_equal(0, @jar.cookies(url).length) end def test_cookies_no_host url = URI 'file:///path/' @jar.add(HTTP::Cookie.new(cookie_values(:origin => url))) assert_equal(0, @jar.cookies(url).length) end def test_clear url = URI 'http://rubyforge.org/' # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(cookie_values(:origin => url)) @jar.add(cookie) @jar.add(HTTP::Cookie.new(cookie_values(:name => 'Baz', :origin => url))) assert_equal(2, @jar.cookies(url).length) @jar.clear assert_equal(0, @jar.cookies(url).length) end def test_save_cookies_yaml url = URI 'http://rubyforge.org/' # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(cookie_values(:origin => url)) s_cookie = HTTP::Cookie.new(cookie_values(:name => 'Bar', :expires => nil, :origin => url)) @jar.add(cookie) @jar.add(s_cookie) @jar.add(HTTP::Cookie.new(cookie_values(:name => 'Baz', :for_domain => false, :origin => url))) assert_equal(3, @jar.cookies(url).length) Dir.mktmpdir do |dir| value = @jar.save(File.join(dir, "cookies.yml")) assert_same @jar, value @jar2.load(File.join(dir, "cookies.yml")) cookies = @jar2.cookies(url).sort_by { |cookie| cookie.name } assert_equal(2, cookies.length) assert_equal('Baz', cookies[0].name) assert_equal(false, cookies[0].for_domain) assert_equal('Foo', cookies[1].name) assert_equal(true, cookies[1].for_domain) end assert_equal(3, @jar.cookies(url).length) end def test_save_load_signature Dir.mktmpdir { |dir| filename = File.join(dir, "cookies.yml") @jar.save(filename, :format => :cookiestxt, :session => true) @jar.save(filename, :format => :cookiestxt, :session => true) @jar.save(filename, :format => :cookiestxt) @jar.save(filename, :cookiestxt, :session => true) @jar.save(filename, :cookiestxt) @jar.save(filename, HTTP::CookieJar::CookiestxtSaver) @jar.save(filename, HTTP::CookieJar::CookiestxtSaver.new) @jar.save(filename, :session => true) @jar.save(filename) assert_raises(ArgumentError) { @jar.save() } assert_raises(ArgumentError) { @jar.save(filename, :nonexistent) } assert_raises(TypeError) { @jar.save(filename, { :format => :cookiestxt }, { :session => true }) } assert_raises(ArgumentError) { @jar.save(filename, :cookiestxt, { :session => true }, { :format => :cookiestxt }) } @jar.load(filename, :format => :cookiestxt, :linefeed => "\n") @jar.load(filename, :format => :cookiestxt, :linefeed => "\n") @jar.load(filename, :format => :cookiestxt) @jar.load(filename, HTTP::CookieJar::CookiestxtSaver) @jar.load(filename, HTTP::CookieJar::CookiestxtSaver.new) @jar.load(filename, :cookiestxt, :linefeed => "\n") @jar.load(filename, :cookiestxt) @jar.load(filename, :linefeed => "\n") @jar.load(filename) assert_raises(ArgumentError) { @jar.load() } assert_raises(ArgumentError) { @jar.load(filename, :nonexistent) } assert_raises(TypeError) { @jar.load(filename, { :format => :cookiestxt }, { :linefeed => "\n" }) } assert_raises(ArgumentError) { @jar.load(filename, :cookiestxt, { :linefeed => "\n" }, { :format => :cookiestxt }) } } end def test_save_session_cookies_yaml url = URI 'http://rubyforge.org/' # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(cookie_values) s_cookie = HTTP::Cookie.new(cookie_values(:name => 'Bar', :expires => nil)) @jar.add(cookie) @jar.add(s_cookie) @jar.add(HTTP::Cookie.new(cookie_values(:name => 'Baz'))) assert_equal(3, @jar.cookies(url).length) Dir.mktmpdir do |dir| @jar.save(File.join(dir, "cookies.yml"), :format => :yaml, :session => true) @jar2.load(File.join(dir, "cookies.yml")) assert_equal(3, @jar2.cookies(url).length) end assert_equal(3, @jar.cookies(url).length) end def test_save_and_read_cookiestxt url = URI 'http://rubyforge.org/foo/' # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(cookie_values) expires = cookie.expires s_cookie = HTTP::Cookie.new(cookie_values(:name => 'Bar', :expires => nil)) cookie2 = HTTP::Cookie.new(cookie_values(:name => 'Baz', :value => 'Foo#Baz', :path => '/foo/', :for_domain => false)) h_cookie = HTTP::Cookie.new(cookie_values(:name => 'Quux', :value => 'Foo#Quux', :httponly => true)) ma_cookie = HTTP::Cookie.new(cookie_values(:name => 'Maxage', :value => 'Foo#Maxage', :max_age => 15000)) @jar.add(cookie) @jar.add(s_cookie) @jar.add(cookie2) @jar.add(h_cookie) @jar.add(ma_cookie) assert_equal(5, @jar.cookies(url).length) Dir.mktmpdir do |dir| filename = File.join(dir, "cookies.txt") @jar.save(filename, :cookiestxt) content = File.read(filename) filename2 = File.join(dir, "cookies2.txt") open(filename2, 'w') { |w| w.puts '# HTTP Cookie File' @jar.save(w, :cookiestxt, :header => nil) } assert_equal content, File.read(filename2) assert_match(/^\.rubyforge\.org\t.*\tFoo\t/, content) assert_match(/^rubyforge\.org\t.*\tBaz\t/, content) assert_match(/^#HttpOnly_\.rubyforge\.org\t/, content) @jar2.load(filename, :cookiestxt) # HACK test the format cookies = @jar2.cookies(url) assert_equal(4, cookies.length) cookies.each { |cookie| case cookie.name when 'Foo' assert_equal 'Bar', cookie.value assert_equal expires, cookie.expires assert_equal 'rubyforge.org', cookie.domain assert_equal true, cookie.for_domain assert_equal '/', cookie.path assert_equal false, cookie.httponly? when 'Baz' assert_equal 'Foo#Baz', cookie.value assert_equal 'rubyforge.org', cookie.domain assert_equal false, cookie.for_domain assert_equal '/foo/', cookie.path assert_equal false, cookie.httponly? when 'Quux' assert_equal 'Foo#Quux', cookie.value assert_equal expires, cookie.expires assert_equal 'rubyforge.org', cookie.domain assert_equal true, cookie.for_domain assert_equal '/', cookie.path assert_equal true, cookie.httponly? when 'Maxage' assert_equal 'Foo#Maxage', cookie.value assert_equal nil, cookie.max_age assert_in_delta ma_cookie.expires, cookie.expires, 1 else raise end } end assert_equal(5, @jar.cookies(url).length) end def test_load_yaml_mechanize @jar.load(test_file('mechanize.yml'), :yaml) assert_equal 4, @jar.to_a.size com_nid, com_pref = @jar.cookies('http://www.google.com/') assert_equal 'NID', com_nid.name assert_equal 'Sun, 23 Sep 2063 08:20:15 GMT', com_nid.expires.httpdate assert_equal 'google.com', com_nid.domain_name.hostname assert_equal 'PREF', com_pref.name assert_equal 'Tue, 24 Mar 2065 08:20:15 GMT', com_pref.expires.httpdate assert_equal 'google.com', com_pref.domain_name.hostname cojp_nid, cojp_pref = @jar.cookies('http://www.google.co.jp/') assert_equal 'NID', cojp_nid.name assert_equal 'Sun, 23 Sep 2063 08:20:16 GMT', cojp_nid.expires.httpdate assert_equal 'google.co.jp', cojp_nid.domain_name.hostname assert_equal 'PREF', cojp_pref.name assert_equal 'Tue, 24 Mar 2065 08:20:16 GMT', cojp_pref.expires.httpdate assert_equal 'google.co.jp', cojp_pref.domain_name.hostname end def test_expire_cookies url = URI 'http://rubyforge.org/' # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(cookie_values) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) # Add a second cookie @jar.add(HTTP::Cookie.new(cookie_values(:name => 'Baz'))) assert_equal(2, @jar.cookies(url).length) # Make sure we can get the cookie from different paths assert_equal(2, @jar.cookies(URI('http://rubyforge.org/login')).length) # Expire the first cookie @jar.add(HTTP::Cookie.new(cookie_values(:expires => Time.now - (10 * 86400)))) assert_equal(1, @jar.cookies(url).length) # Expire the second cookie @jar.add(HTTP::Cookie.new(cookie_values( :name => 'Baz', :expires => Time.now - (10 * 86400)))) assert_equal(0, @jar.cookies(url).length) end def test_session_cookies values = cookie_values(:expires => nil) url = URI 'http://rubyforge.org/' # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(values) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) # Add a second cookie @jar.add(HTTP::Cookie.new(values.merge(:name => 'Baz'))) assert_equal(2, @jar.cookies(url).length) # Make sure we can get the cookie from different paths assert_equal(2, @jar.cookies(URI('http://rubyforge.org/login')).length) # Expire the first cookie @jar.add(HTTP::Cookie.new(values.merge(:expires => Time.now - (10 * 86400)))) assert_equal(1, @jar.cookies(url).length) # Expire the second cookie @jar.add(HTTP::Cookie.new(values.merge(:name => 'Baz', :expires => Time.now - (10 * 86400)))) assert_equal(0, @jar.cookies(url).length) # When given a URI with a blank path, CookieJar#cookies should return # cookies with the path '/': url = URI 'http://rubyforge.org' assert_equal '', url.path assert_equal(0, @jar.cookies(url).length) # Now add a cookie with the path set to '/': @jar.add(HTTP::Cookie.new(values.merge(:name => 'has_root_path', :path => '/'))) assert_equal(1, @jar.cookies(url).length) end def test_paths url = URI 'http://rubyforge.org/login' values = cookie_values(:path => "/login", :expires => nil, :origin => url) # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(values) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length) # Add a second cookie @jar.add(HTTP::Cookie.new(values.merge( :name => 'Baz' ))) assert_equal(2, @jar.cookies(url).length) # Make sure we don't get the cookie in a different path assert_equal(0, @jar.cookies(URI('http://rubyforge.org/hello')).length) assert_equal(0, @jar.cookies(URI('http://rubyforge.org/')).length) # Expire the first cookie @jar.add(HTTP::Cookie.new(values.merge( :expires => Time.now - (10 * 86400)))) assert_equal(1, @jar.cookies(url).length) # Expire the second cookie @jar.add(HTTP::Cookie.new(values.merge( :name => 'Baz', :expires => Time.now - (10 * 86400)))) assert_equal(0, @jar.cookies(url).length) end def test_ssl_cookies # thanks to michal "ocher" ochman for reporting the bug responsible for this test. values = cookie_values(:expires => nil) values_ssl = values.merge(:name => 'Baz', :domain => "#{values[:domain]}:443") url = URI 'https://rubyforge.org/login' cookie = HTTP::Cookie.new(values) @jar.add(cookie) assert_equal(1, @jar.cookies(url).length, "did not handle SSL cookie") cookie = HTTP::Cookie.new(values_ssl) @jar.add(cookie) assert_equal(2, @jar.cookies(url).length, "did not handle SSL cookie with :443") end def test_secure_cookie nurl = URI 'http://rubyforge.org/login' surl = URI 'https://rubyforge.org/login' nncookie = HTTP::Cookie.new(cookie_values(:name => 'Foo1', :origin => nurl)) sncookie = HTTP::Cookie.new(cookie_values(:name => 'Foo1', :origin => surl)) nscookie = HTTP::Cookie.new(cookie_values(:name => 'Foo2', :secure => true, :origin => nurl)) sscookie = HTTP::Cookie.new(cookie_values(:name => 'Foo2', :secure => true, :origin => surl)) @jar.add(nncookie) @jar.add(sncookie) @jar.add(nscookie) @jar.add(sscookie) assert_equal('Foo1', @jar.cookies(nurl).map { |c| c.name }.sort.join(' ') ) assert_equal('Foo1 Foo2', @jar.cookies(surl).map { |c| c.name }.sort.join(' ') ) end def test_delete cookie1 = HTTP::Cookie.new(cookie_values) cookie2 = HTTP::Cookie.new(:name => 'Foo', :value => '', :domain => 'rubyforge.org', :for_domain => false, :path => '/') cookie3 = HTTP::Cookie.new(:name => 'Foo', :value => '', :domain => 'rubyforge.org', :for_domain => true, :path => '/') @jar.add(cookie1) @jar.delete(cookie2) if mozilla_store? assert_equal(1, @jar.to_a.length) @jar.delete(cookie3) end assert_equal(0, @jar.to_a.length) end def test_accessed_at orig = HTTP::Cookie.new(cookie_values(:expires => nil)) @jar.add(orig) time = orig.accessed_at assert_in_delta 1.0, time, Time.now, "accessed_at is initialized to the current time" cookie, = @jar.to_a assert_equal time, cookie.accessed_at, "accessed_at is not updated by each()" cookie, = @jar.cookies("http://rubyforge.org/") assert_send [cookie.accessed_at, :>, time], "accessed_at is not updated by each(url)" end def test_max_cookies slimit = HTTP::Cookie::MAX_COOKIES_TOTAL + @gc_threshold limit_per_domain = HTTP::Cookie::MAX_COOKIES_PER_DOMAIN uri = URI('http://www.example.org/') date = Time.at(Time.now.to_i + 86400) (1..(limit_per_domain + 1)).each { |i| @jar << HTTP::Cookie.new(cookie_values( :name => 'Foo%d' % i, :value => 'Bar%d' % i, :domain => uri.host, :for_domain => true, :path => '/dir%d/' % (i / 2), :origin => uri )).tap { |cookie| cookie.created_at = i == 42 ? date - i : date } } assert_equal limit_per_domain + 1, @jar.to_a.size @jar.cleanup count = @jar.to_a.size assert_equal limit_per_domain, count assert_equal [*1..(limit_per_domain + 1)] - [42], @jar.map { |cookie| cookie.name[/(\d+)$/].to_i }.sort hlimit = HTTP::Cookie::MAX_COOKIES_TOTAL n = hlimit / limit_per_domain * 2 (1..n).each { |i| (1..(limit_per_domain + 1)).each { |j| uri = URI('http://www%d.example.jp/' % i) @jar << HTTP::Cookie.new(cookie_values( :name => 'Baz%d' % j, :value => 'www%d.example.jp' % j, :domain => uri.host, :for_domain => true, :path => '/dir%d/' % (i / 2), :origin => uri )).tap { |cookie| cookie.created_at = i == j ? date - i : date } count += 1 } } assert_send [count, :>, slimit] assert_send [@jar.to_a.size, :<=, slimit] @jar.cleanup assert_equal hlimit, @jar.to_a.size assert_equal false, @jar.any? { |cookie| cookie.domain == cookie.value } end def test_parse set_cookie = [ "name=Akinori; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/", "country=Japan; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/", "city=Tokyo; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/", ].join(', ') cookies = @jar.parse(set_cookie, 'http://rubyforge.org/') assert_equal %w[Akinori Japan Tokyo], cookies.map { |c| c.value } assert_equal %w[Tokyo Japan Akinori], @jar.to_a.sort_by { |c| c.name }.map { |c| c.value } end def test_parse_with_block set_cookie = [ "name=Akinori; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/", "country=Japan; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/", "city=Tokyo; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/", ].join(', ') cookies = @jar.parse(set_cookie, 'http://rubyforge.org/') { |c| c.name != 'city' } assert_equal %w[Akinori Japan], cookies.map { |c| c.value } assert_equal %w[Japan Akinori], @jar.to_a.sort_by { |c| c.name }.map { |c| c.value } end def test_expire_by_each_and_cleanup uri = URI('http://www.example.org/') ts = Time.now.to_f if ts % 1 > 0.5 sleep 0.5 ts += 0.5 end expires = Time.at(ts.floor) time = expires if mozilla_store? # MozillaStore only has the time precision of seconds. time = expires expires -= 1 end 0.upto(2) { |i| c = HTTP::Cookie.new('Foo%d' % (3 - i), 'Bar', :expires => expires + i, :origin => uri) @jar << c @jar2 << c } assert_equal %w[Foo1 Foo2], @jar.cookies.map(&:name) assert_equal %w[Foo1 Foo2], @jar2.cookies(uri).map(&:name) sleep_until time + 1 assert_equal %w[Foo1], @jar.cookies.map(&:name) assert_equal %w[Foo1], @jar2.cookies(uri).map(&:name) sleep_until time + 2 @jar.cleanup @jar2.cleanup assert_send [@jar, :empty?] assert_send [@jar2, :empty?] end end class WithHashStore < Test::Unit::TestCase include CommonTests def test_new jar = HTTP::CookieJar.new(:store => :hash) assert_instance_of HTTP::CookieJar::HashStore, jar.store assert_raises(ArgumentError) { jar = HTTP::CookieJar.new(:store => :nonexistent) } jar = HTTP::CookieJar.new(:store => HTTP::CookieJar::HashStore.new) assert_instance_of HTTP::CookieJar::HashStore, jar.store jar = HTTP::CookieJar.new(:store => HTTP::CookieJar::HashStore) end def test_clone jar = @jar.clone assert_not_send [ @jar.store, :equal?, jar.store ] assert_not_send [ @jar.store.instance_variable_get(:@jar), :equal?, jar.store.instance_variable_get(:@jar) ] assert_equal @jar.cookies, jar.cookies end end class WithMozillaStore < Test::Unit::TestCase include CommonTests def setup super( { :store => :mozilla, :filename => ":memory:" }, { :store => :mozilla, :filename => ":memory:" }) end def add_and_delete(jar) jar.parse("name=Akinori; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/", 'http://rubyforge.org/') jar.parse("country=Japan; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/", 'http://rubyforge.org/') jar.delete(HTTP::Cookie.new("name", :domain => 'rubyforge.org')) end def test_clone assert_raises(TypeError) { @jar.clone } end def test_close add_and_delete(@jar) assert_not_send [@jar.store, :closed?] @jar.store.close assert_send [@jar.store, :closed?] @jar.store.close # should do nothing assert_send [@jar.store, :closed?] end def test_finalizer db = nil loop { jar = HTTP::CookieJar.new(:store => :mozilla, :filename => ':memory:') add_and_delete(jar) db = jar.store.instance_variable_get(:@db) class << db alias close_orig close def close STDERR.print "[finalizer is called]" STDERR.flush close_orig end end break } end def test_upgrade_mozillastore Dir.mktmpdir { |dir| filename = File.join(dir, 'cookies.sqlite') sqlite = SQLite3::Database.new(filename) sqlite.execute(<<-'SQL') CREATE TABLE moz_cookies ( id INTEGER PRIMARY KEY, name TEXT, value TEXT, host TEXT, path TEXT, expiry INTEGER, isSecure INTEGER, isHttpOnly INTEGER) SQL sqlite.execute(<<-'SQL') PRAGMA user_version = 1 SQL begin st_insert = sqlite.prepare(<<-'SQL') INSERT INTO moz_cookies ( id, name, value, host, path, expiry, isSecure, isHttpOnly ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) SQL st_insert.execute(1, 'name1', 'value1', '.example.co.jp', '/', 2312085765, 0, 0) st_insert.execute(2, 'name1', 'value2', '.example.co.jp', '/', 2312085765, 0, 0) st_insert.execute(3, 'name1', 'value3', 'www.example.co.jp', '/', 2312085765, 0, 0) ensure st_insert.close if st_insert end sqlite.close jar = HTTP::CookieJar.new(:store => :mozilla, :filename => filename) assert_equal 2, jar.to_a.size assert_equal 2, jar.cookies('http://www.example.co.jp/').size cookie, *rest = jar.cookies('http://host.example.co.jp/') assert_send [rest, :empty?] assert_equal 'value2', cookie.value } end end if begin require 'sqlite3' true rescue LoadError STDERR.puts 'sqlite3 missing?' false end end