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;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.Attributes;
@ -76,7 +80,7 @@ public class HttpCookie
private final int _version;
private final boolean _httpOnly;
private final long _expiration;
private final SameSite _sameSite;
private final Map<String, String> _attributes;
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)
{
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)
{
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;
_value = value;
@ -115,29 +125,26 @@ public class HttpCookie
_comment = comment;
_version = version;
_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);
if (cookies.size() != 1)
throw new IllegalStateException();
_name = name;
_value = value;
_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);
_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();
String tmp = _attributes.remove("Max-Age");
_maxAge = StringUtil.isBlank(tmp) ? -1L : Long.valueOf(tmp);
_expiration = _maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(_maxAge);
// support for SameSite values has not yet been added to java.net.HttpCookie
_sameSite = getSameSiteFromComment(cookie.getComment());
_httpOnly = Boolean.parseBoolean(_attributes.remove("HttpOnly"));
_secure = Boolean.parseBoolean(_attributes.remove("Secure"));
_comment = _attributes.remove("Comment");
}
/**
@ -209,7 +216,10 @@ public class HttpCookie
*/
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");
if (_httpOnly)
buf.append("; HttpOnly");
if (_sameSite != null)
String sameSite = _attributes.get("SameSite");
if (sameSite != null)
{
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();
}
@ -491,6 +510,109 @@ public class HttpCookie
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
@ -563,7 +685,7 @@ public class HttpCookie
return _cookie;
}
}
/**
* Check that samesite is set on the cookie. If not, use a
* context default value, if one has been set.

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.http;
import java.util.Collections;
import java.util.stream.Stream;
import org.eclipse.jetty.http.HttpCookie.SameSite;
@ -64,9 +65,28 @@ public class HttpCookieTest
}
@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
@ -131,6 +151,9 @@ public class HttpCookieTest
httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1, HttpCookie.SameSite.STRICT);
assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Strict", httpCookie.getRFC6265SetCookie());
httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1, 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()

View File

@ -147,28 +147,15 @@ public interface Response extends Content.Sink
CookieCompliance compliance = httpConfiguration.getResponseCookieCompliance();
HttpCookie oldCookie;
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
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;
}
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));
return;

View File

@ -15,10 +15,13 @@ package org.eclipse.jetty.session;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@ -77,6 +80,7 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
private String _sessionDomain;
private String _sessionPath;
private String _sessionComment;
private final Map<String, String> _sessionAttributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private boolean _secureCookies = false;
private boolean _secureRequestOnly = true;
private int _maxCookieAge = -1;
@ -356,28 +360,6 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
{
_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();
@ -479,65 +461,6 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
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
public String getSessionCookie()
{
@ -565,6 +488,25 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
_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
public SessionIdManager getSessionIdManager()
{

View File

@ -14,9 +14,11 @@
package org.eclipse.jetty.session;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Condition;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
@ -148,6 +150,14 @@ public class Session
_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.
*/

View File

