From 82f6383bc1f26eccb6e34e4e4129cd3b89d17954 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Thu, 2 Feb 2023 11:14:52 +0100 Subject: [PATCH] Fixes #9288 - Jetty 12 - Use oej.http.HttpCookie in jetty-client. * Replaced usages of java.net.HttpCookie with oej.http.HttpCookie. * Moved server-side only methods from HttpCookie to HttpCookieUtils. * Introduced and implemented oej.http.HttpCookieStore. * Removed now obsolete oej.util.HttpCookieStore. Signed-off-by: Simone Bordet --- .../client/http/HTTPClientDocs.java | 37 +- .../org/eclipse/jetty/client/HttpClient.java | 90 +- .../org/eclipse/jetty/client/Request.java | 2 +- .../client/transport/HttpConnection.java | 9 +- .../jetty/client/transport/HttpRequest.java | 2 +- .../eclipse/jetty/client/HttpClientTest.java | 4 +- .../eclipse/jetty/client/HttpCookieTest.java | 19 +- .../org/eclipse/jetty/http/DateGenerator.java | 24 - .../org/eclipse/jetty/http/HttpCookie.java | 1010 +++++++++-------- .../eclipse/jetty/http/HttpCookieStore.java | 383 +++++++ .../jetty/http/HttpCookieStoreTest.java | 276 +++++ .../org/eclipse/jetty/proxy/ProxyHandler.java | 4 +- .../eclipse/jetty/server/HttpCookieUtils.java | 436 +++++++ .../org/eclipse/jetty/server/Response.java | 10 +- .../eclipse/jetty/server}/HttpCookieTest.java | 181 ++- .../eclipse/jetty/util/HttpCookieStore.java | 142 --- .../core/client/CoreClientUpgradeRequest.java | 2 +- .../ee10/proxy/AbstractProxyServlet.java | 4 +- .../jetty/ee10/proxy/ProxyServletTest.java | 6 +- .../jetty/ee10/servlet/ServletApiRequest.java | 3 +- .../ee10/servlet/ServletApiResponse.java | 10 +- .../ee10/servlet/SessionHandlerTest.java | 3 +- .../jetty/ee10/session/SessionRenewTest.java | 4 +- .../websocket/client/WebSocketClient.java | 17 - .../DelegatedJettyClientUpgradeRequest.java | 4 +- .../org/eclipse/jetty/ee9/nested/Request.java | 7 +- .../eclipse/jetty/ee9/nested/Response.java | 21 +- .../eclipse/jetty/ee9/nested/RequestTest.java | 9 +- .../jetty/ee9/nested/ResponseTest.java | 11 +- .../jetty/ee9/nested/SessionHandlerTest.java | 3 +- .../jetty/ee9/proxy/AbstractProxyServlet.java | 4 +- .../jetty/ee9/proxy/ProxyServletTest.java | 6 +- .../jetty/ee9/session/SessionRenewTest.java | 4 +- .../ee9/websocket/client/WebSocketClient.java | 17 - .../DelegatedJettyClientUpgradeRequest.java | 4 +- 35 files changed, 1956 insertions(+), 812 deletions(-) create mode 100644 jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookieStore.java create mode 100644 jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieStoreTest.java create mode 100644 jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpCookieUtils.java rename jetty-core/{jetty-http/src/test/java/org/eclipse/jetty/http => jetty-server/src/test/java/org/eclipse/jetty/server}/HttpCookieTest.java (63%) delete mode 100644 jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/HttpCookieStore.java diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java index 90857287c2fe..10a753b5db31 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java @@ -17,8 +17,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.CookieStore; -import java.net.HttpCookie; import java.net.URI; import java.nio.ByteBuffer; import java.nio.file.Path; @@ -54,6 +52,8 @@ import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.fcgi.client.transport.HttpClientTransportOverFCGI; +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpCookieStore; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; @@ -67,7 +67,6 @@ import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.HttpCookieStore; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -541,8 +540,8 @@ public void getCookies() throws Exception httpClient.start(); // tag::getCookies[] - CookieStore cookieStore = httpClient.getCookieStore(); - List cookies = cookieStore.get(URI.create("http://domain.com/path")); + HttpCookieStore cookieStore = httpClient.getHttpCookieStore(); + List cookies = cookieStore.match(URI.create("http://domain.com/path")); // end::getCookies[] } @@ -552,11 +551,12 @@ public void setCookie() throws Exception httpClient.start(); // tag::setCookie[] - CookieStore cookieStore = httpClient.getCookieStore(); - HttpCookie cookie = new HttpCookie("foo", "bar"); - cookie.setDomain("domain.com"); - cookie.setPath("/"); - cookie.setMaxAge(TimeUnit.DAYS.toSeconds(1)); + HttpCookieStore cookieStore = httpClient.getHttpCookieStore(); + HttpCookie cookie = HttpCookie.build("foo", "bar") + .domain("domain.com") + .path("/") + .maxAge(TimeUnit.DAYS.toSeconds(1)) + .build(); cookieStore.add(URI.create("http://domain.com"), cookie); // end::setCookie[] } @@ -568,7 +568,7 @@ public void requestCookie() throws Exception // tag::requestCookie[] ContentResponse response = httpClient.newRequest("http://domain.com/path") - .cookie(new HttpCookie("foo", "bar")) + .cookie(HttpCookie.from("foo", "bar")) .send(); // end::requestCookie[] } @@ -579,9 +579,9 @@ public void removeCookie() throws Exception httpClient.start(); // tag::removeCookie[] - CookieStore cookieStore = httpClient.getCookieStore(); + HttpCookieStore cookieStore = httpClient.getHttpCookieStore(); URI uri = URI.create("http://domain.com"); - List cookies = cookieStore.get(uri); + List cookies = cookieStore.match(uri); for (HttpCookie cookie : cookies) { cookieStore.remove(uri, cookie); @@ -595,7 +595,7 @@ public void emptyCookieStore() throws Exception httpClient.start(); // tag::emptyCookieStore[] - httpClient.setCookieStore(new HttpCookieStore.Empty()); + httpClient.setHttpCookieStore(new HttpCookieStore.Empty()); // end::emptyCookieStore[] } @@ -605,17 +605,18 @@ public void filteringCookieStore() throws Exception httpClient.start(); // tag::filteringCookieStore[] - class GoogleOnlyCookieStore extends HttpCookieStore + class GoogleOnlyCookieStore extends HttpCookieStore.Default { @Override - public void add(URI uri, HttpCookie cookie) + public boolean add(URI uri, HttpCookie cookie) { if (uri.getHost().endsWith("google.com")) - super.add(uri, cookie); + return super.add(uri, cookie); + return false; } } - httpClient.setCookieStore(new GoogleOnlyCookieStore()); + httpClient.setHttpCookieStore(new GoogleOnlyCookieStore()); // end::filteringCookieStore[] } diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index 45736485e0d4..c3060faefc21 100644 --- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -43,6 +43,8 @@ import org.eclipse.jetty.client.transport.HttpDestination; import org.eclipse.jetty.client.transport.HttpRequest; import org.eclipse.jetty.http.HttpCompliance; +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpCookieStore; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; @@ -81,7 +83,7 @@ * and HTTP parameters (such as whether to follow redirects).

*

HttpClient transparently pools connections to servers, but allows direct control of connections * for cases where this is needed.

- *

HttpClient also acts as a central configuration point for cookies, via {@link #getCookieStore()}.

+ *

HttpClient also acts as a central configuration point for cookies, via {@link #getHttpCookieStore()}.

*

Typical usage:

*
  * HttpClient httpClient = new HttpClient();
@@ -121,8 +123,8 @@ public class HttpClient extends ContainerLifeCycle
     private final HttpClientTransport transport;
     private final ClientConnector connector;
     private AuthenticationStore authenticationStore = new HttpAuthenticationStore();
-    private CookieManager cookieManager;
-    private CookieStore cookieStore;
+    private HttpCookieStore cookieStore;
+    private HttpCookieParser cookieParser;
     private SocketAddressResolver resolver;
     private HttpField agentField = new HttpField(HttpHeader.USER_AGENT, USER_AGENT);
     private boolean followRedirects = true;
@@ -222,8 +224,9 @@ protected void doStart() throws Exception
 
         decoderFactories.put(new GZIPContentDecoder.Factory(retainableByteBufferPool));
 
-        cookieManager = newCookieManager();
-        cookieStore = cookieManager.getCookieStore();
+        if (cookieStore == null)
+            cookieStore = new HttpCookieStore.Default();
+        cookieParser = new HttpCookieParser();
 
         transport.setHttpClient(this);
 
@@ -236,11 +239,6 @@ protected void doStart() throws Exception
         }
     }
 
-    private CookieManager newCookieManager()
-    {
-        return new CookieManager(getCookieStore(), CookiePolicy.ACCEPT_ALL);
-    }
-
     @Override
     protected void doStop() throws Exception
     {
@@ -274,7 +272,7 @@ public List getRequestListeners()
     /**
      * @return the cookie store associated with this instance
      */
-    public CookieStore getCookieStore()
+    public HttpCookieStore getHttpCookieStore()
     {
         return cookieStore;
     }
@@ -282,12 +280,11 @@ public CookieStore getCookieStore()
     /**
      * @param cookieStore the cookie store associated with this instance
      */
-    public void setCookieStore(CookieStore cookieStore)
+    public void setHttpCookieStore(HttpCookieStore cookieStore)
     {
         if (isStarted())
             throw new IllegalStateException();
         this.cookieStore = Objects.requireNonNull(cookieStore);
-        this.cookieManager = newCookieManager();
     }
 
     public void putCookie(URI uri, HttpField field)
@@ -297,9 +294,8 @@ public void putCookie(URI uri, HttpField field)
             String value = field.getValue();
             if (value != null)
             {
-                Map> header = new HashMap<>(1);
-                header.put(field.getHeader().asString(), List.of(value));
-                cookieManager.put(uri, header);
+                HttpCookie cookie = cookieParser.parse(uri, field);
+                cookieStore.add(uri, cookie);
             }
         }
         catch (IOException x)
@@ -1154,4 +1150,66 @@ public ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.C
             sslContextFactory = getSslContextFactory();
         return new SslClientConnectionFactory(sslContextFactory, getRetainableByteBufferPool(), getExecutor(), connectionFactory);
     }
