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:
parent
af13a72978
commit
f82844e2a2
|
@ -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();
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in New Issue