From d02932f6900551493cee5db221e05203b64df522 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 7 Feb 2023 17:28:01 +0100 Subject: [PATCH] Fixes #9288 - Jetty 12 - Use oej.http.HttpCookie in jetty-client. (#9289) * 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. * Introduced HttpScheme.isSecure(String), to avoid code duplication. * Fixed handling of cookie "localhost" domain in HttpClient. Signed-off-by: Simone Bordet --- .../client/http/HTTPClientDocs.java | 37 +- .../org/eclipse/jetty/client/HttpClient.java | 110 +- .../org/eclipse/jetty/client/Request.java | 2 +- .../transport/HttpClientTransportDynamic.java | 5 +- .../client/transport/HttpConnection.java | 24 +- .../client/transport/HttpDestination.java | 8 +- .../jetty/client/transport/HttpRequest.java | 2 +- .../eclipse/jetty/client/HttpClientTest.java | 4 +- .../eclipse/jetty/client/HttpCookieTest.java | 89 +- .../jetty/fcgi/proxy/FastCGIProxyHandler.java | 2 +- .../org/eclipse/jetty/http/DateGenerator.java | 24 - .../org/eclipse/jetty/http/HttpCookie.java | 1106 ++++++++++------- .../eclipse/jetty/http/HttpCookieStore.java | 388 ++++++ .../org/eclipse/jetty/http/HttpScheme.java | 5 + .../jetty/http/HttpCookieStoreTest.java | 278 +++++ .../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 | 183 ++- .../HttpClientTransportDynamicTest.java | 3 +- .../eclipse/jetty/util/HttpCookieStore.java | 142 --- .../core/client/CoreClientUpgradeRequest.java | 5 +- .../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 | 16 - .../DelegatedJettyClientUpgradeRequest.java | 8 +- .../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 | 13 +- .../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 | 16 - .../DelegatedJettyClientUpgradeRequest.java | 8 +- 40 files changed, 2133 insertions(+), 879 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 (60%) 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 0655aa7bd5f8..bdc586689df7 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(byteBufferPool));
 
-        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,25 +280,20 @@ 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)
     {
         try
         {
-            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);
+            if (cookie != null)
+                cookieStore.add(uri, cookie);
         }
         catch (IOException x)
         {
@@ -1138,20 +1131,77 @@ public static int normalizePort(String scheme, int port)
         return HttpScheme.getDefaultPort(scheme);
     }
 
-    public boolean isDefaultPort(String scheme, int port)
-    {
-        return HttpScheme.getDefaultPort(scheme) == port;
-    }
-
-    public static boolean isSchemeSecure(String scheme)
-    {
-        return HttpScheme.HTTPS.is(scheme) || HttpScheme.WSS.is(scheme);
-    }
-
     public ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.Client sslContextFactory, ClientConnectionFactory connectionFactory)
     {
         if (sslContextFactory == null)
             sslContextFactory = getSslContextFactory();
         return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), 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();
