From da35211a1a26be0dbf901d947adfcf28efd0c416 Mon Sep 17 00:00:00 2001 From: mansal Date: Thu, 30 Jan 2025 09:55:08 -0500 Subject: [PATCH] WIP Improvements on base message cookie handling --- .../http/netty/message/NettyBaseMessage.java | 151 ++++++++++---- .../netty/message/NettyRequestMessage.java | 2 +- .../netty/message/NettyResponseMessage.java | 4 +- .../http/netty/cookie/CookieDecoder.java | 186 ++++++++---------- .../http/netty/cookie/CookieEncoder.java | 144 +++++++++++--- .../test/com/ibm/ws/netty/CookieTests.java | 172 ---------------- 6 files changed, 306 insertions(+), 353 deletions(-) delete mode 100644 dev/com.ibm.ws.transport.http/test/com/ibm/ws/netty/CookieTests.java diff --git a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyBaseMessage.java b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyBaseMessage.java index ab2d93066839..566e0e016c8c 100644 --- a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyBaseMessage.java +++ b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyBaseMessage.java @@ -56,6 +56,8 @@ import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http2.HttpConversionUtil; import io.openliberty.http.constants.HttpGenerics; +import io.openliberty.http.netty.cookie.CookieDecoder; +import io.openliberty.http.netty.cookie.CookieEncoder; /** * @@ -491,10 +493,31 @@ public int getLimitOfTokenSize() { @Override public byte[] getCookieValue(String name) { - // TODO Auto-generated method stub - return null; + if (name == null) { return null;} + + HttpCookie cookie = getCookie(name); + if(cookie == null){ return null;} + + String value = cookie.getValue(); + if(value == null || value.isEmpty()){ + return null; + } + return value.getBytes(StandardCharsets.UTF_8); } + /** + * Populates the provided {@code list} with all values associated to the + * given cookie {@code name} for the specific {@code header}. + * + * This method checks the local cache for the specified header. If the header + * has not been parsed yet, the {@link #parseAllCookies(CookieCacheData, HttpHeaderKeys)} + * is used to decode cookie values for that header. All matching cookie values + * are returned and added to the provided list. + * + * @param name the name of the cookie whose value should be retrieved + * @param header the {@link HttpHeaderKeys} indicating desired header name + * @param list a modifiable {@link List} where matching cookie values will be added + */ protected void getAllCookieValues(String name, HttpHeaderKeys header, List list) { if (!cookieCacheExists(header) && !containsHeader(header)) { return; @@ -504,13 +527,63 @@ protected void getAllCookieValues(String name, HttpHeaderKeys header, List vals = getHeaders(header); - int size = vals.size(); + List headerFields = getHeaders(header); + int size = headerFields.size(); if (size != 0) { for (int i = cache.getHeaderIndex(); i < size; i++) { - List list = getCookieParser().parse(vals.get(i).asBytes(), header); - cache.addParsedCookies(list); + String headerValue = headerFields.get(i).asString(); + List decodedCookies = CookieDecoder.decode(headerValue, header); + cache.addParsedCookies(decodedCookies); cache.incrementHeaderIndex(); // search the list of new cookies from this header instance - Iterator it = list.iterator(); - while (it.hasNext()) { - cookie = it.next(); - // cookie names are case-sensitive - if (cookie.getName().equals(name)) { + for(HttpCookie c: decodedCookies){ + if (c.getName().equals(name)) { if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { Tr.debug(tc, "Found parsed Cookie-->" + name); } - return cookie; + return c; } } } @@ -555,24 +626,37 @@ protected HttpCookie getCookie(String name, HttpHeaderKeys header) { @Override public List getAllCookies() { List list = new LinkedList(); - getAllCookies(HttpHeaderKeys.HDR_SET_COOKIE, list); - getAllCookies(HttpHeaderKeys.HDR_SET_COOKIE2, list); + + if(isIncoming()){ + getAllCookies(HttpHeaderKeys.HDR_COOKIE, list); + getAllCookies(HttpHeaderKeys.HDR_COOKIE2, list); + }else{ + getAllCookies(HttpHeaderKeys.HDR_SET_COOKIE, list); + getAllCookies(HttpHeaderKeys.HDR_SET_COOKIE2, list); + } + if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(tc, "getAllCookies: Found " + list.size() + " instances"); + Tr.debug(tc, "getAllCookies: Found " + list.size() + " cookie(s). Is incoming: "+isIncoming()); } return list; } @Override public List getAllCookies(String name) { - List list = new LinkedList(); - if (null != name) { + if(name == null){ return list;} + + + if (isIncoming()) { + getAllCookies(name, HttpHeaderKeys.HDR_COOKIE, list); + getAllCookies(name, HttpHeaderKeys.HDR_COOKIE2, list); + } else { getAllCookies(name, HttpHeaderKeys.HDR_SET_COOKIE, list); getAllCookies(name, HttpHeaderKeys.HDR_SET_COOKIE2, list); } if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { - Tr.debug(tc, "getAllCookies: Found " + list.size() + " instances of " + name); + Tr.debug(tc, "getAllCookies: Found " + list.size() + + " cookie(s) matching [" + name + "]. Is incoming: "+ isIncoming()); } return list; } @@ -736,27 +820,20 @@ private void parseAllCookies(CookieCacheData cache, HttpHeaderKeys header) { // Iterate through the unparsed cookie header instances // in storage and add them to the list to be returned - List vals = getHeaders(header); - int size = vals.size(); - if (size != 0) { - for (int i = cache.getHeaderIndex(); i < size; i++) { - cache.addParsedCookies(getCookieParser().parse(vals.get(i).asBytes(), header)); + List headerList = getHeaders(header); + int size = headerList.size(); + + if(size == 0) {return;} + + for (int i = cache.getHeaderIndex(); i < size; i++) { + String headerValue = headerList.get(i).asString(); + + cache.addParsedCookies(CookieDecoder.decode(headerValue, header)); cache.incrementHeaderIndex(); - } } } - /** - * Get access to the cookie parser for this message. - * - * @return An instance of the Cookie header parser - */ - private CookieHeaderByteParser getCookieParser() { - if (null == this.cookieParser) { - this.cookieParser = new CookieHeaderByteParser(); - } - return this.cookieParser; - } + @Override public boolean isIncoming() { diff --git a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyRequestMessage.java b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyRequestMessage.java index 210b682c671c..f7073f9e78c0 100644 --- a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyRequestMessage.java +++ b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyRequestMessage.java @@ -995,7 +995,7 @@ public List getAllCookies() { List list = new LinkedList(); List cookieHeaders = headers.getAll(HttpHeaderNames.COOKIE); for(String cookie: cookieHeaders){ - list.addAll(CookieDecoder.decode(cookie)); + list.addAll(CookieDecoder.decodeServerCookies(cookie)); } return list; diff --git a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyResponseMessage.java b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyResponseMessage.java index ecd36ed48489..aad9c53a42ab 100644 --- a/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyResponseMessage.java +++ b/dev/com.ibm.ws.transport.http/src/com/ibm/ws/http/netty/message/NettyResponseMessage.java @@ -580,8 +580,8 @@ public List getAllCookieValues(String name) { List list = new LinkedList(); if (null != name) { - getAllCookieValues(name, HttpHeaderKeys.HDR_SET_COOKIE, list); - getAllCookieValues(name, HttpHeaderKeys.HDR_SET_COOKIE2, list); + list.addAll(getAllCookieValues(name, HttpHeaderKeys.HDR_SET_COOKIE)); + list.addAll(getAllCookieValues(name, HttpHeaderKeys.HDR_SET_COOKIE2)); } if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) { Tr.debug(tc, "getAllCookieValues: Found " + list.size() + " instances of " + name); diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/netty/cookie/CookieDecoder.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/netty/cookie/CookieDecoder.java index 9be777d23b71..d3c3978cabc3 100644 --- a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/netty/cookie/CookieDecoder.java +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/netty/cookie/CookieDecoder.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023, 2024 IBM Corporation and others. + * Copyright (c) 2023, 2025 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at @@ -10,135 +10,103 @@ package io.openliberty.http.netty.cookie; import java.util.Collections; -import java.util.LinkedList; import java.util.List; -import java.util.Set; - -import com.ibm.ws.http.dispatcher.internal.HttpDispatcher; +import com.ibm.ws.http.channel.internal.cookies.CookieHeaderByteParser; import com.ibm.wsspi.http.HttpCookie; - -import io.netty.handler.codec.http.cookie.Cookie; -import io.netty.handler.codec.http.cookie.ServerCookieDecoder; +import com.ibm.wsspi.http.channel.values.HttpHeaderKeys; /** - * A utility class that decodes HTTP cookie header strings into a list of - * {@link HttpCookie} instances. This class supports both standard - * servlet-style cookies and uses Netty's cookie decoding for broader - * compatibility when not in a Servlet 6 environment. - * - * When Servlet 6 cookies are in use, the cookie string is parsed manually - * to allow for additional flexibility and handling of attributes that may - * begin with a dollar sign ('$'). For other scenarios, cookies are decoded - * using Netty’s {@link ServerCookieDecoder}, which operates in LAX mode - * by default. - * - * Decoding Modes: LAX vs STRICT - * - * Netty’s {@link ServerCookieDecoder} provides two modes for decoding cookies: + * A utility class that decodes HTTP "Cookie" and "Set-Cookie" headers. This + * considers cookies that adhere to either legacy or current RFC specifications. + * After parsing the header String, a list of {@link HttpCookie} objects is + * provided. * - * STRICT Mode: Enforces a strict interpretation of the RFC 6265 - * specification. Cookies that deviate from the RFC’s formatting requirements - * (e.g., improper character usage, missing key-value pairs) will be rejected. - * This mode ensures maximum standards compliance but may discard cookies - * that are slightly non-compliant. - * - * LAX Mode: Allows a more permissive decoding of cookies, accepting - * certain non-standard or legacy formatting patterns. While it may decode - * cookies that STRICT mode would reject, it provides broader compatibility - * with cookies encountered in real-world scenarios. - * - * - * By default, this class uses LAX mode to maximize compatibility. + * Note: Support for "Cookie2" and "Set-Cookie2" (described by RFC 2965), is + * considered depracated. Most modern clients and browsers follow the newer + * RFC 6265. This utility provides parsing of these older versions for + * compatibility considerations. */ public class CookieDecoder { - private CookieDecoder(){} - + private CookieDecoder(){ + //Private singleton + } + /** - * Decodes the provided cookie header string into a list of - * {@link HttpCookie} objects. + * Decodes a "Cookie" header string into a list of {@link HttpCookie} objects. * - * If Servlet 6 cookies are enabled, the cookie string is parsed - * directly to accommodate additional features or attributes required - * for Servlet 6. + * Use {@link #decodeSerCookies2(String)} if needing to decode RFC 2965 "Cookie2" + * headers. * - * If Servlet 6 cookies are not enabled, this method leverages the - * use of Netty’s LAX mode decoder, which accepts a wide range of - * cookie formats. - * - * @param cookieString the raw cookie header string, as received from an - * HTTP object - * @return a list of decoded {@link HttpCookie} instances, or an empty list - * if the input is null or empty + * @param cookieString the "Cookie" header value, e.g. "name=value; foo=bar" + * @return a list of {@link HttpCookie} instances, or an empty list if the input + * is {@code null} or empty. */ - public static List decode(String cookieString) { - - if (cookieString == null || cookieString.isEmpty()) { - return Collections.emptyList(); - } - - List list = new LinkedList(); - boolean isServlet6Cookie = HttpDispatcher.useEE10Cookies(); - boolean foundDollarSign = false; - String skipDollarSignName = null; - String name = null; - String value = null; - String trimmedRawValue = null; - - if (isServlet6Cookie) { - String[] rawCookies = cookieString.split(";"); - for (String rawCookie : rawCookies) { - trimmedRawValue = rawCookie.trim(); - if(trimmedRawValue.isEmpty()){ - continue; - } - - foundDollarSign = ('$' == trimmedRawValue.charAt(0)); - - String[] splitCookie = rawCookie.split("=", 2); - if (splitCookie.length == 2) { - name = splitCookie[0].trim(); - value = splitCookie[1].trim(); - - } else { - name = trimmedRawValue; - } - - if (foundDollarSign) { - skipDollarSignName = name.substring(1); - //$version skipped per Servlet specification - if ("version".equalsIgnoreCase(skipDollarSignName)) { - continue; - } - } + public static List decodeServerCookies(String cookieString) { + + return decode(cookieString, HttpHeaderKeys.HDR_COOKIE); + } - list.add(new HttpCookie(name, value)); - name = null; - value = null; - } + /** + * Decodes a "Cookie2" header string (as described by RFC 2965) into a list + * of {@link HttpCookie} objects. + * + * Note: modern clients generally do not send Cookie2 headers, so this is + * considered a deprecated type of header. + * + * @param cookieString the "Cookie2" header value + * @return a list of {@link HttpCookie} instances, or an empty list if the + * input is {@code null} or empty. + */ + public static List decodeServerCookies2(String cookieString){ + return decode(cookieString, HttpHeaderKeys.HDR_COOKIE2); + } - } else { - Set cookies = decodeNetty(cookieString); - for (Cookie c : cookies) { - list.add(new HttpCookie(c.name(), c.value())); - } - } + /** + * Decodes a "Set-Cookie" header string into a list of {@link HttpCookie} objects. + * + * Use {@link #decodeSetCookieHeader2(String)} if needing to decode RFC 2965 + * "Set-Cookie2" headers. + * + * @param cookieString the "Set-Cookie" header value, e.g "name=value; Path=/" + * @return a list of {@link HttpCookie} instances, or an empty list if the + * input is {@code null} or empty. + */ + public static List decodeSetCookieHeader(String cookieString){ + return decode(cookieString, HttpHeaderKeys.HDR_SET_COOKIE); + } - return list; + /** + * Decodes a "Set-Cookie2" header string (as described by RFC 2965) into a + * list of {@link HttpCookie} objects. + * + * Note: modern clients generally do not support Set-Cookie2, so this is + * considered a deprecated type of header. This is present for compatibility + * with older implementation specifications. + * + * @param cookieString the "Set-Cookie2" header value + * @return a list of {@link HttpCookie} instances, or an empty list if the + * input is {@code null} or empty. + */ + public static List decodeSetCookie2Header(String cookieString){ + return decode(cookieString, HttpHeaderKeys.HDR_SET_COOKIE2); } + /** - * Decodes the given cookie header string using Netty’s LAX mode cookie - * decoding. + * Decodes a cookie header string into a list of {@link HttpCookie} objetcts. + * The header key parameter determines the type of cookie header being parsed. * - * @param cookieString the raw cookie header string, as received from an HTTP object - * @return a set of decoded Netty {@link Cookie} instances, or an empty set - * if the input is null or empty + * @param cookieString the cookie header value + * @param header the cookie header type used to determine parsing logic + * @return a list of {@link HttpCookie} instances, or an empty list if the + * input is {@code null} or empty. */ - public static Set decodeNetty(String cookieString) { + public static List decode(String cookieString, HttpHeaderKeys header){ if (cookieString == null || cookieString.isEmpty()) { - return Collections.emptySet(); + return Collections.emptyList(); } - return ServerCookieDecoder.LAX.decode(cookieString); + CookieHeaderByteParser parser = new CookieHeaderByteParser(); + return parser.parse(cookieString.getBytes(), header); } } \ No newline at end of file diff --git a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/netty/cookie/CookieEncoder.java b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/netty/cookie/CookieEncoder.java index 67f49e9a2eed..1c8f47b06a07 100644 --- a/dev/com.ibm.ws.transport.http/src/io/openliberty/http/netty/cookie/CookieEncoder.java +++ b/dev/com.ibm.ws.transport.http/src/io/openliberty/http/netty/cookie/CookieEncoder.java @@ -9,6 +9,10 @@ *******************************************************************************/ package io.openliberty.http.netty.cookie; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -31,8 +35,12 @@ public class CookieEncoder { private CookieEncoder() {} + private static final String EXPIRES_ATTRIBUTE = "expires"; private static final String SAMESITE_ATTRIBUTE = "samesite"; private static final String PARTITIONED_ATTRIBUTE = "partitioned"; + private static final SimpleDateFormat RFC1123_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); + private static final int MAX_MAX_AGE_SECONDS = Integer.MAX_VALUE; // 2,147,483,647 seconds + /** * Encodes the given cookie into a string suitable for a Set-Cookie header, @@ -44,50 +52,122 @@ private CookieEncoder() {} * @return a Set-Cookie header value */ public static String encode(HttpCookie cookie, HeaderKeys header, HttpChannelConfig config) { - if (config.useSameSiteConfig()) { + if(cookie == null){ + throw new IllegalArgumentException("Cookie cannot be null"); + } + if(header == null){ + throw new IllegalArgumentException("HeaderKeys cannot be null"); + } - String currentSameSite = cookie.getAttribute("samesite"); + determineAndSetVersion(cookie, config); + applyVersionAttributes(cookie, config); - if(currentSameSite == null){ - String cookieName = cookie.getName(); - String sameSiteValue = config.getSameSiteCookies().get(cookieName); + return CookieUtils.toString(cookie, header, config.isv0CookieDateRFC1123compat(), config.shouldSkipCookiePathQuotes()); + } - if(sameSiteValue == null && config.onlySameSiteStar()){ - sameSiteValue = config.getSameSiteCookies().get(HttpConfigConstants.WILDCARD_CHAR); - } + private static void determineAndSetVersion(HttpCookie cookie, HttpChannelConfig config){ + if(config.useSameSiteConfig()){ + cookie.setVersion(1); + } else { - if(sameSiteValue == null){ - for(Pattern pattern: config.getSameSitePatterns().keySet()){ - Matcher matcher = pattern.matcher(cookieName); - if(matcher.matches()){ - sameSiteValue = config.getSameSitePatterns().get(pattern); - break; - } - } - } + int version = 0; - if(sameSiteValue != null){ - cookie.setAttribute(SAMESITE_ATTRIBUTE, sameSiteValue); + boolean hasSameSite = cookie.getAttribute(SAMESITE_ATTRIBUTE) != null; + boolean hasMaxAge = cookie.getMaxAge() > -1; - if(HttpConfigConstants.SameSite.NONE.getName().equalsIgnoreCase(sameSiteValue)){ - if(!cookie.isSecure()){ - cookie.setSecure(true); - } - if(config.getPartitioned() == Boolean.TRUE && cookie.getAttribute(PARTITIONED_ATTRIBUTE) == null){ - cookie.setAttribute(PARTITIONED_ATTRIBUTE, ""); + if(hasSameSite || hasMaxAge){ + version = 1; + } + cookie.setVersion(version); + } + } + + private static void applyVersionAttributes(HttpCookie cookie, HttpChannelConfig config){ + if(cookie.getVersion() == 1){ + String sameSite = cookie.getAttribute(SAMESITE_ATTRIBUTE); + + if(config.useSameSiteConfig()){ + if(sameSite == null){ + String name = cookie.getName(); + String configSameSiteValue = config.getSameSiteCookies().get(name); + + if(configSameSiteValue == null && config.onlySameSiteStar()){ + configSameSiteValue = config.getSameSiteCookies().get(HttpConfigConstants.WILDCARD_CHAR); + } + + if(configSameSiteValue == null){ + for(Pattern pattern: config.getSameSitePatterns().keySet()){ + Matcher matcher = pattern.matcher(name); + if(matcher.matches()){ + configSameSiteValue = config.getSameSitePatterns().get(pattern); + break; + } } } + + if(configSameSiteValue != null){ + sameSite = configSameSiteValue; + cookie.setAttribute(SAMESITE_ATTRIBUTE, configSameSiteValue); + } } - } else { - // If SameSite already set, check if it's None and ensure partitioned if needed - if (HttpConfigConstants.SameSite.NONE.getName().equalsIgnoreCase(currentSameSite)) { - if (config.getPartitioned() == Boolean.TRUE && cookie.getAttribute(PARTITIONED_ATTRIBUTE) == null) { - cookie.setAttribute(PARTITIONED_ATTRIBUTE, ""); + } + try{ + SameSite value = SameSite.from(sameSite); + if(value.requiresSecure() && !cookie.isSecure()){ + cookie.setSecure(true); + } + } catch(IllegalArgumentException e){ + //User set an invalid SameSite cookie, leave as is. + } + + if(config.getPartitioned() && cookie.getAttribute(PARTITIONED_ATTRIBUTE) == null) { + cookie.setAttribute(PARTITIONED_ATTRIBUTE, ""); + } + + handleExpiresToMaxAge(cookie); + } + } + + private static void handleExpiresToMaxAge(HttpCookie cookie){ + boolean hasExpires = cookie.getAttribute(EXPIRES_ATTRIBUTE) != null; + boolean hasMaxAge = cookie.getMaxAge() > -1; + + if(hasExpires && !hasMaxAge){ + String expires = cookie.getAttribute(EXPIRES_ATTRIBUTE); + Date expiresDate = parseExpires(expires); + if(expiresDate != null){ + long currentTime = System.currentTimeMillis(); + long maxAgeSeconds = TimeUnit.MILLISECONDS.toSeconds(expiresDate.getTime() - currentTime); + + //Prevent overflow since setMaxAge uses int + if(maxAgeSeconds > MAX_MAX_AGE_SECONDS){ + maxAgeSeconds = MAX_MAX_AGE_SECONDS; + }else if(maxAgeSeconds < 0){ + maxAgeSeconds = 0; } + + System.out.println("Setting max age to: " + maxAgeSeconds); + cookie.setMaxAge((int)maxAgeSeconds); + cookie.setAttribute(EXPIRES_ATTRIBUTE, null); + }else { + cookie.setMaxAge(0); } } + } + + private static boolean isValidSameSite(String sameSite){ + if(sameSite == null){ + return false; + } + String normalized = sameSite.toLowerCase().trim(); + return "lax".equals(normalized)||"strict".equals(normalized)||"none".equals(normalized); + } + + private static Date parseExpires(String expires){ + try { + return RFC1123_DATE_FORMAT.parse(expires); + } catch(ParseException e){ + return null; } - - return CookieUtils.toString(cookie, header, config.isv0CookieDateRFC1123compat(), config.shouldSkipCookiePathQuotes()); } } diff --git a/dev/com.ibm.ws.transport.http/test/com/ibm/ws/netty/CookieTests.java b/dev/com.ibm.ws.transport.http/test/com/ibm/ws/netty/CookieTests.java deleted file mode 100644 index 7fea5707e7b9..000000000000 --- a/dev/com.ibm.ws.transport.http/test/com/ibm/ws/netty/CookieTests.java +++ /dev/null @@ -1,172 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2024 IBM Corporation and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - *******************************************************************************/ -package com.ibm.ws.netty; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.mock; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; - -import org.junit.Before; -import org.junit.Test; - -import com.ibm.ws.http.channel.internal.HttpChannelConfig; -import com.ibm.wsspi.genericbnf.HeaderKeys; -import com.ibm.wsspi.http.HttpCookie; -import com.ibm.wsspi.http.channel.values.HttpHeaderKeys; - -import io.openliberty.http.netty.cookie.CookieDecoder; -import io.openliberty.http.netty.cookie.CookieEncoder; - -/** - * Tests the functionality of the {@link CookieEncoder} and {@link CookieDecoder} utilities. - * These tests ensure that cookies are properly decoded from header strings and encoded into - * appropriate header formats, respecting configuration settings related to SameSite, partitioning, - * and other cookie attributes. - */ -public class CookieTests{ - - private HttpChannelConfig config; - private HeaderKeys header; - - /** - * Sets up mock configuration and header objects before each test. - */ - @Before - public void setUp() { - config = mock(HttpChannelConfig.class); - header = mock(HeaderKeys.class); - } - - /** - * Tests that a simple cookie header is decoded correctly into a list - * of {@link HttpCookie} instances. The input should represent a valid Cookie header - * (e.g., "Cookie: myCookie=myValue") without attributes such as Path or HttpOnly, - * which are not part of the Cookie header format. - */ - @Test - public void testDecodeCookie() { - // Using a simple cookie pair without Path and HttpOnly attributes - String cookieString = "myCookie=myValue"; - List cookies = CookieDecoder.decode(cookieString); - - assertThat("Expected one cookie to be decoded", cookies, hasSize(1)); - HttpCookie c = cookies.get(0); - assertThat("Cookie name should match input", c.getName(), is("myCookie")); - assertThat("Cookie value should match input", c.getValue(), is("myValue")); - } - - /** - * Tests that an empty cookie string returns an empty list of {@link HttpCookie} objects. - * This verifies behavior when no cookies are present. - */ - @Test - public void testDecodeEmptyString() { - List cookies = CookieDecoder.decode(""); - assertThat("Expected no cookies to be decoded from empty string", cookies, is(empty())); - } - - /** - * Tests that a cookie string containing attributes prefixed with a dollar sign ('$') - * is decoded correctly. This checks that special attributes are handled as expected. - * The cookie string here should still represent a valid cookie header line. - */ - @Test - public void testDecodeServlet6WithDollarSign() { - String cookieString = "$version=1; cookieFlavor=vanilla"; - List cookies = CookieDecoder.decode(cookieString); - - assertThat("Expected one cookie after ignoring $version attribute", cookies, hasSize(1)); - HttpCookie c = cookies.get(0); - assertThat("Cookie name should be 'cookieFlavor'", c.getName(), is("cookieFlavor")); - assertThat("Cookie value should be 'vanilla'", c.getValue(), is("vanilla")); - } - - /** - * Tests that a cookie without an existing SameSite attribute is encoded using the - * configuration SameSite value. This verifies that SameSite attributes are - * properly applied when they are present in configuration mappings. - */ - @Test - public void testEncodeAddsSameSiteStrict() { - HttpCookie cookie = new HttpCookie("testCookie", "value"); - - when(config.useSameSiteConfig()).thenReturn(true); - - Map sameSiteMap = new HashMap<>(); - sameSiteMap.put("testCookie", "Strict"); - when(config.getSameSiteCookies()).thenReturn(sameSiteMap); - when(config.onlySameSiteStar()).thenReturn(false); - when(config.getSameSitePatterns()).thenReturn(Collections.emptyMap()); - when(config.getPartitioned()).thenReturn(false); - when(config.isv0CookieDateRFC1123compat()).thenReturn(false); - when(config.shouldSkipCookiePathQuotes()).thenReturn(false); - - String encoded = CookieEncoder.encode(cookie, HttpHeaderKeys.HDR_SET_COOKIE, config).toLowerCase(); - assertThat("Encoded cookie should include samesite=strict", encoded, containsString("samesite=strict")); - } - - /** - * Tests that a cookie with a SameSite=None configuration is automatically made secure - * and partitioned if the configuration requires it. This ensures that special rules for - * SameSite=None are correctly enforced at encoding time. - */ - @Test - public void testEncodeSameSiteNoneMakesCookieSecureAndPartitioned() { - HttpCookie cookie = new HttpCookie("testCookie", "value"); - - when(config.useSameSiteConfig()).thenReturn(true); - - Map sameSiteMap = new HashMap<>(); - sameSiteMap.put("testCookie", "None"); - when(config.getSameSiteCookies()).thenReturn(sameSiteMap); - when(config.onlySameSiteStar()).thenReturn(false); - when(config.getSameSitePatterns()).thenReturn(Collections.emptyMap()); - when(config.getPartitioned()).thenReturn(true); - when(config.isv0CookieDateRFC1123compat()).thenReturn(false); - when(config.shouldSkipCookiePathQuotes()).thenReturn(false); - - String encoded = CookieEncoder.encode(cookie, HttpHeaderKeys.HDR_SET_COOKIE, config).toLowerCase(); - assertThat("Encoded cookie should have samesite=none", encoded, containsString("samesite=none")); - assertThat("Encoded cookie should be secure", encoded, containsString("secure")); - assertThat("Encoded cookie should be partitioned", encoded, containsString("partitioned")); - } - - /** - * Tests that a cookie name matching a configured pattern is assigned the proper SameSite - * attribute. This ensures that pattern-based configuration rules are applied during encoding. - */ - @Test - public void testEncodePatternMatchSameSiteLax() { - HttpCookie cookie = new HttpCookie("user_login", "xyz"); - - when(config.useSameSiteConfig()).thenReturn(true); - when(config.onlySameSiteStar()).thenReturn(false); - when(config.getSameSiteCookies()).thenReturn(Collections.emptyMap()); - - Map patternMap = new HashMap<>(); - patternMap.put(Pattern.compile("user_.*"), "Lax"); - when(config.getSameSitePatterns()).thenReturn(patternMap); - - when(config.getPartitioned()).thenReturn(false); - when(config.isv0CookieDateRFC1123compat()).thenReturn(false); - when(config.shouldSkipCookiePathQuotes()).thenReturn(false); - - String encoded = CookieEncoder.encode(cookie, HttpHeaderKeys.HDR_SET_COOKIE, config).toLowerCase(); - assertThat("Encoded cookie should include samesite=Lax for pattern match", encoded, containsString("samesite=lax")); - } - -}