diff --git a/core/src/main/java/io/undertow/server/Connectors.java b/core/src/main/java/io/undertow/server/Connectors.java index 8bb06b0e77..75ebe208f3 100644 --- a/core/src/main/java/io/undertow/server/Connectors.java +++ b/core/src/main/java/io/undertow/server/Connectors.java @@ -113,15 +113,15 @@ public class Connectors { } } - KNOWN_ATTRIBUTE_NAMES.add("Path"); - KNOWN_ATTRIBUTE_NAMES.add("Domain"); - KNOWN_ATTRIBUTE_NAMES.add("Discard"); - KNOWN_ATTRIBUTE_NAMES.add("Secure"); - KNOWN_ATTRIBUTE_NAMES.add("HttpOnly"); - KNOWN_ATTRIBUTE_NAMES.add("Max-Age"); - KNOWN_ATTRIBUTE_NAMES.add("Expires"); - KNOWN_ATTRIBUTE_NAMES.add("Comment"); - KNOWN_ATTRIBUTE_NAMES.add("SameSite"); + KNOWN_ATTRIBUTE_NAMES.add(Cookie.COOKIE_PATH_ATTR); + KNOWN_ATTRIBUTE_NAMES.add(Cookie.COOKIE_DOMAIN_ATTR); + KNOWN_ATTRIBUTE_NAMES.add(Cookie.COOKIE_DISCARD_ATTR); + KNOWN_ATTRIBUTE_NAMES.add(Cookie.COOKIE_SECURE_ATTR); + KNOWN_ATTRIBUTE_NAMES.add(Cookie.COOKIE_HTTP_ONLY_ATTR); + KNOWN_ATTRIBUTE_NAMES.add(Cookie.COOKIE_MAX_AGE_ATTR); + KNOWN_ATTRIBUTE_NAMES.add(Cookie.COOKIE_EXPIRES_ATTR); + KNOWN_ATTRIBUTE_NAMES.add(Cookie.COOKIE_COMMENT_ATTR); + KNOWN_ATTRIBUTE_NAMES.add(Cookie.COOKIE_SAME_SITE_ATTR); } /** * Flattens the exchange cookie map into the response header map. This should be called by a diff --git a/core/src/main/java/io/undertow/server/handlers/Cookie.java b/core/src/main/java/io/undertow/server/handlers/Cookie.java index 8b4d894cb5..776458e734 100644 --- a/core/src/main/java/io/undertow/server/handlers/Cookie.java +++ b/core/src/main/java/io/undertow/server/handlers/Cookie.java @@ -29,6 +29,16 @@ */ public interface Cookie extends Comparable { + String COOKIE_COMMENT_ATTR = "Comment"; + String COOKIE_DOMAIN_ATTR = "Domain"; + String COOKIE_MAX_AGE_ATTR = "Max-Age"; + String COOKIE_PATH_ATTR = "Path"; + String COOKIE_SECURE_ATTR = "Secure"; + String COOKIE_HTTP_ONLY_ATTR = "HttpOnly"; + String COOKIE_SAME_SITE_ATTR = "SameSite"; + String COOKIE_DISCARD_ATTR = "Discard"; + String COOKIE_EXPIRES_ATTR = "Expires"; + String getName(); String getValue(); @@ -75,6 +85,7 @@ default boolean isSameSite() { return false; } + @Deprecated(forRemoval = true) default Cookie setSameSite(final boolean sameSite) { throw new UnsupportedOperationException("Not implemented"); } @@ -101,6 +112,7 @@ default String getAttribute(final String name) { /** * Sets an attribute for the cookie. If the value is {@code null}, the attribute is removed. If the value is not * {@code null}, the attribute is added to the attributes for this cookie. + * If name match pre-existing attribute, like "{@link Cookie#COOKIE_PATH_ATTR}" it will override that value * * @param name the name of the attribute * @param value the value of the attribute or {@code null} to remove it diff --git a/core/src/main/java/io/undertow/server/handlers/CookieImpl.java b/core/src/main/java/io/undertow/server/handlers/CookieImpl.java index 1e6ba2cc70..6f96b75973 100644 --- a/core/src/main/java/io/undertow/server/handlers/CookieImpl.java +++ b/core/src/main/java/io/undertow/server/handlers/CookieImpl.java @@ -19,12 +19,17 @@ package io.undertow.server.handlers; import java.util.Arrays; +import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import java.util.TreeMap; import io.undertow.UndertowLogger; import io.undertow.UndertowMessages; +import io.undertow.util.DateUtils; /** * @author Stuart Douglas @@ -32,18 +37,36 @@ */ public class CookieImpl implements Cookie { + private static final Set STANDARD_ATTR_NAMES; + static { + Set tmp = new HashSet(8); + tmp.add(COOKIE_COMMENT_ATTR); + tmp.add(COOKIE_DOMAIN_ATTR); + tmp.add(COOKIE_MAX_AGE_ATTR); + tmp.add(COOKIE_PATH_ATTR); + tmp.add(COOKIE_SECURE_ATTR); + tmp.add(COOKIE_HTTP_ONLY_ATTR); + tmp.add(COOKIE_SAME_SITE_ATTR); + tmp.add(COOKIE_DISCARD_ATTR); + STANDARD_ATTR_NAMES = Collections.unmodifiableSet(tmp); + } + + private static final Integer DEFAULT_MAX_AGE = Integer.valueOf(-1); + private static final boolean DEFAULT_HTTP_ONLY = false; + private static final boolean DEFAULT_SECURE = false; + private static final boolean DEFAULT_DISCARD = false; + private final String name; private String value; private String path; private String domain; - private Integer maxAge; + private Integer maxAge = DEFAULT_MAX_AGE; private Date expires; private boolean discard; - private boolean secure; - private boolean httpOnly; + private boolean secure = DEFAULT_SECURE; + private boolean httpOnly = DEFAULT_HTTP_ONLY; private int version = 0; private String comment; - private boolean sameSite; private String sameSiteMode; private final Map attributes; @@ -58,6 +81,16 @@ public CookieImpl(final String name) { this.attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } + public CookieImpl(final String name, final String value, final Cookie cookiePrimer) { + this.name = name; + this.value = value; + this.attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + //attribis will be synced one way or ther other, might as well just iterate over attrib + for (Entry primers : cookiePrimer.getAttributes().entrySet()) { + this.setAttribute(primers.getKey(), primers.getValue()); + } + } + public String getName() { return name; } @@ -77,6 +110,7 @@ public String getPath() { public CookieImpl setPath(final String path) { this.path = path; + setAttribute(COOKIE_PATH_ATTR, path, false); return this; } @@ -86,6 +120,7 @@ public String getDomain() { public CookieImpl setDomain(final String domain) { this.domain = domain; + setAttribute(COOKIE_DOMAIN_ATTR, domain, false); return this; } @@ -95,6 +130,7 @@ public Integer getMaxAge() { public CookieImpl setMaxAge(final Integer maxAge) { this.maxAge = maxAge; + setAttribute(COOKIE_MAX_AGE_ATTR, String.valueOf(maxAge), false); return this; } @@ -104,6 +140,7 @@ public boolean isDiscard() { public CookieImpl setDiscard(final boolean discard) { this.discard = discard; + setAttribute(COOKIE_DISCARD_ATTR, String.valueOf(discard), false); return this; } @@ -113,6 +150,7 @@ public boolean isSecure() { public CookieImpl setSecure(final boolean secure) { this.secure = secure; + setAttribute(COOKIE_SECURE_ATTR, String.valueOf(secure), false); return this; } @@ -131,6 +169,7 @@ public boolean isHttpOnly() { public CookieImpl setHttpOnly(final boolean httpOnly) { this.httpOnly = httpOnly; + setAttribute(COOKIE_HTTP_ONLY_ATTR, String.valueOf(httpOnly), false); return this; } @@ -140,6 +179,11 @@ public Date getExpires() { public CookieImpl setExpires(final Date expires) { this.expires = expires; + if(expires != null) { + setAttribute(COOKIE_EXPIRES_ATTR, DateUtils.toDateString(expires), false); + } else { + setAttribute(COOKIE_EXPIRES_ATTR, null, false); + } return this; } @@ -148,18 +192,19 @@ public String getComment() { } public Cookie setComment(final String comment) { + setAttribute(COOKIE_COMMENT_ATTR, comment, false); this.comment = comment; return this; } @Override public boolean isSameSite() { - return sameSite; + return this.sameSiteMode != null; } @Override public Cookie setSameSite(final boolean sameSite) { - this.sameSite = sameSite; + //NOP return this; } @@ -174,7 +219,7 @@ public Cookie setSameSiteMode(final String mode) { if (m != null) { UndertowLogger.REQUEST_LOGGER.tracef("Setting SameSite mode to [%s] for cookie [%s]", m, this.name); this.sameSiteMode = m; - this.setSameSite(true); + setAttribute(COOKIE_SAME_SITE_ATTR, mode, false); } else { UndertowLogger.REQUEST_LOGGER.warnf(UndertowMessages.MESSAGES.invalidSameSiteMode(mode, Arrays.toString(CookieSameSiteMode.values())), "Ignoring specified SameSite mode [%s] for cookie [%s]", mode, this.name); } @@ -188,9 +233,78 @@ public String getAttribute(final String name) { @Override public Cookie setAttribute(final String name, final String value) { + return setAttribute(name, value, true); + } + + protected Cookie setAttribute(final String name, final String value, boolean performSync) { + // less than ideal, but users may want to fiddle with it like that, we need to sync if (value != null) { + if (performSync) { + switch (name) { + case COOKIE_COMMENT_ATTR: + this.comment = value; + break; + case COOKIE_DOMAIN_ATTR: + this.domain = value; + break; + case COOKIE_HTTP_ONLY_ATTR: + this.httpOnly = Boolean.parseBoolean(value); + break; + case COOKIE_MAX_AGE_ATTR: + this.maxAge = Integer.parseInt(value); + break; + case COOKIE_PATH_ATTR: + this.path = value; + break; + case COOKIE_SAME_SITE_ATTR: + // enum will match constant name, no inner representation + this.sameSiteMode = CookieSameSiteMode.valueOf(value.toUpperCase()).toString(); + break; + case COOKIE_SECURE_ATTR: + this.secure = Boolean.valueOf(value); + break; + case COOKIE_DISCARD_ATTR: + this.discard = Boolean.valueOf(value); + break; + case COOKIE_EXPIRES_ATTR: + this.expires = DateUtils.parseDate(value); + break; + } + } + attributes.put(name, value); } else { + switch (name) { + case COOKIE_COMMENT_ATTR: + this.comment = null; + break; + case COOKIE_DOMAIN_ATTR: + this.domain = null; + break; + case COOKIE_HTTP_ONLY_ATTR: + this.httpOnly = DEFAULT_HTTP_ONLY; + break; + case COOKIE_MAX_AGE_ATTR: + this.maxAge = DEFAULT_MAX_AGE; + break; + case COOKIE_PATH_ATTR: + this.path = null; + break; + case COOKIE_SAME_SITE_ATTR: + // enum will match constant name, no inner representation + this.sameSiteMode = null; + break; + case COOKIE_SECURE_ATTR: + this.secure = DEFAULT_SECURE; + break; + case COOKIE_DISCARD_ATTR: + this.discard = DEFAULT_DISCARD; + break; + case COOKIE_EXPIRES_ATTR: + this.expires = null; + break; + } + attributes.remove(name); } return this; diff --git a/core/src/main/java/io/undertow/server/session/SessionCookieConfig.java b/core/src/main/java/io/undertow/server/session/SessionCookieConfig.java index 65e64bee44..cb2ad2819b 100644 --- a/core/src/main/java/io/undertow/server/session/SessionCookieConfig.java +++ b/core/src/main/java/io/undertow/server/session/SessionCookieConfig.java @@ -18,6 +18,8 @@ package io.undertow.server.session; +import java.util.Map; + import io.undertow.UndertowLogger; import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.Cookie; @@ -33,15 +35,9 @@ public class SessionCookieConfig implements SessionConfig { public static final String DEFAULT_SESSION_ID = "JSESSIONID"; - + public static final String DEFAULT_PATH = "/"; private String cookieName = DEFAULT_SESSION_ID; - private String path = "/"; - private String domain; - private boolean discard; - private boolean secure; - private boolean httpOnly; - private int maxAge = -1; - private String comment; + private CookieImpl kernel = new CookieImpl(cookieName); @Override @@ -49,30 +45,24 @@ public String rewriteUrl(final String originalUrl, final String sessionId) { return originalUrl; } + public SessionCookieConfig() { + super(); + //NOTE some client dont consider lack of path as "/"... + this.kernel.setPath(DEFAULT_PATH); + } + @Override public void setSessionId(final HttpServerExchange exchange, final String sessionId) { - Cookie cookie = new CookieImpl(cookieName, sessionId) - .setPath(path) - .setDomain(domain) - .setDiscard(discard) - .setSecure(secure) - .setHttpOnly(httpOnly) - .setComment(comment); - if (maxAge > 0) { - cookie.setMaxAge(maxAge); - } + + Cookie cookie = new CookieImpl(cookieName, sessionId, this.kernel); + exchange.setResponseCookie(cookie); UndertowLogger.SESSION_LOGGER.tracef("Setting session cookie session id %s on %s", sessionId, exchange); } @Override public void clearSession(final HttpServerExchange exchange, final String sessionId) { - Cookie cookie = new CookieImpl(cookieName, sessionId) - .setPath(path) - .setDomain(domain) - .setDiscard(discard) - .setSecure(secure) - .setHttpOnly(httpOnly) + Cookie cookie = new CookieImpl(cookieName, sessionId, this.kernel) .setMaxAge(0); exchange.setResponseCookie(cookie); UndertowLogger.SESSION_LOGGER.tracef("Clearing session cookie session id %s on %s", sessionId, exchange); @@ -103,65 +93,78 @@ public SessionCookieConfig setCookieName(final String cookieName) { } public String getPath() { - return path; + return this.kernel.getPath(); } public SessionCookieConfig setPath(final String path) { - this.path = path; + this.kernel.setPath(path); return this; } public String getDomain() { - return domain; + return this.kernel.getDomain(); } public SessionCookieConfig setDomain(final String domain) { - this.domain = domain; + this.kernel.setDomain(domain); return this; } public boolean isDiscard() { - return discard; + return this.kernel.isDiscard(); } public SessionCookieConfig setDiscard(final boolean discard) { - this.discard = discard; + this.kernel.setDiscard(discard); return this; } public boolean isSecure() { - return secure; + return this.kernel.isSecure(); } public SessionCookieConfig setSecure(final boolean secure) { - this.secure = secure; + this.kernel.setSecure(secure); return this; } public boolean isHttpOnly() { - return httpOnly; + return this.kernel.isHttpOnly(); } public SessionCookieConfig setHttpOnly(final boolean httpOnly) { - this.httpOnly = httpOnly; + this.kernel.setHttpOnly(httpOnly); return this; } public int getMaxAge() { - return maxAge; + return kernel.getMaxAge(); } public SessionCookieConfig setMaxAge(final int maxAge) { - this.maxAge = maxAge; + this.kernel.setMaxAge(maxAge); return this; } public String getComment() { - return comment; + return this.kernel.getComment(); } public SessionCookieConfig setComment(final String comment) { - this.comment = comment; + this.kernel.setComment(comment); return this; } + + public SessionCookieConfig setAttribute(final String name, final String value) { + kernel.setAttribute(name, value); + return this; + } + + public String getAttribute(final String name) { + return kernel.getAttribute(name); + } + + public Map getAttributes() { + return kernel.getAttributes(); + } } diff --git a/core/src/main/java/io/undertow/util/Cookies.java b/core/src/main/java/io/undertow/util/Cookies.java index a892900734..45a485b26a 100644 --- a/core/src/main/java/io/undertow/util/Cookies.java +++ b/core/src/main/java/io/undertow/util/Cookies.java @@ -173,7 +173,6 @@ private static void handleValue(CookieImpl cookie, String key, String value) { } else if (key.equalsIgnoreCase("comment")) { cookie.setComment(value); } else if (key.equalsIgnoreCase("samesite")) { - cookie.setSameSite(true); cookie.setSameSiteMode(value); } else { cookie.setAttribute(key, value); diff --git a/servlet/src/main/java/io/undertow/servlet/spec/SessionCookieConfigImpl.java b/servlet/src/main/java/io/undertow/servlet/spec/SessionCookieConfigImpl.java index 5169035153..1148834103 100644 --- a/servlet/src/main/java/io/undertow/servlet/spec/SessionCookieConfigImpl.java +++ b/servlet/src/main/java/io/undertow/servlet/spec/SessionCookieConfigImpl.java @@ -18,35 +18,21 @@ package io.undertow.servlet.spec; +import java.util.Map; + import io.undertow.server.HttpServerExchange; import io.undertow.server.session.SessionConfig; import io.undertow.servlet.UndertowServletMessages; - import jakarta.servlet.SessionCookieConfig; -import java.util.Collections; -import java.util.Map; -import java.util.TreeMap; - /** * @author Stuart Douglas */ public class SessionCookieConfigImpl implements SessionCookieConfig, SessionConfig { - private static final String COOKIE_COMMENT_ATTR = "Comment"; - private static final String COOKIE_DOMAIN_ATTR = "Domain"; - private static final String COOKIE_MAX_AGE_ATTR = "Max-Age"; - private static final String COOKIE_PATH_ATTR = "Path"; - private static final String COOKIE_SECURE_ATTR = "Secure"; - private static final String COOKIE_HTTP_ONLY_ATTR = "HttpOnly"; - private final ServletContextImpl servletContext; private final io.undertow.server.session.SessionCookieConfig delegate; private SessionConfig fallback; - private static final int DEFAULT_MAX_AGE = -1; - private static final boolean DEFAULT_HTTP_ONLY = false; - private static final boolean DEFAULT_SECURE = false; - private final Map attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); public SessionCookieConfigImpl(final ServletContextImpl servletContext) { this.servletContext = servletContext; @@ -55,7 +41,7 @@ public SessionCookieConfigImpl(final ServletContextImpl servletContext) { @Override public String rewriteUrl(final String originalUrl, final String sessionid) { - if(fallback != null) { + if (fallback != null) { return fallback.rewriteUrl(originalUrl, sessionid); } return originalUrl; @@ -74,10 +60,10 @@ public void clearSession(final HttpServerExchange exchange, final String session @Override public String findSessionId(final HttpServerExchange exchange) { String existing = delegate.findSessionId(exchange); - if(existing != null) { + if (existing != null) { return existing; } - if(fallback != null) { + if (fallback != null) { return fallback.findSessionId(exchange); } return null; @@ -89,8 +75,8 @@ public SessionCookieSource sessionCookieSource(HttpServerExchange exchange) { if (existing != null) { return SessionCookieSource.COOKIE; } - if(fallback != null) { - String id = fallback.findSessionId(exchange); + if (fallback != null) { + String id = fallback.findSessionId(exchange); return id != null ? fallback.sessionCookieSource(exchange) : SessionCookieSource.NONE; } return SessionCookieSource.NONE; @@ -101,86 +87,78 @@ public String getName() { } public void setName(final String name) { - if(servletContext.isInitialized()) { + if (servletContext.isInitialized()) { throw UndertowServletMessages.MESSAGES.servletContextAlreadyInitialized(); } delegate.setCookieName(name); } public String getDomain() { - return getAttribute(COOKIE_DOMAIN_ATTR); + return delegate.getDomain(); } public void setDomain(final String domain) { - if(servletContext.isInitialized()) { + if (servletContext.isInitialized()) { throw UndertowServletMessages.MESSAGES.servletContextAlreadyInitialized(); } delegate.setDomain(domain); - setAttribute(COOKIE_DOMAIN_ATTR, domain); } public String getPath() { - return getAttribute(COOKIE_PATH_ATTR); + return delegate.getPath(); } public void setPath(final String path) { - if(servletContext.isInitialized()) { + if (servletContext.isInitialized()) { throw UndertowServletMessages.MESSAGES.servletContextAlreadyInitialized(); } delegate.setPath(path); - setAttribute(COOKIE_PATH_ATTR, path); } @Deprecated public String getComment() { - return getAttribute(COOKIE_COMMENT_ATTR); + return delegate.getComment(); } @Deprecated public void setComment(final String comment) { - if(servletContext.isInitialized()) { + if (servletContext.isInitialized()) { throw UndertowServletMessages.MESSAGES.servletContextAlreadyInitialized(); } delegate.setComment(comment); - setAttribute(COOKIE_COMMENT_ATTR, comment); } public boolean isHttpOnly() { - String value = getAttribute(COOKIE_HTTP_ONLY_ATTR); - return value == null ? DEFAULT_HTTP_ONLY : Boolean.parseBoolean(value); + return delegate.isHttpOnly(); } public void setHttpOnly(final boolean httpOnly) { - if(servletContext.isInitialized()) { + if (servletContext.isInitialized()) { throw UndertowServletMessages.MESSAGES.servletContextAlreadyInitialized(); } delegate.setHttpOnly(httpOnly); - setAttribute(COOKIE_HTTP_ONLY_ATTR, String.valueOf(httpOnly)); } public boolean isSecure() { - String value = getAttribute(COOKIE_SECURE_ATTR); - return value == null ? DEFAULT_SECURE : Boolean.parseBoolean(value); } + return delegate.isSecure(); + } public void setSecure(final boolean secure) { - if(servletContext.isInitialized()) { + if (servletContext.isInitialized()) { throw UndertowServletMessages.MESSAGES.servletContextAlreadyInitialized(); } delegate.setSecure(secure); - setAttribute(COOKIE_SECURE_ATTR, String.valueOf(secure)); } public int getMaxAge() { - String value = getAttribute(COOKIE_MAX_AGE_ATTR); - return value == null ? DEFAULT_MAX_AGE : Integer.parseInt(value); + return delegate.getMaxAge(); } public void setMaxAge(final int maxAge) { - if(servletContext.isInitialized()) { + if (servletContext.isInitialized()) { throw UndertowServletMessages.MESSAGES.servletContextAlreadyInitialized(); } this.delegate.setMaxAge(maxAge); - setAttribute(COOKIE_MAX_AGE_ATTR, String.valueOf(maxAge)); } public SessionConfig getFallback() { @@ -193,19 +171,19 @@ public void setFallback(final SessionConfig fallback) { @Override public void setAttribute(final String name, final String value) { - if(servletContext.isInitialized()) { + if (servletContext.isInitialized()) { throw UndertowServletMessages.MESSAGES.servletContextAlreadyInitialized(); } - attributes.put(name, value); + delegate.setAttribute(name, value); } @Override public String getAttribute(final String name) { - return attributes.get(name); + return delegate.getAttribute(name); } @Override public Map getAttributes() { - return Collections.unmodifiableMap(attributes); + return delegate.getAttributes(); } } diff --git a/servlet/src/test/java/io/undertow/servlet/test/session/SessionAttributesTestCase.java b/servlet/src/test/java/io/undertow/servlet/test/session/SessionAttributesTestCase.java new file mode 100644 index 0000000000..0e4934b4db --- /dev/null +++ b/servlet/src/test/java/io/undertow/servlet/test/session/SessionAttributesTestCase.java @@ -0,0 +1,135 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2025 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.undertow.servlet.test.session; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; + +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xnio.OptionMap; + +import io.undertow.UndertowOptions; +import io.undertow.server.handlers.PathHandler; +import io.undertow.servlet.api.DeploymentInfo; +import io.undertow.servlet.api.DeploymentManager; +import io.undertow.servlet.api.ListenerInfo; +import io.undertow.servlet.api.ServletContainer; +import io.undertow.servlet.api.ServletInfo; +import io.undertow.servlet.test.util.TestClassIntrospector; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.HttpClientUtils; +import io.undertow.testutils.TestHttpClient; +import io.undertow.util.StatusCodes; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; +import jakarta.servlet.ServletException; +import jakarta.servlet.SessionTrackingMode; + +/** + * @author baranowb + */ +@RunWith(DefaultServer.class) +public class SessionAttributesTestCase { + + + @BeforeClass + public static void setup() throws ServletException { + + + final PathHandler pathHandler = new PathHandler(); + final ServletContainer container = ServletContainer.Factory.newInstance(); + DeploymentInfo builder = new DeploymentInfo() + .setClassLoader(SessionAttributesTestCase.class.getClassLoader()) + .setContextPath("/servletContext") + .setClassIntrospecter(TestClassIntrospector.INSTANCE) + .setDeploymentName("servletContext.war") + .addListener(new ListenerInfo(SessionAttributesTestCase.SessionCookieConfigListener.class)) + .addServlets(new ServletInfo("servlet", SessionServlet.class) + .addMappings("/aa/attributes")); + DeploymentManager manager = container.addDeployment(builder); + manager.deploy(); + try { + pathHandler.addPrefixPath(builder.getContextPath(), manager.start()); + } catch (ServletException e) { + throw new RuntimeException(e); + } + DefaultServer.setUndertowOptions(OptionMap.create(UndertowOptions.ENABLE_RFC6265_COOKIE_VALIDATION, Boolean.TRUE)); + DefaultServer.setRootHandler(pathHandler); + } + + @AfterClass + public static void deSetup() throws ServletException { + DefaultServer.setUndertowOptions(OptionMap.EMPTY); + } + + @Test + public void testSameSiteAndCustomAttribute() throws IOException { + TestHttpClient client = new TestHttpClient(); + try { + HttpGet get = new HttpGet(DefaultServer.getDefaultServerURL() + "/servletContext/aa/attributes"); + HttpResponse result = client.execute(get); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + String response = HttpClientUtils.readResponse(result); + Assert.assertEquals("1", response); + String cookieValue = result.getHeaders("Set-Cookie")[0].getValue(); + Assert.assertTrue(cookieValue, cookieValue.contains("MySessionCookie")); + Assert.assertTrue(cookieValue, cookieValue.contains("/servletContext/aa/")); + Assert.assertTrue(cookieValue, cookieValue.contains("SameSite=Strict")); + Assert.assertTrue(cookieValue, cookieValue.contains("Space=Above&Beyond")); + //double back to regular session test thats done by counterparts + result = client.execute(get); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + response = HttpClientUtils.readResponse(result); + Assert.assertEquals("2", response); + + result = client.execute(get); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + response = HttpClientUtils.readResponse(result); + Assert.assertEquals("3", response); + + + } finally { + client.getConnectionManager().shutdown(); + } + } + + private static class SessionCookieConfigListener implements ServletContextListener { + @Override + public void contextInitialized(final ServletContextEvent sce) { + final ServletContext servletContext = sce.getServletContext(); + servletContext.getSessionCookieConfig().setName("MySessionCookie"); + servletContext.getSessionCookieConfig().setPath("/servletContext/aa/"); + servletContext.getSessionCookieConfig().setAttribute("SameSite", "Strict"); + servletContext.getSessionCookieConfig().setAttribute("Space", "Above&Beyond"); + servletContext.setSessionTrackingModes(new HashSet<>(Arrays.asList(SessionTrackingMode.COOKIE, SessionTrackingMode.URL))); + } + + @Override + public void contextDestroyed(final ServletContextEvent sce) { + + } + } +} \ No newline at end of file