From 532101a102295a459ece80032efb1b5ac95df6ec Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Wed, 17 Oct 2012 22:13:03 +0900 Subject: [PATCH] Introduce a new cookie object attribute "origin". Change the signature of HTTP::Cookie.parse() so that it only optionally takes an origin URI. When one is given, the method checks if each piece of cookie in the header value is valid and acceptable from the origin to ignore unacceptable cookies. --- lib/http/cookie.rb | 44 +++++++++++++++++----- test/test_http_cookie.rb | 79 +++++++++++++++++++++++++++------------- 2 files changed, 88 insertions(+), 35 deletions(-) diff --git a/lib/http/cookie.rb b/lib/http/cookie.rb index b258b32..b0c6a97 100644 --- a/lib/http/cookie.rb +++ b/lib/http/cookie.rb @@ -31,6 +31,8 @@ class HTTP::Cookie attr_accessor :created_at attr_accessor :accessed_at + attr_accessor :origin + # :call-seq: # new(name, value) # new(name, value, attr_hash) @@ -95,12 +97,21 @@ class HTTP::Cookie class << self include URIFix if defined?(URIFix) - # Parses a Set-Cookie header value +set_cookie+ sent from +origin+ - # into an array of Cookie objects. Parts (separated by commas) - # that are malformed are ignored. + # Parses a Set-Cookie header value +set_cookie+ into an array of + # Cookie objects. Parts (separated by commas) that are malformed + # are ignored. # # If a block is given, each cookie object is passed to the block. - def parse(origin, set_cookie, logger = nil) + # + # The cookie's origin URI/URL and a logger object can be passed in + # +options+ with the keywords +:origin+ and +:logger+, + # respectively. + def parse(set_cookie, options = nil, &block) + if options + logger = options[:logger] + origin = options[:origin] and origin = URI(origin) + end + [].tap { |cookies| set_cookie.split(/,(?=[^;,]*=)|,$/).each { |c| cookie_elem = c.split(/;+/) @@ -163,15 +174,21 @@ class HTTP::Cookie end end - cookie.path ||= (origin + './').path cookie.secure ||= false - cookie.domain ||= origin.host # RFC 6265 4.1.2.2 cookie.expires = Time.now + cookie.max_age if cookie.max_age cookie.session = !cookie.expires - # Move this in to the cookie jar + if origin + begin + cookie.origin = origin + rescue => e + logger.warn("Invalid cookie for the origin: #{origin} (#{e})") if logger + next + end + end + yield cookie if block_given? cookies << cookie @@ -202,6 +219,15 @@ class HTTP::Cookie @domain = @domain_name.hostname end + def origin=(origin) + origin = URI(origin) + acceptable_from_uri?(origin) or + raise ArgumentError, "unacceptable cookie sent from URI #{origin}" + self.domain ||= origin.host + self.path ||= (origin + './').path + @origin = origin + end + def expires=(t) @expires = t && (t.is_a?(Time) ? t.httpdate : t.to_s) end @@ -222,7 +248,7 @@ class HTTP::Cookie # RFC 6265 5.3 # When the user agent "receives a cookie": - return host.hostname == domain unless @for_domain + return domain.nil? || host.hostname == domain unless @for_domain if host.cookie_domain?(@domain_name) true @@ -236,7 +262,7 @@ class HTTP::Cookie def valid_for_uri?(uri) return false if secure? && uri.scheme != 'https' - acceptable_from_uri?(uri) && uri.path.start_with?(path) + acceptable_from_uri?(uri) && (@path.nil? || uri.path.start_with?(@path)) end def to_s diff --git a/test/test_http_cookie.rb b/test/test_http_cookie.rb index 88881b2..cbe8a11 100644 --- a/test/test_http_cookie.rb +++ b/test/test_http_cookie.rb @@ -34,7 +34,7 @@ class TestHTTPCookie < Test::Unit::TestCase dates.each do |date| cookie = "PREF=1; expires=#{date}" silently do - HTTP::Cookie.parse(url, cookie) { |c| + HTTP::Cookie.parse(cookie, :origin => url) { |c| assert c.expires, "Tried parsing: #{date}" assert_equal(true, c.expires < yesterday) } @@ -47,7 +47,7 @@ class TestHTTPCookie < Test::Unit::TestCase uri = URI.parse 'http://example' - HTTP::Cookie.parse uri, cookie_str do |cookie| + HTTP::Cookie.parse cookie_str, :origin => uri do |cookie| assert_equal 'a', cookie.name assert_equal 'b', cookie.value end @@ -58,7 +58,7 @@ class TestHTTPCookie < Test::Unit::TestCase uri = URI.parse 'http://example' - HTTP::Cookie.parse uri, cookie_str do |cookie| + HTTP::Cookie.parse cookie_str, :origin => uri do |cookie| assert_equal 'foo', cookie.name assert_equal 'bar', cookie.value assert_equal '/', cookie.path @@ -72,7 +72,7 @@ class TestHTTPCookie < Test::Unit::TestCase uri = URI.parse 'http://example' - HTTP::Cookie.parse uri, cookie_str do |cookie| + HTTP::Cookie.parse cookie_str, :origin => uri do |cookie| assert_equal 'quoted', cookie.name assert_equal '"value"', cookie.value end @@ -81,7 +81,7 @@ class TestHTTPCookie < Test::Unit::TestCase def test_parse_weird_cookie cookie = 'n/a, ASPSESSIONIDCSRRQDQR=FBLDGHPBNDJCPCGNCPAENELB; path=/' url = URI.parse('http://www.searchinnovation.com/') - HTTP::Cookie.parse(url, cookie) { |c| + HTTP::Cookie.parse(cookie, :origin => url) { |c| assert_equal('ASPSESSIONIDCSRRQDQR', c.name) assert_equal('FBLDGHPBNDJCPCGNCPAENELB', c.value) } @@ -90,7 +90,7 @@ class TestHTTPCookie < Test::Unit::TestCase def test_double_semicolon double_semi = 'WSIDC=WEST;; domain=.williams-sonoma.com; path=/' url = URI.parse('http://williams-sonoma.com/') - HTTP::Cookie.parse(url, double_semi) { |cookie| + HTTP::Cookie.parse(double_semi, :origin => url) { |cookie| assert_equal('WSIDC', cookie.name) assert_equal('WEST', cookie.value) } @@ -99,7 +99,7 @@ class TestHTTPCookie < Test::Unit::TestCase 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://localhost/') - HTTP::Cookie.parse(url, bad_cookie) { |cookie| + HTTP::Cookie.parse(bad_cookie, :origin => url) { |cookie| assert_nil(cookie.version) } end @@ -107,7 +107,7 @@ class TestHTTPCookie < Test::Unit::TestCase def test_parse_bad_max_age bad_cookie = 'PRETANET=TGIAqbFXtt; Name=/PRETANET; Path=/; Max-Age=1.2; Content-type=text/html; Domain=192.168.6.196; expires=Friday, 13-November-2026 23:01:46 GMT;' url = URI.parse('http://localhost/') - HTTP::Cookie.parse(url, bad_cookie) { |cookie| + HTTP::Cookie.parse(bad_cookie, :origin => url) { |cookie| assert_nil(cookie.max_age) } end @@ -122,7 +122,7 @@ class TestHTTPCookie < Test::Unit::TestCase silently do dates.each do |date| cookie = "PREF=1; expires=#{date}" - HTTP::Cookie.parse(url, cookie) { |c| + HTTP::Cookie.parse(cookie, :origin => url) { |c| assert_equal(true, c.expires.nil?) } end @@ -134,7 +134,7 @@ class TestHTTPCookie < Test::Unit::TestCase cookie_str = 'a=b; domain=.example.com' - cookie = HTTP::Cookie.parse(url, cookie_str).first + cookie = HTTP::Cookie.parse(cookie_str, :origin => url).first assert_equal 'example.com', cookie.domain assert cookie.for_domain? @@ -145,7 +145,7 @@ class TestHTTPCookie < Test::Unit::TestCase cookie_str = 'a=b; domain=example.com' - cookie = HTTP::Cookie.parse(url, cookie_str).first + cookie = HTTP::Cookie.parse(cookie_str, :origin => url).first assert_equal 'example.com', cookie.domain assert cookie.for_domain? @@ -156,7 +156,7 @@ class TestHTTPCookie < Test::Unit::TestCase cookie_str = 'a=b;' - cookie = HTTP::Cookie.parse(url, cookie_str).first + cookie = HTTP::Cookie.parse(cookie_str, :origin => url).first assert_equal 'example.com', cookie.domain assert !cookie.for_domain? @@ -167,17 +167,17 @@ class TestHTTPCookie < Test::Unit::TestCase date = 'Mon, 19 Feb 2012 19:26:04 GMT' - cookie = HTTP::Cookie.parse(url, "name=Akinori; expires=#{date}").first + cookie = HTTP::Cookie.parse("name=Akinori; expires=#{date}", :origin => url).first assert_equal Time.at(1329679564), cookie.expires - cookie = HTTP::Cookie.parse(url, 'name=Akinori; max-age=3600').first + cookie = HTTP::Cookie.parse('name=Akinori; max-age=3600', :origin => url).first assert_in_delta Time.now + 3600, cookie.expires, 1 # Max-Age has precedence over Expires - cookie = HTTP::Cookie.parse(url, "name=Akinori; max-age=3600; expires=#{date}").first + cookie = HTTP::Cookie.parse("name=Akinori; max-age=3600; expires=#{date}", :origin => url).first assert_in_delta Time.now + 3600, cookie.expires, 1 - cookie = HTTP::Cookie.parse(url, "name=Akinori; expires=#{date}; max-age=3600").first + cookie = HTTP::Cookie.parse("name=Akinori; expires=#{date}; max-age=3600", :origin => url).first assert_in_delta Time.now + 3600, cookie.expires, 1 end @@ -191,7 +191,7 @@ class TestHTTPCookie < Test::Unit::TestCase 'name=Akinori; expires=', 'name=Akinori; max-age=', ].each { |str| - cookie = HTTP::Cookie.parse(url, str).first + cookie = HTTP::Cookie.parse(str, :origin => url).first assert cookie.session, str } @@ -199,13 +199,13 @@ class TestHTTPCookie < Test::Unit::TestCase 'name=Akinori; expires=Mon, 19 Feb 2012 19:26:04 GMT', 'name=Akinori; max-age=3600', ].each { |str| - cookie = HTTP::Cookie.parse(url, str).first + cookie = HTTP::Cookie.parse(str, :origin => url).first assert !cookie.session, str } end def test_parse_many - url = URI 'http://example/' + url = URI 'http://localhost/' cookie_str = "abc, " \ "name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/, " \ @@ -221,7 +221,7 @@ class TestHTTPCookie < Test::Unit::TestCase "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 url, cookie_str + cookies = HTTP::Cookie.parse cookie_str, :origin => url assert_equal 13, cookies.length name = cookies.find { |c| c.name == 'name' } @@ -278,7 +278,7 @@ class TestHTTPCookie < Test::Unit::TestCase end end cookie = nil - HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie } + HTTP::Cookie.parse(cookie_text, :origin => url) { |p_cookie| cookie = p_cookie } assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) assert_equal('/', cookie.path) @@ -313,7 +313,7 @@ class TestHTTPCookie < Test::Unit::TestCase end end cookie = nil - HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie } + HTTP::Cookie.parse(cookie_text, :origin => url) { |p_cookie| cookie = p_cookie } assert_equal('12345%7D=', cookie.to_s) assert_equal('', cookie.value) @@ -351,7 +351,7 @@ class TestHTTPCookie < Test::Unit::TestCase end end cookie = nil - HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie } + HTTP::Cookie.parse(cookie_text, :origin => url) { |p_cookie| cookie = p_cookie } assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) assert_equal('/', cookie.path) @@ -388,7 +388,7 @@ class TestHTTPCookie < Test::Unit::TestCase end end cookie = nil - HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie } + HTTP::Cookie.parse(cookie_text, :origin => url) { |p_cookie| cookie = p_cookie } assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) assert_equal('/', cookie.path) @@ -424,7 +424,7 @@ class TestHTTPCookie < Test::Unit::TestCase end end cookie = nil - HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie } + HTTP::Cookie.parse(cookie_text, :origin => url) { |p_cookie| cookie = p_cookie } assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) assert_equal('/', cookie.path) @@ -469,7 +469,7 @@ class TestHTTPCookie < Test::Unit::TestCase url = URI.parse('http://host.dom.example.com:8080/') cookie_str = 'a=b; domain=Example.Com' - cookie = HTTP::Cookie.parse(url, cookie_str).first + cookie = HTTP::Cookie.parse(cookie_str, :origin => url).first assert 'example.com', cookie.domain cookie.domain = DomainName(url.host) @@ -485,5 +485,32 @@ class TestHTTPCookie < Test::Unit::TestCase } assert 'example.com', cookie.domain end + + def test_origin= + url = URI.parse('http://example.com/path/') + + cookie_str = 'a=b' + cookie = HTTP::Cookie.parse(cookie_str).first + cookie.origin = url + assert_equal '/path/', cookie.path + assert_equal 'example.com', cookie.domain + assert_equal false, cookie.for_domain + assert_raises(ArgumentError) { + cookie.origin = URI.parse('http://www.example.com/') + } + + cookie_str = 'a=b; domain=.example.com; path=/' + cookie = HTTP::Cookie.parse(cookie_str).first + cookie.origin = url + assert_equal '/', cookie.path + assert_equal 'example.com', cookie.domain + assert_equal true, cookie.for_domain + + cookie_str = 'a=b; domain=example.com' + cookie = HTTP::Cookie.parse(cookie_str).first + assert_raises(ArgumentError) { + cookie.origin = URI.parse('http://example.org/') + } + end end