From 4515632b29884de6f537308a7b8f5ebf66df51e6 Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Sun, 25 Jun 2006 22:34:09 +0000 Subject: [PATCH] Refactoring of the RFC2109 cookie draft spec git-svn-id: https://svn.apache.org/repos/asf/jakarta/httpcomponents/httpclient/trunk@417083 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/http/cookie/impl/RFC2109Spec.java | 206 +++++++++ .../http/cookie/impl/TestAllCookieImpl.java | 1 + .../cookie/impl/TestCookieRFC2109Spec.java | 429 ++++++++++++++++++ 3 files changed, 636 insertions(+) create mode 100644 src/java/org/apache/http/cookie/impl/RFC2109Spec.java create mode 100644 src/test/org/apache/http/cookie/impl/TestCookieRFC2109Spec.java diff --git a/src/java/org/apache/http/cookie/impl/RFC2109Spec.java b/src/java/org/apache/http/cookie/impl/RFC2109Spec.java new file mode 100644 index 000000000..ccf8b3ce4 --- /dev/null +++ b/src/java/org/apache/http/cookie/impl/RFC2109Spec.java @@ -0,0 +1,206 @@ +/* + * $HeadRL$ + * $Revision$ + * $Date$ + * + * ==================================================================== + * + * Copyright 2002-2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.http.cookie.impl; + +import java.util.Arrays; + +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.cookie.Cookie; +import org.apache.http.cookie.CookieOrigin; +import org.apache.http.cookie.CookiePathComparator; +import org.apache.http.cookie.MalformedCookieException; +import org.apache.http.io.CharArrayBuffer; +import org.apache.http.util.DateUtils; + +/** + * RFC 2109 compliant cookie policy + * + * @author B.C. Holmes + * @author Park, Sung-Gu + * @author Doug Sale + * @author Rod Waldhoff + * @author dIon Gillard + * @author Sean C. Sullivan + * @author John Evans + * @author Marc A. Saegesser + * @author Oleg Kalnichevski + * @author Mike Bowler + * + * @since 2.0 + */ + +public class RFC2109Spec extends CookieSpecBase { + + private final static CookiePathComparator PATH_COMPARATOR = new CookiePathComparator(); + + private boolean oneHeader = false; + + /** Default constructor */ + public RFC2109Spec(boolean oneHeader) { + super(); + registerAttribHandler("version", new RFC2109VersionHandler()); + registerAttribHandler("path", new BasicPathHandler()); + registerAttribHandler("domain", new RFC2109DomainHandler()); + registerAttribHandler("max-age", new BasicMaxAgeHandler()); + registerAttribHandler("secure", new BasicSecureHandler()); + registerAttribHandler("comment", new BasicCommentHandler()); + registerAttribHandler("expires", new BasicExpiresHandler( + new String[] { + DateUtils.PATTERN_RFC1123, + DateUtils.PATTERN_RFC1036, + DateUtils.PATTERN_ASCTIME})); + this.oneHeader = oneHeader; + } + + /** Default constructor */ + public RFC2109Spec() { + this(false); + } + + public Cookie[] parse(final Header header, final CookieOrigin origin) + throws MalformedCookieException { + if (header == null) { + throw new IllegalArgumentException("Header may not be null"); + } + if (origin == null) { + throw new IllegalArgumentException("Cookie origin may not be null"); + } + HeaderElement[] elems = header.getElements(); + return parse(elems, origin); + } + + public void validate(final Cookie cookie, final CookieOrigin origin) + throws MalformedCookieException { + if (cookie == null) { + throw new IllegalArgumentException("Cookie may not be null"); + } + String name = cookie.getName(); + if (name.indexOf(' ') != -1) { + throw new MalformedCookieException("Cookie name may not contain blanks"); + } + if (name.startsWith("$")) { + throw new MalformedCookieException("Cookie name may not start with $"); + } + super.validate(cookie, origin); + } + + public Header[] formatCookies(final Cookie[] cookies) { + if (cookies == null) { + throw new IllegalArgumentException("Cookie array may not be null"); + } + if (cookies.length == 0) { + throw new IllegalArgumentException("Cookie array may not be empty"); + } + Arrays.sort(cookies, PATH_COMPARATOR); + if (this.oneHeader) { + return doFormatOneHeader(cookies); + } else { + return doFormatManyHeaders(cookies); + } + } + + private Header[] doFormatOneHeader(final Cookie[] cookies) { + int version = Integer.MAX_VALUE; + // Pick the lowerest common denominator + for (int i = 0; i < cookies.length; i++) { + Cookie cookie = cookies[i]; + if (cookie.getVersion() < version) { + version = cookie.getVersion(); + } + } + CharArrayBuffer buffer = new CharArrayBuffer(40 * cookies.length); + formatParamAsVer(buffer, "$Version", Integer.toString(version), version); + for (int i = 0; i < cookies.length; i++) { + buffer.append("; "); + Cookie cookie = cookies[i]; + formatCookieAsVer(buffer, cookie, version); + } + return new Header[] {new Header("Cookie", buffer.toString())}; + } + + private Header[] doFormatManyHeaders(final Cookie[] cookies) { + Header[] headers = new Header[cookies.length]; + for (int i = 0; i < cookies.length; i++) { + Cookie cookie = cookies[i]; + int version = cookie.getVersion(); + CharArrayBuffer buffer = new CharArrayBuffer(40); + formatParamAsVer(buffer, "$Version", Integer.toString(version), version); + buffer.append("; "); + formatCookieAsVer(buffer, cookies[i], version); + headers[i] = new Header("Cookie", buffer.toString()); + } + return headers; + } + + /** + * Return a name/value string suitable for sending in a "Cookie" + * header as defined in RFC 2109 for backward compatibility with cookie + * version 0 + * @param buffer The char array buffer to use for output + * @param param The parameter. + * @param version The cookie version + */ + private void formatParamAsVer(final CharArrayBuffer buffer, + final String name, final String value, int version) { + buffer.append(name); + buffer.append("="); + if (value != null) { + if (version > 0) { + buffer.append('\"'); + buffer.append(value); + buffer.append('\"'); + } else { + buffer.append(value); + } + } + } + + /** + * Return a string suitable for sending in a "Cookie" header + * as defined in RFC 2109 for backward compatibility with cookie version 0 + * @param buffer The char array buffer to use for output + * @param cookie The {@link Cookie} to be formatted as string + * @param version The version to use. + */ + private void formatCookieAsVer(final CharArrayBuffer buffer, + final Cookie cookie, int version) { + formatParamAsVer(buffer, cookie.getName(), cookie.getValue(), version); + if (cookie.getPath() != null && cookie.isPathAttributeSpecified()) { + buffer.append("; "); + formatParamAsVer(buffer, "$Path", cookie.getPath(), version); + } + if (cookie.getDomain() != null && cookie.isDomainAttributeSpecified()) { + buffer.append("; "); + formatParamAsVer(buffer, "$Domain", cookie.getDomain(), version); + } + } + +} diff --git a/src/test/org/apache/http/cookie/impl/TestAllCookieImpl.java b/src/test/org/apache/http/cookie/impl/TestAllCookieImpl.java index 4881d12f4..0258ac41d 100644 --- a/src/test/org/apache/http/cookie/impl/TestAllCookieImpl.java +++ b/src/test/org/apache/http/cookie/impl/TestAllCookieImpl.java @@ -44,6 +44,7 @@ public class TestAllCookieImpl extends TestCase { suite.addTest(TestRFC2109CookieAttribHandlers.suite()); suite.addTest(TestBrowserCompatSpec.suite()); suite.addTest(TestCookieNetscapeDraft.suite()); + suite.addTest(TestCookieRFC2109Spec.suite()); return suite; } diff --git a/src/test/org/apache/http/cookie/impl/TestCookieRFC2109Spec.java b/src/test/org/apache/http/cookie/impl/TestCookieRFC2109Spec.java new file mode 100644 index 000000000..1b681a725 --- /dev/null +++ b/src/test/org/apache/http/cookie/impl/TestCookieRFC2109Spec.java @@ -0,0 +1,429 @@ +/* + * $HeadURL$ + * $Revision$ + * $Date$ + * ==================================================================== + * + * Copyright 1999-2006 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.http.cookie.impl; + +import org.apache.http.Header; +import org.apache.http.cookie.Cookie; +import org.apache.http.cookie.CookieOrigin; +import org.apache.http.cookie.CookieSpec; +import org.apache.http.cookie.MalformedCookieException; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test cases for RFC2109 cookie spec + * + * @author Oleg Kalnichevski + * + * @version $Revision$ + */ +public class TestCookieRFC2109Spec extends TestCase { + + + // ------------------------------------------------------------ Constructor + + public TestCookieRFC2109Spec(String name) { + super(name); + } + + // ------------------------------------------------------- TestCase Methods + + public static Test suite() { + return new TestSuite(TestCookieRFC2109Spec.class); + } + + public void testParseVersion() throws Exception { + Header header = new Header("Set-Cookie","cookie-name=cookie-value; version=1"); + + CookieSpec cookiespec = new RFC2109Spec(); + CookieOrigin origin = new CookieOrigin("127.0.0.1", 80, "/", false); + Cookie[] parsed = cookiespec.parse(header, origin); + for (int i = 0; i < parsed.length; i++) { + cookiespec.validate(parsed[i], origin); + } + assertEquals("Found 1 cookie.",1,parsed.length); + assertEquals("Name","cookie-name",parsed[0].getName()); + assertEquals("Value","cookie-value",parsed[0].getValue()); + assertEquals("Version",1,parsed[0].getVersion()); + } + + /** + * Test domain equals host + */ + public void testParseDomainEqualsHost() throws Exception { + Header header = new Header("Set-Cookie", + "cookie-name=cookie-value; domain=www.b.com; version=1"); + + CookieSpec cookiespec = new RFC2109Spec(); + CookieOrigin origin = new CookieOrigin("www.b.com", 80, "/", false); + Cookie[] parsed = cookiespec.parse(header, origin); + for (int i = 0; i < parsed.length; i++) { + cookiespec.validate(parsed[i], origin); + } + assertNotNull(parsed); + assertEquals(1, parsed.length); + assertEquals("www.b.com", parsed[0].getDomain()); + } + + /** + * Domain does not start with a dot + */ + public void testParseWithIllegalDomain1() throws Exception { + Header header = new Header("Set-Cookie", + "cookie-name=cookie-value; domain=a.b.com; version=1"); + + CookieSpec cookiespec = new RFC2109Spec(); + CookieOrigin origin = new CookieOrigin("www.a.b.com", 80, "/", false); + try { + Cookie[] parsed = cookiespec.parse(header, origin); + for (int i = 0; i < parsed.length; i++) { + cookiespec.validate(parsed[i], origin); + } + fail("MalformedCookieException should have been thrown"); + } catch (MalformedCookieException e) { + // expected + } + } + + /** + * Domain must have alt least one embedded dot + */ + public void testParseWithIllegalDomain2() throws Exception { + Header header = new Header("Set-Cookie", + "cookie-name=cookie-value; domain=.com; version=1"); + + CookieSpec cookiespec = new RFC2109Spec(); + CookieOrigin origin = new CookieOrigin("b.com", 80, "/", false); + try { + Cookie[] parsed = cookiespec.parse(header, origin); + for (int i = 0; i < parsed.length; i++) { + cookiespec.validate(parsed[i], origin); + } + fail("MalformedCookieException should have been thrown"); + } catch (MalformedCookieException e) { + // expected + } + } + + /** + * Host minus domain may not contain any dots + */ + public void testParseWithIllegalDomain4() throws Exception { + Header header = new Header("Set-Cookie", + "cookie-name=cookie-value; domain=.c.com; version=1"); + + CookieSpec cookiespec = new RFC2109Spec(); + CookieOrigin origin = new CookieOrigin("a.b.c.com", 80, "/", false); + try { + Cookie[] parsed = cookiespec.parse(header, origin); + for (int i = 0; i < parsed.length; i++) { + cookiespec.validate(parsed[i], origin); + } + fail("MalformedCookieException should have been thrown"); + } catch (MalformedCookieException e) { + // expected + } + } + + /** + * Tests if that invalid second domain level cookie gets + * rejected in the strict mode, but gets accepted in the + * browser compatibility mode. + */ + public void testSecondDomainLevelCookie() throws Exception { + Cookie cookie = new Cookie("name", null); + cookie.setDomain(".sourceforge.net"); + cookie.setPath("/"); + cookie.setDomainAttributeSpecified(true); + cookie.setPathAttributeSpecified(true); + + CookieSpec cookiespec = new RFC2109Spec(); + CookieOrigin origin = new CookieOrigin("sourceforge.net", 80, "/", false); + try { + cookiespec.validate(cookie, origin); + fail("MalformedCookieException should have been thrown"); + } catch (MalformedCookieException e) { + // Expected + } + } + + public void testSecondDomainLevelCookieMatch() throws Exception { + Cookie cookie = new Cookie("name", null); + cookie.setDomain(".sourceforge.net"); + cookie.setPath("/"); + cookie.setDomainAttributeSpecified(true); + cookie.setPathAttributeSpecified(true); + + CookieSpec cookiespec = new RFC2109Spec(); + CookieOrigin origin = new CookieOrigin("sourceforge.net", 80, "/", false); + assertFalse(cookiespec.match(cookie, origin)); + } + + public void testParseWithWrongPath() throws Exception { + Header header = new Header("Set-Cookie", + "cookie-name=cookie-value; domain=127.0.0.1; path=/not/just/root"); + + CookieSpec cookiespec = new RFC2109Spec(); + CookieOrigin origin = new CookieOrigin("127.0.0.1", 80, "/", false); + try { + Cookie[] parsed = cookiespec.parse(header, origin); + for (int i = 0; i < parsed.length; i++) { + cookiespec.validate(parsed[i], origin); + } + fail("MalformedCookieException exception should have been thrown"); + } catch (MalformedCookieException e) { + // expected + } + } + + /** + * Tests if cookie constructor rejects cookie name containing blanks. + */ + public void testCookieNameWithBlanks() throws Exception { + Header setcookie = new Header("Set-Cookie", "invalid name="); + CookieSpec cookiespec = new RFC2109Spec(); + CookieOrigin origin = new CookieOrigin("127.0.0.1", 80, "/", false); + try { + Cookie[] parsed = cookiespec.parse(setcookie, origin); + for (int i = 0; i < parsed.length; i++) { + cookiespec.validate(parsed[i], origin); + } + fail("MalformedCookieException exception should have been thrown"); + } catch (MalformedCookieException e) { + // expected + } + } + + /** + * Tests if cookie constructor rejects cookie name starting with $. + */ + public void testCookieNameStartingWithDollarSign() throws Exception { + Header setcookie = new Header("Set-Cookie", "$invalid_name="); + CookieSpec cookiespec = new RFC2109Spec(); + CookieOrigin origin = new CookieOrigin("127.0.0.1", 80, "/", false); + try { + Cookie[] parsed = cookiespec.parse(setcookie, origin); + for (int i = 0; i < parsed.length; i++) { + cookiespec.validate(parsed[i], origin); + } + fail("MalformedCookieException exception should have been thrown"); + } catch (MalformedCookieException e) { + // expected + } + } + + /** + * Tests if default cookie validator rejects cookies originating from a host without domain + * where domain attribute does not match the host of origin + */ + public void testInvalidDomainWithSimpleHostName() throws Exception { + CookieSpec cookiespec = new RFC2109Spec(); + Header header = new Header("Set-Cookie", + "name=\"value\"; version=\"1\"; path=\"/\"; domain=\".mydomain.com\""); + CookieOrigin origin1 = new CookieOrigin("host", 80, "/", false); + Cookie[]cookies = cookiespec.parse(header, origin1); + try { + cookiespec.validate(cookies[0], origin1); + fail("MalformedCookieException must have thrown"); + } + catch(MalformedCookieException expected) { + } + CookieOrigin origin2 = new CookieOrigin("host2", 80, "/", false); + header = new Header("Set-Cookie", + "name=\"value\"; version=\"1\"; path=\"/\"; domain=\"host1\""); + cookies = cookiespec.parse(header, origin2); + try { + cookiespec.validate(cookies[0], origin2); + fail("MalformedCookieException must have thrown"); + } + catch(MalformedCookieException expected) { + } + } + + /** + * Tests if cookie values with embedded comma are handled correctly. + */ + public void testCookieWithComma() throws Exception { + Header header = new Header("Set-Cookie", "a=b,c"); + + CookieSpec cookiespec = new RFC2109Spec(); + CookieOrigin origin = new CookieOrigin("localhost", 80, "/", false); + Cookie[] cookies = cookiespec.parse(header, origin); + assertEquals("number of cookies", 2, cookies.length); + assertEquals("a", cookies[0].getName()); + assertEquals("b", cookies[0].getValue()); + assertEquals("c", cookies[1].getName()); + assertEquals(null, cookies[1].getValue()); + } + + /** + * Tests RFC 2109 compiant cookie formatting. + */ + public void testRFC2109CookieFormatting() throws Exception { + CookieSpec cookiespec = new RFC2109Spec(false); + Header header = new Header("Set-Cookie", + "name=\"value\"; version=\"1\"; path=\"/\"; domain=\".mydomain.com\""); + CookieOrigin origin = new CookieOrigin("myhost.mydomain.com", 80, "/", false); + Cookie[] cookies = cookiespec.parse(header, origin); + cookiespec.validate(cookies[0], origin); + Header[] headers = cookiespec.formatCookies(cookies); + assertNotNull(headers); + assertEquals(1, headers.length); + assertEquals("$Version=\"1\"; name=\"value\"; $Path=\"/\"; $Domain=\".mydomain.com\"", + headers[0].getValue()); + + header = new Header( "Set-Cookie", + "name=value; path=/; domain=.mydomain.com"); + cookies = cookiespec.parse(header, origin); + cookiespec.validate(cookies[0], origin); + headers = cookiespec.formatCookies(cookies); + assertNotNull(headers); + assertEquals(1, headers.length); + assertEquals("$Version=0; name=value; $Path=/; $Domain=.mydomain.com", + headers[0].getValue()); + } + + public void testRFC2109CookiesFormatting() throws Exception { + CookieSpec cookiespec = new RFC2109Spec(true); + Header header = new Header("Set-Cookie", + "name1=value1; path=/; domain=.mydomain.com, " + + "name2=\"value2\"; version=\"1\"; path=\"/\"; domain=\".mydomain.com\""); + CookieOrigin origin = new CookieOrigin("myhost.mydomain.com", 80, "/", false); + Cookie[] cookies = cookiespec.parse(header, origin); + for (int i = 0; i < cookies.length; i++) { + cookiespec.validate(cookies[i], origin); + } + assertNotNull(cookies); + assertEquals(2, cookies.length); + Header[] headers = cookiespec.formatCookies(cookies); + assertNotNull(headers); + assertEquals(1, headers.length); + assertEquals( + "$Version=0; name1=value1; $Path=/; $Domain=.mydomain.com; " + + "name2=value2; $Path=/; $Domain=.mydomain.com", + headers[0].getValue()); + + header = new Header("Set-Cookie", + "name1=value1; version=1; path=/; domain=.mydomain.com, " + + "name2=\"value2\"; version=\"1\"; path=\"/\"; domain=\".mydomain.com\""); + cookies = cookiespec.parse(header, origin); + for (int i = 0; i < cookies.length; i++) { + cookiespec.validate(cookies[i], origin); + } + assertNotNull(cookies); + assertEquals(2, cookies.length); + headers = cookiespec.formatCookies(cookies); + assertEquals( + "$Version=\"1\"; name1=\"value1\"; $Path=\"/\"; $Domain=\".mydomain.com\"; " + + "name2=\"value2\"; $Path=\"/\"; $Domain=\".mydomain.com\"", + headers[0].getValue()); + } + + /** + * Tests if null cookie values are handled correctly. + */ + public void testNullCookieValueFormatting() { + Cookie cookie = new Cookie("name", null); + cookie.setDomain(".whatever.com"); + cookie.setPath("/"); + cookie.setDomainAttributeSpecified(true); + cookie.setPathAttributeSpecified(true); + + CookieSpec cookiespec = new RFC2109Spec(); + Header[] headers = cookiespec.formatCookies(new Cookie[] { cookie }); + assertNotNull(headers); + assertEquals(1, headers.length); + assertEquals("$Version=0; name=; $Path=/; $Domain=.whatever.com", + headers[0].getValue()); + + cookie.setVersion(1); + headers = cookiespec.formatCookies(new Cookie[] { cookie }); + assertNotNull(headers); + assertEquals(1, headers.length); + assertEquals("$Version=\"1\"; name=; $Path=\"/\"; $Domain=\".whatever.com\"", + headers[0].getValue()); + } + + public void testCookieNullDomainNullPathFormatting() { + Cookie cookie = new Cookie("name", null); + cookie.setDomainAttributeSpecified(true); + cookie.setPath("/"); + cookie.setPathAttributeSpecified(true); + + CookieSpec cookiespec = new RFC2109Spec(); + Header[] headers = cookiespec.formatCookies(new Cookie[] { cookie }); + assertNotNull(headers); + assertEquals(1, headers.length); + assertEquals("$Version=0; name=; $Path=/", headers[0].getValue()); + + cookie.setDomainAttributeSpecified(false); + cookie.setPathAttributeSpecified(false); + headers = cookiespec.formatCookies(new Cookie[] { cookie }); + assertNotNull(headers); + assertEquals(1, headers.length); + assertEquals("$Version=0; name=", headers[0].getValue()); + } + + public void testInvalidInput() throws Exception { + CookieSpec cookiespec = new RFC2109Spec(); + try { + cookiespec.parse(null, null); + fail("IllegalArgumentException must have been thrown"); + } catch (IllegalArgumentException ex) { + // expected + } + try { + cookiespec.parse(new Header("Set-Cookie", "name=value"), null); + fail("IllegalArgumentException must have been thrown"); + } catch (IllegalArgumentException ex) { + // expected + } + try { + cookiespec.validate(null, null); + fail("IllegalArgumentException must have been thrown"); + } catch (IllegalArgumentException ex) { + // expected + } + try { + cookiespec.formatCookies(null); + fail("IllegalArgumentException must have been thrown"); + } catch (IllegalArgumentException ex) { + // expected + } + try { + cookiespec.formatCookies(new Cookie[] {}); + fail("IllegalArgumentException must have been thrown"); + } catch (IllegalArgumentException ex) { + // expected + } + } + +} \ No newline at end of file