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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
// TODO consider replacing this with java.net.HttpCookie (once it supports RFC6265)
public class HttpCookie public class HttpCookie
{ {
private static final Logger LOG = LoggerFactory.getLogger(HttpCookie.class); 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(); 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__"; 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_"; private static final String SAME_SITE_COMMENT = "__SAME_SITE_";
public static final String SAME_SITE_NONE_COMMENT = SAME_SITE_COMMENT + "NONE__"; 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"); NONE("None"), STRICT("Strict"), LAX("Lax");
private String attributeValue; private final String attributeValue;
SameSite(String attributeValue) SameSite(String attributeValue)
{ {
@ -77,6 +83,7 @@ public class HttpCookie
private final boolean _httpOnly; private final boolean _httpOnly;
private final long _expiration; private final long _expiration;
private final SameSite _sameSite; private final SameSite _sameSite;
private final boolean _partitioned;
public HttpCookie(String name, String value) 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) 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; _name = name;
_value = value; _value = value;
@ -116,6 +128,7 @@ public class HttpCookie
_version = version; _version = version;
_expiration = maxAge < 0 ? -1 : NanoTime.now() + TimeUnit.SECONDS.toNanos(maxAge); _expiration = maxAge < 0 ? -1 : NanoTime.now() + TimeUnit.SECONDS.toNanos(maxAge);
_sameSite = sameSite; _sameSite = sameSite;
_partitioned = partitioned;
} }
public HttpCookie(String setCookie) public HttpCookie(String setCookie)
@ -136,8 +149,10 @@ public class HttpCookie
_comment = cookie.getComment(); _comment = cookie.getComment();
_version = cookie.getVersion(); _version = cookie.getVersion();
_expiration = _maxAge < 0 ? -1 : NanoTime.now() + TimeUnit.SECONDS.toNanos(_maxAge); _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()); _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 _expiration != -1 && NanoTime.isBefore(_expiration, timeNanos);
} }
/**
* @return whether this cookie is partitioned
*/
public boolean isPartitioned()
{
return _partitioned;
}
/** /**
* @return a string representation of this cookie * @return a string representation of this cookie
*/ */
@ -419,6 +442,8 @@ public class HttpCookie
buf.append("; SameSite="); buf.append("; SameSite=");
buf.append(_sameSite.getAttributeValue()); buf.append(_sameSite.getAttributeValue());
} }
if (isPartitioned())
buf.append("; Partitioned");
return buf.toString(); return buf.toString();
} }
@ -428,23 +453,22 @@ public class HttpCookie
return comment != null && comment.contains(HTTP_ONLY_COMMENT); 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) public static SameSite getSameSiteFromComment(String comment)
{ {
if (comment != null) if (comment == null)
{ return null;
if (comment.contains(SAME_SITE_STRICT_COMMENT))
{ if (comment.contains(SAME_SITE_STRICT_COMMENT))
return SameSite.STRICT; return SameSite.STRICT;
} if (comment.contains(SAME_SITE_LAX_COMMENT))
if (comment.contains(SAME_SITE_LAX_COMMENT)) return SameSite.LAX;
{ if (comment.contains(SAME_SITE_NONE_COMMENT))
return SameSite.LAX; return SameSite.NONE;
}
if (comment.contains(SAME_SITE_NONE_COMMENT))
{
return SameSite.NONE;
}
}
return null; return null;
} }
@ -488,21 +512,25 @@ public class HttpCookie
public static String getCommentWithoutAttributes(String comment) public static String getCommentWithoutAttributes(String comment)
{ {
if (comment == null) if (comment == null)
{
return null; return null;
}
String strippedComment = comment.trim(); String strippedComment = comment.trim();
strippedComment = StringUtil.strip(strippedComment, HTTP_ONLY_COMMENT); 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_NONE_COMMENT);
strippedComment = StringUtil.strip(strippedComment, SAME_SITE_LAX_COMMENT); strippedComment = StringUtil.strip(strippedComment, SAME_SITE_LAX_COMMENT);
strippedComment = StringUtil.strip(strippedComment, SAME_SITE_STRICT_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) 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) if (comment == null && sameSite == null)
return null; return null;
@ -535,6 +563,9 @@ public class HttpCookie
} }
} }
if (partitioned)
builder.append(PARTITIONED_COMMENT);
if (builder.length() == 0) if (builder.length() == 0)
return null; return null;
return builder.toString(); 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); 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()); 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() public static Stream<String> rfc6265BadNameSource()
@ -336,7 +339,8 @@ public class HttpCookieTest
Arguments.of("__HTTP_ONLY____SAME_SITE_NONE__comment", "comment"), Arguments.of("__HTTP_ONLY____SAME_SITE_NONE__comment", "comment"),
// mixed - attributes at start and end // mixed - attributes at start and end
Arguments.of("__SAME_SITE_NONE__comment__HTTP_ONLY__", "comment"), 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); String actualComment = HttpCookie.getCommentWithoutAttributes(rawComment);
if (expectedComment == null) if (expectedComment == null)
{
assertNull(actualComment); assertNull(actualComment);
}
else else
{
assertEquals(actualComment, expectedComment); assertEquals(actualComment, expectedComment);
}
} }
@Test @Test
@ -368,6 +368,8 @@ public class HttpCookieTest
is("__HTTP_ONLY____SAME_SITE_NONE__")); is("__HTTP_ONLY____SAME_SITE_NONE__"));
assertThat(HttpCookie.getCommentWithAttributes("hello", true, HttpCookie.SameSite.LAX), assertThat(HttpCookie.getCommentWithAttributes("hello", true, HttpCookie.SameSite.LAX),
is("hello__HTTP_ONLY____SAME_SITE_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__", false, null), nullValue());
assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__", true, HttpCookie.SameSite.NONE), 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(); 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 java.net.HttpCookie implementation so need to check that
boolean httpOnly = cookie.isHttpOnly() || HttpCookie.isHttpOnlyInComment(comment); boolean httpOnly = cookie.isHttpOnly() || HttpCookie.isHttpOnlyInComment(comment);
boolean partitioned = HttpCookie.isPartitionedInComment(comment);
SameSite sameSite = HttpCookie.getSameSiteFromComment(comment); SameSite sameSite = HttpCookie.getSameSiteFromComment(comment);
comment = HttpCookie.getCommentWithoutAttributes(comment); comment = HttpCookie.getCommentWithoutAttributes(comment);
@ -280,7 +281,8 @@ public class Response implements HttpServletResponse
cookie.getSecure(), cookie.getSecure(),
comment, comment,
cookie.getVersion(), cookie.getVersion(),
sameSite)); sameSite,
partitioned));
} }
} }

