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 763a4f2da6d..6c0f5117441 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 @@ -38,6 +38,7 @@ public interface HttpCookie String PATH_ATTRIBUTE = "Path"; String SAME_SITE_ATTRIBUTE = "SameSite"; String SECURE_ATTRIBUTE = "Secure"; + String PARTITIONED_ATTRIBUTE = "Partitioned"; /** * @return the cookie name @@ -150,6 +151,15 @@ public interface HttpCookie return Boolean.parseBoolean(getAttributes().get(HTTP_ONLY_ATTRIBUTE)); } + /** + * @return whether the {@code Partitioned} attribute is present + * @see #PARTITIONED_ATTRIBUTE + */ + default boolean isPartitioned() + { + return Boolean.parseBoolean(getAttributes().get(PARTITIONED_ATTRIBUTE)); + } + /** * @return the cookie hash code * @see #hashCode(HttpCookie) @@ -260,6 +270,12 @@ public interface HttpCookie return getWrapped().isHttpOnly(); } + @Override + public boolean isPartitioned() + { + return getWrapped().isPartitioned(); + } + @Override public int hashCode() { @@ -560,6 +576,12 @@ public interface HttpCookie throw new IllegalArgumentException("Invalid Secure attribute"); secure(true); } + case "partitioned" -> + { + if (!isTruthy(value)) + throw new IllegalArgumentException("Invalid Partitioned attribute"); + partitioned(true); + } default -> _attributes = lazyAttributePut(_attributes, name, value); } return this; @@ -630,6 +652,15 @@ public interface HttpCookie return this; } + public Builder partitioned(boolean partitioned) + { + if (partitioned) + _attributes = lazyAttributePut(_attributes, PARTITIONED_ATTRIBUTE, Boolean.TRUE.toString()); + else + _attributes = lazyAttributeRemove(_attributes, PARTITIONED_ATTRIBUTE); + return this; + } + /** * @return an immutable {@link HttpCookie} instance. */ @@ -657,8 +688,8 @@ public interface HttpCookie * @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. + * {@link #isSecure()}, {@link #isPartitioned()}, {@link #getComment()}, plus any newly defined + * attributes unknown to this code base. */ static HttpCookie from(String name, String value, Map attributes) { @@ -673,8 +704,8 @@ public interface HttpCookie * @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. + * {@link #isSecure()}, {@link #isPartitioned()}, {@link #getComment()}, plus any newly defined + * attributes unknown to this code base. */ static HttpCookie from(String name, String value, int version, Map attributes) { @@ -786,6 +817,8 @@ public interface HttpCookie { if (httpCookie.getSameSite() != null) throw new IllegalArgumentException("SameSite attribute not supported by " + java.net.HttpCookie.class.getName()); + if (httpCookie.isPartitioned()) + throw new IllegalArgumentException("Partitioned 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()); 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 index 9016f874a98..815807dc123 100644 --- 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 @@ -46,6 +46,7 @@ public final class HttpCookieUtils .with(HttpCookie.PATH_ATTRIBUTE) .with(HttpCookie.SAME_SITE_ATTRIBUTE) .with(HttpCookie.SECURE_ATTRIBUTE) + .with(HttpCookie.PARTITIONED_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"; @@ -187,6 +188,9 @@ public final class HttpCookieUtils if (httpCookie.isHttpOnly()) builder.append(";HttpOnly"); + if (httpCookie.isPartitioned()) + builder.append(";Partitioned"); + HttpCookie.SameSite sameSite = httpCookie.getSameSite(); if (sameSite != null) builder.append(";SameSite=").append(sameSite.getAttributeValue()); @@ -249,6 +253,8 @@ public final class HttpCookieUtils builder.append("; Secure"); if (httpCookie.isHttpOnly()) builder.append("; HttpOnly"); + if (httpCookie.isPartitioned()) + builder.append("; Partitioned"); Map attributes = httpCookie.getAttributes(); diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpCookieTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpCookieTest.java index 1eef279a0b1..0af42986ffd 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpCookieTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/HttpCookieTest.java @@ -152,6 +152,9 @@ public class HttpCookieTest 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", 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(), HttpCookie.PARTITIONED_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; Partitioned; SameSite=Strict", HttpCookieUtils.getRFC6265SetCookie(httpCookie)); } public static Stream rfc6265BadNameSource() diff --git a/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/AbstractSessionManager.java b/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/AbstractSessionManager.java index 9d851731ad0..09b30e721b0 100644 --- a/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/AbstractSessionManager.java +++ b/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/AbstractSessionManager.java @@ -834,8 +834,8 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen } /** - * @return true if session cookies should be HTTP-only (Microsoft extension) - * @see org.eclipse.jetty.http.HttpCookie#isHttpOnly() + * @return true if session cookies should be HTTP only + * @see HttpCookie#isHttpOnly() */ @Override public boolean isHttpOnly() @@ -855,6 +855,28 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen _sessionCookieAttributes.put(HttpCookie.HTTP_ONLY_ATTRIBUTE, Boolean.toString(httpOnly)); } + /** + * @return true if session cookies should have the {@code Partitioned} attribute + * @see HttpCookie#isPartitioned() + */ + @Override + public boolean isPartitioned() + { + return Boolean.parseBoolean(_sessionCookieAttributes.get(HttpCookie.PARTITIONED_ATTRIBUTE)); + } + + /** + * Sets whether session cookies should have the {@code Partitioned} attribute + * + * @param partitioned whether session cookies should have the {@code Partitioned} attribute + * @see HttpCookie + */ + @Override + public void setPartitioned(boolean partitioned) + { + _sessionCookieAttributes.put(HttpCookie.PARTITIONED_ATTRIBUTE, Boolean.toString(partitioned)); + } + /** * Check if id is in use by this context * diff --git a/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/SessionConfig.java b/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/SessionConfig.java index 3281196af35..43c0c459b01 100644 --- a/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/SessionConfig.java +++ b/jetty-core/jetty-session/src/main/java/org/eclipse/jetty/session/SessionConfig.java @@ -95,6 +95,9 @@ public interface SessionConfig @ManagedAttribute("true if cookies use the http only flag") boolean isHttpOnly(); + @ManagedAttribute("true if cookies have the Partitioned attribute") + boolean isPartitioned(); + @ManagedAttribute("if true, secure cookie flag is set on session cookies") boolean isSecureCookies(); @@ -115,6 +118,8 @@ public interface SessionConfig void setHttpOnly(boolean value); + void setPartitioned(boolean value); + void setMaxCookieAge(int value); void setMaxInactiveInterval(int value); diff --git a/jetty-core/jetty-session/src/test/java/org/eclipse/jetty/session/AbstractSessionManagerTest.java b/jetty-core/jetty-session/src/test/java/org/eclipse/jetty/session/AbstractSessionManagerTest.java index 168be1e8be7..07514d92460 100644 --- a/jetty-core/jetty-session/src/test/java/org/eclipse/jetty/session/AbstractSessionManagerTest.java +++ b/jetty-core/jetty-session/src/test/java/org/eclipse/jetty/session/AbstractSessionManagerTest.java @@ -27,9 +27,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -/** - * - */ public class AbstractSessionManagerTest { @Test @@ -51,7 +48,8 @@ public class AbstractSessionManagerTest assertEquals("/test", cookie.getPath()); assertFalse(cookie.isSecure()); assertFalse(cookie.isHttpOnly()); - + assertFalse(cookie.isPartitioned()); + //check cookie with httpOnly and secure sessionManager.setHttpOnly(true); sessionManager.setSecureRequestOnly(true); 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 5cd5865b596..70a145bc116 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 @@ -589,6 +589,12 @@ public class ServletApiResponse implements HttpServletResponse return _cookie.isHttpOnly(); } + @Override + public boolean isPartitioned() + { + return Boolean.parseBoolean(getAttributes().get(HttpCookie.PARTITIONED_ATTRIBUTE)); + } + @Override public Map getAttributes() { 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 4b4e9d51c63..7878774e0f6 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 @@ -76,7 +76,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(WorkDirExtension.class) public class SessionHandlerTest { - public static class SessionConsumer implements Consumer { private ManagedSession _session; @@ -685,6 +684,7 @@ public class SessionHandlerTest sessionCookieConfig.setSecure(false); sessionCookieConfig.setPath("/foo"); sessionCookieConfig.setMaxAge(99); + sessionCookieConfig.setAttribute("Partitioned", "true"); sessionCookieConfig.setAttribute("SameSite", "Strict"); sessionCookieConfig.setAttribute("ham", "cheese"); @@ -694,11 +694,12 @@ public class SessionHandlerTest assertEquals("/foo", cookie.getPath()); assertFalse(cookie.isHttpOnly()); assertFalse(cookie.isSecure()); + assertTrue(cookie.isPartitioned()); assertEquals(99, cookie.getMaxAge()); assertEquals(HttpCookie.SameSite.STRICT, cookie.getSameSite()); String cookieStr = HttpCookieUtils.getRFC6265SetCookie(cookie); - assertThat(cookieStr, containsString("; SameSite=Strict; ham=cheese")); + assertThat(cookieStr, containsString("; Partitioned; SameSite=Strict; ham=cheese")); } @Test 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 e6611b5ce26..eb9265cbc2e 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 @@ -73,11 +73,18 @@ public class Response implements HttpServletResponse public static final int USE_KNOWN_CONTENT_LENGTH = -2; /** - * If this string is found within a cookie comment, then the cookie is HttpOnly + * String used in the {@code Comment} attribute of {@link Cookie} + * to support the {@code HttpOnly} attribute. **/ private static final String HTTP_ONLY_COMMENT = "__HTTP_ONLY__"; /** - * These strings are used by to check for a SameSite specifier in a cookie comment + * String used in the {@code Comment} attribute of {@link Cookie} + * to support the {@code Partitioned} attribute. + **/ + private static final String PARTITIONED_COMMENT = "__PARTITIONED__"; + /** + * The strings used in the {@code Comment} attribute of {@link Cookie} + * to support the {@code SameSite} attribute. **/ private static final String SAME_SITE_COMMENT = "__SAME_SITE_"; private static final String SAME_SITE_NONE_COMMENT = SAME_SITE_COMMENT + "NONE__"; @@ -1480,16 +1487,16 @@ public class Response implements HttpServletResponse private final String _comment; private final boolean _httpOnly; private final SameSite _sameSite; + private final boolean _partitioned; public HttpCookieFacade(Cookie cookie) { _cookie = cookie; - String comment = cookie.getComment(); - - // HttpOnly was supported as a comment in cookie flags before the java.net.HttpCookie implementation so need to check that + // HttpOnly was supported as a comment in cookie flags before the Cookie implementation so need to check that. _httpOnly = cookie.isHttpOnly() || isHttpOnlyInComment(comment); _sameSite = getSameSiteFromComment(comment); + _partitioned = isPartitionedInComment(comment); _comment = getCommentWithoutAttributes(comment); } @@ -1553,6 +1560,12 @@ public class Response implements HttpServletResponse return _httpOnly; } + @Override + public boolean isPartitioned() + { + return _partitioned; + } + @Override public Map getAttributes() { @@ -1568,6 +1581,8 @@ public class Response implements HttpServletResponse attributes.put(SAME_SITE_ATTRIBUTE, _sameSite.getAttributeValue()); if (isSecure()) attributes.put(SECURE_ATTRIBUTE, Boolean.TRUE.toString()); + if (isPartitioned()) + attributes.put(PARTITIONED_ATTRIBUTE, Boolean.TRUE.toString()); return attributes; } @@ -1589,47 +1604,43 @@ public class Response implements HttpServletResponse return HttpCookie.toString(this); } - static boolean isHttpOnlyInComment(String comment) + private static boolean isHttpOnlyInComment(String comment) { return comment != null && comment.contains(HTTP_ONLY_COMMENT); } - static SameSite getSameSiteFromComment(String comment) + private static boolean isPartitionedInComment(String comment) { - if (comment != null) - { - if (comment.contains(SAME_SITE_STRICT_COMMENT)) - { - return SameSite.STRICT; - } - if (comment.contains(SAME_SITE_LAX_COMMENT)) - { - return SameSite.LAX; - } - if (comment.contains(SAME_SITE_NONE_COMMENT)) - { - return SameSite.NONE; - } - } + return comment != null && comment.contains(PARTITIONED_COMMENT); + } + private static SameSite getSameSiteFromComment(String comment) + { + if (comment == null) + return null; + if (comment.contains(SAME_SITE_STRICT_COMMENT)) + return SameSite.STRICT; + if (comment.contains(SAME_SITE_LAX_COMMENT)) + return SameSite.LAX; + if (comment.contains(SAME_SITE_NONE_COMMENT)) + return SameSite.NONE; return null; } private static String getCommentWithoutAttributes(String comment) { if (comment == null) - { return null; - } String strippedComment = comment.trim(); strippedComment = StringUtil.strip(strippedComment, HTTP_ONLY_COMMENT); + strippedComment = StringUtil.strip(strippedComment, PARTITIONED_COMMENT); strippedComment = StringUtil.strip(strippedComment, SAME_SITE_NONE_COMMENT); strippedComment = StringUtil.strip(strippedComment, SAME_SITE_LAX_COMMENT); strippedComment = StringUtil.strip(strippedComment, SAME_SITE_STRICT_COMMENT); - return strippedComment.length() == 0 ? null : strippedComment; + return strippedComment.isEmpty() ? null : strippedComment; } } } diff --git a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java index bb8da4e6abb..1edaa2b7fe1 100644 --- a/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java +++ b/jetty-ee9/jetty-ee9-nested/src/main/java/org/eclipse/jetty/ee9/nested/SessionHandler.java @@ -316,6 +316,12 @@ public class SessionHandler extends ScopedHandler implements SessionConfig.Mutab return _sessionManager.isHttpOnly(); } + @Override + public boolean isPartitioned() + { + return _sessionManager.isPartitioned(); + } + @Override public boolean isSecureCookies() { @@ -352,6 +358,12 @@ public class SessionHandler extends ScopedHandler implements SessionConfig.Mutab _sessionManager.setHttpOnly(value); } + @Override + public void setPartitioned(boolean value) + { + _sessionManager.setPartitioned(value); + } + @Override public void setMaxCookieAge(int 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 146eae693a1..8053c8315ca 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 @@ -54,6 +54,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class SessionHandlerTest { @@ -193,6 +194,7 @@ public class SessionHandlerTest //for < ee10, SameSite cannot be set on the SessionCookieConfig, only on the SessionManager, or //a default value on the context attribute org.eclipse.jetty.cookie.sameSiteDefault mgr.setSameSite(HttpCookie.SameSite.STRICT); + mgr.setPartitioned(true); HttpCookie cookie = mgr.getSessionManager().getSessionCookie(session, false); assertEquals("SPECIAL", cookie.getName()); @@ -200,11 +202,12 @@ public class SessionHandlerTest assertEquals("/foo", cookie.getPath()); assertFalse(cookie.isHttpOnly()); assertFalse(cookie.isSecure()); + assertTrue(cookie.isPartitioned()); assertEquals(99, cookie.getMaxAge()); assertEquals(HttpCookie.SameSite.STRICT, cookie.getSameSite()); String cookieStr = HttpCookieUtils.getRFC6265SetCookie(cookie); - assertThat(cookieStr, containsString("; SameSite=Strict")); + assertThat(cookieStr, containsString("; Partitioned; SameSite=Strict")); } @Test diff --git a/pom.xml b/pom.xml index caa899d587d..b9508a43629 100644 --- a/pom.xml +++ b/pom.xml @@ -126,14 +126,14 @@ - scm:git:https://github.com/eclipse/jetty.project.git - scm:git:git@github.com:eclipse/jetty.project.git - https://github.com/eclipse/jetty.project + scm:git:https://github.com/jetty/jetty.project.git + scm:git:git@github.com:jetty/jetty.project.git + https://github.com/jetty/jetty.project github - https://github.com/eclipse/jetty.project/issues + https://github.com/jetty/jetty.project/issues @@ -1588,8 +1588,8 @@ https://eclipse.dev/jetty/javadoc/jetty-12 ${basedir}/.. - https://github.com/eclipse/jetty.project/tree/jetty-12.0.x - https://github.com/eclipse/jetty.project/tree/jetty-12.0.x/documentation/jetty-documentation/src/main/asciidoc + https://github.com/jetty/jetty.project/tree/jetty-12.0.x + https://github.com/jetty/jetty.project/tree/jetty-12.0.x/documentation/jetty-documentation/src/main/asciidoc http://central.maven.org/maven2 ${project.version} ${maven.build.timestamp}