From 8b7e1463a14cef8a6dcc3a5367ee20c23b8a51ee Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Thu, 6 Sep 2012 22:54:56 +0200 Subject: [PATCH] jetty-9 - HTTP client: implemented cookie functionalities. --- .../org/eclipse/jetty/client/HttpClient.java | 7 + .../eclipse/jetty/client/HttpConnection.java | 23 ++- .../jetty/client/HttpCookieParser.java | 192 ++++++++++++++++++ .../eclipse/jetty/client/HttpCookieStore.java | 127 ++++++++++++ .../eclipse/jetty/client/HttpReceiver.java | 22 ++ .../eclipse/jetty/client/api/CookieStore.java | 30 +++ .../jetty/client/HttpCookieParserTest.java | 60 ++++++ .../jetty/client/HttpCookieStoreTest.java | 139 +++++++++++++ .../eclipse/jetty/client/HttpCookieTest.java | 99 +++++++++ .../jetty/client/HttpDestinationTest.java | 29 +-- .../org/eclipse/jetty/http/HttpCookie.java | 149 ++++++-------- 11 files changed, 774 insertions(+), 103 deletions(-) create mode 100644 jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieParser.java create mode 100644 jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieStore.java create mode 100644 jetty-client/src/main/java/org/eclipse/jetty/client/api/CookieStore.java create mode 100644 jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieParserTest.java create mode 100644 jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java create mode 100644 jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index b48ba444234..1db61c185bd 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -36,6 +36,7 @@ import javax.net.ssl.SSLEngine; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.CookieStore; import org.eclipse.jetty.client.api.Destination; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; @@ -94,6 +95,7 @@ public class HttpClient extends AggregateLifeCycle private final ConcurrentMap destinations = new ConcurrentHashMap<>(); private final ConcurrentMap conversations = new ConcurrentHashMap<>(); + private final CookieStore cookieStore = new HttpCookieStore(); private volatile Executor executor; private volatile ByteBufferPool byteBufferPool; private volatile Scheduler scheduler; @@ -155,6 +157,11 @@ public class HttpClient extends AggregateLifeCycle LOG.info("Stopped {}", this); } + public CookieStore getCookieStore() + { + return cookieStore; + } + public long getIdleTimeout() { return idleTimeout; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java index 029af9a6d75..93a3816330b 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java @@ -18,12 +18,14 @@ package org.eclipse.jetty.client; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.ContentProvider; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; @@ -57,6 +59,11 @@ public class HttpConnection extends AbstractConnection implements Connection return client; } + public HttpDestination getDestination() + { + return destination; + } + @Override public void onOpen() { @@ -109,7 +116,6 @@ public class HttpConnection extends AbstractConnection implements Connection request.idleTimeout(client.getIdleTimeout()); // TODO: follow redirects - // TODO: cookies HttpVersion version = request.version(); HttpFields headers = request.headers(); @@ -136,6 +142,21 @@ public class HttpConnection extends AbstractConnection implements Connection } } + // Cookies + List cookies = client.getCookieStore().getCookies(getDestination(), request.path()); + StringBuilder cookieString = null; + for (int i = 0; i < cookies.size(); ++i) + { + if (cookieString == null) + cookieString = new StringBuilder(); + if (i > 0) + cookieString.append("; "); + HttpCookie cookie = cookies.get(i); + cookieString.append(cookie.getName()).append("=").append(cookie.getValue()); + } + if (cookieString != null) + request.header(HttpHeader.COOKIE.asString(), cookieString.toString()); + // TODO: decoder headers // If we are HTTP 1.1, add the Host header diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieParser.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieParser.java new file mode 100644 index 00000000000..8a7fbca3772 --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieParser.java @@ -0,0 +1,192 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.http.HttpCookie; + +public class HttpCookieParser +{ + private static final String[] DATE_PATTERNS = new String[] + { + "EEE',' dd-MMM-yyyy HH:mm:ss 'GMT'", + "EEE',' dd MMM yyyy HH:mm:ss 'GMT'", + "EEE MMM dd yyyy HH:mm:ss 'GMT'Z" + }; + + private HttpCookieParser() + { + } + + public static List parseCookies(String headerValue) + { + if (headerValue.toLowerCase().contains("expires=")) + { + HttpCookie cookie = parseCookie(headerValue, 0); + if (cookie != null) + return Collections.singletonList(cookie); + else + return Collections.emptyList(); + } + else + { + List result = new ArrayList<>(); + List cookieStrings = splitCookies(headerValue); + for (String cookieString : cookieStrings) + { + HttpCookie cookie = parseCookie(cookieString, 1); + if (cookie != null) + result.add(cookie); + } + return result; + } + } + + private static List splitCookies(String headerValue) + { + // The comma is the separator, but only if it's outside double quotes + List result = new ArrayList<>(); + int start = 0; + int quotes = 0; + for (int i = 0; i < headerValue.length(); ++i) + { + char c = headerValue.charAt(i); + if (c == ',' && quotes % 2 == 0) + { + result.add(headerValue.substring(start, i)); + start = i + 1; + } + else if (c == '"') + { + ++quotes; + } + } + result.add(headerValue.substring(start)); + return result; + } + + private static HttpCookie parseCookie(String cookieString, int version) + { + String[] cookieParts = cookieString.split(";"); + + String nameValue = cookieParts[0]; + int equal = nameValue.indexOf('='); + if (equal < 1) + return null; + + String name = nameValue.substring(0, equal).trim(); + String value = nameValue.substring(equal + 1); + String domain = null; + String path = "/"; + long maxAge = -1; + boolean httpOnly = false; + boolean secure = false; + String comment = null; + for (int i = 1; i < cookieParts.length; ++i) + { + try + { + String[] attributeParts = cookieParts[i].split("=", 2); + String attributeName = attributeParts[0].trim().toLowerCase(); + String attributeValue = attributeParts.length < 2 ? "" : attributeParts[1].trim(); + switch (attributeName) + { + case "domain": + { + domain = attributeValue; + break; + } + case "path": + { + path = attributeValue; + break; + } + case "max-age": + { + maxAge = Long.parseLong(attributeValue); + break; + } + case "expires": + { + maxAge = parseDate(attributeValue); + break; + } + case "secure": + { + secure = true; + break; + } + case "httponly": + { + httpOnly = true; + } + case "comment": + { + comment = attributeValue; + break; + } + case "version": + { + version = Integer.parseInt(attributeValue); + break; + } + default: + { + // Ignore + break; + } + } + } + catch (NumberFormatException x) + { + // Ignore + } + } + + return new HttpCookie(name, value, domain, path, maxAge, httpOnly, secure, comment, version); + } + + private static long parseDate(String attributeValue) + { + for (String pattern : DATE_PATTERNS) + { + try + { + SimpleDateFormat dateFormat = new SimpleDateFormat(pattern, Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + long result = TimeUnit.MILLISECONDS.toSeconds(dateFormat.parse(attributeValue).getTime() - System.currentTimeMillis()); + if (result < 0) + return 0; + } + catch (ParseException x) + { + // Ignore and continue + } + } + return 0; + } +} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieStore.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieStore.java new file mode 100644 index 00000000000..78ae416ec00 --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieStore.java @@ -0,0 +1,127 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.eclipse.jetty.client.api.CookieStore; +import org.eclipse.jetty.client.api.Destination; +import org.eclipse.jetty.http.HttpCookie; + +public class HttpCookieStore implements CookieStore +{ + private final ConcurrentMap> allCookies = new ConcurrentHashMap<>(); + + @Override + public List getCookies(Destination destination, String path) + { + List result = new ArrayList<>(); + + String host = destination.host(); + int port = destination.port(); + String key = host + ":" + port + path; + + // First lookup: direct hit + Map cookies = allCookies.get(key); + if (cookies != null) + accumulateCookies(destination, cookies, result); + + // Second lookup: root path + if (!"/".equals(path)) + { + key = host + ":" + port + "/"; + cookies = allCookies.get(key); + if (cookies != null) + accumulateCookies(destination, cookies, result); + } + + // Third lookup: parent domains + int domains = host.split("\\.").length - 1; + for (int i = 2; i <= domains; ++i) + { + String[] hostParts = host.split("\\.", i); + host = hostParts[hostParts.length - 1]; + key = host + ":" + port + "/"; + cookies = allCookies.get(key); + if (cookies != null) + accumulateCookies(destination, cookies, result); + } + + return result; + } + + private void accumulateCookies(Destination destination, Map cookies, List result) + { + for (Iterator iterator = cookies.values().iterator(); iterator.hasNext(); ) + { + HttpCookie cookie = iterator.next(); + if (cookie.isExpired(System.nanoTime())) + { + iterator.remove(); + } + else + { + if (!"https".equalsIgnoreCase(destination.scheme()) && cookie.isSecure()) + continue; + result.add(cookie); + } + } + } + + @Override + public boolean addCookie(Destination destination, HttpCookie cookie) + { + String destinationDomain = destination.host() + ":" + destination.port(); + + // Check whether it is the same domain + String domain = cookie.getDomain(); + if (domain == null) + domain = destinationDomain; + + if (domain.indexOf(':') < 0) + domain += ":" + ("https".equalsIgnoreCase(destination.scheme()) ? 443 : 80); + + // Cookie domains may start with a ".", like ".domain.com" + // This also avoids that a request to sub.domain.com sets a cookie for domain.com + if (!domain.endsWith(destinationDomain)) + return false; + + // Normalize the path + String path = cookie.getPath(); + if (path == null || path.length() == 0) + path = "/"; + + String key = destination.host() + ":" + destination.port() + path; + Map cookies = allCookies.get(key); + if (cookies == null) + { + cookies = new ConcurrentHashMap<>(); + Map existing = allCookies.putIfAbsent(key, cookies); + if (existing != null) + cookies = existing; + } + cookies.put(path, cookie); + return true; + } +} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java index dcbac1ed783..d2901ec2d5d 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java @@ -21,10 +21,13 @@ package org.eclipse.jetty.client; import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.List; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import org.eclipse.jetty.client.api.CookieStore; import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpParser; import org.eclipse.jetty.http.HttpVersion; @@ -115,6 +118,25 @@ public class HttpReceiver implements HttpParser.ResponseHandler { HttpExchange exchange = connection.getExchange(); exchange.response().headers().put(name, value); + + switch (name.toLowerCase()) + { + case "set-cookie": + case "set-cookie2": + { + CookieStore cookieStore = connection.getHttpClient().getCookieStore(); + HttpDestination destination = connection.getDestination(); + List cookies = HttpCookieParser.parseCookies(value); + for (HttpCookie cookie : cookies) + cookieStore.addCookie(destination, cookie); + break; + } + default: + { + break; + } + } + return false; } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/CookieStore.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/CookieStore.java new file mode 100644 index 00000000000..a3c6e20d1bd --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/CookieStore.java @@ -0,0 +1,30 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.api; + +import java.util.List; + +import org.eclipse.jetty.http.HttpCookie; + +public interface CookieStore +{ + List getCookies(Destination destination, String path); + + boolean addCookie(Destination destination, HttpCookie cookie); +} diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieParserTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieParserTest.java new file mode 100644 index 00000000000..de9f7a1bc82 --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieParserTest.java @@ -0,0 +1,60 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import java.util.List; + +import junit.framework.Assert; +import org.eclipse.jetty.http.HttpCookie; +import org.junit.Test; + +public class HttpCookieParserTest +{ + @Test + public void testParseCookie1() throws Exception + { + String value = "wd=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; domain=.domain.com; httponly; secure"; + List cookies = HttpCookieParser.parseCookies(value); + Assert.assertEquals(1, cookies.size()); + HttpCookie cookie = cookies.get(0); + Assert.assertEquals("wd", cookie.getName()); + Assert.assertEquals("deleted", cookie.getValue()); + Assert.assertEquals(".domain.com", cookie.getDomain()); + Assert.assertEquals("/", cookie.getPath()); + Assert.assertTrue(cookie.isHttpOnly()); + Assert.assertTrue(cookie.isSecure()); + Assert.assertTrue(cookie.isExpired(System.nanoTime())); + } + + @Test + public void testParseCookie2() throws Exception + { + String value = "wd=deleted; max-Age=0; path=/; domain=.domain.com; httponly; version=3"; + List cookies = HttpCookieParser.parseCookies(value); + Assert.assertEquals(1, cookies.size()); + HttpCookie cookie = cookies.get(0); + Assert.assertEquals("wd", cookie.getName()); + Assert.assertEquals("deleted", cookie.getValue()); + Assert.assertEquals(".domain.com", cookie.getDomain()); + Assert.assertEquals("/", cookie.getPath()); + Assert.assertTrue(cookie.isHttpOnly()); + Assert.assertEquals(3, cookie.getVersion()); + Assert.assertTrue(cookie.isExpired(System.nanoTime())); + } +} diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java new file mode 100644 index 00000000000..b7bd5084dd0 --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java @@ -0,0 +1,139 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import java.util.List; + +import org.eclipse.jetty.client.api.CookieStore; +import org.eclipse.jetty.client.api.Destination; +import org.eclipse.jetty.http.HttpCookie; +import org.junit.Assert; +import org.junit.Test; + +public class HttpCookieStoreTest +{ + private HttpClient client = new HttpClient(); + + @Test + public void testCookieStoredIsRetrieved() throws Exception + { + CookieStore cookies = new HttpCookieStore(); + Destination destination = new HttpDestination(client, "http", "localhost", 80); + Assert.assertTrue(cookies.addCookie(destination, new HttpCookie("a", "1"))); + + List result = cookies.getCookies(destination, "/"); + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + HttpCookie cookie = result.get(0); + Assert.assertEquals("a", cookie.getName()); + Assert.assertEquals("1", cookie.getValue()); + } + + @Test + public void testCookieWithChildDomainIsStored() throws Exception + { + CookieStore cookies = new HttpCookieStore(); + Destination destination = new HttpDestination(client, "http", "localhost", 80); + Assert.assertTrue(cookies.addCookie(destination, new HttpCookie("a", "1", "child.localhost", "/"))); + + List result = cookies.getCookies(destination, "/"); + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + HttpCookie cookie = result.get(0); + Assert.assertEquals("a", cookie.getName()); + Assert.assertEquals("1", cookie.getValue()); + } + + @Test + public void testCookieWithParentDomainIsNotStored() throws Exception + { + CookieStore cookies = new HttpCookieStore(); + Destination destination = new HttpDestination(client, "http", "child.localhost", 80); + Assert.assertFalse(cookies.addCookie(destination, new HttpCookie("a", "1", "localhost", "/"))); + } + + @Test + public void testCookieStoredWithPathIsNotRetrievedWithRootPath() throws Exception + { + CookieStore cookies = new HttpCookieStore(); + Destination destination = new HttpDestination(client, "http", "localhost", 80); + Assert.assertTrue(cookies.addCookie(destination, new HttpCookie("a", "1", null, "/path"))); + + List result = cookies.getCookies(destination, "/"); + Assert.assertNotNull(result); + Assert.assertEquals(0, result.size()); + } + + @Test + public void testCookieStoredWithRootPathIsRetrievedWithPath() throws Exception + { + CookieStore cookies = new HttpCookieStore(); + Destination destination = new HttpDestination(client, "http", "localhost", 80); + Assert.assertTrue(cookies.addCookie(destination, new HttpCookie("a", "1", null, "/"))); + + List result = cookies.getCookies(destination, "/path"); + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + HttpCookie cookie = result.get(0); + Assert.assertEquals("a", cookie.getName()); + Assert.assertEquals("1", cookie.getValue()); + } + + @Test + public void testCookieStoredWithParentDomainIsRetrievedWithChildDomain() throws Exception + { + CookieStore cookies = new HttpCookieStore(); + Destination parentDestination = new HttpDestination(client, "http", "localhost.org", 80); + Assert.assertTrue(cookies.addCookie(parentDestination, new HttpCookie("a", "1", null, "/"))); + + Destination childDestination = new HttpDestination(client, "http", "child.localhost.org", 80); + Assert.assertTrue(cookies.addCookie(childDestination, new HttpCookie("b", "2", null, "/"))); + + Destination grandChildDestination = new HttpDestination(client, "http", "grand.child.localhost.org", 80); + Assert.assertTrue(cookies.addCookie(grandChildDestination, new HttpCookie("b", "2", null, "/"))); + + List result = cookies.getCookies(grandChildDestination, "/path"); + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + } + + @Test + public void testExpiredCookieIsNotRetrieved() throws Exception + { + CookieStore cookies = new HttpCookieStore(); + Destination destination = new HttpDestination(client, "http", "localhost.org", 80); + Assert.assertTrue(cookies.addCookie(destination, new HttpCookie("a", "1", null, "/", 0, false, false))); + + List result = cookies.getCookies(destination, "/"); + Assert.assertNotNull(result); + Assert.assertEquals(0, result.size()); + } + + @Test + public void testSecureCookieIsNotRetrieved() throws Exception + { + CookieStore cookies = new HttpCookieStore(); + Destination destination = new HttpDestination(client, "http", "localhost.org", 80); + Assert.assertTrue(cookies.addCookie(destination, new HttpCookie("a", "1", null, "/", 0, false, true))); + + List result = cookies.getCookies(destination, "/"); + Assert.assertNotNull(result); + Assert.assertEquals(0, result.size()); + } +} diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java new file mode 100644 index 00000000000..c79054096cf --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java @@ -0,0 +1,99 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.api.Destination; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.junit.Assert; +import org.junit.Test; + +public class HttpCookieTest extends AbstractHttpClientServerTest +{ + @Test + public void test_CookieIsStored() throws Exception + { + final String name = "foo"; + final String value = "bar"; + start(new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + response.addCookie(new Cookie(name, value)); + baseRequest.setHandled(true); + } + }); + + String scheme = "http"; + String host = "localhost"; + int port = connector.getLocalPort(); + String path = "/path"; + Response response = client.GET(scheme + "://" + host + ":" + port + path).get(5, TimeUnit.SECONDS); + Assert.assertEquals(200, response.status()); + + Destination destination = client.getDestination(scheme, host, port); + List cookies = client.getCookieStore().getCookies(destination, path); + Assert.assertNotNull(cookies); + Assert.assertEquals(1, cookies.size()); + HttpCookie cookie = cookies.get(0); + Assert.assertEquals(name, cookie.getName()); + Assert.assertEquals(value, cookie.getValue()); + } + + @Test + public void test_CookieIsSent() throws Exception + { + final String name = "foo"; + final String value = "bar"; + start(new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + Cookie[] cookies = request.getCookies(); + Assert.assertEquals(1, cookies.length); + Cookie cookie = cookies[0]; + Assert.assertEquals(name, cookie.getName()); + Assert.assertEquals(value, cookie.getValue()); + baseRequest.setHandled(true); + } + }); + + String scheme = "http"; + String host = "localhost"; + int port = connector.getLocalPort(); + String path = "/path"; + Destination destination = client.getDestination(scheme, host, port); + client.getCookieStore().addCookie(destination, new HttpCookie(name, value, null, path)); + + Response response = client.GET(scheme + "://" + host + ":" + port + path).get(5, TimeUnit.SECONDS); + Assert.assertEquals(200, response.status()); + } +} diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpDestinationTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpDestinationTest.java index 6512c813574..83523e9f2f1 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpDestinationTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpDestinationTest.java @@ -130,21 +130,22 @@ public class HttpDestinationTest extends AbstractHttpClientServerTest client.setIdleTimeout(idleTimeout); HttpDestination destination = new HttpDestination(client, "http", "localhost", connector.getLocalPort()); - destination.acquire(); - - // There are no queued requests, so the newly created connection will be idle - Connection connection1 = null; - long start = System.nanoTime(); - while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5) + Connection connection1 = destination.acquire(); + if (connection1 == null) { - connection1 = destination.idleConnections().peek(); - TimeUnit.MILLISECONDS.sleep(50); + // There are no queued requests, so the newly created connection will be idle + long start = System.nanoTime(); + while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5) + { + connection1 = destination.idleConnections().peek(); + TimeUnit.MILLISECONDS.sleep(50); + } + Assert.assertNotNull(connection1); + + TimeUnit.MILLISECONDS.sleep(2 * idleTimeout); + + connection1 = destination.idleConnections().poll(); + Assert.assertNull(connection1); } - Assert.assertNotNull(connection1); - - TimeUnit.MILLISECONDS.sleep(2 * idleTimeout); - - connection1 = destination.idleConnections().poll(); - Assert.assertNull(connection1); } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java index dd63e98132b..767444ce871 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java @@ -18,174 +18,147 @@ package org.eclipse.jetty.http; +import java.util.concurrent.TimeUnit; + public class HttpCookie { private final String _name; private final String _value; private final String _comment; private final String _domain; - private final int _maxAge; + private final long _maxAge; private final String _path; private final boolean _secure; private final int _version; private final boolean _httpOnly; + private final long _expiration; - /* ------------------------------------------------------------ */ public HttpCookie(String name, String value) { - super(); - _name = name; - _value = value; - _comment = null; - _domain = null; - _httpOnly = false; - _maxAge = -1; - _path = null; - _secure = false; - _version = 0; + this(name, value, -1); } - /* ------------------------------------------------------------ */ public HttpCookie(String name, String value, String domain, String path) { - super(); + this(name, value, domain, path, -1, false, false); + } + + public HttpCookie(String name, String value, long maxAge) + { + this(name, value, null, null, maxAge, false, false); + } + + public HttpCookie(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure) + { + this(name, value, domain, path, maxAge, httpOnly, secure, null, 0); + } + + public HttpCookie(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure, String comment, int version) + { _name = name; _value = value; - _comment = null; _domain = domain; - _httpOnly = false; - _maxAge = -1; _path = path; - _secure = false; - _version = 0; - - } - - /* ------------------------------------------------------------ */ - public HttpCookie(String name, String value, int maxAge) - { - super(); - _name = name; - _value = value; - _comment = null; - _domain = null; - _httpOnly = false; _maxAge = maxAge; - _path = null; - _secure = false; - _version = 0; - } - - /* ------------------------------------------------------------ */ - public HttpCookie(String name, String value, String domain, String path, int maxAge, boolean httpOnly, boolean secure) - { - super(); - _comment = null; - _domain = domain; _httpOnly = httpOnly; - _maxAge = maxAge; - _name = name; - _path = path; _secure = secure; - _value = value; - _version = 0; - } - - /* ------------------------------------------------------------ */ - public HttpCookie(String name, String value, String domain, String path, int maxAge, boolean httpOnly, boolean secure, String comment, int version) - { - super(); _comment = comment; - _domain = domain; - _httpOnly = httpOnly; - _maxAge = maxAge; - _name = name; - _path = path; - _secure = secure; - _value = value; _version = version; + _expiration = maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(maxAge); } - /* ------------------------------------------------------------ */ - /** Get the name. - * @return the name + /** + * @return the cookie name */ public String getName() { return _name; } - /* ------------------------------------------------------------ */ - /** Get the value. - * @return the value + /** + * @return the cookie value */ public String getValue() { return _value; } - /* ------------------------------------------------------------ */ - /** Get the comment. - * @return the comment + /** + * @return the cookie comment */ public String getComment() { return _comment; } - /* ------------------------------------------------------------ */ - /** Get the domain. - * @return the domain + /** + * @return the cookie domain */ public String getDomain() { return _domain; } - /* ------------------------------------------------------------ */ - /** Get the maxAge. - * @return the maxAge + /** + * @return the cookie max age in seconds */ - public int getMaxAge() + public long getMaxAge() { return _maxAge; } - /* ------------------------------------------------------------ */ - /** Get the path. - * @return the path + /** + * @return the cookie path */ public String getPath() { return _path; } - /* ------------------------------------------------------------ */ - /** Get the secure. - * @return the secure + /** + * @return whether the cookie is valid for secure domains */ public boolean isSecure() { return _secure; } - /* ------------------------------------------------------------ */ - /** Get the version. - * @return the version + /** + * @return the cookie version */ public int getVersion() { return _version; } - /* ------------------------------------------------------------ */ - /** Get the isHttpOnly. - * @return the isHttpOnly + /** + * @return whether the cookie is valid for the http protocol only */ public boolean isHttpOnly() { return _httpOnly; } + /** + * @param timeNanos the time to check for cookie expiration, in nanoseconds + * @return whether the cookie is expired by the given time + */ + public boolean isExpired(long timeNanos) + { + return _expiration >= 0 && timeNanos >= _expiration; + } + /** + * @return a string representation of this cookie + */ + public String asString() + { + StringBuilder builder = new StringBuilder(); + builder.append(getName()).append("=").append(getValue()); + if (getDomain() != null) + builder.append(";$Domain=").append(getDomain()); + if (getPath() != null) + builder.append(";$Path=").append(getPath()); + return builder.toString(); + } }