@ -13,15 +13,19 @@
package org.eclipse.jetty.session;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SameSite;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil;
/**
* SimpleSessionHandler example
@ -190,4 +194,41 @@ public class SimpleSessionHandler extends AbstractSessionManager implements Hand
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;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
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.Server;
import org.eclipse.jetty.session.Session.APISession;
import org.eclipse.jetty.util.StringUtil;
/**
* TestSessionHandler
@ -173,4 +177,41 @@ public class TestableSessionManager extends AbstractSessionManager
{
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();
// 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);
HttpCookie.SameSite sameSite = HttpCookie.getSameSiteFromComment(comment);
boolean httpOnlyFromComment = cookie.isHttpOnly() || HttpCookie.isHttpOnlyInComment(comment);
HttpCookie.SameSite sameSiteFromComment = HttpCookie.getSameSiteFromComment(comment);
comment = HttpCookie.getCommentWithoutAttributes(comment);
addCookie(new HttpCookie(
cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
httpOnly,
cookie.getSecure(),
comment,
cookie.getVersion(),
sameSite));
//old style cookie
if (sameSiteFromComment != null || httpOnlyFromComment)
{
addCookie(new HttpCookie(
cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
httpOnlyFromComment,
cookie.getSecure(),
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)

View File

@ -20,8 +20,10 @@ import java.util.EventListener;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArrayList;
import jakarta.servlet.ServletContext;
@ -37,6 +39,7 @@ import jakarta.servlet.http.HttpSessionIdListener;
import jakarta.servlet.http.HttpSessionListener;
import org.eclipse.jetty.ee10.servlet.ServletContextRequest.ServletApiRequest;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SameSite;
import org.eclipse.jetty.http.Syntax;
import org.eclipse.jetty.server.Handler;
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.session.AbstractSessionManager;
import org.eclipse.jetty.session.Session;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -102,8 +106,6 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Ne
*/
public final class CookieConfig implements SessionCookieConfig
{
private final Map<String, String> _attributes = new HashMap<>();
@Override
public String getComment()
{
@ -125,21 +127,57 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Ne
@Override
public void setAttribute(String name, String value)
{
// TODO check that context is not available
_attributes.put(name, value);
checkState();
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
public String getAttribute(String name)
{
// TODO use these attributes
return _attributes.get(name);
String lcase = name.toLowerCase(Locale.ENGLISH);
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
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
@ -353,6 +391,60 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Ne
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.
*
@ -588,7 +680,28 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Ne
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)
{
if (sessionTrackingModes != null &&

View File

@ -61,6 +61,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -459,6 +460,45 @@ public class SessionHandlerTest
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
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.util.ArrayUtil;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.xml.XmlParser;
import org.eclipse.jetty.xml.XmlParser.Node;
@ -715,274 +716,103 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor
String name = cookieConfig.getString("name", false, true);
if (name != null)
{
Origin origin = context.getMetaData().getOrigin("cookie-config.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);
}
addSessionConfigAttribute(context, descriptor, "name", name);
}
// <domain>
String domain = cookieConfig.getString("domain", false, true);
if (domain != null)
{
Origin origin = context.getMetaData().getOrigin("cookie-config.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);
}
addSessionConfigAttribute(context, descriptor, "domain", domain);
}
// <path>
String path = cookieConfig.getString("path", false, true);
if (path != null)
{
Origin origin = context.getMetaData().getOrigin("cookie-config.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);
}
addSessionConfigAttribute(context, descriptor, "path", path);
}
// <comment>
String comment = cookieConfig.getString("comment", false, true);
if (comment != null)
{
Origin origin = context.getMetaData().getOrigin("cookie-config.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);
}
addSessionConfigAttribute(context, descriptor, "comment", comment);
}
// <http-only>true/false
tNode = cookieConfig.get("http-only");
if (tNode != null)
{
boolean httpOnly = Boolean.parseBoolean(tNode.toString(false, true));
Origin origin = context.getMetaData().getOrigin("cookie-config.http-only");
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);
}
//TODO: note that this is not http-only
addSessionConfigAttribute(context, descriptor, "HttpOnly", tNode.toString(false, true));
}
// <secure>true/false
tNode = cookieConfig.get("secure");
if (tNode != null)
{
boolean secure = Boolean.parseBoolean(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);
}
addSessionConfigAttribute(context, descriptor, "secure", tNode.toString(false, true));
}
// <max-age>
tNode = cookieConfig.get("max-age");
if (tNode != null)
{
int maxAge = Integer.parseInt(tNode.toString(false, true));
Origin origin = context.getMetaData().getOrigin("cookie-config.max-age");
switch (origin)
{
case NotSet:
{
//no <cookie-config><max-age> set yet, accept it
context.getSessionHandler().getSessionCookieConfig().setMaxAge(maxAge);
context.getMetaData().setOrigin("cookie-config.max-age", descriptor);
break;
}
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);
}
addSessionConfigAttribute(context, descriptor, "max-age", tNode.toString(false, true));
}
Iterator<XmlParser.Node> attributes = cookieConfig.iterator("attribute");
while (attributes.hasNext())
{
XmlParser.Node attribute = attributes.next();
String aname = attribute.getString("attribute-name", false, true);
String avalue = attribute.getString("attribute-value", false, true);
addSessionConfigAttribute(context, descriptor, aname, avalue);
}
}
}
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)
{
String extension = node.getString("extension", false, true);

View File

@ -14,6 +14,8 @@
package org.eclipse.jetty.ee10.webapp;
import java.io.File;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.server.Server;
@ -24,7 +26,10 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
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;
public class StandardDescriptorProcessorTest
@ -57,5 +62,64 @@ public class StandardDescriptorProcessorTest
wac.setDescriptor(webXml.toURI().toURL().toString());
wac.start();
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>
<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-config>

View File

@ -306,14 +306,28 @@ public class Request implements HttpServletRequest
HttpHeader header = field.getHeader();
if (header == HttpHeader.SET_COOKIE)
{
HttpCookie cookie = (field instanceof SetCookieHttpField)
? ((SetCookieHttpField)field).getHttpCookie()
: new HttpCookie(field.getValue());
if (cookie.getMaxAge() > 0)
cookies.put(cookie.getName(), cookie.getValue());
String cookieName;
String cookieValue;
long cookieMaxAge;
if (field instanceof SetCookieHttpField)
{
HttpCookie cookie = ((SetCookieHttpField)field).getHttpCookie();
cookieName = cookie.getName();
cookieValue = cookie.getValue();
cookieMaxAge = cookie.getMaxAge();
}
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)
{
CookieCompliance compliance = getHttpChannel().getHttpConfiguration().getResponseCookieCompliance();
HttpCookie oldCookie;
if (field instanceof SetCookieHttpField)
oldCookie = ((SetCookieHttpField)field).getHttpCookie();
if (field instanceof HttpCookie.SetCookieHttpField)
{
if (!HttpCookie.match(((HttpCookie.SetCookieHttpField)field).getHttpCookie(), cookie.getName(), cookie.getDomain(), cookie.getPath()))
continue;
}
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;
}
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));
return;

View File

@ -19,6 +19,7 @@ import java.util.EnumSet;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
@ -39,6 +40,7 @@ import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionIdListener;
import jakarta.servlet.http.HttpSessionListener;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SameSite;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.session.AbstractSessionManager;
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.SessionIdManager;
import org.eclipse.jetty.session.SessionManager;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -567,6 +570,81 @@ public class SessionHandler extends ScopedHandler implements SessionConfig.Mutab
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
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.Enumeration;
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.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.LocalConnector;
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.SessionData;
import org.eclipse.jetty.session.SessionManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class SessionHandlerTest
@ -147,6 +156,47 @@ public class SessionHandlerTest
{
_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
public void testSessionTrackingMode()
@ -370,4 +420,81 @@ public class SessionHandlerTest
assertThat(content, containsString("Session=" + id.substring(0, id.indexOf(".node0"))));
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.start();
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>
<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-config>
</web-app>