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 <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2023-11-17 11:21:19 +01:00
parent af13a72978
commit f82844e2a2
4 changed files with 97 additions and 52 deletions

View File

@ -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 == 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();

View File

@ -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<String> 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,14 +350,10 @@ public class HttpCookieTest
{
String actualComment = HttpCookie.getCommentWithoutAttributes(rawComment);
if (expectedComment == null)
{
assertNull(actualComment);
}
else
{
assertEquals(actualComment, expectedComment);
}
}
@Test
public void testGetCommentWithAttributes()
@ -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),

View File

@ -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));
}
}

View File

@ -632,15 +632,14 @@ public class SessionHandler extends ScopedHandler
*/
public HttpCookie getSessionCookie(HttpSession session, String contextPath, boolean requestIsSecure)
{
if (isUsingCookies())
{
if (!isUsingCookies())
return null;
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(
String comment = cookieConfig.getComment();
return new HttpCookie(
getSessionCookieName(_cookieConfig),
id,
cookieConfig.getDomain(),
@ -648,13 +647,11 @@ public class SessionHandler extends ScopedHandler
cookieConfig.getMaxAge(),
cookieConfig.isHttpOnly(),
cookieConfig.isSecure() || (isSecureRequestOnly() && requestIsSecure),
HttpCookie.getCommentWithoutAttributes(cookieConfig.getComment()),
HttpCookie.getCommentWithoutAttributes(comment),
0,
HttpCookie.getSameSiteFromComment(cookieConfig.getComment()));
return cookie;
}
return null;
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}