+            HttpCookie cookie = store.cookie;
+            store.cookie = null;
+            return cookie;
+        }
+
+        private static class Store implements CookieStore
+        {
+            private HttpCookie cookie;
+
+            @Override
+            public void add(URI uri, java.net.HttpCookie cookie)
+            {
+                String domain = cookie.getDomain();
+                if ("localhost.local".equals(domain))
+                    cookie.setDomain("localhost");
+                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/HttpClientTransportDynamic.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportDynamic.java
index 955b23cd569f..0ff5e68f2e5e 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportDynamic.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpClientTransportDynamic.java
@@ -28,12 +28,12 @@
 import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory;
 import org.eclipse.jetty.client.AbstractConnectorHttpClientTransport;
 import org.eclipse.jetty.client.Destination;
-import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.client.HttpClientTransport;
 import org.eclipse.jetty.client.MultiplexConnectionPool;
 import org.eclipse.jetty.client.Origin;
 import org.eclipse.jetty.client.Request;
 import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpScheme;
 import org.eclipse.jetty.http.HttpVersion;
 import org.eclipse.jetty.io.ClientConnectionFactory;
 import org.eclipse.jetty.io.ClientConnector;
@@ -130,7 +130,8 @@ private static ClientConnector findClientConnector(ClientConnectionFactory.Info[
     @Override
     public Origin newOrigin(Request request)
     {
-        boolean secure = HttpClient.isSchemeSecure(request.getScheme());
+        String scheme = request.getScheme();
+        boolean secure = HttpScheme.isSecure(scheme);
         String http1 = "http/1.1";
         String http2 = secure ? "h2" : "h2c";
         List protocols = List.of();
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..ac1a5000df8e 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,15 @@
 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.HttpScheme;
 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;
@@ -156,13 +156,17 @@ protected void normalizeRequest(HttpRequest request)
         }
 
         ProxyConfiguration.Proxy proxy = destination.getProxy();
-        if (proxy instanceof HttpProxy && !HttpClient.isSchemeSecure(request.getScheme()))
+        if (proxy instanceof HttpProxy)
         {
-            URI uri = request.getURI();
-            if (uri != null)
+            String scheme = request.getScheme();
+            if (!HttpScheme.isSecure(scheme))
             {
-                path = uri.toString();
-                request.path(path);
+                URI uri = request.getURI();
+                if (uri != null)
+                {
+                    path = uri.toString();
+                    request.path(path);
+                }
             }
         }
 
@@ -210,12 +214,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/HttpDestination.java b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpDestination.java
index 38d015cc0ffe..9afcb61b83cd 100644
--- a/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpDestination.java
+++ b/jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/transport/HttpDestination.java
@@ -32,6 +32,7 @@
 import org.eclipse.jetty.client.Response;
 import org.eclipse.jetty.http.HttpField;
 import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpScheme;
 import org.eclipse.jetty.io.ClientConnectionFactory;
 import org.eclipse.jetty.io.CyclicTimeouts;
 import org.eclipse.jetty.util.BlockingArrayQueue;
@@ -84,8 +85,9 @@ public HttpDestination(HttpClient client, Origin origin, boolean intrinsicallySe
         this.requestTimeouts = new RequestTimeouts(client.getScheduler());
 
         String host = HostPort.normalizeHost(getHost());
-        if (!client.isDefaultPort(getScheme(), getPort()))
-            host += ":" + getPort();
+        int port = getPort();
+        if (port != HttpScheme.getDefaultPort(getScheme()))
+            host += ":" + port;
         hostField = new HttpField(HttpHeader.HOST, host);
 
         ProxyConfiguration proxyConfig = client.getProxyConfiguration();
@@ -199,7 +201,7 @@ private ClientConnectionFactory newSslClientConnectionFactory(SslContextFactory.
     @Override
     public boolean isSecure()
     {
-        return HttpClient.isSchemeSecure(getScheme());
+        return HttpScheme.isSecure(getScheme());
     }
 
     @Override
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..f2471ee217d6 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;
 
@@ -49,7 +48,7 @@ public void testCookieIsStored(Scenario scenario) throws Exception
             @Override
             protected void service(Request request, org.eclipse.jetty.server.Response response)
             {
-                org.eclipse.jetty.server.Response.addCookie(response, org.eclipse.jetty.http.HttpCookie.from(name, value));
+                org.eclipse.jetty.server.Response.addCookie(response, HttpCookie.from(name, value));
             }
         });
 
@@ -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);
@@ -79,10 +78,10 @@ public void testCookieIsSent(Scenario scenario) throws Exception
             @Override
             protected void service(Request request, org.eclipse.jetty.server.Response response)
             {
-                List cookies = Request.getCookies(request);
+                List cookies = Request.getCookies(request);
                 assertNotNull(cookies);
                 assertEquals(1, cookies.size());
-                org.eclipse.jetty.http.HttpCookie cookie = cookies.get(0);
+                HttpCookie cookie = cookies.get(0);
                 assertEquals(name, cookie.getName());
                 assertEquals(value, cookie.getValue());
             }
@@ -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";
@@ -142,10 +141,10 @@ private void testPerRequestCookieIsSent(Scenario scenario, CookieStore cookieSto
             @Override
             protected void service(Request request, org.eclipse.jetty.server.Response response)
             {
-                List cookies = Request.getCookies(request);
+                List cookies = Request.getCookies(request);
                 assertNotNull(cookies);
                 assertEquals(1, cookies.size());
-                org.eclipse.jetty.http.HttpCookie cookie = cookies.get(0);
+                HttpCookie cookie = cookies.get(0);
                 assertEquals(name, cookie.getName());
                 assertEquals(value, cookie.getValue());
             }
@@ -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());
@@ -180,18 +179,18 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
                 int r = (int)request.getHeaders().getLongField(headerName);
                 if ("/foo".equals(target) && r == 0)
                 {
-                    org.eclipse.jetty.http.HttpCookie cookie = org.eclipse.jetty.http.HttpCookie.from(cookieName, cookieValue);
+                    HttpCookie cookie = HttpCookie.from(cookieName, cookieValue);
                     org.eclipse.jetty.server.Response.addCookie(response, cookie);
                 }
                 else
                 {
-                    List cookies = Request.getCookies(request);
+                    List cookies = Request.getCookies(request);
                     switch (target)
                     {
                         case "/", "/foo", "/foo/bar" ->
                         {
                             assertEquals(1, cookies.size(), target);
-                            org.eclipse.jetty.http.HttpCookie cookie = cookies.get(0);
+                            HttpCookie cookie = cookies.get(0);
                             assertEquals(cookieName, cookie.getName(), target);
                             assertEquals(cookieValue, cookie.getValue(), target);
                         }
@@ -235,19 +234,19 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
                 int r = (int)request.getHeaders().getLongField(headerName);
                 if ("/foo/bar".equals(target) && r == 0)
                 {
-                    org.eclipse.jetty.http.HttpCookie cookie = org.eclipse.jetty.http.HttpCookie.from(cookieName, cookieValue);
+                    HttpCookie cookie = HttpCookie.from(cookieName, cookieValue);
                     org.eclipse.jetty.server.Response.addCookie(response, cookie);
                 }
                 else
                 {
-                    List cookies = Request.getCookies(request);
+                    List cookies = Request.getCookies(request);
                     switch (target)
                     {
                         case "/", "/foo", "/foobar" -> assertEquals(0, cookies.size(), target);
                         case "/foo/", "/foo/bar", "/foo/bar/baz" ->
                         {
                             assertEquals(1, cookies.size(), target);
-                            org.eclipse.jetty.http.HttpCookie cookie = cookies.get(0);
+                            HttpCookie cookie = cookies.get(0);
                             assertEquals(cookieName, cookie.getName(), target);
                             assertEquals(cookieValue, cookie.getValue(), target);
                         }
@@ -291,19 +290,19 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
                 int r = (int)request.getHeaders().getLongField(headerName);
                 if ("/foo".equals(target) && r == 0)
                 {
-                    org.eclipse.jetty.http.HttpCookie cookie = org.eclipse.jetty.http.HttpCookie.from(cookieName, cookieValue, Map.of(org.eclipse.jetty.http.HttpCookie.PATH_ATTRIBUTE, "/foo/bar"));
+                    HttpCookie cookie = HttpCookie.from(cookieName, cookieValue, Map.of(HttpCookie.PATH_ATTRIBUTE, "/foo/bar"));
                     org.eclipse.jetty.server.Response.addCookie(response, cookie);
                 }
                 else
                 {
-                    List cookies = Request.getCookies(request);
+                    List cookies = Request.getCookies(request);
                     switch (target)
                     {
                         case "/", "/foo", "/foo/barbaz" -> assertEquals(0, cookies.size(), target);
                         case "/foo/bar", "/foo/bar/" ->
                         {
                             assertEquals(1, cookies.size(), target);
-                            org.eclipse.jetty.http.HttpCookie cookie = cookies.get(0);
+                            HttpCookie cookie = cookies.get(0);
                             assertEquals(cookieName, cookie.getName(), target);
                             assertEquals(cookieValue, cookie.getValue(), target);
                         }
@@ -347,19 +346,19 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
                 int r = (int)request.getHeaders().getLongField(headerName);
                 if ("/foo/bar".equals(target) && r == 0)
                 {
-                    org.eclipse.jetty.http.HttpCookie cookie = org.eclipse.jetty.http.HttpCookie.from(cookieName, cookieValue, Map.of(org.eclipse.jetty.http.HttpCookie.PATH_ATTRIBUTE, "/foo"));
+                    HttpCookie cookie = HttpCookie.from(cookieName, cookieValue, Map.of(HttpCookie.PATH_ATTRIBUTE, "/foo"));
                     org.eclipse.jetty.server.Response.addCookie(response, cookie);
                 }
                 else
                 {
-                    List cookies = Request.getCookies(request);
+                    List cookies = Request.getCookies(request);
                     switch (target)
                     {
                         case "/", "/foobar" -> assertEquals(0, cookies.size(), target);
                         case "/foo", "/foo/", "/foo/bar" ->
                         {
                             assertEquals(1, cookies.size(), target);
-                            org.eclipse.jetty.http.HttpCookie cookie = cookies.get(0);
+                            HttpCookie cookie = cookies.get(0);
                             assertEquals(cookieName, cookie.getName(), target);
                             assertEquals(cookieValue, cookie.getValue(), target);
                         }
@@ -404,21 +403,21 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
                 int r = (int)request.getHeaders().getLongField(headerName);
                 if ("/foo".equals(target) && r == 0)
                 {
-                    org.eclipse.jetty.http.HttpCookie cookie = org.eclipse.jetty.http.HttpCookie.from(cookieName, cookieValue1, Map.of(org.eclipse.jetty.http.HttpCookie.PATH_ATTRIBUTE, "/foo"));
+                    HttpCookie cookie = HttpCookie.from(cookieName, cookieValue1, Map.of(HttpCookie.PATH_ATTRIBUTE, "/foo"));
                     org.eclipse.jetty.server.Response.addCookie(response, cookie);
-                    cookie = org.eclipse.jetty.http.HttpCookie.from(cookieName, cookieValue2, Map.of(org.eclipse.jetty.http.HttpCookie.PATH_ATTRIBUTE, "/foo"));
+                    cookie = HttpCookie.from(cookieName, cookieValue2, Map.of(HttpCookie.PATH_ATTRIBUTE, "/foo"));
                     org.eclipse.jetty.server.Response.addCookie(response, cookie);
                 }
                 else
                 {
-                    List cookies = Request.getCookies(request);
+                    List cookies = Request.getCookies(request);
                     switch (target)
                     {
                         case "/" -> assertEquals(0, cookies.size(), target);
                         case "/foo", "/foo/bar" ->
                         {
                             assertEquals(1, cookies.size(), target);
-                            org.eclipse.jetty.http.HttpCookie cookie = cookies.get(0);
+                            HttpCookie cookie = cookies.get(0);
                             assertEquals(cookieName, cookie.getName(), target);
                             assertEquals(cookieValue2, cookie.getValue(), target);
                         }
@@ -463,28 +462,28 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
                 int r = (int)request.getHeaders().getLongField(headerName);
                 if ("/foo".equals(target) && r == 0)
                 {
-                    org.eclipse.jetty.http.HttpCookie cookie = org.eclipse.jetty.http.HttpCookie.from(cookieName, cookieValue1, Map.of(org.eclipse.jetty.http.HttpCookie.PATH_ATTRIBUTE, "/foo"));
+                    HttpCookie cookie = HttpCookie.from(cookieName, cookieValue1, Map.of(HttpCookie.PATH_ATTRIBUTE, "/foo"));
                     org.eclipse.jetty.server.Response.addCookie(response, cookie);
-                    cookie = org.eclipse.jetty.http.HttpCookie.from(cookieName, cookieValue2, Map.of(org.eclipse.jetty.http.HttpCookie.PATH_ATTRIBUTE, "/bar"));
+                    cookie = HttpCookie.from(cookieName, cookieValue2, Map.of(HttpCookie.PATH_ATTRIBUTE, "/bar"));
                     org.eclipse.jetty.server.Response.addCookie(response, cookie);
                 }
                 else
                 {
-                    List cookies = Request.getCookies(request);
+                    List cookies = Request.getCookies(request);
                     switch (target)
                     {
                         case "/" -> assertEquals(0, cookies.size(), target);
                         case "/foo", "/foo/bar" ->
                         {
                             assertEquals(1, cookies.size(), target);
-                            org.eclipse.jetty.http.HttpCookie cookie1 = cookies.get(0);
+                            HttpCookie cookie1 = cookies.get(0);
                             assertEquals(cookieName, cookie1.getName(), target);
                             assertEquals(cookieValue1, cookie1.getValue(), target);
                         }
                         case "/bar", "/bar/foo" ->
                         {
                             assertEquals(1, cookies.size(), target);
-                            org.eclipse.jetty.http.HttpCookie cookie2 = cookies.get(0);
+                            HttpCookie cookie2 = cookies.get(0);
                             assertEquals(cookieName, cookie2.getName(), target);
                             assertEquals(cookieValue2, cookie2.getValue(), target);
                         }
@@ -529,29 +528,29 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
                 int r = (int)request.getHeaders().getLongField(headerName);
                 if ("/foo".equals(target) && r == 0)
                 {
-                    org.eclipse.jetty.http.HttpCookie cookie = org.eclipse.jetty.http.HttpCookie.from(cookieName, cookieValue1, Map.of(org.eclipse.jetty.http.HttpCookie.PATH_ATTRIBUTE, "/foo"));
+                    HttpCookie cookie = HttpCookie.from(cookieName, cookieValue1, Map.of(HttpCookie.PATH_ATTRIBUTE, "/foo"));
                     org.eclipse.jetty.server.Response.addCookie(response, cookie);
-                    cookie = org.eclipse.jetty.http.HttpCookie.from(cookieName, cookieValue2, Map.of(org.eclipse.jetty.http.HttpCookie.PATH_ATTRIBUTE, "/foo/bar"));
+                    cookie = HttpCookie.from(cookieName, cookieValue2, Map.of(HttpCookie.PATH_ATTRIBUTE, "/foo/bar"));
                     org.eclipse.jetty.server.Response.addCookie(response, cookie);
                 }
                 else
                 {
-                    List cookies = Request.getCookies(request);
+                    List cookies = Request.getCookies(request);
                     switch (target)
                     {
                         case "/" -> assertEquals(0, cookies.size(), target);
                         case "/foo" ->
                         {
                             assertEquals(1, cookies.size(), target);
-                            org.eclipse.jetty.http.HttpCookie cookie = cookies.get(0);
+                            HttpCookie cookie = cookies.get(0);
                             assertEquals(cookieName, cookie.getName(), target);
                             assertEquals(cookieValue1, cookie.getValue(), target);
                         }
                         case "/foo/bar" ->
                         {
                             assertEquals(2, cookies.size(), target);
-                            org.eclipse.jetty.http.HttpCookie cookie1 = cookies.get(0);
-                            org.eclipse.jetty.http.HttpCookie cookie2 = cookies.get(1);
+                            HttpCookie cookie1 = cookies.get(0);
+                            HttpCookie cookie2 = cookies.get(1);
                             assertEquals(cookieName, cookie1.getName(), target);
                             assertEquals(cookieName, cookie2.getName(), target);
                             Set values = new HashSet<>();
@@ -599,19 +598,19 @@ protected void service(Request request, org.eclipse.jetty.server.Response respon
                 int r = (int)request.getHeaders().getLongField(headerName);
                 if ("/foo/bar".equals(target) && r == 0)
                 {
-                    org.eclipse.jetty.http.HttpCookie cookie = org.eclipse.jetty.http.HttpCookie.from(cookieName, cookieValue, Map.of(org.eclipse.jetty.http.HttpCookie.PATH_ATTRIBUTE, "/foo/"));
+                    HttpCookie cookie = HttpCookie.from(cookieName, cookieValue, Map.of(HttpCookie.PATH_ATTRIBUTE, "/foo/"));
                     org.eclipse.jetty.server.Response.addCookie(response, cookie);
                 }
                 else
                 {
-                    List cookies = Request.getCookies(request);
+                    List cookies = Request.getCookies(request);
                     switch (target)
                     {
                         case "/", "/foo", "/foobar" -> assertEquals(0, cookies.size(), target);
                         case "/foo/", "/foo/bar" ->
                         {
                             assertEquals(1, cookies.size(), target);
-                            org.eclipse.jetty.http.HttpCookie cookie = cookies.get(0);
+                            HttpCookie cookie = cookies.get(0);
                             assertEquals(cookieName, cookie.getName(), target);
                             assertEquals(cookieValue, cookie.getValue(), target);
                         }
diff --git a/jetty-core/jetty-fcgi/jetty-fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/FastCGIProxyHandler.java b/jetty-core/jetty-fcgi/jetty-fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/FastCGIProxyHandler.java
index 852ec7436446..a7a88399cada 100644
--- a/jetty-core/jetty-fcgi/jetty-fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/FastCGIProxyHandler.java
+++ b/jetty-core/jetty-fcgi/jetty-fcgi-proxy/src/main/java/org/eclipse/jetty/fcgi/proxy/FastCGIProxyHandler.java
@@ -316,7 +316,7 @@ protected void sendProxyToServerRequest(Request clientToProxyRequest, org.eclips
         // If the Host header is missing, add it.
         if (!proxyToServerRequest.getHeaders().contains(HttpHeader.HOST))
         {
-            if (!getHttpClient().isDefaultPort(scheme, serverPort))
+            if (serverPort != HttpScheme.getDefaultPort(scheme))
                 serverName += ":" + serverPort;
             String host = serverName;
             proxyToServerRequest.headers(headers -> headers
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..787fe71fe849 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,167 +13,88 @@
 
 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 SAME_SITE_DEFAULT_ATTRIBUTE = "org.eclipse.jetty.cookie.sameSiteDefault"; - - enum SameSite - { - NONE("None"), - STRICT("Strict"), - LAX("Lax"); - - private final String attributeValue; - - SameSite(String attributeValue) - { - this.attributeValue = attributeValue; - } - - public String getAttributeValue() - { - return this.attributeValue; - } - - private static final Index CACHE = new Index.Builder() - .caseSensitive(false) - .with(NONE.attributeValue, NONE) - .with(STRICT.attributeValue, STRICT) - .with(LAX.attributeValue, LAX) - .build(); - - public static SameSite from(String sameSite) - { - if (sameSite == null) - return null; - return CACHE.get(sameSite); - } - } + String getName(); /** - * Create new HttpCookie from specific values. - * - * @param name the name of the cookie - * @param value the value of the cookie + * @return the cookie value */ - static HttpCookie from(String name, String value) - { - return new Immutable(name, value, 0, null); - } + String getValue(); /** - * Create 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()}. + * @return the value of the {@code Version} attribute */ - static HttpCookie from(String name, String value, Map attributes) - { - return new Immutable(name, value, 0, attributes); - } + int getVersion(); /** - * Create 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()}. + * @return the attributes associated with this cookie */ - static HttpCookie from(String name, String value, int version, Map attributes) - { - if (attributes == null || attributes.isEmpty()) - return new Immutable(name, value, version, Collections.emptyMap()); - - Map attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - attrs.putAll(attributes); - - return new Immutable(name, value, version, attrs); - } + Map getAttributes(); /** - * @param cookie A cookie to base the new cookie on. - * @param additionalAttributes Additional name value pairs of strings to use as additional attributes - * @return A new cookie based on the passed cookie plus additional attributes. + * @return the value of the {@code Expires} attribute, or {@code null} if not present + * @see #EXPIRES_ATTRIBUTE */ - static HttpCookie from(HttpCookie cookie, String... additionalAttributes) + default Instant getExpires() { - if (additionalAttributes.length % 2 != 0) - throw new IllegalArgumentException("additional attributes must have name and value"); - Map attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - attributes.putAll(Objects.requireNonNull(cookie).getAttributes()); - for (int i = 0; i < additionalAttributes.length; i += 2) - attributes.put(additionalAttributes[i], additionalAttributes[i + 1]); - return new Immutable(cookie.getName(), cookie.getValue(), cookie.getVersion(), attributes); + String expires = getAttributes().get(EXPIRES_ATTRIBUTE); + return expires == null ? null : parseExpires(expires); } /** - * @return the cookie name - */ - String getName(); - - /** - * @return the cookie value + * @return the value of the {@code Max-Age} attribute, in seconds, or {@code -1} if not present + * @see #MAX_AGE_ATTRIBUTE */ - String getValue(); + default long getMaxAge() + { + String ma = getAttributes().get(MAX_AGE_ATTRIBUTE); + return ma == null ? -1 : Long.parseLong(ma); + } /** - * @return the cookie version + * @return whether the cookie is expired */ - int getVersion(); + default boolean isExpired() + { + if (getMaxAge() == 0) + return true; + Instant expires = getExpires(); + return expires != null && Instant.now().isAfter(expires); + } /** - * @return the cookie comment. - * Equivalent to a `get` of {@link #COMMENT_ATTRIBUTE} on {@link #getAttributes()}. + *

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

+ * + * @return the value of the {@code Comment} attribute + * @see #COMMENT_ATTRIBUTE */ default String getComment() { @@ -181,8 +102,10 @@ default String getComment() } /** - * @return the cookie domain. - * Equivalent to a `get` of {@link #DOMAIN_ATTRIBUTE} on {@link #getAttributes()}. + *

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

+ * + * @return the value of the {@code Domain} attribute + * @see #DOMAIN_ATTRIBUTE */ default String getDomain() { @@ -190,18 +113,10 @@ default String getDomain() } /** - * @return the cookie max age in seconds - * Equivalent to a `get` of {@link #MAX_AGE_ATTRIBUTE} on {@link #getAttributes()}. - */ - default long getMaxAge() - { - String ma = getAttributes().get(MAX_AGE_ATTRIBUTE); - return ma == null ? -1 : Long.parseLong(ma); - } - - /** - * @return the cookie path - * Equivalent to a `get` of {@link #PATH_ATTRIBUTE} on {@link #getAttributes()}. + *

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

+ * + * @return the value of the {@code Path} attribute + * @see #PATH_ATTRIBUTE */ default String getPath() { @@ -209,8 +124,8 @@ default String getPath() } /** - * @return whether the cookie is valid for secure domains - * Equivalent to a `get` of {@link #SECURE_ATTRIBUTE} on {@link #getAttributes()}. + * @return whether the {@code Secure} attribute is present + * @see #SECURE_ATTRIBUTE */ default boolean isSecure() { @@ -218,8 +133,8 @@ default boolean isSecure() } /** - * @return the cookie {@code SameSite} attribute value - * Equivalent to a `get` of {@link #SAME_SITE_ATTRIBUTE} on {@link #getAttributes()}. + * @return the value of the {@code SameSite} attribute + * @see #SAME_SITE_ATTRIBUTE */ default SameSite getSameSite() { @@ -227,8 +142,8 @@ default SameSite getSameSite() } /** - * @return whether the cookie is valid for the http protocol only - * Equivalent to a `get` of {@link #HTTP_ONLY_ATTRIBUTE} on {@link #getAttributes()}. + * @return whether the {@code HttpOnly} attribute is present + * @see #HTTP_ONLY_ATTRIBUTE */ default boolean isHttpOnly() { @@ -236,41 +151,136 @@ default boolean isHttpOnly() } /** - * @return the attributes associated with this cookie + * @return the cookie hash code + * @see #hashCode(HttpCookie) */ - Map getAttributes(); + @Override + int hashCode(); /** - * @return a string representation of this cookie + * @param obj the object to test for equality + * @return whether this cookie is equal to the given object + * @see #equals(HttpCookie, Object) */ - default String asString() - { - return HttpCookie.asString(this); - } + @Override + boolean equals(Object obj); /** - * @return a string representation of this cookie + *

A wrapper for {@code HttpCookie} instances.

*/ - static String asString(HttpCookie httpCookie) + class Wrapper implements HttpCookie { - StringBuilder builder = new StringBuilder(); - builder.append(httpCookie.getName()).append("=").append(httpCookie.getValue()); - String domain = httpCookie.getDomain(); - if (domain != null) - builder.append(";$Domain=").append(domain); - String path = httpCookie.getPath(); - if (path != null) - builder.append(";$Path=").append(path); - return builder.toString(); - } + private final HttpCookie wrapped; - static String toString(HttpCookie httpCookie) - { - return "%x@%s".formatted(httpCookie.hashCode(), asString(httpCookie)); + 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 HttpCookie. + *

Immutable implementation of {@link HttpCookie}.

*/ class Immutable implements HttpCookie { @@ -279,463 +289,609 @@ class Immutable implements HttpCookie private final int _version; private final 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) - { - _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); - } - - Immutable(String name, String value, int version, Map attributes) + private Immutable(String name, String value, int version, Map attributes) { _name = name; _value = value; _version = version; - _attributes = attributes == null ? Collections.emptyMap() : attributes; + _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 cookie {@code SameSite} attribute value - */ @Override - public SameSite getSameSite() + public Map getAttributes() { - String val = _attributes.get(SAME_SITE_ATTRIBUTE); - if (val == null) - return null; - return SameSite.valueOf(val.toUpperCase(Locale.ENGLISH)); + return _attributes; } - /** - * @return the attributes associated with this cookie - */ @Override - public Map getAttributes() + public int hashCode() { - return _attributes; + return HttpCookie.hashCode(this); } @Override - public String toString() + public boolean equals(Object obj) { - return "%x@%s".formatted(hashCode(), asString()); + return HttpCookie.equals(this, obj); } - } - private static void quoteOnlyOrAppend(StringBuilder buf, String s, boolean quote) - { - if (quote) - QuotedStringTokenizer.quoteOnly(buf, s); - else - buf.append(s); + @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 + *

The possible values for the {@code SameSite} attribute, defined + * in the follow-up of RFC 6265, at the time of this writing defined at + * RFC 6265bis.

*/ - private static boolean isQuoteNeededForCookie(String s) + enum SameSite { - if (s == null || s.length() == 0) - return true; + /** + * The value {@code None} for the {@code SameSite} attribute + */ + NONE("None"), + /** + * The value {@code Strict} for the {@code SameSite} attribute + */ + STRICT("Strict"), + /** + * The value {@code Lax} for the {@code SameSite} attribute + */ + LAX("Lax"); - if (QuotedStringTokenizer.isQuoted(s)) - return false; + private final String attributeValue; - for (int i = 0; i < s.length(); i++) + SameSite(String attributeValue) { - char c = s.charAt(i); - if (__COOKIE_DELIM.indexOf(c) >= 0) - return true; + this.attributeValue = attributeValue; + } - if (c < 0x20 || c >= 0x7f) - throw new IllegalArgumentException("Illegal character in cookie value"); + /** + * @return the {@code SameSite} attribute value + */ + public String getAttributeValue() + { + return this.attributeValue; } - return false; - } + private static final Index CACHE = new Index.Builder() + .caseSensitive(false) + .with(NONE.attributeValue, NONE) + .with(STRICT.attributeValue, STRICT) + .with(LAX.attributeValue, LAX) + .build(); - static String getSetCookie(HttpCookie httpCookie, CookieCompliance compliance) - { - if (compliance == CookieCompliance.RFC6265) - return getRFC6265SetCookie(httpCookie); - if (compliance == CookieCompliance.RFC2965) - return getRFC2965SetCookie(httpCookie); - throw new IllegalStateException(); + /** + * @param sameSite the {@code SameSite} attribute value + * @return the enum constant associated with the {@code SameSite} attribute value, + * or {@code null} if the value is not a known {@code SameSite} attribute value + */ + public static SameSite from(String sameSite) + { + if (sameSite == null) + return null; + return CACHE.get(sameSite); + } } - static String getRFC2965SetCookie(HttpCookie httpCookie) + /** + *

A {@link HttpCookie} that wraps a {@link java.net.HttpCookie}.

+ */ + class JavaNetHttpCookie implements HttpCookie { - // Check arguments - String name = httpCookie.getName(); - if (name == null || name.length() == 0) - throw new IllegalArgumentException("Bad cookie name"); + private final java.net.HttpCookie _httpCookie; + private Map _attributes; - // Format value and params - StringBuilder buf = new StringBuilder(); + private JavaNetHttpCookie(java.net.HttpCookie httpCookie) + { + _httpCookie = httpCookie; + } - // 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); + @Override + public String getComment() + { + return _httpCookie.getComment(); + } - buf.append('='); + @Override + public String getDomain() + { + return _httpCookie.getDomain(); + } - // Append the value - String value = httpCookie.getValue(); - boolean quoteValue = isQuoteNeededForCookie(value); - quoteOnlyOrAppend(buf, value, quoteValue); + @Override + public long getMaxAge() + { + return _httpCookie.getMaxAge(); + } - // 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); + @Override + public String getPath() + { + return _httpCookie.getPath(); + } - String path = httpCookie.getPath(); - boolean hasPath = path != null && path.length() > 0; - boolean quotePath = hasPath && isQuoteNeededForCookie(path); + @Override + public boolean isSecure() + { + return _httpCookie.getSecure(); + } - // 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; + @Override + public String getName() + { + return _httpCookie.getName(); + } - // Append version - if (version == 1) - buf.append(";Version=1"); - else if (version > 1) - buf.append(";Version=").append(version); + @Override + public String getValue() + { + return _httpCookie.getValue(); + } - // Append path - if (hasPath) + @Override + public int getVersion() { - buf.append(";Path="); - quoteOnlyOrAppend(buf, path, quotePath); + return _httpCookie.getVersion(); } - // Append domain - if (hasDomain) + @Override + public boolean isHttpOnly() { - buf.append(";Domain="); - quoteOnlyOrAppend(buf, domain, quoteDomain); + return _httpCookie.isHttpOnly(); } - // Handle max-age and/or expires - long maxAge = httpCookie.getMaxAge(); - if (maxAge >= 0) + @Override + public Map getAttributes() { - // 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); + 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; + } - buf.append(";Max-Age="); - buf.append(maxAge); + @Override + public int hashCode() + { + return HttpCookie.hashCode(this); } - // add the other fields - if (httpCookie.isSecure()) - buf.append(";Secure"); - if (httpCookie.isHttpOnly()) - buf.append(";HttpOnly"); - if (comment != null) + @Override + public boolean equals(Object obj) { - buf.append(";Comment="); - quoteOnlyOrAppend(buf, comment, isQuoteNeededForCookie(comment)); + return HttpCookie.equals(this, obj); + } + + @Override + public String toString() + { + return HttpCookie.toString(this); } - return buf.toString(); } - static String getRFC6265SetCookie(HttpCookie httpCookie) + /** + *

A builder for {@link HttpCookie} instances.

+ *

The typical usage is to use one of the + * {@link HttpCookie#build(String, String) build methods} + * to obtain the builder, and then chain method calls to + * customize the cookie attributes and finally calling + * the {@link #build()} method, for example:

+ *
{@code
+     * HttpCookie cookie = HttpCookie.build("name", "value")
+     *     .maxAge(24 * 60 * 60)
+     *     .domain("example.com")
+     *     .path("/")
+     *     .build();
+     * }
+ * + * @see HttpCookie#build(String, String) + * @see #build() + */ + class Builder { - // Check arguments - String name = httpCookie.getName(); - if (name == null || name.length() == 0) - throw new IllegalArgumentException("Bad cookie name"); + private final String _name; + private final String _value; + private final int _version; + private Map _attributes; - // 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); + private Builder(String name, String value, int version) + { + _name = name; + _value = value; + _version = version; + } + + public Builder attribute(String name, String value) + { + _attributes = lazyAttributePut(_attributes, name, value); + return this; + } - // Format value and params - StringBuilder buf = new StringBuilder(); - buf.append(name).append('=').append(value == null ? "" : value); + public Builder comment(String comment) + { + _attributes = lazyAttributePut(_attributes, COMMENT_ATTRIBUTE, comment); + return this; + } - // Append path - String path = httpCookie.getPath(); - if (path != null && path.length() > 0) - buf.append("; Path=").append(path); + public Builder domain(String domain) + { + _attributes = lazyAttributePut(_attributes, DOMAIN_ATTRIBUTE, domain); + return this; + } - // 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"); + public Builder maxAge(long maxAge) + { + if (maxAge >= 0) + _attributes = lazyAttributePut(_attributes, MAX_AGE_ATTRIBUTE, Long.toString(maxAge)); + else + _attributes = lazyAttributeRemove(_attributes, MAX_AGE_ATTRIBUTE); + return this; + } - Map attributes = httpCookie.getAttributes(); + public Builder expires(Instant expires) + { + if (expires != null) + _attributes = lazyAttributePut(_attributes, EXPIRES_ATTRIBUTE, formatExpires(expires)); + else + _attributes = lazyAttributeRemove(_attributes, EXPIRES_ATTRIBUTE); + return this; + } - String sameSiteAttr = attributes.get(SAME_SITE_ATTRIBUTE); - if (sameSiteAttr != null) + public Builder path(String path) { - buf.append("; SameSite="); - buf.append(sameSiteAttr); + _attributes = lazyAttributePut(_attributes, PATH_ATTRIBUTE, path); + return this; } - else + + public Builder secure(boolean secure) { - SameSite sameSite = httpCookie.getSameSite(); - if (sameSite != null) - { - buf.append("; SameSite="); - buf.append(sameSite.getAttributeValue()); - } + if (secure) + _attributes = lazyAttributePut(_attributes, SECURE_ATTRIBUTE, Boolean.TRUE.toString()); + else + _attributes = lazyAttributeRemove(_attributes, SECURE_ATTRIBUTE); + return this; } - //Add all other attributes - for (Map.Entry e : attributes.entrySet()) + /** + * @return an immutable {@link HttpCookie} instance. + */ + public HttpCookie build() { - if (KNOWN_ATTRIBUTES.contains(e.getKey())) - continue; - buf.append("; ").append(e.getKey()).append("="); - buf.append(e.getValue()); + return new Immutable(_name, _value, _version, lazyAttributes(_attributes)); } + } - return buf.toString(); + /** + * Creates a new {@code HttpCookie} from the given name and value. + * + * @param name the name of the cookie + * @param value the value of the cookie + */ + static HttpCookie from(String name, String value) + { + return from(name, value, 0, null); } /** - * 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 + * Creates a new {@code HttpCookie} from the given name, value 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()}, plus any newly defined attributes unknown to this + * code base. */ - static SameSite getSameSiteDefault(Attributes contextAttributes) + static HttpCookie from(String name, String value, Map attributes) { - if (contextAttributes == null) - return null; - Object o = contextAttributes.getAttribute(SAME_SITE_DEFAULT_ATTRIBUTE); - if (o == null) - { - if (LOG.isDebugEnabled()) - LOG.debug("No default value for SameSite"); - return null; - } + return from(name, value, 0, attributes); + } - if (o instanceof SameSite) - return (SameSite)o; + /** + * Creates a new {@code HttpCookie} from the given name, value, version 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()}, plus any newly defined attributes unknown to this + * code base. + */ + static HttpCookie from(String name, String value, int version, Map attributes) + { + if (attributes == null || attributes.isEmpty()) + return new Immutable(name, value, version, Collections.emptyMap()); - try - { - SameSite samesite = Enum.valueOf(SameSite.class, o.toString().trim().toUpperCase(Locale.ENGLISH)); - contextAttributes.setAttribute(SAME_SITE_DEFAULT_ATTRIBUTE, samesite); - return samesite; - } - catch (Exception e) + Map attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + attrs.putAll(attributes); + + return new Immutable(name, value, version, attrs); + } + + /** + * @param cookie A cookie to base the new cookie on. + * @param additionalAttributes Additional name value pairs of strings to use as additional attributes + * @return A new cookie based on the passed cookie plus additional attributes. + */ + static HttpCookie from(HttpCookie cookie, String... additionalAttributes) + { + if (additionalAttributes.length % 2 != 0) + throw new IllegalArgumentException("additional attributes must have name and value"); + Map attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + attributes.putAll(Objects.requireNonNull(cookie).getAttributes()); + for (int i = 0; i < additionalAttributes.length; i += 2) { - LOG.warn("Bad default value {} for SameSite", o); - throw new IllegalStateException(e); + attributes.put(additionalAttributes[i], additionalAttributes[i + 1]); } + return from(cookie.getName(), cookie.getValue(), cookie.getVersion(), 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. - *

+ * Creates a new {@code HttpCookie} copied from the given {@link java.net.HttpCookie}. * - * @param setCookieHeader the header as a string - * @return a map containing the name, value, domain, path. max-age of the set cookie header + * @param httpCookie the {@link java.net.HttpCookie} instance to copy + * @return a new {@code HttpCookie} copied from the {@link java.net.HttpCookie} + * @see #asJavaNetHttpCookie(HttpCookie) */ - static Map extractBasics(String setCookieHeader) + static HttpCookie from(java.net.HttpCookie httpCookie) { - //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; + return new JavaNetHttpCookie(httpCookie); } /** - * Check if the Set-Cookie header represented as a string is for the name, domain and path given. + * Creates a {@link Builder} to build a {@code HttpCookie}. * - * @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 + * @param name the cookie name + * @param value the cookie value + * @return a new {@link Builder} initialized with the given values */ - static boolean match(String setCookieHeader, String name, String domain, String path) + static Builder build(String name, 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); + return build(name, value, 0); } /** - * Check if the HttpCookie is for the given name, domain and path. + * Creates a {@link Builder} to build a {@code HttpCookie}. * - * @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 + * @param name the cookie name + * @param value the cookie value + * @param version the cookie version + * @return a new {@link Builder} initialized with the given values */ - static boolean match(HttpCookie cookie, String name, String domain, String path) + static Builder build(String name, String value, int version) { - if (cookie == null) - return false; - return match(cookie.getName(), cookie.getDomain(), cookie.getPath(), name, domain, path); + return new Builder(name, value, version); } /** - * Check if all old parameters match the new parameters. + * Creates a {@link Builder} to build a {@code HttpCookie}. * - * @return true if old and new names match exactly and the old and new domains match case-insensitively and the paths match exactly + * @param httpCookie the cookie to copy + * @return a new {@link Builder} initialized with the given cookie */ - private static boolean match(String oldName, String oldDomain, String oldPath, String newName, String newDomain, String newPath) + static Builder build(HttpCookie httpCookie) { - if (oldName == null) + Builder builder = new Builder(httpCookie.getName(), httpCookie.getValue(), httpCookie.getVersion()); + for (Map.Entry entry : httpCookie.getAttributes().entrySet()) { - if (newName != null) - return false; + builder = builder.attribute(entry.getKey(), entry.getValue()); } - else if (!oldName.equals(newName)) - return false; + return builder; + } - if (oldDomain == null) - { - if (newDomain != null) - return false; - } - else if (!oldDomain.equalsIgnoreCase(newDomain)) - return false; + /** + * Creates a {@link Builder} to build a {@code HttpCookie}. + * + * @param httpCookie the {@link java.net.HttpCookie} to copy + * @return a new {@link Builder} initialized with the given cookie + */ + static Builder build(java.net.HttpCookie httpCookie) + { + 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()); + } + + /** + * Converts a {@code HttpCookie} to a {@link java.net.HttpCookie}. + * + * @param httpCookie the cookie to convert + * @return a new {@link java.net.HttpCookie} + * @see #from(java.net.HttpCookie) + */ + static java.net.HttpCookie asJavaNetHttpCookie(HttpCookie httpCookie) + { + 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; + } - if (oldPath == null) - return newPath == null; + /** + *

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) + */ + static int hashCode(HttpCookie httpCookie) + { + String domain = httpCookie.getDomain(); + if (domain != null) + domain = domain.toLowerCase(Locale.ENGLISH); + return Objects.hash(httpCookie.getName(), domain, httpCookie.getPath()); + } - return oldPath.equals(newPath); + /** + *

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) + */ + 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()); } - class SetCookieHttpField extends HttpField + private static boolean equalsIgnoreCase(String obj1, String obj2) { - final HttpCookie _cookie; + if (obj1 == obj2) + return true; + if (obj1 == null || obj2 == null) + return false; + return obj1.equalsIgnoreCase(obj2); + } - public SetCookieHttpField(HttpCookie cookie, CookieCompliance compliance) - { - super(HttpHeader.SET_COOKIE, getSetCookie(cookie, compliance)); - this._cookie = 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 + */ + private static String asString(HttpCookie httpCookie) + { + StringBuilder builder = new StringBuilder(); + builder.append(httpCookie.getName()).append("=").append(httpCookie.getValue()); + String domain = httpCookie.getDomain(); + if (domain != null) + builder.append(";Domain=").append(domain); + String path = httpCookie.getPath(); + if (path != null) + builder.append(";Path=").append(path); + return builder.toString(); + } - public HttpCookie getHttpCookie() - { - return _cookie; - } + /** + *

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 "%s@%x[%s]".formatted(httpCookie.getClass().getSimpleName(), httpCookie.hashCode(), asString(httpCookie)); } /** - * Check that samesite is set on the cookie. If not, use a - * context default value, if one has been set. + *

Formats the {@link Instant} associated with the + * {@code Expires} attribute into a RFC 1123 string.

* - * @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 + * @param expires the expiration instant + * @return the instant formatted as an RFC 1123 string + * @see #parseExpires(String) */ - static HttpCookie checkSameSite(HttpCookie cookie, Attributes attributes) + static String formatExpires(Instant expires) { - if (cookie == null || cookie.getSameSite() != null) - return cookie; + return DateTimeFormatter.RFC_1123_DATE_TIME + .withZone(ZoneOffset.UTC) + .format(expires); + } + + /** + *

Parses the {@code Expires} attribute value + * (in RFC 1123 format) into an {@link Instant}.

+ * + * @param expires an instant in the RFC 1123 string format + * @return an {@link Instant} parsed from the given string + */ + static Instant parseExpires(String expires) + { + // 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(); + } - //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 + private static Map lazyAttributePut(Map attributes, String key, String value) + { + if (value == null) + return attributes; + if (attributes == null) + attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + attributes.put(key, value); + return attributes; + } - return HttpCookie.from(cookie, HttpCookie.SAME_SITE_ATTRIBUTE, contextDefault.getAttributeValue()); + private static Map lazyAttributeRemove(Map attributes, String key) + { + if (attributes == null) + return null; + attributes.remove(key); + return attributes; + } + + private static Map lazyAttributes(Map attributes) + { + return attributes == null || attributes.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(attributes); } } 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..63852648c2af --- /dev/null +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookieStore.java @@ -0,0 +1,388 @@ +// +// ======================================================================== +// 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? + + boolean secure = HttpScheme.isSecure(uri.getScheme()); + // Do not accept a secure cookie sent over an insecure channel. + if (cookie.isSecure() && !secure) + return false; + + 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[]{true}; + 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] = false; + 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.isSecure(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/main/java/org/eclipse/jetty/http/HttpScheme.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpScheme.java index 5d22d35f3b0a..477c2b99eb5c 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpScheme.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpScheme.java @@ -86,4 +86,9 @@ public static int normalizePort(String scheme, int port) HttpScheme httpScheme = scheme == null ? null : CACHE.get(scheme); return httpScheme == null ? port : httpScheme.normalizePort(port); } + + public static boolean isSecure(String scheme) + { + return HttpScheme.HTTPS.is(scheme) || HttpScheme.WSS.is(scheme); + } } 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..5330ac10e3cf --- /dev/null +++ b/jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieStoreTest.java @@ -0,0 +1,278 @@ +// +// ======================================================================== +// 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"); + // Dumb server sending a secure cookie on clear-text scheme. + assertFalse(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())); + assertTrue(store.add(secureURI, HttpCookie.from("n3", "v3"))); + + 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 60% 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..48e4e71b8c3f 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;Domain=domain;Path=path;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;Domain=domain;Path=path;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); - assertThat(setCookie, Matchers.startsWith("\"ev erything\"=\"va lue\";Version=1;Path=\"pa th\";Domain=\"do main\";Expires=")); + String setCookie = HttpCookieUtils.getRFC2965SetCookie(httpCookie); + assertThat(setCookie, Matchers.startsWith("\"ev erything\"=\"va lue\";Version=1;Domain=\"do main\";Path=\"pa th\";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-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTransportDynamicTest.java b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTransportDynamicTest.java index 755e4f5e0446..12470f05b08b 100644 --- a/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTransportDynamicTest.java +++ b/jetty-core/jetty-tests/jetty-test-client-transports/src/test/java/org/eclipse/jetty/test/client/transport/HttpClientTransportDynamicTest.java @@ -272,7 +272,8 @@ private void testProtocolSelection(HttpScheme scheme) throws Exception public Origin newOrigin(org.eclipse.jetty.client.Request request) { // Use prior-knowledge, i.e. negotiate==false. - boolean secure = HttpClient.isSchemeSecure(request.getScheme()); + String scheme1 = request.getScheme(); + boolean secure = HttpScheme.isSecure(scheme1); List protocols = HttpVersion.HTTP_2 == request.getVersion() ? http2.getProtocols(secure) : h1.getProtocols(secure); return new Origin(request.getScheme(), request.getHost(), request.getPort(), request.getTag(), new Origin.Protocol(protocols, false)); } 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 e53481e36e7c..064f21f80723 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; @@ -465,10 +465,11 @@ else if (values.length == 1) // We can upgrade customize(endPoint); + String scheme = request.getScheme(); Negotiated negotiated = new Negotiated( request.getURI(), negotiatedSubProtocol, - HttpClient.isSchemeSecure(request.getScheme()), + HttpScheme.isSecure(scheme), extensionStack, WebSocketConstants.SPEC_VERSION_STRING); 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 7a6f15133435..17c9411fe636 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; @@ -323,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 ByteBufferPool getByteBufferPool() - { - return getHttpClient().getByteBufferPool(); - } - @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..940d7c3fed1c 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 @@ -21,11 +21,11 @@ import java.util.Map; import java.util.stream.Collectors; -import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.ee10.websocket.api.ExtensionConfig; import org.eclipse.jetty.ee10.websocket.api.UpgradeRequest; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.UrlEncoded; @@ -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 @@ -154,7 +156,7 @@ public boolean hasSubProtocol(String test) @Override public boolean isSecure() { - return HttpClient.isSchemeSecure(delegate.getURI().getScheme()); + return HttpScheme.isSecure(delegate.getURI().getScheme()); } @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 7b495c740cdd..d0156c77e366 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 65ab1c3c6dbe..8c172e50bb84 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..ae1169d3d876 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)); @@ -1996,7 +1997,7 @@ public void testAddCookieComplianceRFC2965() String set = response.getHttpFields().get("Set-Cookie"); - assertEquals("name=value;Version=1;Path=/path;Domain=domain;Secure;HttpOnly;Comment=comment", set); + assertEquals("name=value;Version=1;Domain=domain;Path=/path;Secure;HttpOnly;Comment=comment", set); } /** @@ -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 87387e557e5e..6049d87f50cf 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; @@ -324,21 +323,6 @@ public void setConnectTimeout(long ms) getHttpClient().setConnectTimeout(ms); } - public CookieStore getCookieStore() - { - return getHttpClient().getCookieStore(); - } - - public void setCookieStore(CookieStore cookieStore) - { - getHttpClient().setCookieStore(cookieStore); - } - - public ByteBufferPool getByteBufferPool() - { - return getHttpClient().getByteBufferPool(); - } - @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..9ffc833b960c 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 @@ -21,11 +21,11 @@ import java.util.Map; import java.util.stream.Collectors; -import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.ee9.websocket.api.ExtensionConfig; import org.eclipse.jetty.ee9.websocket.api.UpgradeRequest; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.UrlEncoded; @@ -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 @@ -154,7 +156,7 @@ public boolean hasSubProtocol(String test) @Override public boolean isSecure() { - return HttpClient.isSchemeSecure(delegate.getURI().getScheme()); + return HttpScheme.isSecure(delegate.getURI().getScheme()); } @Override