+
+    private static class HttpCookieParser extends CookieManager
+    {
+        public HttpCookieParser()
+        {
+            super(new Store(), CookiePolicy.ACCEPT_ALL);
+        }
+
+        public HttpCookie parse(URI uri, HttpField field) throws IOException
+        {
+            // TODO: hacky implementation waiting for a real HttpCookie parser.
+            String value = field.getValue();
+            if (value == null)
+                return null;
+            Map> header = new HashMap<>(1);
+            header.put(field.getHeader().asString(), List.of(value));
+            put(uri, header);
+            Store store = (Store)getCookieStore();
+            return store.cookie;
+        }
+
+        private static class Store implements CookieStore
+        {
+            private HttpCookie cookie;
+
+            @Override
+            public void add(URI uri, java.net.HttpCookie cookie)
+            {
+                this.cookie = HttpCookie.from(cookie);
+            }
+
+            @Override
+            public List get(URI uri)
+            {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public List getCookies()
+            {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public List getURIs()
+            {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public boolean remove(URI uri, java.net.HttpCookie cookie)
+            {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public boolean removeAll()
+            {
+                throw new UnsupportedOperationException();
+            }
+        }
+    }
 }
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Request.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Request.java
index 41170ddc6feb..8e7bf1e10c16 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Request.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/Request.java
@@ -14,7 +14,6 @@
 package org.eclipse.jetty.client;
 
 import java.io.IOException;
-import java.net.HttpCookie;
 import java.net.URI;
 import java.net.URLEncoder;
 import java.nio.ByteBuffer;
@@ -30,6 +29,7 @@
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 
+import org.eclipse.jetty.http.HttpCookie;
 import org.eclipse.jetty.http.HttpFields;
 import org.eclipse.jetty.http.HttpMethod;
 import org.eclipse.jetty.http.HttpVersion;
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java
index 95e4ca21b34d..e0f6696cfcfb 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpConnection.java
@@ -13,8 +13,6 @@
 
 package org.eclipse.jetty.client.transport;
 
-import java.net.CookieStore;
-import java.net.HttpCookie;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -30,13 +28,14 @@
 import org.eclipse.jetty.client.ProxyConfiguration;
 import org.eclipse.jetty.client.Request;
 import org.eclipse.jetty.client.Response;
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.http.HttpCookieStore;
 import org.eclipse.jetty.http.HttpField;
 import org.eclipse.jetty.http.HttpFields;
 import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.http.HttpVersion;
 import org.eclipse.jetty.io.CyclicTimeouts;
 import org.eclipse.jetty.util.Attachable;
-import org.eclipse.jetty.util.HttpCookieStore;
 import org.eclipse.jetty.util.NanoTime;
 import org.eclipse.jetty.util.thread.AutoLock;
 import org.eclipse.jetty.util.thread.Scheduler;
@@ -210,12 +209,12 @@ protected void normalizeRequest(HttpRequest request)
 
         // Cookies
         StringBuilder cookies = convertCookies(request.getCookies(), null);
-        CookieStore cookieStore = getHttpClient().getCookieStore();
+        HttpCookieStore cookieStore = getHttpClient().getHttpCookieStore();
         if (cookieStore != null && cookieStore.getClass() != HttpCookieStore.Empty.class)
         {
             URI uri = request.getURI();
             if (uri != null)
-                cookies = convertCookies(HttpCookieStore.matchPath(uri, cookieStore.get(uri)), cookies);
+                cookies = convertCookies(cookieStore.match(uri), cookies);
         }
         if (cookies != null)
         {
diff --git a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpRequest.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpRequest.java
index c3a0dd68bd25..19276be6692e 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpRequest.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpRequest.java
@@ -14,7 +14,6 @@
 package org.eclipse.jetty.client.transport;
 
 import java.io.IOException;
-import java.net.HttpCookie;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URLDecoder;
@@ -48,6 +47,7 @@
 import org.eclipse.jetty.client.Request;
 import org.eclipse.jetty.client.Response;
 import org.eclipse.jetty.client.Result;
+import org.eclipse.jetty.http.HttpCookie;
 import org.eclipse.jetty.http.HttpField;
 import org.eclipse.jetty.http.HttpFields;
 import org.eclipse.jetty.http.HttpHeader;
diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java
index 04ec16bb8f35..2468b376e4bd 100644
--- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java
+++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java
@@ -17,7 +17,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.net.HttpCookie;
 import java.net.InetSocketAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
@@ -48,6 +47,7 @@
 import org.eclipse.jetty.client.transport.HttpRequest;
 import org.eclipse.jetty.client.transport.internal.HttpConnectionOverHTTP;
 import org.eclipse.jetty.http.BadMessageException;
+import org.eclipse.jetty.http.HttpCookie;
 import org.eclipse.jetty.http.HttpField;
 import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.http.HttpHeaderValue;
@@ -122,7 +122,7 @@ public void testStoppingClosesConnections(Scenario scenario) throws Exception
 
         Origin origin = destination.getOrigin();
         String uri = origin.getScheme() + "://" + origin.getAddress();
-        client.getCookieStore().add(URI.create(uri), new HttpCookie("foo", "bar"));
+        client.getHttpCookieStore().add(URI.create(uri), HttpCookie.from("foo", "bar"));
 
         client.stop();
 
diff --git a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java
index ed0b0e34708c..44b7d76b76c8 100644
--- a/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java
+++ b/jetty-core/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java
@@ -13,8 +13,6 @@
 
 package org.eclipse.jetty.client;
 
-import java.net.CookieStore;
-import java.net.HttpCookie;
 import java.net.URI;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -23,9 +21,10 @@
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
+import org.eclipse.jetty.http.HttpCookie;
+import org.eclipse.jetty.http.HttpCookieStore;
 import org.eclipse.jetty.http.HttpStatus;
 import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.util.HttpCookieStore;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ArgumentsSource;
 
@@ -60,7 +59,7 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
         Response response = client.GET(uri);
         assertEquals(200, response.getStatus());
 
-        List cookies = client.getCookieStore().get(URI.create(uri));
+        List cookies = client.getHttpCookieStore().match(URI.create(uri));
         assertNotNull(cookies);
         assertEquals(1, cookies.size());
         HttpCookie cookie = cookies.get(0);
@@ -92,8 +91,8 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
         int port = connector.getLocalPort();
         String path = "/path";
         String uri = scenario.getScheme() + "://" + host + ":" + port;
-        HttpCookie cookie = new HttpCookie(name, value);
-        client.getCookieStore().add(URI.create(uri), cookie);
+        HttpCookie cookie = HttpCookie.from(name, value);
+        client.getHttpCookieStore().add(URI.create(uri), cookie);
 
         Response response = client.GET(scenario.getScheme() + "://" + host + ":" + port + path);
         assertEquals(200, response.getStatus());
@@ -116,7 +115,7 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
             .scheme(scenario.getScheme())
             .send();
         assertEquals(200, response.getStatus());
-        assertTrue(client.getCookieStore().getCookies().isEmpty());
+        assertTrue(client.getHttpCookieStore().all().isEmpty());
     }
 
     @ParameterizedTest
@@ -133,7 +132,7 @@ public void testPerRequestCookieIsSentWithEmptyCookieStore(Scenario scenario) th
         testPerRequestCookieIsSent(scenario, new HttpCookieStore.Empty());
     }
 
-    private void testPerRequestCookieIsSent(Scenario scenario, CookieStore cookieStore) throws Exception
+    private void testPerRequestCookieIsSent(Scenario scenario, HttpCookieStore cookieStore) throws Exception
     {
         final String name = "foo";
         final String value = "bar";
@@ -153,12 +152,12 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
         startClient(scenario, client ->
         {
             if (cookieStore != null)
-                client.setCookieStore(cookieStore);
+                client.setHttpCookieStore(cookieStore);
         });
 
         ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
             .scheme(scenario.getScheme())
-            .cookie(new HttpCookie(name, value))
+            .cookie(HttpCookie.from(name, value))
             .timeout(5, TimeUnit.SECONDS)
             .send();
         assertEquals(200, response.getStatus());
diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateGenerator.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateGenerator.java
index 1c6e8f310d4c..d613a5717d29 100644
--- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateGenerator.java
+++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/DateGenerator.java
@@ -70,30 +70,6 @@ public static String formatDate(Instant instant)
         return formatDate(instant.toEpochMilli());
     }
 
-    /**
-     * Format "EEE, dd-MMM-yyyy HH:mm:ss 'GMT'" for cookies
-     *
-     * @param buf the buffer to put the formatted date into
-     * @param date the date in milliseconds
-     */
-    public static void formatCookieDate(StringBuilder buf, long date)
-    {
-        __dateGenerator.get().doFormatCookieDate(buf, date);
-    }
-
-    /**
-     * Format "EEE, dd-MMM-yyyy HH:mm:ss 'GMT'" for cookies
-     *
-     * @param date the date in milliseconds
-     * @return the formatted date
-     */
-    public static String formatCookieDate(long date)
-    {
-        StringBuilder buf = new StringBuilder(28);
-        formatCookieDate(buf, date);
-        return buf.toString();
-    }
-
     private final StringBuilder buf = new StringBuilder(32);
     private final GregorianCalendar gc = new GregorianCalendar(__GMT);
 
diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java
index 74fda8ac33f0..d5fe69705777 100644
--- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java
+++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java
@@ -13,51 +13,335 @@
 
 package org.eclipse.jetty.http;
 
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.TreeMap;
 
-import org.eclipse.jetty.util.Attributes;
 import org.eclipse.jetty.util.Index;
-import org.eclipse.jetty.util.QuotedStringTokenizer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
- * Jetty Management of RFC6265 HTTP Cookies (with fallback support for RFC2965)
+ * 

Implementation of RFC6265 HTTP Cookies (with fallback support for RFC2965).

*/ public interface HttpCookie { - Logger LOG = LoggerFactory.getLogger(HttpCookie.class); - - String __COOKIE_DELIM = "\",;\\ \t"; - String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim(); - String COMMENT_ATTRIBUTE = "Comment"; String DOMAIN_ATTRIBUTE = "Domain"; + String EXPIRES_ATTRIBUTE = "Expires"; String HTTP_ONLY_ATTRIBUTE = "HttpOnly"; String MAX_AGE_ATTRIBUTE = "Max-Age"; String PATH_ATTRIBUTE = "Path"; String SAME_SITE_ATTRIBUTE = "SameSite"; String SECURE_ATTRIBUTE = "Secure"; - Index KNOWN_ATTRIBUTES = new Index.Builder().caseSensitive(false) - .with(COMMENT_ATTRIBUTE) - .with(DOMAIN_ATTRIBUTE) - .with(HTTP_ONLY_ATTRIBUTE) - .with(MAX_AGE_ATTRIBUTE) - .with(PATH_ATTRIBUTE) - .with(SAME_SITE_ATTRIBUTE) - .with(SECURE_ATTRIBUTE) - .build(); /** - * Name of context attribute with default SameSite cookie value + * @return the cookie name + */ + String getName(); + + /** + * @return the cookie value */ - String SAME_SITE_DEFAULT_ATTRIBUTE = "org.eclipse.jetty.cookie.sameSiteDefault"; + String getValue(); + + /** + * @return the value of the {@code Version} attribute + */ + int getVersion(); + + /** + * @return the attributes associated with this cookie + */ + Map getAttributes(); + + /** + * @return the value of the {@code Expires} attribute, or {@code null} if not present + * @see #EXPIRES_ATTRIBUTE + */ + default Instant getExpires() + { + String expires = getAttributes().get(EXPIRES_ATTRIBUTE); + return expires == null ? null : parseExpires(expires); + } + + /** + * @return the value of the {@code Max-Age} attribute, in seconds, or {@code -1} if not present + * @see #MAX_AGE_ATTRIBUTE + */ + default long getMaxAge() + { + String ma = getAttributes().get(MAX_AGE_ATTRIBUTE); + return ma == null ? -1 : Long.parseLong(ma); + } + + /** + * @return whether the cookie is expired + */ + default boolean isExpired() + { + if (getMaxAge() == 0) + return true; + Instant expires = getExpires(); + return expires != null && Instant.now().isAfter(expires); + } + + /** + *

Equivalent to {@code getAttributes().get(COMMENT_ATTRIBUTE)}.

+ * + * @return the value of the {@code Comment} attribute + * @see #COMMENT_ATTRIBUTE + */ + default String getComment() + { + return getAttributes().get(COMMENT_ATTRIBUTE); + } + + /** + *

Equivalent to {@code getAttributes().get(DOMAIN_ATTRIBUTE)}.

+ * + * @return the value of the {@code Domain} attribute + * @see #DOMAIN_ATTRIBUTE + */ + default String getDomain() + { + return getAttributes().get(DOMAIN_ATTRIBUTE); + } + + /** + *

Equivalent to {@code getAttributes().get(PATH_ATTRIBUTE)}.

+ * + * @return the value of the {@code Path} attribute + * @see #PATH_ATTRIBUTE + */ + default String getPath() + { + return getAttributes().get(PATH_ATTRIBUTE); + } + + /** + * @return whether the {@code Secure} attribute is present + * @see #SECURE_ATTRIBUTE + */ + default boolean isSecure() + { + return Boolean.parseBoolean(getAttributes().get(SECURE_ATTRIBUTE)); + } + + /** + * @return the value of the {@code SameSite} attribute + * @see #SAME_SITE_ATTRIBUTE + */ + default SameSite getSameSite() + { + return SameSite.from(getAttributes().get(SAME_SITE_ATTRIBUTE)); + } + + /** + * @return whether the {@code HttpOnly} attribute is present + * @see #HTTP_ONLY_ATTRIBUTE + */ + default boolean isHttpOnly() + { + return Boolean.parseBoolean(getAttributes().get(HTTP_ONLY_ATTRIBUTE)); + } + + @Override + int hashCode(); + + @Override + boolean equals(Object obj); + + /** + *

A wrapper for {@code HttpCookie} instances.

+ */ + class Wrapper implements HttpCookie + { + private final HttpCookie wrapped; + + public Wrapper(HttpCookie wrapped) + { + this.wrapped = wrapped; + } + + public HttpCookie getWrapped() + { + return wrapped; + } + + @Override + public String getName() + { + return getWrapped().getName(); + } + + @Override + public String getValue() + { + return getWrapped().getValue(); + } + + @Override + public int getVersion() + { + return getWrapped().getVersion(); + } + + @Override + public Map getAttributes() + { + return getWrapped().getAttributes(); + } + + @Override + public Instant getExpires() + { + return getWrapped().getExpires(); + } + + @Override + public long getMaxAge() + { + return getWrapped().getMaxAge(); + } + + @Override + public boolean isExpired() + { + return getWrapped().isExpired(); + } + + @Override + public String getComment() + { + return getWrapped().getComment(); + } + + @Override + public String getDomain() + { + return getWrapped().getDomain(); + } + + @Override + public String getPath() + { + return getWrapped().getPath(); + } + + @Override + public boolean isSecure() + { + return getWrapped().isSecure(); + } + + @Override + public SameSite getSameSite() + { + return getWrapped().getSameSite(); + } + + @Override + public boolean isHttpOnly() + { + return getWrapped().isHttpOnly(); + } + + @Override + public int hashCode() + { + return HttpCookie.hashCode(this); + } + + @Override + public boolean equals(Object obj) + { + return HttpCookie.equals(this, obj); + } + + @Override + public String toString() + { + return HttpCookie.toString(this); + } + } + + /** + *

Immutable implementation of {@link HttpCookie}.

+ */ + class Immutable implements HttpCookie + { + private final String _name; + private final String _value; + private final int _version; + private final Map _attributes; + + private Immutable(String name, String value, int version, Map attributes) + { + _name = name; + _value = value; + _version = version; + _attributes = attributes == null || attributes.isEmpty() ? Collections.emptyMap() : attributes; + } + + /** + * @return the cookie name + */ + @Override + public String getName() + { + return _name; + } + + /** + * @return the cookie value + */ + @Override + public String getValue() + { + return _value; + } + + /** + * @return the cookie version + */ + @Override + public int getVersion() + { + return _version; + } + + /** + * @return the attributes associated with this cookie + */ + @Override + public Map getAttributes() + { + return _attributes; + } + + @Override + public int hashCode() + { + return HttpCookie.hashCode(this); + } + + @Override + public boolean equals(Object obj) + { + return HttpCookie.equals(this, obj); + } + + @Override + public String toString() + { + return HttpCookie.toString(this); + } + } enum SameSite { @@ -93,7 +377,7 @@ public static SameSite from(String sameSite) } /** - * Create new HttpCookie from specific values. + * Creates new HttpCookie from specific values. * * @param name the name of the cookie * @param value the value of the cookie @@ -104,14 +388,14 @@ static HttpCookie from(String name, String value) } /** - * Create new HttpCookie from specific values and attributes. + * Creates new HttpCookie from specific values and attributes. * * @param name the name of the cookie * @param value the value of the cookie * @param attributes the map of attributes to use with this cookie (this map is used for field values * such as {@link #getDomain()}, {@link #getPath()}, {@link #getMaxAge()}, {@link #isHttpOnly()}, - * {@link #isSecure()}, {@link #getComment()}. These attributes are removed from the stored - * attributes returned from {@link #getAttributes()}. + * {@link #isSecure()}, {@link #getComment()}, plus any newly defined attributes unknown to this + * code base. */ static HttpCookie from(String name, String value, Map attributes) { @@ -119,15 +403,15 @@ static HttpCookie from(String name, String value, Map attributes } /** - * Create new HttpCookie from specific values and attributes. + * Creates new HttpCookie from specific values and attributes. * * @param name the name of the cookie * @param value the value of the cookie * @param version the version of the cookie (only used in RFC2965 mode) * @param attributes the map of attributes to use with this cookie (this map is used for field values * such as {@link #getDomain()}, {@link #getPath()}, {@link #getMaxAge()}, {@link #isHttpOnly()}, - * {@link #isSecure()}, {@link #getComment()}. These attributes are removed from the stored - * attributes returned from {@link #getAttributes()}. + * {@link #isSecure()}, {@link #getComment()}, plus any newly defined attributes unknown to this + * code base. */ static HttpCookie from(String name, String value, int version, Map attributes) { @@ -156,100 +440,114 @@ static HttpCookie from(HttpCookie cookie, String... additionalAttributes) return new Immutable(cookie.getName(), cookie.getValue(), cookie.getVersion(), attributes); } - /** - * @return the cookie name - */ - String getName(); - - /** - * @return the cookie value - */ - String getValue(); - - /** - * @return the cookie version - */ - int getVersion(); - - /** - * @return the cookie comment. - * Equivalent to a `get` of {@link #COMMENT_ATTRIBUTE} on {@link #getAttributes()}. - */ - default String getComment() + static HttpCookie from(java.net.HttpCookie httpCookie) { - return getAttributes().get(COMMENT_ATTRIBUTE); + return new JavaNetHttpCookie(httpCookie); } - /** - * @return the cookie domain. - * Equivalent to a `get` of {@link #DOMAIN_ATTRIBUTE} on {@link #getAttributes()}. - */ - default String getDomain() + static Builder build(String name, String value) { - return getAttributes().get(DOMAIN_ATTRIBUTE); + return new Builder(name, value); } - /** - * @return the cookie max age in seconds - * Equivalent to a `get` of {@link #MAX_AGE_ATTRIBUTE} on {@link #getAttributes()}. - */ - default long getMaxAge() + static Builder build(String name, String value, int version) { - String ma = getAttributes().get(MAX_AGE_ATTRIBUTE); - return ma == null ? -1 : Long.parseLong(ma); + return new Builder(name, value, version); } - /** - * @return the cookie path - * Equivalent to a `get` of {@link #PATH_ATTRIBUTE} on {@link #getAttributes()}. - */ - default String getPath() + static Builder build(HttpCookie httpCookie) { - return getAttributes().get(PATH_ATTRIBUTE); + Builder builder = new Builder(httpCookie.getName(), httpCookie.getValue(), httpCookie.getVersion()); + for (Map.Entry entry : httpCookie.getAttributes().entrySet()) + builder = builder.attribute(entry.getKey(), entry.getValue()); + return builder; } - /** - * @return whether the cookie is valid for secure domains - * Equivalent to a `get` of {@link #SECURE_ATTRIBUTE} on {@link #getAttributes()}. - */ - default boolean isSecure() + static Builder build(java.net.HttpCookie httpCookie) { - return Boolean.parseBoolean(getAttributes().get(SECURE_ATTRIBUTE)); + return new Builder(httpCookie.getName(), httpCookie.getValue(), httpCookie.getVersion()) + .comment(httpCookie.getComment()) + .domain(httpCookie.getDomain()) + .httpOnly(httpCookie.isHttpOnly()) + .maxAge(httpCookie.getMaxAge()) + .path(httpCookie.getPath()) + .secure(httpCookie.getSecure()); } - /** - * @return the cookie {@code SameSite} attribute value - * Equivalent to a `get` of {@link #SAME_SITE_ATTRIBUTE} on {@link #getAttributes()}. - */ - default SameSite getSameSite() + static java.net.HttpCookie asJavaNetHttpCookie(HttpCookie httpCookie) { - return SameSite.from(getAttributes().get(SAME_SITE_ATTRIBUTE)); + if (httpCookie.getSameSite() != null) + throw new IllegalArgumentException("SameSite attribute not supported by " + java.net.HttpCookie.class.getName()); + java.net.HttpCookie cookie = new java.net.HttpCookie(httpCookie.getName(), httpCookie.getValue()); + cookie.setVersion(httpCookie.getVersion()); + cookie.setComment(httpCookie.getComment()); + cookie.setDomain(httpCookie.getDomain()); + cookie.setHttpOnly(httpCookie.isHttpOnly()); + cookie.setMaxAge(httpCookie.getMaxAge()); + cookie.setPath(httpCookie.getPath()); + cookie.setSecure(httpCookie.isSecure()); + return cookie; } /** - * @return whether the cookie is valid for the http protocol only - * Equivalent to a `get` of {@link #HTTP_ONLY_ATTRIBUTE} on {@link #getAttributes()}. + *

Implementation of {@link Object#hashCode()} compatible with RFC 6265.

+ * + * @param httpCookie the cookie to be hashed + * @return the hash code of the cookie + * @see #equals(HttpCookie, Object) */ - default boolean isHttpOnly() + static int hashCode(HttpCookie httpCookie) { - return Boolean.parseBoolean(getAttributes().get(HTTP_ONLY_ATTRIBUTE)); + String domain = httpCookie.getDomain(); + if (domain != null) + domain = domain.toLowerCase(Locale.ENGLISH); + return Objects.hash(httpCookie.getName(), domain, httpCookie.getPath()); } /** - * @return the attributes associated with this cookie + *

Implementation of {@link Object#equals(Object)} compatible with RFC 6265.

+ *

Two cookies are equal if they have the same name (case-sensitive), the same + * domain (case-insensitive) and the same path (case-sensitive).

+ * + * @param cookie1 the first cookie to equal + * @param obj the second cookie to equal + * @return whether the cookies are equal + * @see #hashCode(HttpCookie) */ - Map getAttributes(); + static boolean equals(HttpCookie cookie1, Object obj) + { + if (cookie1 == obj) + return true; + if (cookie1 == null || obj == null) + return false; + if (!(obj instanceof HttpCookie cookie2)) + return false; + // RFC 2965 section. 3.3.3 and RFC 6265 section 4.1.2. + // Names are case-sensitive. + if (!Objects.equals(cookie1.getName(), cookie2.getName())) + return false; + // Domains are case-insensitive. + if (!equalsIgnoreCase(cookie1.getDomain(), cookie2.getDomain())) + return false; + // Paths are case-sensitive. + return Objects.equals(cookie1.getPath(), cookie2.getPath()); + } - /** - * @return a string representation of this cookie - */ - default String asString() + private static boolean equalsIgnoreCase(String obj1, String obj2) { - return HttpCookie.asString(this); + if (obj1 == obj2) + return true; + if (obj1 == null || obj2 == null) + return false; + return obj1.equalsIgnoreCase(obj2); } /** - * @return a string representation of this cookie + *

Formats this cookie into a string suitable to be used + * in {@code Cookie} or {@code Set-Cookie} headers.

+ * + * @param httpCookie the cookie to format + * @return a header string representation of the cookie */ static String asString(HttpCookie httpCookie) { @@ -264,478 +562,242 @@ static String asString(HttpCookie httpCookie) return builder.toString(); } + /** + *

Formats this cookie into a string suitable to be used + * for logging.

+ * + * @param httpCookie the cookie to format + * @return a logging string representation of the cookie + */ static String toString(HttpCookie httpCookie) { - return "%x@%s".formatted(httpCookie.hashCode(), asString(httpCookie)); + return "%s@%x[%s]".formatted(httpCookie.getClass().getSimpleName(), httpCookie.hashCode(), asString(httpCookie)); } - /** - * Immutable implementation of HttpCookie. - */ - class Immutable implements HttpCookie + class JavaNetHttpCookie implements HttpCookie { - private final String _name; - private final String _value; - private final int _version; - private final Map _attributes; + private final java.net.HttpCookie _httpCookie; + private Map _attributes; - Immutable(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure, String comment, int version, SameSite sameSite, Map attributes) + private JavaNetHttpCookie(java.net.HttpCookie httpCookie) { - _name = name; - _value = value; - _version = version; - Map attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - if (attributes != null) - attrs.putAll(attributes); - attrs.put(DOMAIN_ATTRIBUTE, domain); - attrs.put(PATH_ATTRIBUTE, path); - attrs.put(MAX_AGE_ATTRIBUTE, Long.toString(maxAge)); - attrs.put(HTTP_ONLY_ATTRIBUTE, Boolean.toString(httpOnly)); - attrs.put(SECURE_ATTRIBUTE, Boolean.toString(secure)); - attrs.put(COMMENT_ATTRIBUTE, comment); - attrs.put(SAME_SITE_ATTRIBUTE, sameSite == null ? null : sameSite.getAttributeValue()); - _attributes = Collections.unmodifiableMap(attrs); + _httpCookie = httpCookie; } - Immutable(String name, String value, int version, Map attributes) + @Override + public String getComment() { - _name = name; - _value = value; - _version = version; - _attributes = attributes == null ? Collections.emptyMap() : attributes; + return _httpCookie.getComment(); + } + + @Override + public String getDomain() + { + return _httpCookie.getDomain(); + } + + @Override + public long getMaxAge() + { + return _httpCookie.getMaxAge(); + } + + @Override + public String getPath() + { + return _httpCookie.getPath(); + } + + @Override + public boolean isSecure() + { + return _httpCookie.getSecure(); } - /** - * @return the cookie name - */ @Override public String getName() { - return _name; + return _httpCookie.getName(); } - /** - * @return the cookie value - */ @Override public String getValue() { - return _value; + return _httpCookie.getValue(); } - /** - * @return the cookie version - */ @Override public int getVersion() { - return _version; + return _httpCookie.getVersion(); } - /** - * @return the cookie {@code SameSite} attribute value - */ @Override - public SameSite getSameSite() + public boolean isHttpOnly() { - String val = _attributes.get(SAME_SITE_ATTRIBUTE); - if (val == null) - return null; - return SameSite.valueOf(val.toUpperCase(Locale.ENGLISH)); + return _httpCookie.isHttpOnly(); } - /** - * @return the attributes associated with this cookie - */ @Override public Map getAttributes() { + if (_attributes == null) + { + Map attributes = lazyAttributePut(null, COMMENT_ATTRIBUTE, getComment()); + attributes = lazyAttributePut(attributes, DOMAIN_ATTRIBUTE, getDomain()); + if (isHttpOnly()) + attributes = lazyAttributePut(attributes, HTTP_ONLY_ATTRIBUTE, Boolean.TRUE.toString()); + if (getMaxAge() >= 0) + attributes = lazyAttributePut(attributes, MAX_AGE_ATTRIBUTE, Long.toString(getMaxAge())); + attributes = lazyAttributePut(attributes, PATH_ATTRIBUTE, getPath()); + if (isSecure()) + attributes = lazyAttributePut(attributes, SECURE_ATTRIBUTE, Boolean.TRUE.toString()); + _attributes = HttpCookie.lazyAttributes(attributes); + } return _attributes; } @Override - public String toString() + public int hashCode() { - return "%x@%s".formatted(hashCode(), asString()); + return HttpCookie.hashCode(this); } - } - private static void quoteOnlyOrAppend(StringBuilder buf, String s, boolean quote) - { - if (quote) - QuotedStringTokenizer.quoteOnly(buf, s); - else - buf.append(s); + @Override + public boolean equals(Object obj) + { + return HttpCookie.equals(this, obj); + } + + @Override + public String toString() + { + return HttpCookie.toString(this); + } } /** - * Does a cookie value need to be quoted? - * - * @param s value string - * @return true if quoted; - * @throws IllegalArgumentException If there is a String contains unexpected / illegal characters + * A HttpCookie Builder + * @see HttpCookie#build(String, String) */ - private static boolean isQuoteNeededForCookie(String s) + class Builder { - if (s == null || s.length() == 0) - return true; - - if (QuotedStringTokenizer.isQuoted(s)) - return false; + private final String _name; + private final String _value; + private final int _version; + private Map _attributes; - for (int i = 0; i < s.length(); i++) + private Builder(String name, String value) { - char c = s.charAt(i); - if (__COOKIE_DELIM.indexOf(c) >= 0) - return true; - - if (c < 0x20 || c >= 0x7f) - throw new IllegalArgumentException("Illegal character in cookie value"); + this(name, value, 0); } - return false; - } - - static String getSetCookie(HttpCookie httpCookie, CookieCompliance compliance) - { - if (compliance == CookieCompliance.RFC6265) - return getRFC6265SetCookie(httpCookie); - if (compliance == CookieCompliance.RFC2965) - return getRFC2965SetCookie(httpCookie); - throw new IllegalStateException(); - } - - static String getRFC2965SetCookie(HttpCookie httpCookie) - { - // Check arguments - String name = httpCookie.getName(); - if (name == null || name.length() == 0) - throw new IllegalArgumentException("Bad cookie name"); - - // Format value and params - StringBuilder buf = new StringBuilder(); - - // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting - boolean quoteName = isQuoteNeededForCookie(name); - quoteOnlyOrAppend(buf, name, quoteName); - - buf.append('='); - - // Append the value - String value = httpCookie.getValue(); - boolean quoteValue = isQuoteNeededForCookie(value); - quoteOnlyOrAppend(buf, value, quoteValue); - - // Look for domain and path fields and check if they need to be quoted - String domain = httpCookie.getDomain(); - boolean hasDomain = domain != null && domain.length() > 0; - boolean quoteDomain = hasDomain && isQuoteNeededForCookie(domain); - - String path = httpCookie.getPath(); - boolean hasPath = path != null && path.length() > 0; - boolean quotePath = hasPath && isQuoteNeededForCookie(path); - - // Upgrade the version if we have a comment or we need to quote value/path/domain or if they were already quoted - int version = httpCookie.getVersion(); - String comment = httpCookie.getComment(); - if (version == 0 && (comment != null || quoteName || quoteValue || quoteDomain || quotePath || - QuotedStringTokenizer.isQuoted(name) || QuotedStringTokenizer.isQuoted(value) || - QuotedStringTokenizer.isQuoted(path) || QuotedStringTokenizer.isQuoted(domain))) - version = 1; - - // Append version - if (version == 1) - buf.append(";Version=1"); - else if (version > 1) - buf.append(";Version=").append(version); - - // Append path - if (hasPath) + private Builder(String name, String value, int version) { - buf.append(";Path="); - quoteOnlyOrAppend(buf, path, quotePath); + _name = name; + _value = value; + _version = version; } - // Append domain - if (hasDomain) + public Builder attribute(String name, String value) { - buf.append(";Domain="); - quoteOnlyOrAppend(buf, domain, quoteDomain); + _attributes = lazyAttributePut(_attributes, name, value); + return this; } - // Handle max-age and/or expires - long maxAge = httpCookie.getMaxAge(); - if (maxAge >= 0) + public Builder comment(String comment) { - // Always use expires - // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies - buf.append(";Expires="); - if (maxAge == 0) - buf.append(__01Jan1970_COOKIE); - else - DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * maxAge); - - buf.append(";Max-Age="); - buf.append(maxAge); + _attributes = lazyAttributePut(_attributes, COMMENT_ATTRIBUTE, comment); + return this; } - // add the other fields - if (httpCookie.isSecure()) - buf.append(";Secure"); - if (httpCookie.isHttpOnly()) - buf.append(";HttpOnly"); - if (comment != null) + public Builder domain(String domain) { - buf.append(";Comment="); - quoteOnlyOrAppend(buf, comment, isQuoteNeededForCookie(comment)); + _attributes = lazyAttributePut(_attributes, DOMAIN_ATTRIBUTE, domain); + return this; } - return buf.toString(); - } - - static String getRFC6265SetCookie(HttpCookie httpCookie) - { - // Check arguments - String name = httpCookie.getName(); - if (name == null || name.length() == 0) - throw new IllegalArgumentException("Bad cookie name"); - - // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting - // Per RFC6265, Cookie.name follows RFC2616 Section 2.2 token rules - Syntax.requireValidRFC2616Token(name, "RFC6265 Cookie name"); - // Ensure that Per RFC6265, Cookie.value follows syntax rules - String value = httpCookie.getValue(); - Syntax.requireValidRFC6265CookieValue(value); - // Format value and params - StringBuilder buf = new StringBuilder(); - buf.append(name).append('=').append(value == null ? "" : value); - - // Append path - String path = httpCookie.getPath(); - if (path != null && path.length() > 0) - buf.append("; Path=").append(path); - - // Append domain - String domain = httpCookie.getDomain(); - if (domain != null && domain.length() > 0) - buf.append("; Domain=").append(domain); - - // Handle max-age and/or expires - long maxAge = httpCookie.getMaxAge(); - if (maxAge >= 0) - { - // Always use expires - // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies - buf.append("; Expires="); - if (maxAge == 0) - buf.append(__01Jan1970_COOKIE); + public Builder httpOnly(boolean httpOnly) + { + if (httpOnly) + _attributes = lazyAttributePut(_attributes, HTTP_ONLY_ATTRIBUTE, Boolean.TRUE.toString()); else - DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * maxAge); - - buf.append("; Max-Age="); - buf.append(maxAge); + _attributes = lazyAttributeRemove(_attributes, HTTP_ONLY_ATTRIBUTE); + return this; } - // add the other fields - if (httpCookie.isSecure()) - buf.append("; Secure"); - if (httpCookie.isHttpOnly()) - buf.append("; HttpOnly"); - - Map attributes = httpCookie.getAttributes(); - - String sameSiteAttr = attributes.get(SAME_SITE_ATTRIBUTE); - if (sameSiteAttr != null) - { - buf.append("; SameSite="); - buf.append(sameSiteAttr); - } - else + public Builder maxAge(long maxAge) { - SameSite sameSite = httpCookie.getSameSite(); - if (sameSite != null) - { - buf.append("; SameSite="); - buf.append(sameSite.getAttributeValue()); - } + if (maxAge >= 0) + _attributes = lazyAttributePut(_attributes, MAX_AGE_ATTRIBUTE, Long.toString(maxAge)); + else + _attributes = lazyAttributeRemove(_attributes, MAX_AGE_ATTRIBUTE); + return this; } - //Add all other attributes - for (Map.Entry e : attributes.entrySet()) + public Builder expires(Instant expires) { - if (KNOWN_ATTRIBUTES.contains(e.getKey())) - continue; - buf.append("; ").append(e.getKey()).append("="); - buf.append(e.getValue()); + if (expires != null) + _attributes = lazyAttributePut(_attributes, EXPIRES_ATTRIBUTE, formatExpires(expires)); + else + _attributes = lazyAttributeRemove(_attributes, EXPIRES_ATTRIBUTE); + return this; } - return buf.toString(); - } - - /** - * Get the default value for SameSite cookie attribute, if one - * has been set for the given context. - * - * @param contextAttributes the context to check for default SameSite value - * @return the default SameSite value or null if one does not exist - * @throws IllegalStateException if the default value is not a permitted value - */ - static SameSite getSameSiteDefault(Attributes contextAttributes) - { - if (contextAttributes == null) - return null; - Object o = contextAttributes.getAttribute(SAME_SITE_DEFAULT_ATTRIBUTE); - if (o == null) + public Builder path(String path) { - if (LOG.isDebugEnabled()) - LOG.debug("No default value for SameSite"); - return null; + _attributes = lazyAttributePut(_attributes, PATH_ATTRIBUTE, path); + return this; } - if (o instanceof SameSite) - return (SameSite)o; - - try + public Builder secure(boolean secure) { - SameSite samesite = Enum.valueOf(SameSite.class, o.toString().trim().toUpperCase(Locale.ENGLISH)); - contextAttributes.setAttribute(SAME_SITE_DEFAULT_ATTRIBUTE, samesite); - return samesite; + if (secure) + _attributes = lazyAttributePut(_attributes, SECURE_ATTRIBUTE, Boolean.TRUE.toString()); + else + _attributes = lazyAttributeRemove(_attributes, SECURE_ATTRIBUTE); + return this; } - catch (Exception e) + + public HttpCookie build() { - LOG.warn("Bad default value {} for SameSite", o); - throw new IllegalStateException(e); + return new Immutable(_name, _value, _version, lazyAttributes(_attributes)); } } - /** - * Extract the bare minimum of info from a Set-Cookie header string. - * - *

- * Ideally this method should not be necessary, however as java.net.HttpCookie - * does not yet support generic attributes, we have to use it in a minimal - * fashion. When it supports attributes, we could look at reverting to a - * constructor on o.e.j.h.HttpCookie to take the set-cookie header string. - *

- * - * @param setCookieHeader the header as a string - * @return a map containing the name, value, domain, path. max-age of the set cookie header - */ - static Map extractBasics(String setCookieHeader) - { - //Parse the bare minimum - List cookies = java.net.HttpCookie.parse(setCookieHeader); - if (cookies.size() != 1) - return Collections.emptyMap(); - java.net.HttpCookie cookie = cookies.get(0); - Map fields = new HashMap<>(); - fields.put("name", cookie.getName()); - fields.put("value", cookie.getValue()); - fields.put("domain", cookie.getDomain()); - fields.put("path", cookie.getPath()); - fields.put("max-age", Long.toString(cookie.getMaxAge())); - return fields; - } - - /** - * Check if the Set-Cookie header represented as a string is for the name, domain and path given. - * - * @param setCookieHeader a Set-Cookie header - * @param name the cookie name to check - * @param domain the cookie domain to check - * @param path the cookie path to check - * @return true if all of the name, domain and path match the Set-Cookie header, false otherwise - */ - static boolean match(String setCookieHeader, String name, String domain, String path) + private static Map lazyAttributePut(Map attributes, String key, String value) { - //Parse the bare minimum - List cookies = java.net.HttpCookie.parse(setCookieHeader); - if (cookies.size() != 1) - return false; - - java.net.HttpCookie cookie = cookies.get(0); - return match(cookie.getName(), cookie.getDomain(), cookie.getPath(), name, domain, path); + if (value == null) + return attributes; + if (attributes == null) + attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + attributes.put(key, value); + return attributes; } - /** - * Check if the HttpCookie is for the given name, domain and path. - * - * @param cookie the jetty HttpCookie to check - * @param name the cookie name to check - * @param domain the cookie domain to check - * @param path the cookie path to check - * @return true if name, domain, and path, match all match the HttpCookie, false otherwise - */ - static boolean match(HttpCookie cookie, String name, String domain, String path) + private static Map lazyAttributeRemove(Map attributes, String key) { - if (cookie == null) - return false; - return match(cookie.getName(), cookie.getDomain(), cookie.getPath(), name, domain, path); + if (attributes == null) + return null; + attributes.remove(key); + return attributes; } - /** - * Check if all old parameters match the new parameters. - * - * @return true if old and new names match exactly and the old and new domains match case-insensitively and the paths match exactly - */ - private static boolean match(String oldName, String oldDomain, String oldPath, String newName, String newDomain, String newPath) + private static Map lazyAttributes(Map attributes) { - if (oldName == null) - { - if (newName != null) - return false; - } - else if (!oldName.equals(newName)) - return false; - - if (oldDomain == null) - { - if (newDomain != null) - return false; - } - else if (!oldDomain.equalsIgnoreCase(newDomain)) - return false; - - if (oldPath == null) - return newPath == null; - - return oldPath.equals(newPath); + return attributes == null || attributes.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(attributes); } - class SetCookieHttpField extends HttpField + static String formatExpires(Instant expires) { - final HttpCookie _cookie; - - public SetCookieHttpField(HttpCookie cookie, CookieCompliance compliance) - { - super(HttpHeader.SET_COOKIE, getSetCookie(cookie, compliance)); - this._cookie = cookie; - } - - public HttpCookie getHttpCookie() - { - return _cookie; - } + return DateTimeFormatter.RFC_1123_DATE_TIME + .withZone(ZoneOffset.UTC) + .format(expires); } - /** - * Check that samesite is set on the cookie. If not, use a - * context default value, if one has been set. - * - * @param cookie the cookie to check - * @param attributes the context to check settings - * @return either the original cookie, or a new one that has the samesit default set - */ - static HttpCookie checkSameSite(HttpCookie cookie, Attributes attributes) + private static Instant parseExpires(String expires) { - if (cookie == null || cookie.getSameSite() != null) - return cookie; - - //sameSite is not set, use the default configured for the context, if one exists - SameSite contextDefault = HttpCookie.getSameSiteDefault(attributes); - if (contextDefault == null) - return cookie; //no default set - - return HttpCookie.from(cookie, HttpCookie.SAME_SITE_ATTRIBUTE, contextDefault.getAttributeValue()); + // TODO: RFC 1123 format only for now, see https://www.rfc-editor.org/rfc/rfc2616#section-3.3.1. + return ZonedDateTime.parse(expires, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant(); } } diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookieStore.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookieStore.java new file mode 100644 index 000000000000..1eb74b8f0cda --- /dev/null +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookieStore.java @@ -0,0 +1,383 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import java.net.URI; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.eclipse.jetty.util.NanoTime; +import org.eclipse.jetty.util.thread.AutoLock; + +/** + *

A container for {@link HttpCookie}s.

+ *

HTTP cookies are stored along with a {@link URI} via {@link #add(URI, HttpCookie)} + * and retrieved via {@link #match(URI)}, which implements the path matching algorithm + * defined by RFC 6265.

+ */ +public interface HttpCookieStore +{ + /** + *

Adds a cookie to this store, if possible.

+ *

The cookie may not be added for various reasons; for example, + * it may be already expired, or its domain attribute does not + * match that of the URI, etc.

+ *

The cookie is associated with the given {@code URI}, so that + * a call to {@link #match(URI)} returns the cookie only if the + * URIs match.

+ * + * @param uri the {@code URI} associated with the cookie + * @param cookie the cookie to add + * @return whether the cookie has been added + */ + public boolean add(URI uri, HttpCookie cookie); + + /** + * @return all the cookies + */ + public List all(); + + /** + *

Returns the cookies that match the given {@code URI}.

+ * + * @param uri the {@code URI} to match against + * @return a list of cookies that match the given {@code URI} + */ + public List match(URI uri); + + /** + *

Removes the cookie associated with the given {@code URI}.

+ * + * @param uri the {@code URI} associated with the cookie to remove + * @param cookie the cookie to remove + * @return whether the cookie has been removed + */ + public boolean remove(URI uri, HttpCookie cookie); + + /** + *

Removes all the cookies from this store.

+ * + * @return whether the store modified by this call + */ + public boolean clear(); + + /** + *

An implementation of {@link HttpCookieStore} that does not store any cookie.

+ */ + public static class Empty implements HttpCookieStore + { + @Override + public boolean add(URI uri, HttpCookie cookie) + { + return false; + } + + @Override + public List all() + { + return List.of(); + } + + @Override + public List match(URI uri) + { + return List.of(); + } + + @Override + public boolean remove(URI uri, HttpCookie cookie) + { + return false; + } + + @Override + public boolean clear() + { + return false; + } + } + + /** + *

A default implementation of {@link HttpCookieStore}.

+ */ + public static class Default implements HttpCookieStore + { + private final AutoLock lock = new AutoLock(); + private final Map> cookies = new HashMap<>(); + + @Override + public boolean add(URI uri, HttpCookie cookie) + { + // TODO: reject if cookie size is too big? + + String cookieDomain = cookie.getDomain(); + if (cookieDomain != null) + { + cookieDomain = cookieDomain.toLowerCase(Locale.ENGLISH); + if (cookieDomain.startsWith(".")) + cookieDomain = cookieDomain.substring(1); + // RFC 6265 section 4.1.2.3, ignore Domain if ends with ".". + if (cookieDomain.endsWith(".")) + cookieDomain = uri.getHost(); + // Reject top-level domains. + // TODO: should also reject "top" domain such as co.uk, gov.au, etc. + if (!cookieDomain.contains(".")) + { + if (!cookieDomain.equals("localhost")) + return false; + } + + String domain = uri.getHost(); + if (domain != null) + { + domain = domain.toLowerCase(Locale.ENGLISH); + // If uri.host==foo.example.com, only accept + // cookie.domain==(foo.example.com|example.com). + if (!domain.endsWith(cookieDomain)) + return false; + int beforeMatch = domain.length() - cookieDomain.length() - 1; + if (beforeMatch >= 0 && domain.charAt(beforeMatch) != '.') + return false; + } + } + else + { + // No explicit cookie domain, use the origin domain. + cookieDomain = uri.getHost(); + } + + // Cookies are stored under their domain, so that: + // - add(sub.example.com, cookie[Domain]=null) => Key[domain=sub.example.com] + // - add(sub.example.com, cookie[Domain]=example.com) => Key[domain=example.com] + // This facilitates the matching algorithm. + Key key = new Key(uri.getScheme(), cookieDomain); + boolean[] result = new boolean[1]; + try (AutoLock ignored = lock.lock()) + { + cookies.compute(key, (k, v) -> + { + // RFC 6265, section 4.1.2. + // Evict an existing cookie with + // same name, domain and path. + if (v != null) + v.remove(cookie); + + // Add only non-expired cookies. + if (cookie.isExpired()) + { + result[0] = true; + return v == null || v.isEmpty() ? null : v; + } + + if (v == null) + v = new ArrayList<>(); + v.add(new Cookie(cookie)); + return v; + }); + } + + return !result[0]; + } + + @Override + public List all() + { + try (AutoLock ignored = lock.lock()) + { + return cookies.values().stream() + .flatMap(Collection::stream) + .toList(); + } + } + + @Override + public List match(URI uri) + { + List result = new ArrayList<>(); + String scheme = uri.getScheme(); + boolean secure = HttpScheme.HTTPS.is(scheme); + String uriDomain = uri.getHost(); + String path = uri.getPath(); + if (path == null || path.trim().isEmpty()) + path = "/"; + + try (AutoLock ignored = lock.lock()) + { + // Given the way cookies are stored in the Map, the matching + // algorithm starts with the URI domain and iterates chopping + // its subdomains, accumulating the results. + // For example, for uriDomain = sub.example.com, the cookies + // Map is accessed with the following Keys: + // - Key[domain=sub.example.com] + // - chop domain to example.com + // - Key[domain=example.com] + // - chop domain to com + // invalid domain, exit iteration. + String domain = uriDomain; + while (true) + { + Key key = new Key(scheme, domain); + List stored = cookies.get(key); + Iterator iterator = stored == null ? Collections.emptyIterator() : stored.iterator(); + while (iterator.hasNext()) + { + HttpCookie cookie = iterator.next(); + + // Check and remove expired cookies. + if (cookie.isExpired()) + { + iterator.remove(); + continue; + } + + // Check whether the cookie is secure. + if (cookie.isSecure() && !secure) + continue; + + // Match the domain. + if (!domainMatches(uriDomain, key.domain, cookie.getDomain())) + continue; + + // Match the path. + if (!pathMatches(path, cookie.getPath())) + continue; + + result.add(cookie); + } + + int dot = domain.indexOf('.'); + if (dot < 0) + break; + // Remove one subdomain. + domain = domain.substring(dot + 1); + // Exit if the top-level domain was reached. + if (domain.indexOf('.') < 0) + break; + } + } + + return result; + } + + private static boolean domainMatches(String uriDomain, String domain, String cookieDomain) + { + if (uriDomain == null) + return true; + if (domain != null) + domain = domain.toLowerCase(Locale.ENGLISH); + uriDomain = uriDomain.toLowerCase(Locale.ENGLISH); + if (cookieDomain != null) + cookieDomain = cookieDomain.toLowerCase(Locale.ENGLISH); + if (cookieDomain == null || cookieDomain.endsWith(".")) + { + // RFC 6265, section 4.1.2.3. + // No cookie domain -> cookie sent only to origin server. + return uriDomain.equals(domain); + } + if (cookieDomain.startsWith(".")) + cookieDomain = cookieDomain.substring(1); + if (uriDomain.endsWith(cookieDomain)) + { + // The domain is the same as, or a subdomain of, the cookie domain. + int beforeMatch = uriDomain.length() - cookieDomain.length() - 1; + // Domains are the same. + if (beforeMatch == -1) + return true; + // Verify it is a proper subdomain such as bar.foo.com, + // not just a suffix of a domain such as bazfoo.com. + return uriDomain.charAt(beforeMatch) == '.'; + } + return false; + } + + private static boolean pathMatches(String path, String cookiePath) + { + if (cookiePath == null) + return true; + // RFC 6265, section 5.1.4, path matching algorithm. + if (path.equals(cookiePath)) + return true; + if (path.startsWith(cookiePath)) + return cookiePath.endsWith("/") || path.charAt(cookiePath.length()) == '/'; + return false; + } + + @Override + public boolean remove(URI uri, HttpCookie cookie) + { + Key key = new Key(uri.getScheme(), uri.getHost()); + try (AutoLock ignored = lock.lock()) + { + boolean[] result = new boolean[1]; + cookies.compute(key, (k, v) -> + { + if (v == null) + return null; + boolean removed = v.remove(cookie); + result[0] = removed; + return v.isEmpty() ? null : v; + }); + return result[0]; + } + } + + @Override + public boolean clear() + { + try (AutoLock ignored = lock.lock()) + { + if (cookies.isEmpty()) + return false; + cookies.clear(); + return true; + } + } + + private record Key(String scheme, String domain) + { + private Key(String scheme, String domain) + { + this.scheme = scheme; + this.domain = domain.toLowerCase(Locale.ENGLISH); + } + } + + private static class Cookie extends HttpCookie.Wrapper + { + private final long creationNanoTime = NanoTime.now(); + + public Cookie(HttpCookie wrapped) + { + super(wrapped); + } + + @Override + public boolean isExpired() + { + long maxAge = getMaxAge(); + if (maxAge >= 0 && NanoTime.secondsSince(creationNanoTime) > maxAge) + return true; + Instant expires = getExpires(); + return expires != null && Instant.now().isAfter(expires); + } + } + } +} diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieStoreTest.java b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieStoreTest.java new file mode 100644 index 000000000000..ac827cf99b61 --- /dev/null +++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieStoreTest.java @@ -0,0 +1,276 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import java.net.URI; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HttpCookieStoreTest +{ + @Test + public void testRejectCookieForTopDomain() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI uri = URI.create("http://example.com"); + assertFalse(store.add(uri, HttpCookie.build("n", "v").domain("com").build())); + assertFalse(store.add(uri, HttpCookie.build("n", "v").domain(".com").build())); + } + + @Test + public void testRejectExpiredCookie() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI uri = URI.create("http://example.com"); + assertFalse(store.add(uri, HttpCookie.build("n", "v").maxAge(0).build())); + assertFalse(store.add(uri, HttpCookie.build("n", "v").expires(Instant.now().minusSeconds(1)).build())); + } + + @Test + public void testRejectCookieForNonMatchingDomain() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI uri = URI.create("http://example.com"); + assertFalse(store.add(uri, HttpCookie.build("n", "v").domain("sub.example.com").build())); + assertFalse(store.add(uri, HttpCookie.build("n", "v").domain("foo.com").build())); + } + + @Test + public void testAcceptCookieForMatchingDomain() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI uri = URI.create("http://sub.example.com"); + assertTrue(store.add(uri, HttpCookie.build("n", "v").domain("sub.example.com").build())); + assertTrue(store.add(uri, HttpCookie.build("n", "v").domain("example.com").build())); + } + + @Test + public void testAcceptCookieForLocalhost() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI uri = URI.create("http://localhost"); + assertTrue(store.add(uri, HttpCookie.build("n", "v").domain("localhost").build())); + } + + @Test + public void testReplaceCookie() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI uri = URI.create("http://example.com"); + assertTrue(store.add(uri, HttpCookie.from("n", "v1"))); + // Replace the cookie with another that has a different value. + assertTrue(store.add(uri, HttpCookie.from("n", "v2"))); + List matches = store.match(uri); + assertEquals(1, matches.size()); + assertEquals("v2", matches.get(0).getValue()); + } + + @Test + public void testReplaceCookieWithDomain() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI uri = URI.create("http://example.com"); + assertTrue(store.add(uri, HttpCookie.build("n", "v1").domain("example.com").build())); + // Replace the cookie with another that has a different value. + // Domain comparison must be case-insensitive. + assertTrue(store.add(uri, HttpCookie.build("n", "v2").domain("EXAMPLE.COM").build())); + List matches = store.match(uri); + assertEquals(1, matches.size()); + assertEquals("v2", matches.get(0).getValue()); + } + + @Test + public void testReplaceCookieWithPath() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI uri = URI.create("http://example.com/path"); + assertTrue(store.add(uri, HttpCookie.build("n", "v1").path("/path").build())); + // Replace the cookie with another that has a different value. + // Path comparison must be case-sensitive. + assertTrue(store.add(uri, HttpCookie.build("n", "v2").path("/path").build())); + List matches = store.match(uri); + assertEquals(1, matches.size()); + assertEquals("v2", matches.get(0).getValue()); + // Same path but different case should generate another cookie. + assertTrue(store.add(uri, HttpCookie.build("n", "v3").path("/PATH").build())); + matches = store.all(); + assertEquals(2, matches.size()); + } + + @Test + public void testMatch() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI cookieURI = URI.create("http://example.com"); + assertTrue(store.add(cookieURI, HttpCookie.from("n", "v1"))); + + // Same domain with a path must match. + URI uri = URI.create("http://example.com/path"); + List matches = store.match(uri); + assertEquals(1, matches.size()); + + // Subdomain must not match because the cookie was added without + // Domain attribute, so must be sent only to the origin domain. + uri = URI.create("http://sub.example.com"); + matches = store.match(uri); + assertEquals(0, matches.size()); + + // Different domain must not match. + uri = URI.create("http://foo.com"); + matches = store.match(uri); + assertEquals(0, matches.size()); + } + + @Test + public void testMatchWithDomain() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI cookieURI = URI.create("http://sub.example.com"); + assertTrue(store.add(cookieURI, HttpCookie.build("n", "v1").domain("example.com").build())); + + // Same domain with a path must match. + URI uri = URI.create("http://sub.example.com/path"); + List matches = store.match(uri); + assertEquals(1, matches.size()); + + // Parent domain must match. + uri = URI.create("http://example.com"); + matches = store.match(uri); + assertEquals(1, matches.size()); + + // Different subdomain must match due to the Domain attribute. + uri = URI.create("http://bar.example.com"); + matches = store.match(uri); + assertEquals(1, matches.size()); + } + + @Test + public void testMatchManyWithDomain() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI cookieURI = URI.create("http://sub.example.com"); + assertTrue(store.add(cookieURI, HttpCookie.build("n1", "v1").domain("example.com").build())); + cookieURI = URI.create("http://example.com"); + assertTrue(store.add(cookieURI, HttpCookie.from("n2", "v2"))); + + URI uri = URI.create("http://sub.example.com/path"); + List matches = store.match(uri); + assertEquals(1, matches.size()); + + // Parent domain must match. + uri = URI.create("http://example.com"); + matches = store.match(uri); + assertEquals(2, matches.size()); + + // Different subdomain must match due to the Domain attribute. + uri = URI.create("http://bar.example.com"); + matches = store.match(uri); + assertEquals(1, matches.size()); + } + + @Test + public void testMatchManyWithPath() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI cookieURI = URI.create("http://example.com"); + assertTrue(store.add(cookieURI, HttpCookie.build("n1", "v1").path("/path").build())); + cookieURI = URI.create("http://example.com"); + assertTrue(store.add(cookieURI, HttpCookie.from("n2", "v2"))); + + URI uri = URI.create("http://example.com"); + List matches = store.match(uri); + assertEquals(1, matches.size()); + + uri = URI.create("http://example.com/"); + matches = store.match(uri); + assertEquals(1, matches.size()); + + uri = URI.create("http://example.com/other"); + matches = store.match(uri); + assertEquals(1, matches.size()); + + uri = URI.create("http://example.com/path"); + matches = store.match(uri); + assertEquals(2, matches.size()); + + uri = URI.create("http://example.com/path/"); + matches = store.match(uri); + assertEquals(2, matches.size()); + + uri = URI.create("http://example.com/path/more"); + matches = store.match(uri); + assertEquals(2, matches.size()); + } + + @Test + public void testExpiredCookieDoesNotMatch() throws Exception + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI cookieURI = URI.create("http://example.com"); + long expireSeconds = 1; + assertTrue(store.add(cookieURI, HttpCookie.build("n1", "v1").maxAge(expireSeconds).build())); + + TimeUnit.SECONDS.sleep(2 * expireSeconds); + + List matches = store.match(cookieURI); + assertEquals(0, matches.size()); + } + + @Test + public void testRemove() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI cookieURI = URI.create("http://example.com/path"); + assertTrue(store.add(cookieURI, HttpCookie.from("n1", "v1"))); + + URI removeURI = URI.create("http://example.com"); + // Cookie value should not matter. + assertTrue(store.remove(removeURI, HttpCookie.from("n1", "n2"))); + assertFalse(store.remove(removeURI, HttpCookie.from("n1", "n2"))); + } + + @Test + public void testSecureCookie() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI uri = URI.create("http://example.com"); + assertTrue(store.add(uri, HttpCookie.build("n1", "v1").secure(true).build())); + URI secureURI = URI.create("https://example.com"); + assertTrue(store.add(secureURI, HttpCookie.build("n2", "v2").secure(true).build())); + + List matches = store.match(uri); + assertEquals(0, matches.size()); + + matches = store.match(secureURI); + assertEquals(2, matches.size()); + } + + @Test + public void testClear() + { + HttpCookieStore store = new HttpCookieStore.Default(); + URI cookieURI = URI.create("http://example.com"); + assertTrue(store.add(cookieURI, HttpCookie.from("n1", "v1"))); + + assertTrue(store.clear()); + assertFalse(store.clear()); + } +} diff --git a/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java index cfd72e8b243e..32ad05d818e9 100644 --- a/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java +++ b/jetty-core/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java @@ -35,6 +35,7 @@ import org.eclipse.jetty.client.ProtocolHandlers; import org.eclipse.jetty.client.Result; import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; +import org.eclipse.jetty.http.HttpCookieStore; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; @@ -48,7 +49,6 @@ import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.HttpCookieStore; import org.eclipse.jetty.util.QuotedStringTokenizer; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.component.LifeCycle; @@ -203,7 +203,7 @@ protected HttpClient newHttpClient() protected void configureHttpClient(HttpClient httpClient) { httpClient.setFollowRedirects(false); - httpClient.setCookieStore(new HttpCookieStore.Empty()); + httpClient.setHttpCookieStore(new HttpCookieStore.Empty()); } protected static String requestId(Request clientToProxyRequest) diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpCookieUtils.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpCookieUtils.java new file mode 100644 index 000000000000..3b3eb0124c69 --- /dev/null +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/HttpCookieUtils.java @@ -0,0 +1,436 @@ +// +// ======================================================================== +// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.eclipse.jetty.http.CookieCompliance; +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.Syntax; +import org.eclipse.jetty.util.Attributes; +import org.eclipse.jetty.util.Index; +import org.eclipse.jetty.util.QuotedStringTokenizer; + +/** + *

Utility methods for server-side HTTP cookie handling.

+ */ +public final class HttpCookieUtils +{ + /** + * Name of context attribute with default SameSite cookie value + */ + public static final String SAME_SITE_DEFAULT_ATTRIBUTE = "org.eclipse.jetty.cookie.sameSiteDefault"; + + private static final Index KNOWN_ATTRIBUTES = new Index.Builder().caseSensitive(false) + .with(HttpCookie.COMMENT_ATTRIBUTE) + .with(HttpCookie.DOMAIN_ATTRIBUTE) + .with(HttpCookie.EXPIRES_ATTRIBUTE) + .with(HttpCookie.HTTP_ONLY_ATTRIBUTE) + .with(HttpCookie.MAX_AGE_ATTRIBUTE) + .with(HttpCookie.PATH_ATTRIBUTE) + .with(HttpCookie.SAME_SITE_ATTRIBUTE) + .with(HttpCookie.SECURE_ATTRIBUTE) + .build(); + // RFC 1123 format of epoch for the Expires attribute. + private static final String EPOCH_EXPIRES = "Thu, 01 Jan 1970 00:00:00 GMT"; + + /** + * Check that samesite is set on the cookie. If not, use a + * context default value, if one has been set. + * + * @param cookie the cookie to check + * @param attributes the context to check settings + * @return either the original cookie, or a new one that has the samesit default set + */ + public static HttpCookie checkSameSite(HttpCookie cookie, Attributes attributes) + { + if (cookie == null || cookie.getSameSite() != null) + return cookie; + + //sameSite is not set, use the default configured for the context, if one exists + HttpCookie.SameSite contextDefault = getSameSiteDefault(attributes); + if (contextDefault == null) + return cookie; //no default set + + return HttpCookie.from(cookie, HttpCookie.SAME_SITE_ATTRIBUTE, contextDefault.getAttributeValue()); + } + + /** + * Extract the bare minimum of info from a Set-Cookie header string. + * + *

+ * Ideally this method should not be necessary, however as java.net.HttpCookie + * does not yet support generic attributes, we have to use it in a minimal + * fashion. When it supports attributes, we could look at reverting to a + * constructor on o.e.j.h.HttpCookie to take the set-cookie header string. + *

+ * + * @param setCookieHeader the header as a string + * @return a map containing the name, value, domain, path. max-age of the set cookie header + */ + public static Map extractBasics(String setCookieHeader) + { + //Parse the bare minimum + List cookies = java.net.HttpCookie.parse(setCookieHeader); + if (cookies.size() != 1) + return Collections.emptyMap(); + java.net.HttpCookie cookie = cookies.get(0); + Map fields = new HashMap<>(); + fields.put("name", cookie.getName()); + fields.put("value", cookie.getValue()); + fields.put("domain", cookie.getDomain()); + fields.put("path", cookie.getPath()); + fields.put("max-age", Long.toString(cookie.getMaxAge())); + return fields; + } + + /** + * Get the default value for SameSite cookie attribute, if one + * has been set for the given context. + * + * @param contextAttributes the context to check for default SameSite value + * @return the default SameSite value or null if one does not exist + * @throws IllegalStateException if the default value is not a permitted value + */ + public static HttpCookie.SameSite getSameSiteDefault(Attributes contextAttributes) + { + if (contextAttributes == null) + return null; + Object o = contextAttributes.getAttribute(SAME_SITE_DEFAULT_ATTRIBUTE); + if (o == null) + return null; + + if (o instanceof HttpCookie.SameSite) + return (HttpCookie.SameSite)o; + + try + { + HttpCookie.SameSite samesite = Enum.valueOf(HttpCookie.SameSite.class, o.toString().trim().toUpperCase(Locale.ENGLISH)); + contextAttributes.setAttribute(SAME_SITE_DEFAULT_ATTRIBUTE, samesite); + return samesite; + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + } + + public static String getSetCookie(HttpCookie httpCookie, CookieCompliance compliance) + { + if (compliance == CookieCompliance.RFC6265) + return getRFC6265SetCookie(httpCookie); + if (compliance == CookieCompliance.RFC2965) + return getRFC2965SetCookie(httpCookie); + throw new IllegalStateException(); + } + + public static String getRFC2965SetCookie(HttpCookie httpCookie) + { + // Check arguments + String name = httpCookie.getName(); + if (name == null || name.length() == 0) + throw new IllegalArgumentException("Invalid cookie name"); + + StringBuilder builder = new StringBuilder(); + + quoteIfNeededAndAppend(name, builder); + + builder.append('='); + + String value = httpCookie.getValue(); + quoteIfNeededAndAppend(value, builder); + + // Look for domain and path fields and check if they need to be quoted. + String domain = httpCookie.getDomain(); + boolean hasDomain = domain != null && domain.length() > 0; + boolean quoteDomain = hasDomain && isQuoteNeeded(domain); + + String path = httpCookie.getPath(); + boolean hasPath = path != null && path.length() > 0; + boolean quotePath = hasPath && isQuoteNeeded(path); + + // Upgrade the version if we have a comment or we need to quote value/path/domain or if they were already quoted + int version = httpCookie.getVersion(); + String comment = httpCookie.getComment(); + if (version == 0 && (comment != null || isQuoteNeeded(name) || isQuoteNeeded(value) || quoteDomain || quotePath || + QuotedStringTokenizer.isQuoted(name) || QuotedStringTokenizer.isQuoted(value) || + QuotedStringTokenizer.isQuoted(path) || QuotedStringTokenizer.isQuoted(domain))) + version = 1; + + if (version == 1) + builder.append(";Version=1"); + else if (version > 1) + builder.append(";Version=").append(version); + + if (hasDomain) + { + builder.append(";Domain="); + if (quoteDomain) + QuotedStringTokenizer.quoteOnly(builder, domain); + else + builder.append(domain); + } + + if (hasPath) + { + builder.append(";Path="); + if (quotePath) + QuotedStringTokenizer.quoteOnly(builder, path); + else + builder.append(path); + } + + // Handle max-age and/or expires + long maxAge = httpCookie.getMaxAge(); + if (maxAge >= 0) + { + // Always add the Expires attribute too, as some + // browsers do not handle max-age even with v1 cookies. + builder.append(";Expires="); + if (maxAge == 0) + builder.append(EPOCH_EXPIRES); + else + builder.append(HttpCookie.formatExpires(Instant.now().plusSeconds(maxAge))); + + builder.append(";Max-Age="); + builder.append(maxAge); + } + + if (httpCookie.isSecure()) + builder.append(";Secure"); + + if (httpCookie.isHttpOnly()) + builder.append(";HttpOnly"); + + HttpCookie.SameSite sameSite = httpCookie.getSameSite(); + if (sameSite != null) + builder.append(";SameSite=").append(sameSite.getAttributeValue()); + + if (comment != null) + { + builder.append(";Comment="); + quoteIfNeededAndAppend(comment, builder); + } + + return builder.toString(); + } + + public static String getRFC6265SetCookie(HttpCookie httpCookie) + { + // Check arguments + String name = httpCookie.getName(); + if (name == null || name.length() == 0) + throw new IllegalArgumentException("Bad cookie name"); + + // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting + // Per RFC6265, Cookie.name follows RFC2616 Section 2.2 token rules + Syntax.requireValidRFC2616Token(name, "RFC6265 Cookie name"); + // Ensure that Per RFC6265, Cookie.value follows syntax rules + String value = httpCookie.getValue(); + Syntax.requireValidRFC6265CookieValue(value); + + // Format value and params + StringBuilder builder = new StringBuilder(); + builder.append(name).append('=').append(value == null ? "" : value); + + // Append path + String path = httpCookie.getPath(); + if (path != null && path.length() > 0) + builder.append("; Path=").append(path); + + // Append domain + String domain = httpCookie.getDomain(); + if (domain != null && domain.length() > 0) + builder.append("; Domain=").append(domain); + + // Handle max-age and/or expires + long maxAge = httpCookie.getMaxAge(); + if (maxAge >= 0) + { + // Always use expires + // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies + builder.append("; Expires="); + if (maxAge == 0) + builder.append(EPOCH_EXPIRES); + else + builder.append(HttpCookie.formatExpires(Instant.now().plusSeconds(maxAge))); + + builder.append("; Max-Age="); + builder.append(maxAge); + } + + // add the other fields + if (httpCookie.isSecure()) + builder.append("; Secure"); + if (httpCookie.isHttpOnly()) + builder.append("; HttpOnly"); + + Map attributes = httpCookie.getAttributes(); + + String sameSiteAttr = attributes.get(HttpCookie.SAME_SITE_ATTRIBUTE); + if (sameSiteAttr != null) + { + builder.append("; SameSite="); + builder.append(sameSiteAttr); + } + else + { + HttpCookie.SameSite sameSite = httpCookie.getSameSite(); + if (sameSite != null) + { + builder.append("; SameSite="); + builder.append(sameSite.getAttributeValue()); + } + } + + //Add all other attributes + for (Map.Entry e : attributes.entrySet()) + { + if (KNOWN_ATTRIBUTES.contains(e.getKey())) + continue; + builder.append("; ").append(e.getKey()).append("="); + builder.append(e.getValue()); + } + + return builder.toString(); + } + + /** + *

Whether a cookie name/value/attribute needs to be quoted.

+ * + * @param text the text to check + * @return whether the text needs to be quoted + * @throws IllegalArgumentException if the text contains illegal characters + */ + private static boolean isQuoteNeeded(String text) + { + if (text == null || text.length() == 0) + return true; + + if (QuotedStringTokenizer.isQuoted(text)) + return false; + + for (int i = 0; i < text.length(); i++) + { + char c = text.charAt(i); + if ("\",;\\ \t".indexOf(c) >= 0) + return true; + + if (c < 0x20 || c >= 0x7F) + throw new IllegalArgumentException("Illegal character in cookie value"); + } + + return false; + } + + /** + * Check if the Set-Cookie header represented as a string is for the name, domain and path given. + * + * @param setCookieHeader a Set-Cookie header + * @param name the cookie name to check + * @param domain the cookie domain to check + * @param path the cookie path to check + * @return true if all of the name, domain and path match the Set-Cookie header, false otherwise + */ + public static boolean match(String setCookieHeader, String name, String domain, String path) + { + //Parse the bare minimum + List cookies = java.net.HttpCookie.parse(setCookieHeader); + if (cookies.size() != 1) + return false; + + java.net.HttpCookie cookie = cookies.get(0); + return match(cookie.getName(), cookie.getDomain(), cookie.getPath(), name, domain, path); + } + + /** + * Check if the HttpCookie is for the given name, domain and path. + * + * @param cookie the jetty HttpCookie to check + * @param name the cookie name to check + * @param domain the cookie domain to check + * @param path the cookie path to check + * @return true if name, domain, and path, match all match the HttpCookie, false otherwise + */ + public static boolean match(HttpCookie cookie, String name, String domain, String path) + { + if (cookie == null) + return false; + return match(cookie.getName(), cookie.getDomain(), cookie.getPath(), name, domain, path); + } + + /** + * Check if all old parameters match the new parameters. + * + * @return true if old and new names match exactly and the old and new domains match case-insensitively and the paths match exactly + */ + private static boolean match(String oldName, String oldDomain, String oldPath, String newName, String newDomain, String newPath) + { + if (oldName == null) + { + if (newName != null) + return false; + } + else if (!oldName.equals(newName)) + return false; + + if (oldDomain == null) + { + if (newDomain != null) + return false; + } + else if (!oldDomain.equalsIgnoreCase(newDomain)) + return false; + + if (oldPath == null) + return newPath == null; + + return oldPath.equals(newPath); + } + + private static void quoteIfNeededAndAppend(String text, StringBuilder builder) + { + if (isQuoteNeeded(text)) + QuotedStringTokenizer.quoteOnly(builder, text); + else + builder.append(text); + } + + private HttpCookieUtils() + { + } + + public static class SetCookieHttpField extends HttpField + { + private final HttpCookie _cookie; + + public SetCookieHttpField(HttpCookie cookie, CookieCompliance compliance) + { + super(HttpHeader.SET_COOKIE, getSetCookie(cookie, compliance)); + this._cookie = cookie; + } + + public HttpCookie getHttpCookie() + { + return _cookie; + } + } +} diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 53f3f19e588e..3df2552217e7 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -180,7 +180,7 @@ static void addCookie(Response response, HttpCookie cookie) throw new IllegalArgumentException("Cookie.name cannot be blank/null"); Request request = response.getRequest(); - response.getHeaders().add(new HttpCookie.SetCookieHttpField(HttpCookie.checkSameSite(cookie, request.getContext()), + response.getHeaders().add(new HttpCookieUtils.SetCookieHttpField(HttpCookieUtils.checkSameSite(cookie, request.getContext()), request.getConnectionMetaData().getHttpConfiguration().getResponseCookieCompliance())); // Expire responses with set-cookie headers so they do not get cached. @@ -202,18 +202,18 @@ static void replaceCookie(Response response, HttpCookie cookie) if (field.getHeader() == HttpHeader.SET_COOKIE) { CookieCompliance compliance = httpConfiguration.getResponseCookieCompliance(); - if (field instanceof HttpCookie.SetCookieHttpField) + if (field instanceof HttpCookieUtils.SetCookieHttpField) { - if (!HttpCookie.match(((HttpCookie.SetCookieHttpField)field).getHttpCookie(), cookie.getName(), cookie.getDomain(), cookie.getPath())) + if (!HttpCookieUtils.match(((HttpCookieUtils.SetCookieHttpField)field).getHttpCookie(), cookie.getName(), cookie.getDomain(), cookie.getPath())) continue; } else { - if (!HttpCookie.match(field.getValue(), cookie.getName(), cookie.getDomain(), cookie.getPath())) + if (!HttpCookieUtils.match(field.getValue(), cookie.getName(), cookie.getDomain(), cookie.getPath())) continue; } - i.set(new HttpCookie.SetCookieHttpField(HttpCookie.checkSameSite(cookie, request.getContext()), compliance)); + i.set(new HttpCookieUtils.SetCookieHttpField(HttpCookieUtils.checkSameSite(cookie, request.getContext()), compliance)); return; } } diff --git a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpCookieTest.java similarity index 63% rename from jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java rename to jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpCookieTest.java index 33a8418e9b3b..739c1d4880a5 100644 --- a/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpCookieTest.java @@ -11,11 +11,12 @@ // ======================================================================== // -package org.eclipse.jetty.http; +package org.eclipse.jetty.server; import java.util.Map; import java.util.stream.Stream; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpCookie.SameSite; import org.eclipse.jetty.util.AttributesMap; import org.hamcrest.Matchers; @@ -25,7 +26,11 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; @@ -40,49 +45,49 @@ public void testDefaultSameSite() AttributesMap context = new AttributesMap(); //test null value for default - assertNull(HttpCookie.getSameSiteDefault(context)); + assertNull(HttpCookieUtils.getSameSiteDefault(context)); //test good value for default as SameSite enum - context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, SameSite.LAX); - assertEquals(SameSite.LAX, HttpCookie.getSameSiteDefault(context)); + context.setAttribute(HttpCookieUtils.SAME_SITE_DEFAULT_ATTRIBUTE, SameSite.LAX); + assertEquals(SameSite.LAX, HttpCookieUtils.getSameSiteDefault(context)); //test good value for default as String - context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "NONE"); - assertEquals(SameSite.NONE, HttpCookie.getSameSiteDefault(context)); + context.setAttribute(HttpCookieUtils.SAME_SITE_DEFAULT_ATTRIBUTE, "NONE"); + assertEquals(SameSite.NONE, HttpCookieUtils.getSameSiteDefault(context)); //test case for default as String - context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "sTrIcT"); - assertEquals(SameSite.STRICT, HttpCookie.getSameSiteDefault(context)); + context.setAttribute(HttpCookieUtils.SAME_SITE_DEFAULT_ATTRIBUTE, "sTrIcT"); + assertEquals(SameSite.STRICT, HttpCookieUtils.getSameSiteDefault(context)); //test bad value for default as String - context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "fooBAR"); + context.setAttribute(HttpCookieUtils.SAME_SITE_DEFAULT_ATTRIBUTE, "fooBAR"); assertThrows(IllegalStateException.class, - () -> HttpCookie.getSameSiteDefault(context)); + () -> HttpCookieUtils.getSameSiteDefault(context)); } @Test public void testMatchCookie() { //match with header string - assertTrue(HttpCookie.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar", + assertTrue(HttpCookieUtils.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar", "everything", "domain", "path")); - assertFalse(HttpCookie.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar", + assertFalse(HttpCookieUtils.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar", "something", "domain", "path")); - assertFalse(HttpCookie.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar", + assertFalse(HttpCookieUtils.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar", "everything", "realm", "path")); - assertFalse(HttpCookie.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar", + assertFalse(HttpCookieUtils.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar", "everything", "domain", "street")); //match including set-cookie:, this is really testing the java.net.HttpCookie parser, but worth throwing in there - assertTrue(HttpCookie.match("Set-Cookie: everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar", + assertTrue(HttpCookieUtils.match("Set-Cookie: everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar", "everything", "domain", "path")); //match via cookie HttpCookie httpCookie = HttpCookie.from("everything", "value", 0, Map.of(HttpCookie.DOMAIN_ATTRIBUTE, "domain", HttpCookie.PATH_ATTRIBUTE, "path", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(0), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true), HttpCookie.COMMENT_ATTRIBUTE, "comment")); - assertTrue(HttpCookie.match(httpCookie, "everything", "domain", "path")); - assertFalse(HttpCookie.match(httpCookie, "something", "domain", "path")); - assertFalse(HttpCookie.match(httpCookie, "everything", "realm", "path")); - assertFalse(HttpCookie.match(httpCookie, "everything", "domain", "street")); + assertTrue(HttpCookieUtils.match(httpCookie, "everything", "domain", "path")); + assertFalse(HttpCookieUtils.match(httpCookie, "something", "domain", "path")); + assertFalse(HttpCookieUtils.match(httpCookie, "everything", "realm", "path")); + assertFalse(HttpCookieUtils.match(httpCookie, "everything", "domain", "street")); } @Test @@ -91,33 +96,33 @@ public void testSetRFC2965Cookie() throws Exception HttpCookie httpCookie; httpCookie = HttpCookie.from("null", null, -1, Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(-1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(false), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(false))); - assertEquals("null=", HttpCookie.getRFC2965SetCookie(httpCookie)); + assertEquals("null=", HttpCookieUtils.getRFC2965SetCookie(httpCookie)); httpCookie = HttpCookie.from("minimal", "value", -1, Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(-1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(false), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(false))); - assertEquals("minimal=value", HttpCookie.getRFC2965SetCookie(httpCookie)); + assertEquals("minimal=value", HttpCookieUtils.getRFC2965SetCookie(httpCookie)); httpCookie = HttpCookie.from("everything", "something", 0, Map.of(HttpCookie.DOMAIN_ATTRIBUTE, "domain", HttpCookie.PATH_ATTRIBUTE, "path", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(0), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true), HttpCookie.COMMENT_ATTRIBUTE, "noncomment")); - assertEquals("everything=something;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=noncomment", HttpCookie.getRFC2965SetCookie(httpCookie)); + assertEquals("everything=something;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=noncomment", HttpCookieUtils.getRFC2965SetCookie(httpCookie)); httpCookie = HttpCookie.from("everything", "value", 0, Map.of(HttpCookie.DOMAIN_ATTRIBUTE, "domain", HttpCookie.PATH_ATTRIBUTE, "path", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(0), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true), HttpCookie.COMMENT_ATTRIBUTE, "comment")); - assertEquals("everything=value;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment", HttpCookie.getRFC2965SetCookie(httpCookie)); + assertEquals("everything=value;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment", HttpCookieUtils.getRFC2965SetCookie(httpCookie)); httpCookie = HttpCookie.from("ev erything", "va lue", 1, Map.of(HttpCookie.DOMAIN_ATTRIBUTE, "do main", HttpCookie.PATH_ATTRIBUTE, "pa th", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true), HttpCookie.COMMENT_ATTRIBUTE, "co mment")); - String setCookie = HttpCookie.getRFC2965SetCookie(httpCookie); + String setCookie = HttpCookieUtils.getRFC2965SetCookie(httpCookie); assertThat(setCookie, Matchers.startsWith("\"ev erything\"=\"va lue\";Version=1;Path=\"pa th\";Domain=\"do main\";Expires=")); assertThat(setCookie, Matchers.endsWith(" GMT;Max-Age=1;Secure;HttpOnly;Comment=\"co mment\"")); httpCookie = HttpCookie.from("name", "value", 0, Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(-1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(false), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(false))); - setCookie = HttpCookie.getRFC2965SetCookie(httpCookie); + setCookie = HttpCookieUtils.getRFC2965SetCookie(httpCookie); assertEquals(-1, setCookie.indexOf("Version=")); httpCookie = HttpCookie.from("name", "v a l u e", 0, Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(-1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(false), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(false))); - setCookie = HttpCookie.getRFC2965SetCookie(httpCookie); + setCookie = HttpCookieUtils.getRFC2965SetCookie(httpCookie); httpCookie = HttpCookie.from("json", "{\"services\":[\"cwa\", \"aa\"]}", -1, Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(-1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(false), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(false))); - assertEquals("json=\"{\\\"services\\\":[\\\"cwa\\\", \\\"aa\\\"]}\"", HttpCookie.getRFC2965SetCookie(httpCookie)); + assertEquals("json=\"{\\\"services\\\":[\\\"cwa\\\", \\\"aa\\\"]}\"", HttpCookieUtils.getRFC2965SetCookie(httpCookie)); httpCookie = HttpCookie.from("name", "value%=", 0, Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(-1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(false), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(false))); - setCookie = HttpCookie.getRFC2965SetCookie(httpCookie); + setCookie = HttpCookieUtils.getRFC2965SetCookie(httpCookie); assertEquals("name=value%=", setCookie); } @@ -127,26 +132,26 @@ public void testSetRFC6265Cookie() throws Exception HttpCookie httpCookie; httpCookie = HttpCookie.from("null", null, -1, Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(-1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(false), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(false))); - assertEquals("null=", HttpCookie.getRFC6265SetCookie(httpCookie)); + assertEquals("null=", HttpCookieUtils.getRFC6265SetCookie(httpCookie)); httpCookie = HttpCookie.from("minimal", "value", -1, Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(-1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(false), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(false))); - assertEquals("minimal=value", HttpCookie.getRFC6265SetCookie(httpCookie)); + assertEquals("minimal=value", HttpCookieUtils.getRFC6265SetCookie(httpCookie)); //test cookies with same name, domain and path httpCookie = HttpCookie.from("everything", "something", -1, Map.of(HttpCookie.DOMAIN_ATTRIBUTE, "domain", HttpCookie.PATH_ATTRIBUTE, "path", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(0), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true))); - assertEquals("everything=something; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly", HttpCookie.getRFC6265SetCookie(httpCookie)); + assertEquals("everything=something; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly", HttpCookieUtils.getRFC6265SetCookie(httpCookie)); httpCookie = HttpCookie.from("everything", "value", -1, Map.of(HttpCookie.DOMAIN_ATTRIBUTE, "domain", HttpCookie.PATH_ATTRIBUTE, "path", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(0), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true))); - assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly", HttpCookie.getRFC6265SetCookie(httpCookie)); + assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly", HttpCookieUtils.getRFC6265SetCookie(httpCookie)); httpCookie = HttpCookie.from("everything", "value", Map.of(HttpCookie.DOMAIN_ATTRIBUTE, "domain", HttpCookie.PATH_ATTRIBUTE, "path", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(0), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true), HttpCookie.SAME_SITE_ATTRIBUTE, SameSite.NONE.getAttributeValue())); - assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=None", HttpCookie.getRFC6265SetCookie(httpCookie)); + assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=None", HttpCookieUtils.getRFC6265SetCookie(httpCookie)); httpCookie = HttpCookie.from("everything", "value", Map.of(HttpCookie.DOMAIN_ATTRIBUTE, "domain", HttpCookie.PATH_ATTRIBUTE, "path", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(0), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true), HttpCookie.SAME_SITE_ATTRIBUTE, SameSite.LAX.getAttributeValue())); - assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax", HttpCookie.getRFC6265SetCookie(httpCookie)); + assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax", HttpCookieUtils.getRFC6265SetCookie(httpCookie)); httpCookie = HttpCookie.from("everything", "value", Map.of(HttpCookie.DOMAIN_ATTRIBUTE, "domain", HttpCookie.PATH_ATTRIBUTE, "path", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(0), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true), HttpCookie.SAME_SITE_ATTRIBUTE, SameSite.STRICT.getAttributeValue())); - assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Strict", HttpCookie.getRFC6265SetCookie(httpCookie)); + assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Strict", HttpCookieUtils.getRFC6265SetCookie(httpCookie)); } public static Stream rfc6265BadNameSource() @@ -172,7 +177,7 @@ public void testSetRFC6265CookieBadName(String badNameExample) () -> { HttpCookie httpCookie = HttpCookie.from(badNameExample, "value", -1, Map.of(HttpCookie.PATH_ATTRIBUTE, "/", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true))); - HttpCookie.getRFC6265SetCookie(httpCookie); + HttpCookieUtils.getRFC6265SetCookie(httpCookie); }); // make sure that exception mentions just how mad of a name it truly is assertThat("message", ex.getMessage(), @@ -209,7 +214,7 @@ public void testSetRFC6265CookieBadValue(String badValueExample) () -> { HttpCookie httpCookie = HttpCookie.from("name", badValueExample, -1, Map.of(HttpCookie.PATH_ATTRIBUTE, "/", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true))); - HttpCookie.getRFC6265SetCookie(httpCookie); + HttpCookieUtils.getRFC6265SetCookie(httpCookie); }); assertThat("message", ex.getMessage(), containsString("RFC6265")); } @@ -255,4 +260,106 @@ public void testSetRFC6265CookieGoodValue(String goodValueExample) HttpCookie.from("name", goodValueExample, -1, Map.of(HttpCookie.PATH_ATTRIBUTE, "/", HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(1), HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(true), HttpCookie.SECURE_ATTRIBUTE, Boolean.toString(true))); // should not throw an exception } + + @Test + public void testBuilderSimple() + { + HttpCookie httpCookie = HttpCookie.build("name", "value").build(); + assertThat(httpCookie.getName(), equalToIgnoringCase("name")); + assertThat(httpCookie.getValue(), equalTo("value")); + assertThat(httpCookie.getVersion(), equalTo(0)); + assertThat(httpCookie.getAttributes(), anEmptyMap()); + } + + @Test + public void testBuilderNull() + { + HttpCookie httpCookie = HttpCookie.build("name", "value") + .attribute(null, null) + .comment(null) + .domain(null) + .httpOnly(false) + .maxAge(-1) + .secure(false) + .path(null) + .build(); + assertThat(httpCookie.getName(), equalToIgnoringCase("name")); + assertThat(httpCookie.getValue(), equalTo("value")); + assertThat(httpCookie.getVersion(), equalTo(0)); + assertThat(httpCookie.getAttributes(), anEmptyMap()); + } + + @Test + public void testBuilderFull() + { + HttpCookie httpCookie = HttpCookie.build("name", "value") + .attribute("some", "value") + .comment("comment") + .domain("domain") + .httpOnly(true) + .maxAge(42) + .secure(true) + .path("/path") + .build(); + assertThat(httpCookie.getName(), equalToIgnoringCase("name")); + assertThat(httpCookie.getValue(), equalTo("value")); + assertThat(httpCookie.getVersion(), equalTo(0)); + assertThat(httpCookie.getAttributes().keySet(), containsInAnyOrder( + "some", + HttpCookie.COMMENT_ATTRIBUTE, + HttpCookie.DOMAIN_ATTRIBUTE, + HttpCookie.HTTP_ONLY_ATTRIBUTE, + HttpCookie.MAX_AGE_ATTRIBUTE, + HttpCookie.SECURE_ATTRIBUTE, + HttpCookie.PATH_ATTRIBUTE)); + assertThat(httpCookie.getAttributes().values(), containsInAnyOrder( + "value", + Boolean.TRUE.toString(), + Boolean.TRUE.toString(), + "comment", + "domain", + "42", + "/path")); + } + + @Test + public void testJavaNetHttpCookie() + { + java.net.HttpCookie cookie = new java.net.HttpCookie("name", "value"); + cookie.setVersion(1); + cookie.setComment("comment"); + cookie.setDomain("domain"); + cookie.setHttpOnly(true); + cookie.setMaxAge(42); + cookie.setPath("/path"); + cookie.setSecure(true); + + HttpCookie httpCookie = HttpCookie.from(cookie); + + assertThat(httpCookie.getName(), equalTo("name")); + assertThat(httpCookie.getValue(), equalTo("value")); + assertThat(httpCookie.getVersion(), equalTo(1)); + assertThat(httpCookie.getDomain(), equalTo("domain")); + assertThat(httpCookie.getMaxAge(), equalTo(42L)); + assertThat(httpCookie.isSecure(), equalTo(true)); + + assertThat(httpCookie.getAttributes().keySet(), containsInAnyOrder( + HttpCookie.COMMENT_ATTRIBUTE, + HttpCookie.DOMAIN_ATTRIBUTE, + HttpCookie.HTTP_ONLY_ATTRIBUTE, + HttpCookie.MAX_AGE_ATTRIBUTE, + HttpCookie.SECURE_ATTRIBUTE, + HttpCookie.PATH_ATTRIBUTE)); + assertThat(httpCookie.getAttributes().values(), containsInAnyOrder( + Boolean.TRUE.toString(), + Boolean.TRUE.toString(), + "comment", + "domain", + "42", + "/path")); + + java.net.HttpCookie cookie2 = HttpCookie.asJavaNetHttpCookie(httpCookie); + assertEquals(cookie, cookie2); + } + } diff --git a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/HttpCookieStore.java b/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/HttpCookieStore.java deleted file mode 100644 index d2066cc70c93..000000000000 --- a/jetty-core/jetty-util/src/main/java/org/eclipse/jetty/util/HttpCookieStore.java +++ /dev/null @@ -1,142 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.util; - -import java.net.CookieManager; -import java.net.CookieStore; -import java.net.HttpCookie; -import java.net.URI; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Implementation of {@link CookieStore} that delegates to an instance created by {@link CookieManager} - * via {@link CookieManager#getCookieStore()}. - */ -public class HttpCookieStore implements CookieStore -{ - private final CookieStore delegate; - - public HttpCookieStore() - { - delegate = new CookieManager().getCookieStore(); - } - - @Override - public void add(URI uri, HttpCookie cookie) - { - delegate.add(uri, cookie); - } - - @Override - public List get(URI uri) - { - return delegate.get(uri); - } - - @Override - public List getCookies() - { - return delegate.getCookies(); - } - - @Override - public List getURIs() - { - return delegate.getURIs(); - } - - @Override - public boolean remove(URI uri, HttpCookie cookie) - { - return delegate.remove(uri, cookie); - } - - @Override - public boolean removeAll() - { - return delegate.removeAll(); - } - - public static List matchPath(URI uri, List cookies) - { - if (cookies == null || cookies.isEmpty()) - return Collections.emptyList(); - List result = new ArrayList<>(4); - String path = uri.getPath(); - if (path == null || path.trim().isEmpty()) - path = "/"; - for (HttpCookie cookie : cookies) - { - String cookiePath = cookie.getPath(); - if (cookiePath == null) - { - result.add(cookie); - } - else - { - // RFC 6265, section 5.1.4, path matching algorithm. - if (path.equals(cookiePath)) - { - result.add(cookie); - } - else if (path.startsWith(cookiePath)) - { - if (cookiePath.endsWith("/") || path.charAt(cookiePath.length()) == '/') - result.add(cookie); - } - } - } - return result; - } - - public static class Empty implements CookieStore - { - @Override - public void add(URI uri, HttpCookie cookie) - { - } - - @Override - public List get(URI uri) - { - return Collections.emptyList(); - } - - @Override - public List getCookies() - { - return Collections.emptyList(); - } - - @Override - public List getURIs() - { - return Collections.emptyList(); - } - - @Override - public boolean remove(URI uri, HttpCookie cookie) - { - return false; - } - - @Override - public boolean removeAll() - { - return false; - } - } -} diff --git a/jetty-core/jetty-websocket/jetty-websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/CoreClientUpgradeRequest.java b/jetty-core/jetty-websocket/jetty-websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/CoreClientUpgradeRequest.java index b91222f4f784..71853263d304 100644 --- a/jetty-core/jetty-websocket/jetty-websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/CoreClientUpgradeRequest.java +++ b/jetty-core/jetty-websocket/jetty-websocket-core-client/src/main/java/org/eclipse/jetty/websocket/core/client/CoreClientUpgradeRequest.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.websocket.core.client; import java.io.IOException; -import java.net.HttpCookie; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -31,6 +30,7 @@ import org.eclipse.jetty.client.Request; import org.eclipse.jetty.client.Response; import org.eclipse.jetty.client.Result; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; diff --git a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java index 17f67e6f49bd..b94661fdf1d1 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java +++ b/jetty-ee10/jetty-ee10-proxy/src/main/java/org/eclipse/jetty/ee10/proxy/AbstractProxyServlet.java @@ -40,6 +40,7 @@ import org.eclipse.jetty.client.Request; import org.eclipse.jetty.client.Response; import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; +import org.eclipse.jetty.http.HttpCookieStore; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; @@ -47,7 +48,6 @@ import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.io.ClientConnector; -import org.eclipse.jetty.util.HttpCookieStore; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; @@ -276,7 +276,7 @@ protected HttpClient createHttpClient() throws ServletException client.setFollowRedirects(false); // Must not store cookies, otherwise cookies of different clients will mix. - client.setCookieStore(new HttpCookieStore.Empty()); + client.setHttpCookieStore(new HttpCookieStore.Empty()); Executor executor; String value = config.getInitParameter("maxThreads"); diff --git a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java index 43489b03f831..255b09601b74 100644 --- a/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java +++ b/jetty-ee10/jetty-ee10-proxy/src/test/java/org/eclipse/jetty/ee10/proxy/ProxyServletTest.java @@ -21,7 +21,6 @@ import java.io.PrintWriter; import java.io.Writer; import java.net.ConnectException; -import java.net.HttpCookie; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -76,6 +75,7 @@ import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletContextRequest; import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; @@ -1194,7 +1194,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) .send(); assertEquals(200, response1.getStatus()); assertTrue(response1.getHeaders().contains(PROXIED_HEADER)); - List cookies = client.getCookieStore().getCookies(); + List cookies = client.getHttpCookieStore().all(); assertEquals(1, cookies.size()); assertEquals(name, cookies.get(0).getName()); assertEquals(value1, cookies.get(0).getValue()); @@ -1209,7 +1209,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) .send(); assertEquals(200, response2.getStatus()); assertTrue(response2.getHeaders().contains(PROXIED_HEADER)); - cookies = client2.getCookieStore().getCookies(); + cookies = client2.getHttpCookieStore().all(); assertEquals(1, cookies.size()); assertEquals(name, cookies.get(0).getName()); assertEquals(value2, cookies.get(0).getValue()); diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java index 45f0652af0b8..967b4201bd12 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiRequest.java @@ -67,6 +67,7 @@ import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.server.ConnectionMetaData; import org.eclipse.jetty.server.FormFields; +import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Session; import org.eclipse.jetty.session.AbstractSessionManager; @@ -528,7 +529,7 @@ public PushBuilder newPushBuilder() pushCookies.append(cookies); for (String setCookie : setCookies) { - Map cookieFields = HttpCookie.extractBasics(setCookie); + Map cookieFields = HttpCookieUtils.extractBasics(setCookie); String cookieName = cookieFields.get("name"); String cookieValue = cookieFields.get("value"); String cookieMaxAge = cookieFields.get("max-age"); diff --git a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java index 222a126ce884..ba3f2d5aafd2 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java +++ b/jetty-ee10/jetty-ee10-servlet/src/main/java/org/eclipse/jetty/ee10/servlet/ServletApiResponse.java @@ -721,9 +721,15 @@ public Map getAttributes() } @Override - public String asString() + public int hashCode() { - return HttpCookie.asString(this); + return HttpCookie.hashCode(this); + } + + @Override + public boolean equals(Object obj) + { + return HttpCookie.equals(this, obj); } @Override diff --git a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SessionHandlerTest.java b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SessionHandlerTest.java index 80a64ec4677a..782fa255ca31 100644 --- a/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SessionHandlerTest.java +++ b/jetty-ee10/jetty-ee10-servlet/src/test/java/org/eclipse/jetty/ee10/servlet/SessionHandlerTest.java @@ -39,6 +39,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.logging.StacklessLogging; +import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -494,7 +495,7 @@ public void testSessionCookie() throws Exception assertEquals(99, cookie.getMaxAge()); assertEquals(HttpCookie.SameSite.STRICT, cookie.getSameSite()); - String cookieStr = HttpCookie.getRFC6265SetCookie(cookie); + String cookieStr = HttpCookieUtils.getRFC6265SetCookie(cookie); assertThat(cookieStr, containsString("; SameSite=Strict; ham=cheese")); } diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/test/java/org/eclipse/jetty/ee10/session/SessionRenewTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/test/java/org/eclipse/jetty/ee10/session/SessionRenewTest.java index 442c2e35e3e1..c0c8156fb5c6 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/test/java/org/eclipse/jetty/ee10/session/SessionRenewTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-sessions/jetty-ee10-test-sessions-common/src/test/java/org/eclipse/jetty/ee10/session/SessionRenewTest.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.ee10.session; import java.io.IOException; -import java.net.HttpCookie; import java.util.concurrent.TimeUnit; import jakarta.servlet.ServletException; @@ -29,6 +28,7 @@ import org.eclipse.jetty.client.Request; import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.session.DefaultSessionCache; import org.eclipse.jetty.session.DefaultSessionCacheFactory; import org.eclipse.jetty.session.DefaultSessionIdManager; @@ -172,7 +172,7 @@ public void testSessionRenewalMultiContext() throws Exception //make a request to change the sessionid Request request = client.newRequest("http://localhost:" + port + contextPathA + servletMapping + "?action=renew"); - request.cookie(new HttpCookie(SessionManager.__DefaultSessionCookie, "1234")); + request.cookie(HttpCookie.from(SessionManager.__DefaultSessionCookie, "1234")); ContentResponse renewResponse = request.send(); assertEquals(HttpServletResponse.SC_OK, renewResponse.getStatus()); String newSessionCookie = renewResponse.getHeaders().get("Set-Cookie"); diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee10/websocket/client/WebSocketClient.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee10/websocket/client/WebSocketClient.java index b23fb35da406..7e2a1fe9cef8 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee10/websocket/client/WebSocketClient.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee10/websocket/client/WebSocketClient.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.ee10.websocket.client; import java.io.IOException; -import java.net.CookieStore; import java.net.SocketAddress; import java.net.URI; import java.time.Duration; @@ -40,7 +39,6 @@ import org.eclipse.jetty.ee10.websocket.common.JettyWebSocketFrameHandlerFactory; import org.eclipse.jetty.ee10.websocket.common.SessionTracker; import org.eclipse.jetty.io.Connection; -import org.eclipse.jetty.io.RetainableByteBufferPool; import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.component.Graceful; @@ -323,21 +321,6 @@ public void setConnectTimeout(long ms) getHttpClient().setConnectTimeout(ms); } - public CookieStore getCookieStore() - { - return getHttpClient().getCookieStore(); - } - - public void setCookieStore(CookieStore cookieStore) - { - getHttpClient().setCookieStore(cookieStore); - } - - public RetainableByteBufferPool getRetainableByteBufferPool() - { - return getHttpClient().getRetainableByteBufferPool(); - } - @Override public Executor getExecutor() { diff --git a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee10/websocket/client/impl/DelegatedJettyClientUpgradeRequest.java b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee10/websocket/client/impl/DelegatedJettyClientUpgradeRequest.java index 6a2e72ed0988..281e765b75fa 100644 --- a/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee10/websocket/client/impl/DelegatedJettyClientUpgradeRequest.java +++ b/jetty-ee10/jetty-ee10-websocket/jetty-ee10-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee10/websocket/client/impl/DelegatedJettyClientUpgradeRequest.java @@ -49,7 +49,9 @@ public DelegatedJettyClientUpgradeRequest(CoreClientUpgradeRequest delegate) @Override public List getCookies() { - return delegate.getCookies(); + return delegate.getCookies().stream() + .map(org.eclipse.jetty.http.HttpCookie::asJavaNetHttpCookie) + .toList(); } @Override diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java index 5185b33520a2..37e9a89e4bf7 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Request.java @@ -66,7 +66,6 @@ import org.eclipse.jetty.http.ComplianceViolation; import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.HttpCookie; -import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; @@ -79,6 +78,8 @@ import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.RuntimeIOException; +import org.eclipse.jetty.server.HttpCookieUtils; +import org.eclipse.jetty.server.HttpCookieUtils.SetCookieHttpField; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Session; import org.eclipse.jetty.session.AbstractSessionManager; @@ -316,10 +317,10 @@ public PushBuilder newPushBuilder() } else { - Map cookieFields = HttpCookie.extractBasics(field.getValue()); + Map cookieFields = HttpCookieUtils.extractBasics(field.getValue()); cookieName = cookieFields.get("name"); cookieValue = cookieFields.get("value"); - cookieMaxAge = cookieFields.get("max-age") != null ? Long.valueOf(cookieFields.get("max-age")) : -1; + cookieMaxAge = cookieFields.get("max-age") != null ? Long.parseLong(cookieFields.get("max-age")) : -1; } if (cookieMaxAge > 0) diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java index 91f0468d0815..ab4698ca6373 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/Response.java @@ -37,7 +37,6 @@ import org.eclipse.jetty.http.DateGenerator; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpCookie.SameSite; -import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpGenerator; @@ -52,6 +51,8 @@ import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.http.content.HttpContent; import org.eclipse.jetty.io.RuntimeIOException; +import org.eclipse.jetty.server.HttpCookieUtils; +import org.eclipse.jetty.server.HttpCookieUtils.SetCookieHttpField; import org.eclipse.jetty.server.Session; import org.eclipse.jetty.session.SessionManager; import org.eclipse.jetty.util.AtomicBiInteger; @@ -252,7 +253,7 @@ private HttpCookie checkSameSite(HttpCookie cookie) return cookie; //sameSite is not set, use the default configured for the context, if one exists - SameSite contextDefault = HttpCookie.getSameSiteDefault(_channel.getRequest().getContext().getCoreContext()); + SameSite contextDefault = HttpCookieUtils.getSameSiteDefault(_channel.getRequest().getContext().getCoreContext()); if (contextDefault == null) return cookie; //no default set @@ -288,14 +289,14 @@ public void replaceCookie(HttpCookie cookie) { CookieCompliance compliance = getHttpChannel().getHttpConfiguration().getResponseCookieCompliance(); - if (field instanceof HttpCookie.SetCookieHttpField) + if (field instanceof HttpCookieUtils.SetCookieHttpField) { - if (!HttpCookie.match(((HttpCookie.SetCookieHttpField)field).getHttpCookie(), cookie.getName(), cookie.getDomain(), cookie.getPath())) + if (!HttpCookieUtils.match(((HttpCookieUtils.SetCookieHttpField)field).getHttpCookie(), cookie.getName(), cookie.getDomain(), cookie.getPath())) continue; } else { - if (!HttpCookie.match(field.getValue(), cookie.getName(), cookie.getDomain(), cookie.getPath())) + if (!HttpCookieUtils.match(field.getValue(), cookie.getName(), cookie.getDomain(), cookie.getPath())) continue; } @@ -1582,9 +1583,15 @@ public Map getAttributes() } @Override - public String asString() + public int hashCode() { - return HttpCookie.asString(this); + return HttpCookie.hashCode(this); + } + + @Override + public boolean equals(Object obj) + { + return HttpCookie.equals(this, obj); } @Override diff --git a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/RequestTest.java b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/RequestTest.java index bd32acfd0113..3551ac02f084 100644 --- a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/RequestTest.java +++ b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/RequestTest.java @@ -73,6 +73,7 @@ import org.eclipse.jetty.server.Context; import org.eclipse.jetty.server.ForwardedRequestCustomizer; import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.HttpStream; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.LocalConnector.LocalEndPoint; @@ -1870,10 +1871,10 @@ public void testPushBuilder() String uri = "http://host/foo/something"; HttpChannel httpChannel = new HttpChannel(_context, new MockConnectionMetaData(_connector)); Request request = new MockRequest(httpChannel, new HttpInput(httpChannel)); - request.getResponse().getHttpFields().add(new HttpCookie.SetCookieHttpField(HttpCookie.from("good", "thumbsup", Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(100))), CookieCompliance.RFC6265)); - request.getResponse().getHttpFields().add(new HttpCookie.SetCookieHttpField(HttpCookie.from("bonza", "bewdy", Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(1))), CookieCompliance.RFC6265)); - request.getResponse().getHttpFields().add(new HttpCookie.SetCookieHttpField(HttpCookie.from("bad", "thumbsdown", Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(0))), CookieCompliance.RFC6265)); - request.getResponse().getHttpFields().add(new HttpField(HttpHeader.SET_COOKIE, HttpCookie.getSetCookie(HttpCookie.from("ugly", "duckling", Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(100))), CookieCompliance.RFC6265))); + request.getResponse().getHttpFields().add(new HttpCookieUtils.SetCookieHttpField(HttpCookie.from("good", "thumbsup", Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(100))), CookieCompliance.RFC6265)); + request.getResponse().getHttpFields().add(new HttpCookieUtils.SetCookieHttpField(HttpCookie.from("bonza", "bewdy", Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(1))), CookieCompliance.RFC6265)); + request.getResponse().getHttpFields().add(new HttpCookieUtils.SetCookieHttpField(HttpCookie.from("bad", "thumbsdown", Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(0))), CookieCompliance.RFC6265)); + request.getResponse().getHttpFields().add(new HttpField(HttpHeader.SET_COOKIE, HttpCookieUtils.getSetCookie(HttpCookie.from("ugly", "duckling", Map.of(HttpCookie.MAX_AGE_ATTRIBUTE, Long.toString(100))), CookieCompliance.RFC6265))); request.getResponse().getHttpFields().add(new HttpField(HttpHeader.SET_COOKIE, "flow=away; Max-Age=0; Secure; HttpOnly; SameSite=None")); HttpFields.Mutable fields = HttpFields.build(); fields.add(HttpHeader.AUTHORIZATION, "Basic foo"); diff --git a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java index 8bf2e051e4f3..1c0bdbbdee83 100644 --- a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java +++ b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/ResponseTest.java @@ -58,6 +58,7 @@ import org.eclipse.jetty.server.Context; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.HttpStream; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.NetworkConnector; @@ -1942,7 +1943,7 @@ public void testAddCookieInInclude() throws Exception @Test public void testAddCookieSameSiteByComment() throws Exception { - _context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, HttpCookie.SameSite.STRICT); + _context.setAttribute(HttpCookieUtils.SAME_SITE_DEFAULT_ATTRIBUTE, HttpCookie.SameSite.STRICT); Response response = getResponse(); Cookie cookie = new Cookie("name", "value"); @@ -1960,7 +1961,7 @@ public void testAddCookieSameSiteByComment() throws Exception public void testAddCookieSameSiteDefault() throws Exception { Response response = getResponse(); - _context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, HttpCookie.SameSite.STRICT); + _context.setAttribute(HttpCookieUtils.SAME_SITE_DEFAULT_ATTRIBUTE, HttpCookie.SameSite.STRICT); Cookie cookie = new Cookie("name", "value"); cookie.setDomain("domain"); cookie.setPath("/path"); @@ -1974,7 +1975,7 @@ public void testAddCookieSameSiteDefault() throws Exception response.getHttpFields().remove("Set-Cookie"); //test bad default samesite value - _context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "FooBar"); + _context.setAttribute(HttpCookieUtils.SAME_SITE_DEFAULT_ATTRIBUTE, "FooBar"); assertThrows(IllegalStateException.class, () -> response.addCookie(cookie)); @@ -2123,7 +2124,7 @@ public void testReplaceHttpCookie() public void testReplaceHttpCookieSameSite() { Response response = getResponse(); - _context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX"); + _context.setAttribute(HttpCookieUtils.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX"); //replace with no prior does an add response.replaceCookie(HttpCookie.from("Foo", "123456")); String set = response.getHttpFields().get("Set-Cookie"); @@ -2163,7 +2164,7 @@ public void testReplaceParsedHttpCookie() public void testReplaceParsedHttpCookieSiteDefault() { Response response = getResponse(); - _context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX"); + _context.setAttribute(HttpCookieUtils.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX"); response.addHeader(HttpHeader.SET_COOKIE.asString(), "Foo=123456"); response.replaceCookie(HttpCookie.from("Foo", "value")); diff --git a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/SessionHandlerTest.java b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/SessionHandlerTest.java index 61c07fe471d1..84fd88876279 100644 --- a/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/SessionHandlerTest.java +++ b/jetty-ee9/jetty-ee9-nested/src/test/java/org/eclipse/jetty/ee9/nested/SessionHandlerTest.java @@ -31,6 +31,7 @@ import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.server.HttpCookieUtils; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Session; @@ -195,7 +196,7 @@ public void testSessionCookie() throws Exception assertEquals(99, cookie.getMaxAge()); assertEquals(HttpCookie.SameSite.STRICT, cookie.getSameSite()); - String cookieStr = HttpCookie.getRFC6265SetCookie(cookie); + String cookieStr = HttpCookieUtils.getRFC6265SetCookie(cookie); assertThat(cookieStr, containsString("; SameSite=Strict")); } diff --git a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java index 5d2975ff649e..0f67b893ac1b 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java +++ b/jetty-ee9/jetty-ee9-proxy/src/main/java/org/eclipse/jetty/ee9/proxy/AbstractProxyServlet.java @@ -40,6 +40,7 @@ import org.eclipse.jetty.client.Request; import org.eclipse.jetty.client.Response; import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; +import org.eclipse.jetty.http.HttpCookieStore; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; @@ -47,7 +48,6 @@ import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.io.ClientConnector; -import org.eclipse.jetty.util.HttpCookieStore; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; @@ -276,7 +276,7 @@ protected HttpClient createHttpClient() throws ServletException client.setFollowRedirects(false); // Must not store cookies, otherwise cookies of different clients will mix. - client.setCookieStore(new HttpCookieStore.Empty()); + client.setHttpCookieStore(new HttpCookieStore.Empty()); Executor executor; String value = config.getInitParameter("maxThreads"); diff --git a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServletTest.java b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServletTest.java index 2d4af79a1fa7..c6d41f538c7b 100644 --- a/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServletTest.java +++ b/jetty-ee9/jetty-ee9-proxy/src/test/java/org/eclipse/jetty/ee9/proxy/ProxyServletTest.java @@ -21,7 +21,6 @@ import java.io.PrintWriter; import java.io.Writer; import java.net.ConnectException; -import java.net.HttpCookie; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -75,6 +74,7 @@ import org.eclipse.jetty.ee9.servlet.FilterHolder; import org.eclipse.jetty.ee9.servlet.ServletContextHandler; import org.eclipse.jetty.ee9.servlet.ServletHolder; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; @@ -1193,7 +1193,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) .send(); assertEquals(200, response1.getStatus()); assertTrue(response1.getHeaders().contains(PROXIED_HEADER)); - List cookies = client.getCookieStore().getCookies(); + List cookies = client.getHttpCookieStore().all(); assertEquals(1, cookies.size()); assertEquals(name, cookies.get(0).getName()); assertEquals(value1, cookies.get(0).getValue()); @@ -1208,7 +1208,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) .send(); assertEquals(200, response2.getStatus()); assertTrue(response2.getHeaders().contains(PROXIED_HEADER)); - cookies = client2.getCookieStore().getCookies(); + cookies = client2.getHttpCookieStore().all(); assertEquals(1, cookies.size()); assertEquals(name, cookies.get(0).getName()); assertEquals(value2, cookies.get(0).getValue()); diff --git a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-common/src/test/java/org/eclipse/jetty/ee9/session/SessionRenewTest.java b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-common/src/test/java/org/eclipse/jetty/ee9/session/SessionRenewTest.java index 745a94a69255..752fdc9053c4 100644 --- a/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-common/src/test/java/org/eclipse/jetty/ee9/session/SessionRenewTest.java +++ b/jetty-ee9/jetty-ee9-tests/jetty-ee9-test-sessions/jetty-ee9-test-sessions-common/src/test/java/org/eclipse/jetty/ee9/session/SessionRenewTest.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.ee9.session; import java.io.IOException; -import java.net.HttpCookie; import java.util.concurrent.TimeUnit; import jakarta.servlet.ServletException; @@ -29,6 +28,7 @@ import org.eclipse.jetty.client.Request; import org.eclipse.jetty.ee9.nested.SessionHandler.ServletSessionApi; import org.eclipse.jetty.ee9.servlet.ServletContextHandler; +import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.session.DefaultSessionCache; import org.eclipse.jetty.session.DefaultSessionCacheFactory; import org.eclipse.jetty.session.DefaultSessionIdManager; @@ -172,7 +172,7 @@ public void testSessionRenewalMultiContext() throws Exception //make a request to change the sessionid Request request = client.newRequest("http://localhost:" + port + contextPathA + servletMapping + "?action=renew"); - request.cookie(new HttpCookie(SessionManager.__DefaultSessionCookie, "1234")); + request.cookie(HttpCookie.from(SessionManager.__DefaultSessionCookie, "1234")); ContentResponse renewResponse = request.send(); assertEquals(HttpServletResponse.SC_OK, renewResponse.getStatus()); String newSessionCookie = renewResponse.getHeaders().get("Set-Cookie"); diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/WebSocketClient.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/WebSocketClient.java index d83492838d69..756038d754b2 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/WebSocketClient.java +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/WebSocketClient.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.ee9.websocket.client; import java.io.IOException; -import java.net.CookieStore; import java.net.SocketAddress; import java.net.URI; import java.time.Duration; @@ -41,7 +40,6 @@ import org.eclipse.jetty.ee9.websocket.common.JettyWebSocketFrameHandlerFactory; import org.eclipse.jetty.ee9.websocket.common.SessionTracker; import org.eclipse.jetty.io.Connection; -import org.eclipse.jetty.io.RetainableByteBufferPool; import org.eclipse.jetty.util.DecoratedObjectFactory; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.component.Graceful; @@ -324,21 +322,6 @@ public void setConnectTimeout(long ms) getHttpClient().setConnectTimeout(ms); } - public CookieStore getCookieStore() - { - return getHttpClient().getCookieStore(); - } - - public void setCookieStore(CookieStore cookieStore) - { - getHttpClient().setCookieStore(cookieStore); - } - - public RetainableByteBufferPool getRetainableByteBufferPool() - { - return getHttpClient().getRetainableByteBufferPool(); - } - @Override public Executor getExecutor() { diff --git a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/impl/DelegatedJettyClientUpgradeRequest.java b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/impl/DelegatedJettyClientUpgradeRequest.java index 2e4f8ff12433..57da73870322 100644 --- a/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/impl/DelegatedJettyClientUpgradeRequest.java +++ b/jetty-ee9/jetty-ee9-websocket/jetty-ee9-websocket-jetty-client/src/main/java/org/eclipse/jetty/ee9/websocket/client/impl/DelegatedJettyClientUpgradeRequest.java @@ -49,7 +49,9 @@ public DelegatedJettyClientUpgradeRequest(CoreClientUpgradeRequest delegate) @Override public List getCookies() { - return delegate.getCookies(); + return delegate.getCookies().stream() + .map(org.eclipse.jetty.http.HttpCookie::asJavaNetHttpCookie) + .toList(); } @Override