diff --git a/lib/http/cookie.rb b/lib/http/cookie.rb index d3c1559..e6d4d32 100644 --- a/lib/http/cookie.rb +++ b/lib/http/cookie.rb @@ -276,7 +276,7 @@ def parse(set_cookie, origin, options = nil, &block) logger = options[:logger] created_at = options[:created_at] end - origin = HTTP::Cookie::URIParser.instance.convert_to_uri(origin) + origin = HTTP::Cookie::URIParser.parse(origin) [].tap { |cookies| Scanner.new(set_cookie, logger).scan_set_cookie { |name, value, attrs| @@ -456,7 +456,7 @@ def 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 = HTTP::Cookie::URIParser.instance.convert_to_uri(origin) + origin = HTTP::Cookie::URIParser.parse(origin) if URI::HTTP === origin self.domain ||= origin.host self.path ||= (origin + './').path @@ -549,7 +549,7 @@ def expire! # 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 = HTTP::Cookie::URIParser.instance.convert_to_uri(uri) + uri = HTTP::Cookie::URIParser.parse(uri) return false unless URI::HTTP === uri && uri.host host = DomainName.new(uri.host) @@ -586,7 +586,7 @@ def valid_for_uri?(uri) if @domain.nil? raise "cannot tell if this cookie is valid because the domain is unknown" end - uri = HTTP::Cookie::URIParser.instance.convert_to_uri(uri) + uri = HTTP::Cookie::URIParser.parse(uri) # RFC 6265 5.4 return false if secure? && !(URI::HTTPS === uri) acceptable_from_uri?(uri) && HTTP::Cookie.path_match?(@path, uri.path) diff --git a/lib/http/cookie/uri_parser.rb b/lib/http/cookie/uri_parser.rb index 7584898..3dfc99e 100644 --- a/lib/http/cookie/uri_parser.rb +++ b/lib/http/cookie/uri_parser.rb @@ -1,49 +1,36 @@ -require 'singleton' +module HTTP::Cookie::URIParser + module_function -class HTTP::Cookie::URIParser - include Singleton + # Regular Expression taken from RFC 3986 Appendix B + URIREGEX = %r{ + \A + (?: (? [^:/?\#]+ ) : )? + (?: // (? [^/?\#]* ) )? + (? [^?\#]* ) + (?: \? (? [^\#]* ) )? + (?: \# (? .* ) )? + \z + }x - REGEXP = { - ABS_PATH: /\A[^?#]*\z/ - } + # Escape RFC 3986 "reserved" characters minus valid characters for path + # More specifically, gen-delims minus "/" / "?" / "#" + def escape_path(path) + path.sub(/\A[^?#]+/) { |p| p.gsub(/[:\[\]@]+/) { |r| CGI.escape(r) } } + end + # Parse a URI string or object, relaxing the constraints on the path component def parse(uri) - m = / - \A - (?https?) - :\/\/ - ((?.*)@)? - (?[^\/]+) - (:(?\d+))? - (?[^?#]*) - (\?(?[^#]*))? - (\#(?.*))? - /xi.match(uri.to_s) + URI(uri) + rescue URI::InvalidURIError + str = String.try_convert(uri) or + raise ArgumentError, "bad argument (expected URI object or URI string)" - # Not an absolute HTTP/HTTPS URI - return URI::DEFAULT_PARSER.parse(uri) unless m + m = URIREGEX.match(str) or raise - URI.scheme_list[m['scheme'].upcase].new( - m['scheme'], - m['userinfo'], - m['host'], - m['port'], - nil, # registry - m['path'], - nil, # opaque - m['query'], - m['fragment'], - self - ) - end - - def convert_to_uri(uri) - if uri.is_a?(URI::Generic) - uri - elsif uri = String.try_convert(uri) - parse(uri) - else - raise ArgumentError, "bad argument (expected URI object or URI string)" - end + path = m[:path] + str[m.begin(:path)...m.end(:path)] = escape_path(path) + uri = URI.parse(str) + uri.__send__(:set_path, path) + uri end end diff --git a/lib/http/cookie_jar.rb b/lib/http/cookie_jar.rb index 4c8da8b..97910fa 100644 --- a/lib/http/cookie_jar.rb +++ b/lib/http/cookie_jar.rb @@ -156,7 +156,7 @@ def each(uri = nil, &block) # :yield: cookie block_given? or return enum_for(__method__, uri) if uri - uri = HTTP::Cookie::URIParser.instance.convert_to_uri(uri) + uri = HTTP::Cookie::URIParser.parse(uri) return self unless URI::HTTP === uri && uri.host end diff --git a/test/test_http_cookie.rb b/test/test_http_cookie.rb index ea4cc9d..aeb3710 100644 --- a/test/test_http_cookie.rb +++ b/test/test_http_cookie.rb @@ -948,7 +948,7 @@ def test_origin= } cookie = HTTP::Cookie.new('a', 'b') - cookie.origin = HTTP::Cookie::URIParser.instance.parse('http://example.com/path[]/') + cookie.origin = HTTP::Cookie::URIParser.parse('http://example.com/path[]/') assert_equal '/path[]/', cookie.path cookie = HTTP::Cookie.new('a', 'b', :domain => '.example.com') @@ -1033,14 +1033,14 @@ def test_valid_for_uri? ] }, HTTP::Cookie.parse('a4=b; domain=example.com; path=/dir2[]/', - HTTP::Cookie::URIParser.instance.parse('http://example.com/dir[]/file.html')).first => { + HTTP::Cookie::URIParser.parse('http://example.com/dir[]/file.html')).first => { true => [ - HTTP::Cookie::URIParser.instance.parse('https://example.com/dir2[]/file.html'), - HTTP::Cookie::URIParser.instance.parse('http://example.com/dir2[]/file.html'), + HTTP::Cookie::URIParser.parse('https://example.com/dir2[]/file.html'), + HTTP::Cookie::URIParser.parse('http://example.com/dir2[]/file.html'), ], false => [ - HTTP::Cookie::URIParser.instance.parse('https://example.com/dir[]/file.html'), - HTTP::Cookie::URIParser.instance.parse('http://example.com/dir[]/file.html'), + HTTP::Cookie::URIParser.parse('https://example.com/dir[]/file.html'), + HTTP::Cookie::URIParser.parse('http://example.com/dir[]/file.html'), 'file:///dir2/test.html', ] }, @@ -1091,7 +1091,7 @@ def test_valid_for_uri? 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?(HTTP::Cookie::URIParser.instance.parse(url)), "%s: URI(%s)" % [cookie.name, url] + assert_equal expected, cookie.valid_for_uri?(HTTP::Cookie::URIParser.parse(url)), "%s: URI(%s)" % [cookie.name, url] } } } diff --git a/test/test_http_cookie_jar.rb b/test/test_http_cookie_jar.rb index 9d64f78..b84fcc6 100644 --- a/test/test_http_cookie_jar.rb +++ b/test/test_http_cookie_jar.rb @@ -465,7 +465,7 @@ def test_save_session_cookies_yaml end def test_save_and_read_cookiestxt - url = HTTP::Cookie::URIParser.instance.convert_to_uri('https://rubyforge.org/foo[]/') + url = HTTP::Cookie::URIParser.parse('https://rubyforge.org/foo[]/') # Add one cookie with an expiration date in the future cookie = HTTP::Cookie.new(cookie_values) @@ -657,7 +657,7 @@ def test_paths end def test_non_rfc3986_compliant_paths - url = HTTP::Cookie::URIParser.instance.convert_to_uri('http://RubyForge.org/login[]') + url = HTTP::Cookie::URIParser.parse('http://RubyForge.org/login[]') values = cookie_values(:path => "/login[]", :expires => nil, :origin => url) @@ -671,8 +671,8 @@ def test_non_rfc3986_compliant_paths assert_equal(2, @jar.cookies(url).length) # Make sure we don't get the cookie in a different path - assert_equal(0, @jar.cookies(HTTP::Cookie::URIParser.instance.convert_to_uri('http://RubyForge.org/hello[]')).length) - assert_equal(0, @jar.cookies(HTTP::Cookie::URIParser.instance.convert_to_uri('http://RubyForge.org/')).length) + assert_equal(0, @jar.cookies(HTTP::Cookie::URIParser.parse('http://RubyForge.org/hello[]')).length) + assert_equal(0, @jar.cookies(HTTP::Cookie::URIParser.parse('http://RubyForge.org/')).length) # Expire the first cookie @jar.add(HTTP::Cookie.new(values.merge( :expires => Time.now - (10 * 86400))))