* Issue #8408 Implement new session cookie attributes
This commit is contained in:
parent
d210677182
commit
42f72268cf
|
@ -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));
|
||||
|
||||
java.net.HttpCookie cookie = cookies.get(0);
|
||||
//remove all of the well-known attributes, leaving only those pass-through ones
|
||||
_domain = _attributes.remove("Domain");
|
||||
_path = _attributes.remove("Path");
|
||||
|
||||
_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();
|
||||
}
|
||||
|
||||
|
@ -492,6 +511,109 @@ public class HttpCookie
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
@ -357,28 +361,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 && _secureRequestOnly==true && 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()
|
||||
{
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 && _secureRequestOnly==true && 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.
|
||||
*
|
||||
|
@ -589,6 +681,27 @@ 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 &&
|
||||
|
|
|
@ -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;
|
||||
|
@ -460,6 +461,45 @@ public class SessionHandlerTest
|
|||
}
|
||||
}
|
||||
|
||||
@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
|
||||
{
|
||||
|
|
|
@ -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,271 +716,100 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"))));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -300,30 +300,16 @@ public class Response implements HttpServletResponse
|
|||
{
|
||||
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;
|
||||
|
|
|
@ -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 && _secureRequestOnly==true && 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)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
@ -148,6 +157,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 "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue