Jetty 12 : #8408 Support new `<cookie-config>` concepts in Servlet 6 (#8420)

* Issue #8408 Implement new session cookie attributes
This commit is contained in:
Jan Bartel 2022-08-10 16:33:08 +10:00 committed by GitHub
parent d210677182
commit 42f72268cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 893 additions and 405 deletions

View File

@ -13,8 +13,12 @@
package org.eclipse.jetty.http; package org.eclipse.jetty.http;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Attributes;
@ -76,7 +80,7 @@ public class HttpCookie
private final int _version; private final int _version;
private final boolean _httpOnly; private final boolean _httpOnly;
private final long _expiration; private final long _expiration;
private final SameSite _sameSite; private final Map<String, String> _attributes;
public HttpCookie(String name, String value) public HttpCookie(String name, String value)
{ {
@ -100,10 +104,16 @@ public class HttpCookie
public HttpCookie(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure, String comment, int version) public HttpCookie(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure, String comment, int version)
{ {
this(name, value, domain, path, maxAge, httpOnly, secure, comment, version, null); this(name, value, domain, path, maxAge, httpOnly, secure, comment, version, (SameSite)null);
} }
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, Collections.singletonMap("SameSite", sameSite == null ? null : sameSite.getAttributeValue()));
}
public HttpCookie(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure, String comment, int version, Map<String, String> attributes)
{ {
_name = name; _name = name;
_value = value; _value = value;
@ -115,29 +125,26 @@ public class HttpCookie
_comment = comment; _comment = comment;
_version = version; _version = version;
_expiration = maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(maxAge); _expiration = maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(maxAge);
_sameSite = sameSite; _attributes = (attributes == null ? Collections.emptyMap() : attributes);
} }
public HttpCookie(String setCookie) public HttpCookie(String name, String value, int version, Map<String, String> attributes)
{ {
List<java.net.HttpCookie> cookies = java.net.HttpCookie.parse(setCookie); _name = name;
if (cookies.size() != 1) _value = value;
throw new IllegalStateException(); _version = version;
_attributes = (attributes == null ? Collections.emptyMap() : new TreeMap<>(attributes));
//remove all of the well-known attributes, leaving only those pass-through ones
_domain = _attributes.remove("Domain");
_path = _attributes.remove("Path");
java.net.HttpCookie cookie = cookies.get(0); String tmp = _attributes.remove("Max-Age");
_maxAge = StringUtil.isBlank(tmp) ? -1L : Long.valueOf(tmp);
_name = cookie.getName();
_value = cookie.getValue();
_domain = cookie.getDomain();
_path = cookie.getPath();
_maxAge = cookie.getMaxAge();
_httpOnly = cookie.isHttpOnly();
_secure = cookie.getSecure();
_comment = cookie.getComment();
_version = cookie.getVersion();
_expiration = _maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(_maxAge); _expiration = _maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(_maxAge);
// support for SameSite values has not yet been added to java.net.HttpCookie _httpOnly = Boolean.parseBoolean(_attributes.remove("HttpOnly"));
_sameSite = getSameSiteFromComment(cookie.getComment()); _secure = Boolean.parseBoolean(_attributes.remove("Secure"));
_comment = _attributes.remove("Comment");
} }
/** /**
@ -209,7 +216,10 @@ public class HttpCookie
*/ */
public SameSite getSameSite() public SameSite getSameSite()
{ {
return _sameSite; String val = _attributes.get("SameSite");
if (val == null)
return null;
return SameSite.valueOf(val.toUpperCase(Locale.ENGLISH));
} }
/** /**
@ -421,12 +431,21 @@ public class HttpCookie
buf.append("; Secure"); buf.append("; Secure");
if (_httpOnly) if (_httpOnly)
buf.append("; HttpOnly"); buf.append("; HttpOnly");
if (_sameSite != null)
String sameSite = _attributes.get("SameSite");
if (sameSite != null)
{ {
buf.append("; SameSite="); buf.append("; SameSite=");
buf.append(_sameSite.getAttributeValue()); buf.append(sameSite);
} }
//Add all other attributes
_attributes.entrySet().stream().filter(e -> !"SameSite".equals(e.getKey())).forEach(e ->
{
buf.append("; " + e.getKey() + "=");
buf.append(e.getValue());
});
return buf.toString(); return buf.toString();
} }
@ -491,6 +510,109 @@ public class HttpCookie
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
} }
/**
* Extract the bare minimum of info from a Set-Cookie header string.
*
* Ideally this method should not be necessary, however as java.net.HttpCookie
* does not yet support generic attributes, we have to use it in a minimal
* fashion. When it supports attributes, we could look at reverting to a
* constructor on o.e.j.h.HttpCookie to take the set-cookie header string.
*
* @param setCookieHeader the header as a string
* @return a map containing the name, value, domain, path. max-age of the set cookie header
*/
public static Map<String, String> extractBasics(String setCookieHeader)
{
//Parse the bare minimum
List<java.net.HttpCookie> cookies = java.net.HttpCookie.parse(setCookieHeader);
if (cookies.size() != 1)
return Collections.emptyMap();
java.net.HttpCookie cookie = cookies.get(0);
Map<String, String> fields = new HashMap<>();
fields.put("name", cookie.getName());
fields.put("value", cookie.getValue());
fields.put("domain", cookie.getDomain());
fields.put("path", cookie.getPath());
fields.put("max-age", Long.toString(cookie.getMaxAge()));
return fields;
}
/**
* Check if the Set-Cookie header represented as a string is for the name, domain and path given.
*
* @param setCookieHeader a Set-Cookie header
* @param name the cookie name to check
* @param domain the cookie domain to check
* @param path the cookie path to check
* @return true if all of the name, domain and path match the Set-Cookie header, false otherwise
*/
public static boolean match(String setCookieHeader, String name, String domain, String path)
{
//Parse the bare minimum
List<java.net.HttpCookie> cookies = java.net.HttpCookie.parse(setCookieHeader);
if (cookies.size() != 1)
return false;
java.net.HttpCookie cookie = cookies.get(0);
return match(cookie.getName(), cookie.getDomain(), cookie.getPath(), name, domain, path);
}
/**
* Check if the HttpCookie is for the given name, domain and path.
*
* @param cookie the jetty HttpCookie to check
* @param name the cookie name to check
* @param domain the cookie domain to check
* @param path the cookie path to check
* @return true if all of the name, domain and path all match the HttpCookie, false otherwise
*/
public static boolean match(HttpCookie cookie, String name, String domain, String path)
{
if (cookie == null)
return false;
return match(cookie.getName(), cookie.getDomain(), cookie.getPath(), name, domain, path);
}
/**
* Check if all old parameters match the new parameters.
*
* @param oldName
* @param oldDomain
* @param oldPath
* @param newName
* @param newDomain
* @param newPath
* @return true if old and new names match exactly and the old and new domains match case-insensitively and the paths match exactly
*/
private static boolean match(String oldName, String oldDomain, String oldPath, String newName, String newDomain, String newPath)
{
if (oldName == null)
{
if (newName != null)
return false;
}
else if (!oldName.equals(newName))
return false;
if (oldDomain == null)
{
if (newDomain != null)
return false;
}
else if (!oldDomain.equalsIgnoreCase(newDomain))
return false;
if (oldPath == null)
{
if (newPath != null)
return false;
}
else if (!oldPath.equals(newPath))
return false;
return true;
}
/** /**
* @deprecated We should not need to do this now * @deprecated We should not need to do this now
@ -563,7 +685,7 @@ public class HttpCookie
return _cookie; return _cookie;
} }
} }
/** /**
* Check that samesite is set on the cookie. If not, use a * Check that samesite is set on the cookie. If not, use a
* context default value, if one has been set. * context default value, if one has been set.

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.http; package org.eclipse.jetty.http;
import java.util.Collections;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.jetty.http.HttpCookie.SameSite; import org.eclipse.jetty.http.HttpCookie.SameSite;
@ -64,9 +65,28 @@ public class HttpCookieTest
} }
@Test @Test
public void testConstructFromSetCookie() public void testMatchCookie()
{ {
HttpCookie cookie = new HttpCookie("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly"); //match with header string
assertTrue(HttpCookie.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar",
"everything", "domain", "path"));
assertFalse(HttpCookie.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar",
"something", "domain", "path"));
assertFalse(HttpCookie.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar",
"everything", "realm", "path"));
assertFalse(HttpCookie.match("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar",
"everything", "domain", "street"));
//match including set-cookie:, this is really testing the java.net.HttpCookie parser, but worth throwing in there
assertTrue(HttpCookie.match("Set-Cookie: everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Lax; Foo=Bar",
"everything", "domain", "path"));
//match via cookie
HttpCookie httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, "comment", 0);
assertTrue(HttpCookie.match(httpCookie, "everything", "domain", "path"));
assertFalse(HttpCookie.match(httpCookie, "something", "domain", "path"));
assertFalse(HttpCookie.match(httpCookie, "everything", "realm", "path"));
assertFalse(HttpCookie.match(httpCookie, "everything", "domain", "street"));
} }
@Test @Test
@ -131,6 +151,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, Collections.singletonMap("SameSite", "None"));
assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=None", httpCookie.getRFC6265SetCookie());
} }
public static Stream<String> rfc6265BadNameSource() public static Stream<String> rfc6265BadNameSource()

View File

@ -147,28 +147,15 @@ public interface Response extends Content.Sink
CookieCompliance compliance = httpConfiguration.getResponseCookieCompliance(); CookieCompliance compliance = httpConfiguration.getResponseCookieCompliance();
HttpCookie oldCookie; HttpCookie oldCookie;
if (field instanceof HttpCookie.SetCookieHttpField) if (field instanceof HttpCookie.SetCookieHttpField)
oldCookie = ((HttpCookie.SetCookieHttpField)field).getHttpCookie(); {
if (!HttpCookie.match(((HttpCookie.SetCookieHttpField)field).getHttpCookie(), cookie.getName(), cookie.getDomain(), cookie.getPath()))
continue;
}
else else
oldCookie = new HttpCookie(field.getValue());
if (!cookie.getName().equals(oldCookie.getName()))
continue;
if (cookie.getDomain() == null)
{ {
if (oldCookie.getDomain() != null) if (!HttpCookie.match(field.getValue(), cookie.getName(), cookie.getDomain(), cookie.getPath()))
continue; continue;
} }
else if (!cookie.getDomain().equalsIgnoreCase(oldCookie.getDomain()))
continue;
if (cookie.getPath() == null)
{
if (oldCookie.getPath() != null)
continue;
}
else if (!cookie.getPath().equals(oldCookie.getPath()))
continue;
i.set(new HttpCookie.SetCookieHttpField(HttpCookie.checkSameSite(cookie, request.getContext()), compliance)); i.set(new HttpCookie.SetCookieHttpField(HttpCookie.checkSameSite(cookie, request.getContext()), compliance));
return; return;

View File

@ -15,10 +15,13 @@ package org.eclipse.jetty.session;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -77,6 +80,7 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
private String _sessionDomain; private String _sessionDomain;
private String _sessionPath; private String _sessionPath;
private String _sessionComment; private String _sessionComment;
private final Map<String, String> _sessionAttributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private boolean _secureCookies = false; private boolean _secureCookies = false;
private boolean _secureRequestOnly = true; private boolean _secureRequestOnly = true;
private int _maxCookieAge = -1; private int _maxCookieAge = -1;
@ -356,28 +360,6 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
{ {
_refreshCookieAge = ageInSeconds; _refreshCookieAge = ageInSeconds;
} }
@Override
public HttpCookie.SameSite getSameSite()
{
// TODO do this properly
return HttpCookie.getSameSiteFromComment(_sessionComment);
}
/**
* Set Session cookie sameSite mode.
*
* @param sameSite The sameSite setting for Session cookies (or null for no sameSite setting)
*/
@Override
public void setSameSite(HttpCookie.SameSite sameSite)
{
// TODO this can be done properly now?
// Encode in comment whilst not supported by SessionConfig, so that it can be set/saved in
// web.xml and quickstart.
// Always pass false for httpOnly as it has it's own setter.
_sessionComment = HttpCookie.getCommentWithAttributes(_sessionComment, false, sameSite);
}
public abstract Server getServer(); public abstract Server getServer();
@ -479,65 +461,6 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
return _sessionContext; return _sessionContext;
} }
/**
* A session cookie is marked as secure IFF any of the following conditions are true:
* <ol>
* <li>SessionCookieConfig.setSecure == true</li>
* <li>SessionCookieConfig.setSecure == false &amp;&amp; _secureRequestOnly==true &amp;&amp; request is HTTPS</li>
* </ol>
* According to SessionCookieConfig javadoc, case 1 can be used when:
* "... even though the request that initiated the session came over HTTP,
* is to support a topology where the web container is front-ended by an
* SSL offloading load balancer. In this case, the traffic between the client
* and the load balancer will be over HTTPS, whereas the traffic between the
* load balancer and the web container will be over HTTP."
* <p>
* For case 2, you can use _secureRequestOnly to determine if you want the
* Servlet Spec 3.0 default behavior when SessionCookieConfig.setSecure==false,
* which is:
* <cite>
* "they shall be marked as secure only if the request that initiated the
* corresponding session was also secure"
* </cite>
* <p>
* The default for _secureRequestOnly is true, which gives the above behavior. If
* you set it to false, then a session cookie is NEVER marked as secure, even if
* the initiating request was secure.
*
* @param session the session to which the cookie should refer.
* @param contextPath the context to which the cookie should be linked.
* The client will only send the cookie value when requesting resources under this path.
* @param requestIsSecure whether the client is accessing the server over a secure protocol (i.e. HTTPS).
* @return if this <code>SessionManager</code> uses cookies, then this method will return a new
* {@link HttpCookie cookie object} that should be set on the client in order to link future HTTP requests
* with the <code>session</code>. If cookies are not in use, this method returns <code>null</code>.
*/
@Override
public HttpCookie getSessionCookie(Session session, String contextPath, boolean requestIsSecure)
{
if (isUsingCookies())
{
String sessionPath = (_sessionPath == null) ? contextPath : _sessionPath;
sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath;
String id = session.getExtendedId();
HttpCookie cookie = new HttpCookie(
(_sessionCookie == null ? __DefaultSessionCookie : _sessionCookie),
id,
_sessionDomain,
sessionPath,
_maxCookieAge,
_httpOnly,
_secureCookies || (isSecureRequestOnly() && requestIsSecure),
HttpCookie.getCommentWithoutAttributes(_sessionComment),
0,
HttpCookie.getSameSiteFromComment(_sessionComment));
session.onSetCookieGenerated();
return cookie;
}
return null;
}
@Override @Override
public String getSessionCookie() public String getSessionCookie()
{ {
@ -565,6 +488,25 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
_sessionDomain = domain; _sessionDomain = domain;
} }
public void setSessionAttribute(String name, String value)
{
_sessionAttributes.put(name, value);
}
public String getSessionAttribute(String name)
{
return _sessionAttributes.get(name);
}
/**
* @return all of the cookie config attributes EXCEPT for
* those that have explicit setter/getters
*/
public Map<String, String> getSessionAttributes()
{
return Collections.unmodifiableMap(_sessionAttributes);
}
@Override @Override
public SessionIdManager getSessionIdManager() public SessionIdManager getSessionIdManager()
{ {

View File

@ -14,9 +14,11 @@
package org.eclipse.jetty.session; package org.eclipse.jetty.session;
import java.util.Collections; import java.util.Collections;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Condition;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.thread.AutoLock; import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -148,6 +150,14 @@ public class Session
_extendedId = extendedId; _extendedId = extendedId;
} }
public HttpCookie generateSetCookie(String name, String domain, String path, int maxAge,
boolean httpOnly, boolean secure, String comment, int version, Map<String, String> attributes)
{
HttpCookie sessionCookie = new HttpCookie(name, getExtendedId(), domain, path, maxAge, httpOnly, secure, comment, version, attributes);
onSetCookieGenerated();
return sessionCookie;
}
/** /**
* Set the time that the cookie was set and clear the idChanged flag. * Set the time that the cookie was set and clear the idChanged flag.
*/ */

View File

@ -13,15 +13,19 @@
package org.eclipse.jetty.session; package org.eclipse.jetty.session;
import java.util.Collections;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SameSite;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil;
/** /**
* SimpleSessionHandler example * SimpleSessionHandler example
@ -190,4 +194,41 @@ public class SimpleSessionHandler extends AbstractSessionManager implements Hand
Response.replaceCookie(response, sessionManager.getSessionCookie(getCoreSession(), request.getContext().getContextPath(), request.isSecure())); Response.replaceCookie(response, sessionManager.getSessionCookie(getCoreSession(), request.getContext().getContextPath(), request.isSecure()));
} }
} }
@Override
public HttpCookie getSessionCookie(Session session, String contextPath, boolean requestIsSecure)
{
if (isUsingCookies())
{
String sessionPath = getSessionPath();
sessionPath = (sessionPath == null) ? contextPath : sessionPath;
sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath;
SameSite sameSite = HttpCookie.getSameSiteFromComment(getSessionComment());
Map<String, String> attributes = Collections.emptyMap();
if (sameSite != null)
attributes = Collections.singletonMap("SameSite", sameSite.getAttributeValue());
return session.generateSetCookie((getSessionCookie() == null ? __DefaultSessionCookie : getSessionCookie()),
getSessionDomain(),
sessionPath,
getMaxCookieAge(),
isHttpOnly(),
isSecureCookies() || (isSecureRequestOnly() && requestIsSecure),
HttpCookie.getCommentWithoutAttributes(getSessionComment()),
0,
attributes);
}
return null;
}
@Override
public SameSite getSameSite()
{
return HttpCookie.getSameSiteFromComment(getSessionComment());
}
@Override
public void setSameSite(SameSite sameSite)
{
setSessionComment(HttpCookie.getCommentWithAttributes(getSessionComment(), false, sameSite));
}
} }

View File

@ -14,12 +14,16 @@
package org.eclipse.jetty.session; package org.eclipse.jetty.session;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SameSite;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.session.Session.APISession; import org.eclipse.jetty.session.Session.APISession;
import org.eclipse.jetty.util.StringUtil;
/** /**
* TestSessionHandler * TestSessionHandler
@ -173,4 +177,41 @@ public class TestableSessionManager extends AbstractSessionManager
{ {
return _cookieConfig; return _cookieConfig;
} }
@Override
public HttpCookie getSessionCookie(Session session, String contextPath, boolean requestIsSecure)
{
if (isUsingCookies())
{
String sessionPath = getSessionPath();
sessionPath = (sessionPath == null) ? contextPath : sessionPath;
sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath;
SameSite sameSite = HttpCookie.getSameSiteFromComment(getSessionComment());
Map<String, String> attributes = Collections.emptyMap();
if (sameSite != null)
attributes = Collections.singletonMap("SameSite", sameSite.getAttributeValue());
return session.generateSetCookie((getSessionCookie() == null ? __DefaultSessionCookie : getSessionCookie()),
getSessionDomain(),
sessionPath,
getMaxCookieAge(),
isHttpOnly(),
isSecureCookies() || (isSecureRequestOnly() && requestIsSecure),
HttpCookie.getCommentWithoutAttributes(getSessionComment()),
0,
attributes);
}
return null;
}
@Override
public SameSite getSameSite()
{
return HttpCookie.getSameSiteFromComment(getSessionComment());
}
@Override
public void setSameSite(SameSite sameSite)
{
setSessionComment(HttpCookie.getCommentWithAttributes(getSessionComment(), false, sameSite));
}
} }

View File

@ -498,21 +498,33 @@ public class ServletContextResponse extends ContextResponse
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 httpOnlyFromComment = cookie.isHttpOnly() || HttpCookie.isHttpOnlyInComment(comment);
HttpCookie.SameSite sameSite = HttpCookie.getSameSiteFromComment(comment); HttpCookie.SameSite sameSiteFromComment = HttpCookie.getSameSiteFromComment(comment);
comment = HttpCookie.getCommentWithoutAttributes(comment); comment = HttpCookie.getCommentWithoutAttributes(comment);
//old style cookie
addCookie(new HttpCookie( if (sameSiteFromComment != null || httpOnlyFromComment)
cookie.getName(), {
cookie.getValue(), addCookie(new HttpCookie(
cookie.getDomain(), cookie.getName(),
cookie.getPath(), cookie.getValue(),
cookie.getMaxAge(), cookie.getDomain(),
httpOnly, cookie.getPath(),
cookie.getSecure(), cookie.getMaxAge(),
comment, httpOnlyFromComment,
cookie.getVersion(), cookie.getSecure(),
sameSite)); comment,
cookie.getVersion(),
sameSiteFromComment));
}
else
{
//new style cookie, everything is an attribute
addCookie(new HttpCookie(
cookie.getName(),
cookie.getValue(),
cookie.getVersion(),
cookie.getAttributes()));
}
} }
public void addCookie(HttpCookie cookie) public void addCookie(HttpCookie cookie)

View File

@ -20,8 +20,10 @@ import java.util.EventListener;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
@ -37,6 +39,7 @@ import jakarta.servlet.http.HttpSessionIdListener;
import jakarta.servlet.http.HttpSessionListener; import jakarta.servlet.http.HttpSessionListener;
import org.eclipse.jetty.ee10.servlet.ServletContextRequest.ServletApiRequest; import org.eclipse.jetty.ee10.servlet.ServletContextRequest.ServletApiRequest;
import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SameSite;
import org.eclipse.jetty.http.Syntax; import org.eclipse.jetty.http.Syntax;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
@ -45,6 +48,7 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.session.AbstractSessionManager; import org.eclipse.jetty.session.AbstractSessionManager;
import org.eclipse.jetty.session.Session; import org.eclipse.jetty.session.Session;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -102,8 +106,6 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Ne
*/ */
public final class CookieConfig implements SessionCookieConfig public final class CookieConfig implements SessionCookieConfig
{ {
private final Map<String, String> _attributes = new HashMap<>();
@Override @Override
public String getComment() public String getComment()
{ {
@ -125,21 +127,57 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Ne
@Override @Override
public void setAttribute(String name, String value) public void setAttribute(String name, String value)
{ {
// TODO check that context is not available checkState();
_attributes.put(name, value); String lcase = name.toLowerCase(Locale.ENGLISH);
switch (lcase)
{
case "name" -> setName(value);
case "max-age" -> setMaxAge(value == null ? -1 : Integer.parseInt(value));
case "comment" -> setComment(value);
case "domain" -> setDomain(value);
case "httponly" -> setHttpOnly(Boolean.valueOf(value));
case "secure" -> setSecure(Boolean.valueOf(value));
case "path" -> setPath(value);
default -> setSessionAttribute(name, value);
}
} }
@Override @Override
public String getAttribute(String name) public String getAttribute(String name)
{ {
// TODO use these attributes String lcase = name.toLowerCase(Locale.ENGLISH);
return _attributes.get(name); return switch (lcase)
{
case "name" -> getName();
case "max-age" -> Integer.toString(getMaxAge());
case "comment" -> getComment();
case "domain" -> getDomain();
case "httponly" -> String.valueOf(isHttpOnly());
case "secure" -> String.valueOf(isSecure());
case "path" -> getPath();
default -> getSessionAttribute(name);
};
} }
/**
* According to the SessionCookieConfig javadoc, the attributes must also include
* all values set by explicit setters.
* @see SessionCookieConfig
*/
@Override @Override
public Map<String, String> getAttributes() public Map<String, String> getAttributes()
{ {
return Collections.unmodifiableMap(_attributes); Map<String, String> specials = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
specials.put("name", getAttribute("name"));
specials.put("max-age", getAttribute("max-age"));
specials.put("comment", getAttribute("comment"));
specials.put("domain", getAttribute("domain"));
specials.put("httponly", getAttribute("httponly"));
specials.put("secure", getAttribute("secure"));
specials.put("path", getAttribute("path"));
specials.putAll(getSessionAttributes());
return Collections.unmodifiableMap(specials);
} }
@Override @Override
@ -353,6 +391,60 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Ne
return apiRequest == null ? null : apiRequest.getCoreSession(); return apiRequest == null ? null : apiRequest.getCoreSession();
} }
/**
* A session cookie is marked as secure IFF any of the following conditions are true:
* <ol>
* <li>SessionCookieConfig.setSecure == true</li>
* <li>SessionCookieConfig.setSecure == false &amp;&amp; _secureRequestOnly==true &amp;&amp; request is HTTPS</li>
* </ol>
* According to SessionCookieConfig javadoc, case 1 can be used when:
* "... even though the request that initiated the session came over HTTP,
* is to support a topology where the web container is front-ended by an
* SSL offloading load balancer. In this case, the traffic between the client
* and the load balancer will be over HTTPS, whereas the traffic between the
* load balancer and the web container will be over HTTP."
* <p>
* For case 2, you can use _secureRequestOnly to determine if you want the
* Servlet Spec 3.0 default behavior when SessionCookieConfig.setSecure==false,
* which is:
* <cite>
* "they shall be marked as secure only if the request that initiated the
* corresponding session was also secure"
* </cite>
* <p>
* The default for _secureRequestOnly is true, which gives the above behavior. If
* you set it to false, then a session cookie is NEVER marked as secure, even if
* the initiating request was secure.
*
* @param session the session to which the cookie should refer.
* @param contextPath the context to which the cookie should be linked.
* The client will only send the cookie value when requesting resources under this path.
* @param requestIsSecure whether the client is accessing the server over a secure protocol (i.e. HTTPS).
* @return if this <code>SessionManager</code> uses cookies, then this method will return a new
* {@link HttpCookie cookie object} that should be set on the client in order to link future HTTP requests
* with the <code>session</code>. If cookies are not in use, this method returns <code>null</code>.
*/
@Override
public HttpCookie getSessionCookie(Session session, String contextPath, boolean requestIsSecure)
{
if (isUsingCookies())
{
String sessionPath = getSessionPath();
sessionPath = (sessionPath == null) ? contextPath : sessionPath;
sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath;
return session.generateSetCookie((getSessionCookie() == null ? __DefaultSessionCookie : getSessionCookie()),
getSessionDomain(),
sessionPath,
getMaxCookieAge(),
isHttpOnly(),
isSecureCookies() || (isSecureRequestOnly() && requestIsSecure),
null,
0,
getSessionAttributes());
}
return null;
}
/** /**
* Adds an event listener for session-related events. * Adds an event listener for session-related events.
* *
@ -588,7 +680,28 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Ne
return Collections.emptySet(); return Collections.emptySet();
} }
@Override
public HttpCookie.SameSite getSameSite()
{
String sameSite = getSessionAttribute("SameSite");
if (sameSite == null)
return null;
return SameSite.valueOf(sameSite.toUpperCase(Locale.ENGLISH));
}
/**
* Set Session cookie sameSite mode.
* In ee10 this is set as a generic session cookie attribute.
*
* @param sameSite The sameSite setting for Session cookies (or null for no sameSite setting)
*/
@Override
public void setSameSite(HttpCookie.SameSite sameSite)
{
setSessionAttribute("SameSite", sameSite.getAttributeValue());
}
public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes) public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes)
{ {
if (sessionTrackingModes != null && if (sessionTrackingModes != null &&

View File

@ -61,6 +61,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThan;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
@ -459,6 +460,45 @@ public class SessionHandlerTest
return ""; return "";
} }
} }
@Test
public void testSessionCookie() throws Exception
{
Server server = new Server();
MockSessionIdManager idMgr = new MockSessionIdManager(server);
idMgr.setWorkerName("node1");
SessionHandler mgr = new SessionHandler();
MockSessionCache cache = new MockSessionCache(mgr);
cache.setSessionDataStore(new NullSessionDataStore());
mgr.setSessionCache(cache);
mgr.setSessionIdManager(idMgr);
long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
Session session = new Session(mgr, new SessionData("123", "_foo", "0.0.0.0", now, now, now, 30));
session.setExtendedId("123.node1");
SessionCookieConfig sessionCookieConfig = mgr.getSessionCookieConfig();
sessionCookieConfig.setName("SPECIAL");
sessionCookieConfig.setDomain("universe");
sessionCookieConfig.setHttpOnly(false);
sessionCookieConfig.setSecure(false);
sessionCookieConfig.setPath("/foo");
sessionCookieConfig.setMaxAge(99);
sessionCookieConfig.setAttribute("SameSite", "Strict");
sessionCookieConfig.setAttribute("ham", "cheese");
HttpCookie cookie = mgr.getSessionCookie(session, "/bar", false);
assertEquals("SPECIAL", cookie.getName());
assertEquals("universe", cookie.getDomain());
assertEquals("/foo", cookie.getPath());
assertFalse(cookie.isHttpOnly());
assertFalse(cookie.isSecure());
assertEquals(99, cookie.getMaxAge());
assertEquals(HttpCookie.SameSite.STRICT, cookie.getSameSite());
String cookieStr = cookie.getRFC6265SetCookie();
assertThat(cookieStr, containsString("; SameSite=Strict; ham=cheese"));
}
@Test @Test
public void testSecureSessionCookie() throws Exception public void testSecureSessionCookie() throws Exception

View File

@ -45,6 +45,7 @@ import org.eclipse.jetty.ee10.servlet.security.authentication.FormAuthenticator;
import org.eclipse.jetty.http.pathmap.ServletPathSpec; import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.util.ArrayUtil; import org.eclipse.jetty.util.ArrayUtil;
import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.xml.XmlParser; import org.eclipse.jetty.xml.XmlParser;
import org.eclipse.jetty.xml.XmlParser.Node; import org.eclipse.jetty.xml.XmlParser.Node;
@ -715,274 +716,103 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor
String name = cookieConfig.getString("name", false, true); String name = cookieConfig.getString("name", false, true);
if (name != null) if (name != null)
{ {
Origin origin = context.getMetaData().getOrigin("cookie-config.name"); addSessionConfigAttribute(context, descriptor, "name", name);
switch (origin)
{
case NotSet:
{
//no <cookie-config><name> set yet, accept it
context.getSessionHandler().getSessionCookieConfig().setName(name);
context.getMetaData().setOrigin("cookie-config.name", descriptor);
break;
}
case WebXml:
case WebDefaults:
case WebOverride:
{
//<cookie-config><name> set in a web xml, only allow web-default/web-override to change
if (!(descriptor instanceof FragmentDescriptor))
{
context.getSessionHandler().getSessionCookieConfig().setName(name);
context.getMetaData().setOrigin("cookie-config.name", descriptor);
}
break;
}
case WebFragment:
{
//a web-fragment set the value, all web-fragments must have the same value
//TODO: evaluate that is can never be null?!
if (!name.equals(context.getSessionHandler().getSessionCookieConfig().getName()))
throw new IllegalStateException("Conflicting cookie-config name " + name + " in " + descriptor.getResource());
break;
}
default:
unknownOrigin(origin);
}
} }
// <domain> // <domain>
String domain = cookieConfig.getString("domain", false, true); String domain = cookieConfig.getString("domain", false, true);
if (domain != null) if (domain != null)
{ {
Origin origin = context.getMetaData().getOrigin("cookie-config.domain"); addSessionConfigAttribute(context, descriptor, "domain", domain);
switch (origin)
{
case NotSet:
{
//no <cookie-config><domain> set yet, accept it
context.getSessionHandler().getSessionCookieConfig().setDomain(domain);
context.getMetaData().setOrigin("cookie-config.domain", descriptor);
break;
}
case WebXml:
case WebDefaults:
case WebOverride:
{
//<cookie-config><domain> set in a web xml, only allow web-default/web-override to change
if (!(descriptor instanceof FragmentDescriptor))
{
context.getSessionHandler().getSessionCookieConfig().setDomain(domain);
context.getMetaData().setOrigin("cookie-config.domain", descriptor);
}
break;
}
case WebFragment:
{
//a web-fragment set the value, all web-fragments must have the same value
if (!context.getSessionHandler().getSessionCookieConfig().getDomain().equals(domain))
throw new IllegalStateException("Conflicting cookie-config domain " + domain + " in " + descriptor.getResource());
break;
}
default:
unknownOrigin(origin);
}
} }
// <path> // <path>
String path = cookieConfig.getString("path", false, true); String path = cookieConfig.getString("path", false, true);
if (path != null) if (path != null)
{ {
Origin origin = context.getMetaData().getOrigin("cookie-config.path"); addSessionConfigAttribute(context, descriptor, "path", path);
switch (origin)
{
case NotSet:
{
//no <cookie-config><domain> set yet, accept it
context.getSessionHandler().getSessionCookieConfig().setPath(path);
context.getMetaData().setOrigin("cookie-config.path", descriptor);
break;
}
case WebXml:
case WebDefaults:
case WebOverride:
{
//<cookie-config><domain> set in a web xml, only allow web-default/web-override to change
if (!(descriptor instanceof FragmentDescriptor))
{
context.getSessionHandler().getSessionCookieConfig().setPath(path);
context.getMetaData().setOrigin("cookie-config.path", descriptor);
}
break;
}
case WebFragment:
{
//a web-fragment set the value, all web-fragments must have the same value
if (!path.equals(context.getSessionHandler().getSessionCookieConfig().getPath()))
throw new IllegalStateException("Conflicting cookie-config path " + path + " in " + descriptor.getResource());
break;
}
default:
unknownOrigin(origin);
}
} }
// <comment> // <comment>
String comment = cookieConfig.getString("comment", false, true); String comment = cookieConfig.getString("comment", false, true);
if (comment != null) if (comment != null)
{ {
Origin origin = context.getMetaData().getOrigin("cookie-config.comment"); addSessionConfigAttribute(context, descriptor, "comment", comment);
switch (origin)
{
case NotSet:
{
//no <cookie-config><comment> set yet, accept it
context.getSessionHandler().getSessionCookieConfig().setComment(comment);
context.getMetaData().setOrigin("cookie-config.comment", descriptor);
break;
}
case WebXml:
case WebDefaults:
case WebOverride:
{
//<cookie-config><comment> set in a web xml, only allow web-default/web-override to change
if (!(descriptor instanceof FragmentDescriptor))
{
context.getSessionHandler().getSessionCookieConfig().setComment(comment);
context.getMetaData().setOrigin("cookie-config.comment", descriptor);
}
break;
}
case WebFragment:
{
//a web-fragment set the value, all web-fragments must have the same value
if (!context.getSessionHandler().getSessionCookieConfig().getComment().equals(comment))
throw new IllegalStateException("Conflicting cookie-config comment " + comment + " in " + descriptor.getResource());
break;
}
default:
unknownOrigin(origin);
}
} }
// <http-only>true/false // <http-only>true/false
tNode = cookieConfig.get("http-only"); tNode = cookieConfig.get("http-only");
if (tNode != null) if (tNode != null)
{ {
boolean httpOnly = Boolean.parseBoolean(tNode.toString(false, true)); //TODO: note that this is not http-only
Origin origin = context.getMetaData().getOrigin("cookie-config.http-only"); addSessionConfigAttribute(context, descriptor, "HttpOnly", tNode.toString(false, true));
switch (origin)
{
case NotSet:
{
//no <cookie-config><http-only> set yet, accept it
context.getSessionHandler().getSessionCookieConfig().setHttpOnly(httpOnly);
context.getMetaData().setOrigin("cookie-config.http-only", descriptor);
break;
}
case WebXml:
case WebDefaults:
case WebOverride:
{
//<cookie-config><http-only> set in a web xml, only allow web-default/web-override to change
if (!(descriptor instanceof FragmentDescriptor))
{
context.getSessionHandler().getSessionCookieConfig().setHttpOnly(httpOnly);
context.getMetaData().setOrigin("cookie-config.http-only", descriptor);
}
break;
}
case WebFragment:
{
//a web-fragment set the value, all web-fragments must have the same value
if (context.getSessionHandler().getSessionCookieConfig().isHttpOnly() != httpOnly)
throw new IllegalStateException("Conflicting cookie-config http-only " + httpOnly + " in " + descriptor.getResource());
break;
}
default:
unknownOrigin(origin);
}
} }
// <secure>true/false // <secure>true/false
tNode = cookieConfig.get("secure"); tNode = cookieConfig.get("secure");
if (tNode != null) if (tNode != null)
{ {
boolean secure = Boolean.parseBoolean(tNode.toString(false, true)); addSessionConfigAttribute(context, descriptor, "secure", tNode.toString(false, true));
Origin origin = context.getMetaData().getOrigin("cookie-config.secure");
switch (origin)
{
case NotSet:
{
//no <cookie-config><secure> set yet, accept it
context.getSessionHandler().getSessionCookieConfig().setSecure(secure);
context.getMetaData().setOrigin("cookie-config.secure", descriptor);
break;
}
case WebXml:
case WebDefaults:
case WebOverride:
{
//<cookie-config><secure> set in a web xml, only allow web-default/web-override to change
if (!(descriptor instanceof FragmentDescriptor))
{
context.getSessionHandler().getSessionCookieConfig().setSecure(secure);
context.getMetaData().setOrigin("cookie-config.secure", descriptor);
}
break;
}
case WebFragment:
{
//a web-fragment set the value, all web-fragments must have the same value
if (context.getSessionHandler().getSessionCookieConfig().isSecure() != secure)
throw new IllegalStateException("Conflicting cookie-config secure " + secure + " in " + descriptor.getResource());
break;
}
default:
unknownOrigin(origin);
}
} }
// <max-age> // <max-age>
tNode = cookieConfig.get("max-age"); tNode = cookieConfig.get("max-age");
if (tNode != null) if (tNode != null)
{ {
int maxAge = Integer.parseInt(tNode.toString(false, true)); addSessionConfigAttribute(context, descriptor, "max-age", tNode.toString(false, true));
Origin origin = context.getMetaData().getOrigin("cookie-config.max-age"); }
switch (origin)
{ Iterator<XmlParser.Node> attributes = cookieConfig.iterator("attribute");
case NotSet: while (attributes.hasNext())
{ {
//no <cookie-config><max-age> set yet, accept it XmlParser.Node attribute = attributes.next();
context.getSessionHandler().getSessionCookieConfig().setMaxAge(maxAge); String aname = attribute.getString("attribute-name", false, true);
context.getMetaData().setOrigin("cookie-config.max-age", descriptor); String avalue = attribute.getString("attribute-value", false, true);
break; addSessionConfigAttribute(context, descriptor, aname, avalue);
}
case WebXml:
case WebDefaults:
case WebOverride:
{
//<cookie-config><max-age> set in a web xml, only allow web-default/web-override to change
if (!(descriptor instanceof FragmentDescriptor))
{
context.getSessionHandler().getSessionCookieConfig().setMaxAge(maxAge);
context.getMetaData().setOrigin("cookie-config.max-age", descriptor);
}
break;
}
case WebFragment:
{
//a web-fragment set the value, all web-fragments must have the same value
if (context.getSessionHandler().getSessionCookieConfig().getMaxAge() != maxAge)
throw new IllegalStateException("Conflicting cookie-config max-age " + maxAge + " in " + descriptor.getResource());
break;
}
default:
unknownOrigin(origin);
}
} }
} }
} }
public void addSessionConfigAttribute(WebAppContext context, Descriptor descriptor, String name, String value)
{
if (StringUtil.isBlank(name))
return;
Origin origin = context.getMetaData().getOrigin("cookie-config.attribute." + name);
switch (origin)
{
case NotSet:
{
//no <cookie-config> with attribute of that name set yet, accept it.
//if it is the max-age attribute, it must be set as an integer
context.getSessionHandler().getSessionCookieConfig().setAttribute(name, value);
context.getMetaData().setOrigin("cookie-config.attribute." + name, descriptor);
break;
}
case WebXml:
case WebDefaults:
case WebOverride:
{
//<cookie-config> with attribute of that name set in a web xml, only allow web-default/web-override to change
if (!(descriptor instanceof FragmentDescriptor))
{
context.getSessionHandler().getSessionCookieConfig().setAttribute(name, value);
context.getMetaData().setOrigin("cookie-config.attribute." + name, descriptor);
}
break;
}
case WebFragment:
{
//a web-fragment set an attribute of the same name, all web-fragments must have the same value
if (!StringUtil.nonNull(value).equals(StringUtil.nonNull(context.getSessionHandler().getSessionCookieConfig().getAttribute(name))))
throw new IllegalStateException("Conflicting attribute " + name + "=" + value + " in " + descriptor.getResource());
break;
}
default:
unknownOrigin(origin);
}
}
public void visitMimeMapping(WebAppContext context, Descriptor descriptor, XmlParser.Node node) public void visitMimeMapping(WebAppContext context, Descriptor descriptor, XmlParser.Node node)
{ {
String extension = node.getString("extension", false, true); String extension = node.getString("extension", false, true);

View File

@ -14,6 +14,8 @@
package org.eclipse.jetty.ee10.webapp; package org.eclipse.jetty.ee10.webapp;
import java.io.File; import java.io.File;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
@ -24,7 +26,10 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
public class StandardDescriptorProcessorTest public class StandardDescriptorProcessorTest
@ -57,5 +62,64 @@ public class StandardDescriptorProcessorTest
wac.setDescriptor(webXml.toURI().toURL().toString()); wac.setDescriptor(webXml.toURI().toURL().toString());
wac.start(); wac.start();
assertEquals(54, TimeUnit.SECONDS.toMinutes(wac.getSessionHandler().getMaxInactiveInterval())); assertEquals(54, TimeUnit.SECONDS.toMinutes(wac.getSessionHandler().getMaxInactiveInterval()));
//test the CookieConfig attributes and getters, and the getters on SessionHandler
//name
assertEquals("SPECIALSESSIONID", wac.getSessionHandler().getSessionCookieConfig().getName());
assertEquals("SPECIALSESSIONID", wac.getSessionHandler().getSessionCookieConfig().getAttribute("Name"));
assertEquals("SPECIALSESSIONID", wac.getSessionHandler().getSessionCookie());
//comment
assertEquals("nocomment", wac.getSessionHandler().getSessionCookieConfig().getComment());
assertEquals("nocomment", wac.getSessionHandler().getSessionCookieConfig().getAttribute("Comment"));
assertEquals("nocomment", wac.getSessionHandler().getSessionComment());
//domain
assertEquals("universe", wac.getSessionHandler().getSessionCookieConfig().getDomain());
assertEquals("universe", wac.getSessionHandler().getSessionCookieConfig().getAttribute("Domain"));
assertEquals("universe", wac.getSessionHandler().getSessionDomain());
//path
assertEquals("foo", wac.getSessionHandler().getSessionCookieConfig().getPath());
assertEquals("foo", wac.getSessionHandler().getSessionCookieConfig().getAttribute("Path"));
assertEquals("foo", wac.getSessionHandler().getSessionPath());
//max-age
assertEquals(10, wac.getSessionHandler().getSessionCookieConfig().getMaxAge());
assertEquals("10", wac.getSessionHandler().getSessionCookieConfig().getAttribute("Max-Age"));
assertEquals(10, wac.getSessionHandler().getMaxCookieAge());
//secure
assertEquals(false, wac.getSessionHandler().getSessionCookieConfig().isSecure());
assertEquals("false", wac.getSessionHandler().getSessionCookieConfig().getAttribute("Secure"));
assertEquals(false, wac.getSessionHandler().isSecureCookies());
//httponly
assertEquals(false, wac.getSessionHandler().getSessionCookieConfig().isHttpOnly());
assertEquals("false", wac.getSessionHandler().getSessionCookieConfig().getAttribute("HttpOnly"));
assertEquals(false, wac.getSessionHandler().isHttpOnly());
Map<String, String> attributes = wac.getSessionHandler().getSessionCookieConfig().getAttributes();
//SessionCookieConfig javadoc states that all setters must be also represented as attributes
assertThat(wac.getSessionHandler().getSessionCookieConfig().getAttributes().keySet(),
containsInAnyOrder(Arrays.asList(
equalToIgnoringCase("name"),
equalToIgnoringCase("comment"),
equalToIgnoringCase("domain"),
equalToIgnoringCase("path"),
equalToIgnoringCase("max-age"),
equalToIgnoringCase("secure"),
equalToIgnoringCase("httponly"),
equalToIgnoringCase("length"),
equalToIgnoringCase("width"),
equalToIgnoringCase("SameSite"))));
//test the attributes on SessionHandler do NOT contain the well-known ones of Name, Comment, Domain etc etc
assertThat(wac.getSessionHandler().getSessionAttributes().keySet(),
containsInAnyOrder(Arrays.asList(
equalToIgnoringCase("length"),
equalToIgnoringCase("width"),
equalToIgnoringCase("SameSite"))));
} }
} }

