From f82844e2a256c3426df029f028bf0fee8cd4879b Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Fri, 17 Nov 2023 11:21:19 +0100 Subject: [PATCH] Fixes #10891 - Support the "Partitioned" cookie attribute. Added support in oej.http.HttpCookie. Bridged support for Servlet cookies via the cookie Comment attribute. Signed-off-by: Simone Bordet --- .../org/eclipse/jetty/http/HttpCookie.java | 77 +++++++++++++------ .../eclipse/jetty/http/HttpCookieTest.java | 12 +-- .../org/eclipse/jetty/server/Response.java | 4 +- .../jetty/server/session/SessionHandler.java | 56 ++++++++------ 4 files changed, 97 insertions(+), 52 deletions(-) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java index 757daee181f..cd7638bb3e5 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCookie.java @@ -24,7 +24,6 @@ import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -// TODO consider replacing this with java.net.HttpCookie (once it supports RFC6265) public class HttpCookie { private static final Logger LOG = LoggerFactory.getLogger(HttpCookie.class); @@ -33,11 +32,18 @@ public class HttpCookie private static final String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim(); /** - * If this string is found within the comment parsed with {@link #isHttpOnlyInComment(String)} the check will return true + * String used in the {@code Comment} attribute of {@link java.net.HttpCookie}, + * parsed with {@link #isHttpOnlyInComment(String)}, to support the {@code HttpOnly} attribute. **/ public static final String HTTP_ONLY_COMMENT = "__HTTP_ONLY__"; /** - * These strings are used by {@link #getSameSiteFromComment(String)} to check for a SameSite specifier in the comment + * String used in the {@code Comment} attribute of {@link java.net.HttpCookie}, + * parsed with {@link #isPartitionedInComment(String)}, to support the {@code Partitioned} attribute. + **/ + public static final String PARTITIONED_COMMENT = "__PARTITIONED__"; + /** + * The strings used in the {@code Comment} attribute of {@link java.net.HttpCookie}, + * parsed with {@link #getSameSiteFromComment(String)}, to support the {@code SameSite} attribute. **/ private static final String SAME_SITE_COMMENT = "__SAME_SITE_"; public static final String SAME_SITE_NONE_COMMENT = SAME_SITE_COMMENT + "NONE__"; @@ -53,7 +59,7 @@ public class HttpCookie { NONE("None"), STRICT("Strict"), LAX("Lax"); - private String attributeValue; + private final String attributeValue; SameSite(String attributeValue) { @@ -77,6 +83,7 @@ public class HttpCookie private final boolean _httpOnly; private final long _expiration; private final SameSite _sameSite; + private final boolean _partitioned; public HttpCookie(String name, String value) { @@ -104,6 +111,11 @@ public class HttpCookie } public HttpCookie(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure, String comment, int version, SameSite sameSite) + { + this(name, value, domain, path, maxAge, httpOnly, secure, comment, version, sameSite, false); + } + + public HttpCookie(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure, String comment, int version, SameSite sameSite, boolean partitioned) { _name = name; _value = value; @@ -116,6 +128,7 @@ public class HttpCookie _version = version; _expiration = maxAge < 0 ? -1 : NanoTime.now() + TimeUnit.SECONDS.toNanos(maxAge); _sameSite = sameSite; + _partitioned = partitioned; } public HttpCookie(String setCookie) @@ -136,8 +149,10 @@ public class HttpCookie _comment = cookie.getComment(); _version = cookie.getVersion(); _expiration = _maxAge < 0 ? -1 : NanoTime.now() + TimeUnit.SECONDS.toNanos(_maxAge); - // support for SameSite values has not yet been added to java.net.HttpCookie + // Support for SameSite values has not yet been added to java.net.HttpCookie. _sameSite = getSameSiteFromComment(cookie.getComment()); + // Support for Partitioned has not yet been added to java.net.HttpCookie. + _partitioned = isPartitionedInComment(cookie.getComment()); } /** @@ -229,6 +244,14 @@ public class HttpCookie return _expiration != -1 && NanoTime.isBefore(_expiration, timeNanos); } + /** + * @return whether this cookie is partitioned + */ + public boolean isPartitioned() + { + return _partitioned; + } + /** * @return a string representation of this cookie */ @@ -419,6 +442,8 @@ public class HttpCookie buf.append("; SameSite="); buf.append(_sameSite.getAttributeValue()); } + if (isPartitioned()) + buf.append("; Partitioned"); return buf.toString(); } @@ -428,23 +453,22 @@ public class HttpCookie return comment != null && comment.contains(HTTP_ONLY_COMMENT); } + public static boolean isPartitionedInComment(String comment) + { + return comment != null && comment.contains(PARTITIONED_COMMENT); + } + public static SameSite getSameSiteFromComment(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; - } - } + 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; } @@ -488,21 +512,25 @@ public class HttpCookie public 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; } public static String getCommentWithAttributes(String comment, boolean httpOnly, SameSite sameSite) + { + return getCommentWithAttributes(comment, httpOnly, sameSite, false); + } + + public static String getCommentWithAttributes(String comment, boolean httpOnly, SameSite sameSite, boolean partitioned) { if (comment == null && sameSite == null) return null; @@ -535,6 +563,9 @@ public class HttpCookie } } + if (partitioned) + builder.append(PARTITIONED_COMMENT); + if (builder.length() == 0) return null; return builder.toString(); diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java index 67a3c719913..77b9bfcaa13 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpCookieTest.java @@ -131,6 +131,9 @@ public class HttpCookieTest httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1, HttpCookie.SameSite.STRICT); 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 = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1, HttpCookie.SameSite.STRICT, true); + assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Strict; Partitioned", httpCookie.getRFC6265SetCookie()); } public static Stream rfc6265BadNameSource() @@ -336,7 +339,8 @@ public class HttpCookieTest Arguments.of("__HTTP_ONLY____SAME_SITE_NONE__comment", "comment"), // mixed - attributes at start and end Arguments.of("__SAME_SITE_NONE__comment__HTTP_ONLY__", "comment"), - Arguments.of("__HTTP_ONLY__comment__SAME_SITE_NONE__", "comment") + Arguments.of("__HTTP_ONLY__comment__SAME_SITE_NONE__", "comment"), + Arguments.of("__PARTITIONED__comment__SAME_SITE_NONE__", "comment") ); } @@ -346,13 +350,9 @@ public class HttpCookieTest { String actualComment = HttpCookie.getCommentWithoutAttributes(rawComment); if (expectedComment == null) - { assertNull(actualComment); - } else - { assertEquals(actualComment, expectedComment); - } } @Test @@ -368,6 +368,8 @@ public class HttpCookieTest is("__HTTP_ONLY____SAME_SITE_NONE__")); assertThat(HttpCookie.getCommentWithAttributes("hello", true, HttpCookie.SameSite.LAX), is("hello__HTTP_ONLY____SAME_SITE_LAX__")); + assertThat(HttpCookie.getCommentWithAttributes("hello", true, HttpCookie.SameSite.LAX, true), + is("hello__HTTP_ONLY____SAME_SITE_LAX____PARTITIONED__")); assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__", false, null), nullValue()); assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__", true, HttpCookie.SameSite.NONE), diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index 53eda402ad3..34747a40529 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -267,6 +267,7 @@ public class Response implements HttpServletResponse String comment = cookie.getComment(); // HttpOnly was supported as a comment in cookie flags before the java.net.HttpCookie implementation so need to check that boolean httpOnly = cookie.isHttpOnly() || HttpCookie.isHttpOnlyInComment(comment); + boolean partitioned = HttpCookie.isPartitionedInComment(comment); SameSite sameSite = HttpCookie.getSameSiteFromComment(comment); comment = HttpCookie.getCommentWithoutAttributes(comment); @@ -280,7 +281,8 @@ public class Response implements HttpServletResponse cookie.getSecure(), comment, cookie.getVersion(), - sameSite)); + sameSite, + partitioned)); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java index 82106392de9..e1a7e230d62 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java @@ -632,29 +632,26 @@ public class SessionHandler extends ScopedHandler */ public HttpCookie getSessionCookie(HttpSession session, String contextPath, boolean requestIsSecure) { - if (isUsingCookies()) - { - SessionCookieConfig cookieConfig = getSessionCookieConfig(); - String sessionPath = (cookieConfig.getPath() == null) ? contextPath : cookieConfig.getPath(); - sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath; - String id = getExtendedId(session); - HttpCookie cookie = null; - - cookie = new HttpCookie( - getSessionCookieName(_cookieConfig), - id, - cookieConfig.getDomain(), - sessionPath, - cookieConfig.getMaxAge(), - cookieConfig.isHttpOnly(), - cookieConfig.isSecure() || (isSecureRequestOnly() && requestIsSecure), - HttpCookie.getCommentWithoutAttributes(cookieConfig.getComment()), - 0, - HttpCookie.getSameSiteFromComment(cookieConfig.getComment())); - - return cookie; - } - return null; + if (!isUsingCookies()) + return null; + SessionCookieConfig cookieConfig = getSessionCookieConfig(); + String sessionPath = (cookieConfig.getPath() == null) ? contextPath : cookieConfig.getPath(); + sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath; + String id = getExtendedId(session); + String comment = cookieConfig.getComment(); + return new HttpCookie( + getSessionCookieName(_cookieConfig), + id, + cookieConfig.getDomain(), + sessionPath, + cookieConfig.getMaxAge(), + cookieConfig.isHttpOnly(), + cookieConfig.isSecure() || (isSecureRequestOnly() && requestIsSecure), + HttpCookie.getCommentWithoutAttributes(comment), + 0, + HttpCookie.getSameSiteFromComment(comment), + HttpCookie.isPartitionedInComment(comment) + ); } @ManagedAttribute("domain of the session cookie, or null for the default") @@ -802,6 +799,19 @@ public class SessionHandler extends ScopedHandler _httpOnly = httpOnly; } + /** + * Sets whether session cookies should have the {@code Partitioned} attribute. + * + * @param partitioned whether session cookies should have the {@code Partitioned} attribute + * @see HttpCookie + */ + public void setPartitioned(boolean partitioned) + { + // Encode in comment whilst not supported by SessionConfig, + // so that it can be set/saved in web.xml and quickstart. + _sessionComment = HttpCookie.getCommentWithAttributes(_sessionComment, false, null, partitioned); + } + /** * Set Session cookie sameSite mode. * Currently this is encoded in the session comment until sameSite is supported by {@link SessionCookieConfig}