View File

@ -632,29 +632,26 @@ public class SessionHandler extends ScopedHandler
*/ */
public HttpCookie getSessionCookie(HttpSession session, String contextPath, boolean requestIsSecure) public HttpCookie getSessionCookie(HttpSession session, String contextPath, boolean requestIsSecure)
{ {
if (isUsingCookies()) if (!isUsingCookies())
{ return null;
SessionCookieConfig cookieConfig = getSessionCookieConfig(); SessionCookieConfig cookieConfig = getSessionCookieConfig();
String sessionPath = (cookieConfig.getPath() == null) ? contextPath : cookieConfig.getPath(); String sessionPath = (cookieConfig.getPath() == null) ? contextPath : cookieConfig.getPath();
sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath; sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath;
String id = getExtendedId(session); String id = getExtendedId(session);
HttpCookie cookie = null; String comment = cookieConfig.getComment();
return new HttpCookie(
cookie = new HttpCookie( getSessionCookieName(_cookieConfig),
getSessionCookieName(_cookieConfig), id,
id, cookieConfig.getDomain(),
cookieConfig.getDomain(), sessionPath,
sessionPath, cookieConfig.getMaxAge(),
cookieConfig.getMaxAge(), cookieConfig.isHttpOnly(),
cookieConfig.isHttpOnly(), cookieConfig.isSecure() || (isSecureRequestOnly() && requestIsSecure),
cookieConfig.isSecure() || (isSecureRequestOnly() && requestIsSecure), HttpCookie.getCommentWithoutAttributes(comment),
HttpCookie.getCommentWithoutAttributes(cookieConfig.getComment()), 0,
0, HttpCookie.getSameSiteFromComment(comment),
HttpCookie.getSameSiteFromComment(cookieConfig.getComment())); HttpCookie.isPartitionedInComment(comment)
);
return cookie;
}
return null;
} }
@ManagedAttribute("domain of the session cookie, or null for the default") @ManagedAttribute("domain of the session cookie, or null for the default")
@ -802,6 +799,19 @@ public class SessionHandler extends ScopedHandler
_httpOnly = httpOnly; _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. * Set Session cookie sameSite mode.
* Currently this is encoded in the session comment until sameSite is supported by {@link SessionCookieConfig} * Currently this is encoded in the session comment until sameSite is supported by {@link SessionCookieConfig}