View File

@ -6,6 +6,27 @@
<display-name>Test 4 WebApp</display-name> <display-name>Test 4 WebApp</display-name>
<session-config> <session-config>
<cookie-config>
<domain>universe</domain>
<name>SPECIALSESSIONID</name>
<path>foo</path>
<max-age>10</max-age>
<comment>nocomment</comment>
<http-only>false</http-only>
<secure>false</secure>
<attribute>
<attribute-name>length</attribute-name>
<attribute-value>short</attribute-value>
</attribute>
<attribute>
<attribute-name>width</attribute-name>
<attribute-value>long</attribute-value>
</attribute>
<attribute>
<attribute-name>SameSite</attribute-name>
<attribute-value>Strict</attribute-value>
</attribute>
</cookie-config>
<session-timeout>54</session-timeout> <session-timeout>54</session-timeout>
</session-config> </session-config>

View File

@ -306,14 +306,28 @@ public class Request implements HttpServletRequest
HttpHeader header = field.getHeader(); HttpHeader header = field.getHeader();
if (header == HttpHeader.SET_COOKIE) if (header == HttpHeader.SET_COOKIE)
{ {
HttpCookie cookie = (field instanceof SetCookieHttpField) String cookieName;
? ((SetCookieHttpField)field).getHttpCookie() String cookieValue;
: new HttpCookie(field.getValue()); long cookieMaxAge;
if (field instanceof SetCookieHttpField)
if (cookie.getMaxAge() > 0) {
cookies.put(cookie.getName(), cookie.getValue()); HttpCookie cookie = ((SetCookieHttpField)field).getHttpCookie();
cookieName = cookie.getName();
cookieValue = cookie.getValue();
cookieMaxAge = cookie.getMaxAge();
}
else else
cookies.remove(cookie.getName()); {
Map<String, String> cookieFields = HttpCookie.extractBasics(field.getValue());
cookieName = cookieFields.get("name");
cookieValue = cookieFields.get("value");
cookieMaxAge = cookieFields.get("max-age") != null ? Long.valueOf(cookieFields.get("max-age")) : -1;
}
if (cookieMaxAge > 0)
cookies.put(cookieName, cookieValue);
else
cookies.remove(cookieName);
} }
} }

View File

@ -299,31 +299,17 @@ public class Response implements HttpServletResponse
if (field.getHeader() == HttpHeader.SET_COOKIE) if (field.getHeader() == HttpHeader.SET_COOKIE)
{ {
CookieCompliance compliance = getHttpChannel().getHttpConfiguration().getResponseCookieCompliance(); CookieCompliance compliance = getHttpChannel().getHttpConfiguration().getResponseCookieCompliance();
HttpCookie oldCookie; if (field instanceof HttpCookie.SetCookieHttpField)
if (field instanceof SetCookieHttpField) {
oldCookie = ((SetCookieHttpField)field).getHttpCookie(); if (!HttpCookie.match(((HttpCookie.SetCookieHttpField)field).getHttpCookie(), cookie.getName(), cookie.getDomain(), cookie.getPath()))
continue;
}
else else
oldCookie = new HttpCookie(field.getValue());
if (!cookie.getName().equals(oldCookie.getName()))
continue;
if (cookie.getDomain() == null)
{ {
if (oldCookie.getDomain() != null) if (!HttpCookie.match(field.getValue(), cookie.getName(), cookie.getDomain(), cookie.getPath()))
continue; continue;
} }
else if (!cookie.getDomain().equalsIgnoreCase(oldCookie.getDomain()))
continue;
if (cookie.getPath() == null)
{
if (oldCookie.getPath() != null)
continue;
}
else if (!cookie.getPath().equals(oldCookie.getPath()))
continue;
i.set(new SetCookieHttpField(checkSameSite(cookie), compliance)); i.set(new SetCookieHttpField(checkSameSite(cookie), compliance));
return; return;

View File

@ -19,6 +19,7 @@ import java.util.EnumSet;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.EventListener; import java.util.EventListener;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@ -39,6 +40,7 @@ import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionIdListener; import jakarta.servlet.http.HttpSessionIdListener;
import jakarta.servlet.http.HttpSessionListener; import jakarta.servlet.http.HttpSessionListener;
import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SameSite;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.session.AbstractSessionManager; import org.eclipse.jetty.session.AbstractSessionManager;
import org.eclipse.jetty.session.Session; import org.eclipse.jetty.session.Session;
@ -46,6 +48,7 @@ import org.eclipse.jetty.session.SessionCache;
import org.eclipse.jetty.session.SessionConfig; import org.eclipse.jetty.session.SessionConfig;
import org.eclipse.jetty.session.SessionIdManager; import org.eclipse.jetty.session.SessionIdManager;
import org.eclipse.jetty.session.SessionManager; import org.eclipse.jetty.session.SessionManager;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -567,6 +570,81 @@ public class SessionHandler extends ScopedHandler implements SessionConfig.Mutab
return super.resolveRequestedSessionId(request); return super.resolveRequestedSessionId(request);
} }
@Override
public HttpCookie.SameSite getSameSite()
{
return HttpCookie.getSameSiteFromComment(getSessionComment());
}
/**
* Set Session cookie sameSite mode.
*
* @param sameSite The sameSite setting for Session cookies (or null for no sameSite setting)
*/
@Override
public void setSameSite(HttpCookie.SameSite sameSite)
{
setSessionComment(HttpCookie.getCommentWithAttributes(getSessionComment(), false, sameSite));
}
/**
* A session cookie is marked as secure IFF any of the following conditions are true:
* <ol>
* <li>SessionCookieConfig.setSecure == true</li>
* <li>SessionCookieConfig.setSecure == false &amp;&amp; _secureRequestOnly==true &amp;&amp; request is HTTPS</li>
* </ol>
* According to SessionCookieConfig javadoc, case 1 can be used when:
* "... even though the request that initiated the session came over HTTP,
* is to support a topology where the web container is front-ended by an
* SSL offloading load balancer. In this case, the traffic between the client
* and the load balancer will be over HTTPS, whereas the traffic between the
* load balancer and the web container will be over HTTP."
* <p>
* For case 2, you can use _secureRequestOnly to determine if you want the
* Servlet Spec 3.0 default behavior when SessionCookieConfig.setSecure==false,
* which is:
* <cite>
* "they shall be marked as secure only if the request that initiated the
* corresponding session was also secure"
* </cite>
* <p>
* The default for _secureRequestOnly is true, which gives the above behavior. If
* you set it to false, then a session cookie is NEVER marked as secure, even if
* the initiating request was secure.
*
* @param session the session to which the cookie should refer.
* @param contextPath the context to which the cookie should be linked.
* The client will only send the cookie value when requesting resources under this path.
* @param requestIsSecure whether the client is accessing the server over a secure protocol (i.e. HTTPS).
* @return if this <code>SessionManager</code> uses cookies, then this method will return a new
* {@link HttpCookie cookie object} that should be set on the client in order to link future HTTP requests
* with the <code>session</code>. If cookies are not in use, this method returns <code>null</code>.
*/
@Override
public HttpCookie getSessionCookie(Session session, String contextPath, boolean requestIsSecure)
{
if (isUsingCookies())
{
String sessionPath = getSessionPath();
sessionPath = (sessionPath == null) ? contextPath : sessionPath;
sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath;
SameSite sameSite = HttpCookie.getSameSiteFromComment(getSessionComment());
Map<String, String> attributes = Collections.emptyMap();
if (sameSite != null)
attributes = Collections.singletonMap("SameSite", sameSite.getAttributeValue());
return session.generateSetCookie((getSessionCookie() == null ? __DefaultSessionCookie : getSessionCookie()),
getSessionDomain(),
sessionPath,
getMaxCookieAge(),
isHttpOnly(),
isSecureCookies() || (isSecureRequestOnly() && requestIsSecure),
HttpCookie.getCommentWithoutAttributes(getSessionComment()),
0,
attributes);
}
return null;
}
@Override @Override
public void callSessionAttributeListeners(Session session, String name, Object old, Object value) public void callSessionAttributeListeners(Session session, String name, Object old, Object value)
{ {

View File

@ -19,19 +19,27 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashSet; import java.util.HashSet;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import jakarta.servlet.SessionCookieConfig;
import jakarta.servlet.SessionTrackingMode; import jakarta.servlet.SessionTrackingMode;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionEvent; import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener; import jakarta.servlet.http.HttpSessionListener;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.session.AbstractSessionCache;
import org.eclipse.jetty.session.DefaultSessionIdManager;
import org.eclipse.jetty.session.NullSessionDataStore;
import org.eclipse.jetty.session.Session; import org.eclipse.jetty.session.Session;
import org.eclipse.jetty.session.SessionData; import org.eclipse.jetty.session.SessionData;
import org.eclipse.jetty.session.SessionManager;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -43,6 +51,7 @@ import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals; 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.assertThrows;
public class SessionHandlerTest public class SessionHandlerTest
@ -147,6 +156,47 @@ public class SessionHandlerTest
{ {
_server.stop(); _server.stop();
} }
@Test
public void testSessionCookie() throws Exception
{
Server server = new Server();
MockSessionIdManager idMgr = new MockSessionIdManager(server);
idMgr.setWorkerName("node1");
SessionHandler mgr = new SessionHandler();
MockSessionCache cache = new MockSessionCache(mgr.getSessionManager());
cache.setSessionDataStore(new NullSessionDataStore());
mgr.setSessionCache(cache);
mgr.setSessionIdManager(idMgr);
long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
Session session = new Session(mgr.getSessionManager(), new SessionData("123", "_foo", "0.0.0.0", now, now, now, 30));
session.setExtendedId("123.node1");
SessionCookieConfig sessionCookieConfig = mgr.getSessionCookieConfig();
sessionCookieConfig.setName("SPECIAL");
sessionCookieConfig.setDomain("universe");
sessionCookieConfig.setHttpOnly(false);
sessionCookieConfig.setSecure(false);
sessionCookieConfig.setPath("/foo");
sessionCookieConfig.setMaxAge(99);
//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);
HttpCookie cookie = mgr.getSessionManager().getSessionCookie(session, "/bar", false);
assertEquals("SPECIAL", cookie.getName());
assertEquals("universe", cookie.getDomain());
assertEquals("/foo", cookie.getPath());
assertFalse(cookie.isHttpOnly());
assertFalse(cookie.isSecure());
assertEquals(99, cookie.getMaxAge());
assertEquals(HttpCookie.SameSite.STRICT, cookie.getSameSite());
String cookieStr = cookie.getRFC6265SetCookie();
assertThat(cookieStr, containsString("; SameSite=Strict"));
}
@Test @Test
public void testSessionTrackingMode() public void testSessionTrackingMode()
@ -370,4 +420,81 @@ public class SessionHandlerTest
assertThat(content, containsString("Session=" + id.substring(0, id.indexOf(".node0")))); assertThat(content, containsString("Session=" + id.substring(0, id.indexOf(".node0"))));
assertThat(content, containsString("attribute = value")); assertThat(content, containsString("attribute = value"));
} }
public class MockSessionCache extends AbstractSessionCache
{
public MockSessionCache(SessionManager manager)
{
super(manager);
}
@Override
public void shutdown()
{
}
@Override
public Session doGet(String key)
{
return null;
}
@Override
public Session doPutIfAbsent(String key, Session session)
{
return null;
}
@Override
public Session doDelete(String key)
{
return null;
}
@Override
public boolean doReplace(String id, Session oldValue, Session newValue)
{
return false;
}
@Override
public Session newSession(SessionData data)
{
return null;
}
@Override
protected Session doComputeIfAbsent(String id, Function<String, Session> mappingFunction)
{
return mappingFunction.apply(id);
}
}
public class MockSessionIdManager extends DefaultSessionIdManager
{
public MockSessionIdManager(Server server)
{
super(server);
}
@Override
public boolean isIdInUse(String id)
{
return false;
}
@Override
public void expireAll(String id)
{
}
@Override
public String renewSessionId(String oldClusterId, String oldNodeId, org.eclipse.jetty.server.Request request)
{
return "";
}
}
} }

View File

@ -57,5 +57,34 @@ public class StandardDescriptorProcessorTest
wac.setDescriptor(webXml.toURI().toURL().toString()); wac.setDescriptor(webXml.toURI().toURL().toString());
wac.start(); wac.start();
assertEquals(54, TimeUnit.SECONDS.toMinutes(wac.getSessionHandler().getMaxInactiveInterval())); assertEquals(54, TimeUnit.SECONDS.toMinutes(wac.getSessionHandler().getMaxInactiveInterval()));
//test the CookieConfig attributes and getters, and the getters on SessionHandler
//name
assertEquals("SPECIALSESSIONID", wac.getSessionHandler().getSessionCookieConfig().getName());
assertEquals("SPECIALSESSIONID", wac.getSessionHandler().getSessionCookie());
//comment
assertEquals("nocomment", wac.getSessionHandler().getSessionCookieConfig().getComment());
assertEquals("nocomment", wac.getSessionHandler().getSessionComment());
//domain
assertEquals("universe", wac.getSessionHandler().getSessionCookieConfig().getDomain());
assertEquals("universe", wac.getSessionHandler().getSessionDomain());
//path
assertEquals("foo", wac.getSessionHandler().getSessionCookieConfig().getPath());
assertEquals("foo", wac.getSessionHandler().getSessionPath());
//max-age
assertEquals(10, wac.getSessionHandler().getSessionCookieConfig().getMaxAge());
assertEquals(10, wac.getSessionHandler().getMaxCookieAge());
//secure
assertEquals(false, wac.getSessionHandler().getSessionCookieConfig().isSecure());
assertEquals(false, wac.getSessionHandler().isSecureCookies());
//httponly
assertEquals(false, wac.getSessionHandler().getSessionCookieConfig().isHttpOnly());
assertEquals(false, wac.getSessionHandler().isHttpOnly());
} }
} }

View File

@ -6,7 +6,15 @@
<display-name>Test 4 WebApp</display-name> <display-name>Test 4 WebApp</display-name>
<session-config> <session-config>
<cookie-config>
<domain>universe</domain>
<name>SPECIALSESSIONID</name>
<path>foo</path>
<max-age>10</max-age>
<comment>nocomment</comment>
<http-only>false</http-only>
<secure>false</secure>
</cookie-config>
<session-timeout>54</session-timeout> <session-timeout>54</session-timeout>
</session-config> </session-config>
</web-app> </web-app>