From d5a54919692bfb7af3dba0a2b8fb843eccbea8ff Mon Sep 17 00:00:00 2001 From: "James R. Perkins" Date: Wed, 30 Oct 2024 09:07:18 -0700 Subject: [PATCH 1/2] [UNDERTOW-2523] Require a minimum of Java 17 in preparation for Jakarta Servlet 6.1 https://issues.redhat.com/browse/UNDERTOW-2523 Signed-off-by: James R. Perkins --- .github/workflows/ci.yml | 10 +++++----- pom.xml | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd61ce495e..ebed8e8e96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: jobs: build-all: - name: Compile (no tests) with JDK 11 + name: Compile (no tests) with JDK 17 runs-on: ubuntu-latest steps: - uses: n1hility/cancel-previous-runs@v2 @@ -23,11 +23,11 @@ jobs: restore-keys: | m2- - uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: 17 - name: Generate settings.xml for Maven Builds uses: whelk-io/maven-settings-xml-action@v20 with: @@ -60,7 +60,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] module: [core] - jdk: [11, 17, 21] + jdk: [17, 21] openjdk_impl: [ temurin ] steps: - name: Update hosts - linux @@ -122,7 +122,7 @@ jobs: os: [ubuntu-latest] module: [core, servlet, websockets-jsr] proxy: ['-Pproxy', ''] - jdk: [11] + jdk: [17] steps: - name: Update hosts - linux if: matrix.os == 'ubuntu-latest' diff --git a/pom.xml b/pom.xml index 064b1b1ef8..d4abd52d60 100644 --- a/pom.xml +++ b/pom.xml @@ -106,8 +106,9 @@ 5.1.1 1.21 - 11 - 11 + 17 + ${maven.compiler.release} + ${maven.compiler.release} true false false From b6fae03a51c21559557be58cfe1c72bc5225d175 Mon Sep 17 00:00:00 2001 From: "James R. Perkins" Date: Wed, 30 Oct 2024 18:00:00 -0700 Subject: [PATCH 2/2] [UNDERTOW-2523] Update the Jakarta Servlet specification to Jakarta Servlet 6.1. Note this changed the Cookie specification to RFC 6265 which required test changes. https://issues.redhat.com/browse/UNDERTOW-2523 Signed-off-by: James R. Perkins --- .../java/io/undertow/UndertowOptions.java | 13 +++- .../attribute/SecureProtocolAttribute.java | 72 +++++++++++++++++++ .../undertow/server/BasicSSLSessionInfo.java | 35 +++++++++ .../server/ConnectionSSLSessionInfo.java | 5 ++ .../java/io/undertow/server/Connectors.java | 49 +++++++++++-- .../undertow/server/HttpServerExchange.java | 2 +- .../io/undertow/server/SSLSessionInfo.java | 15 +++- .../io/undertow/server/handlers/Cookie.java | 34 +++++++++ .../undertow/server/handlers/CookieImpl.java | 25 +++++++ .../server/handlers/SSLHeaderHandler.java | 4 +- .../protocol/http2/Http2SslSessionInfo.java | 5 ++ .../main/java/io/undertow/util/Cookies.java | 33 ++++++--- .../main/java/io/undertow/util/Headers.java | 58 +++++++-------- ...ndertow.attribute.ExchangeAttributeBuilder | 1 + .../SameSiteCookieHandlerTestCase.java | 8 +-- .../handlers/SecureCookieHandlerTestCase.java | 4 +- .../io/undertow/testutils/DefaultServer.java | 1 + pom.xml | 4 +- .../undertow/servlet/api/DeploymentInfo.java | 2 +- .../SSLInformationAssociationHandler.java | 4 ++ .../servlet/spec/HttpServletResponseImpl.java | 12 +++- .../servlet/spec/ServletCookieAdaptor.java | 30 +++++--- .../undertow/servlet/util/DispatchUtils.java | 6 ++ .../handlers/MarkSecureHandlerTestCase.java | 2 +- .../test/mock/MockRequestTestCase.java | 4 ++ .../cookies/ResponseCookiesTestCase.java | 10 +-- .../security/ssl/SSLAttributesServlet.java | 2 + .../ssl/SSLMetaDataProxyTestCase.java | 6 +- .../security/ssl/SSLMetaDataTestCase.java | 1 + .../wrapper/NonStandardResponseWrapper.java | 15 ++++ websockets-jsr/pom.xml | 1 - 31 files changed, 386 insertions(+), 77 deletions(-) create mode 100644 core/src/main/java/io/undertow/attribute/SecureProtocolAttribute.java diff --git a/core/src/main/java/io/undertow/UndertowOptions.java b/core/src/main/java/io/undertow/UndertowOptions.java index ebde0ca5b5..bdeaa7c020 100644 --- a/core/src/main/java/io/undertow/UndertowOptions.java +++ b/core/src/main/java/io/undertow/UndertowOptions.java @@ -212,6 +212,16 @@ public class UndertowOptions { */ public static final Option ALLOW_EQUALS_IN_COOKIE_VALUE = Option.simple(UndertowOptions.class, "ALLOW_EQUALS_IN_COOKIE_VALUE", Boolean.class); + /** + * If this is true then Undertow will disable RFC6265 compliant cookie parsing for Set-Cookie header instead of legacy backward compatible behavior. + *

+ * default is {@code false} + *

+ */ + public static final Option DISABLE_RFC6265_COOKIE_PARSING = Option.simple(UndertowOptions.class, "DISABLE_RFC6265_COOKIE_PARSING", Boolean.class); + + public static final boolean DEFAULT_DISABLE_RFC6265_COOKIE_PARSING = false; + /** * If this is true then Undertow will enable RFC6265 compliant cookie validation for Set-Cookie header instead of legacy backward compatible behavior. * @@ -219,7 +229,8 @@ public class UndertowOptions { */ public static final Option ENABLE_RFC6265_COOKIE_VALIDATION = Option.simple(UndertowOptions.class, "ENABLE_RFC6265_COOKIE_VALIDATION", Boolean.class); - public static final boolean DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATION = false; + // As of Jakarta 6.1, RFC 6265 is used for the cookie specification https://github.com/jakartaee/servlet/issues/37 + public static final boolean DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATION = true; /** * If we should attempt to use SPDY for HTTPS connections. diff --git a/core/src/main/java/io/undertow/attribute/SecureProtocolAttribute.java b/core/src/main/java/io/undertow/attribute/SecureProtocolAttribute.java new file mode 100644 index 0000000000..0a7327d562 --- /dev/null +++ b/core/src/main/java/io/undertow/attribute/SecureProtocolAttribute.java @@ -0,0 +1,72 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2024 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.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.SSLSessionInfo; + +/** + * An attribute which describes the secure protocol. This is the protocol resolved from the {@link SSLSessionInfo#getSecureProtocol()}. + * @author James R. Perkins + */ +public class SecureProtocolAttribute implements ExchangeAttribute { + + public static final SecureProtocolAttribute INSTANCE = new SecureProtocolAttribute(); + + @Override + public String readAttribute(final HttpServerExchange exchange) { + final SSLSessionInfo ssl = exchange.getConnection().getSslSessionInfo(); + if (ssl == null || ssl.getSecureProtocol() == null) { + return null; + } + return ssl.getSecureProtocol(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("SSL Protocol", newValue); + } + + @Override + public String toString() { + return "%{SECURE_PROTOCOL}"; + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Secure Protocol"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals("%{SECURE_PROTOCOL}")) { + return INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/core/src/main/java/io/undertow/server/BasicSSLSessionInfo.java b/core/src/main/java/io/undertow/server/BasicSSLSessionInfo.java index 9aac8bad40..ab4bac39d5 100644 --- a/core/src/main/java/io/undertow/server/BasicSSLSessionInfo.java +++ b/core/src/main/java/io/undertow/server/BasicSSLSessionInfo.java @@ -43,6 +43,7 @@ public class BasicSSLSessionInfo implements SSLSessionInfo { private final java.security.cert.Certificate[] peerCertificate; private final X509Certificate[] certificate; private final Integer keySize; + private final String secureProtocol; /** * @@ -54,9 +55,24 @@ public class BasicSSLSessionInfo implements SSLSessionInfo { * @throws CertificateException If the client cert could not be decoded */ public BasicSSLSessionInfo(byte[] sessionId, String cypherSuite, String certificate, Integer keySize) throws java.security.cert.CertificateException, CertificateException { + this(sessionId, cypherSuite, certificate, keySize, null); + } + + /** + * + * @param sessionId The SSL session ID + * @param cypherSuite The cypher suite name + * @param certificate A string representation of the client certificate + * @param keySize The key-size used by the cypher + * @param secureProtocol the secure protocol, example {@code TLSv1.2} + * @throws java.security.cert.CertificateException If the client cert could not be decoded + * @throws CertificateException If the client cert could not be decoded + */ + public BasicSSLSessionInfo(byte[] sessionId, String cypherSuite, String certificate, Integer keySize, String secureProtocol) throws java.security.cert.CertificateException, CertificateException { this.sessionId = sessionId; this.cypherSuite = cypherSuite; this.keySize = keySize; + this.secureProtocol = secureProtocol; if (certificate != null) { java.security.cert.CertificateFactory cf = java.security.cert.CertificateFactory.getInstance("X.509"); byte[] certificateBytes = certificate.getBytes(StandardCharsets.US_ASCII); @@ -123,6 +139,20 @@ public BasicSSLSessionInfo(String sessionId, String cypherSuite, String certific this(sessionId == null ? null : fromHex(sessionId), cypherSuite, certificate, keySize); } + /** + * + * @param sessionId The encoded SSL session ID + * @param cypherSuite The cypher suite name + * @param certificate A string representation of the client certificate + * @param keySize The key-size used by the cypher + * @param secureProtocol the secure protocol, example {@code TLSv1.2} + * @throws java.security.cert.CertificateException If the client cert could not be decoded + * @throws CertificateException If the client cert could not be decoded + */ + public BasicSSLSessionInfo(String sessionId, String cypherSuite, String certificate, Integer keySize, String secureProtocol) throws java.security.cert.CertificateException, CertificateException { + this(sessionId == null ? null : fromHex(sessionId), cypherSuite, certificate, keySize, secureProtocol); + } + @Override public byte[] getSessionId() { if(sessionId == null) { @@ -174,6 +204,11 @@ public SSLSession getSSLSession() { return null; } + @Override + public String getSecureProtocol() { + return secureProtocol; + } + private static byte[] fromHex(String sessionId) { try { return HexConverter.convertFromHex(sessionId); diff --git a/core/src/main/java/io/undertow/server/ConnectionSSLSessionInfo.java b/core/src/main/java/io/undertow/server/ConnectionSSLSessionInfo.java index 99d3471818..91a59aaf7b 100644 --- a/core/src/main/java/io/undertow/server/ConnectionSSLSessionInfo.java +++ b/core/src/main/java/io/undertow/server/ConnectionSSLSessionInfo.java @@ -145,6 +145,11 @@ public SSLSession getSSLSession() { return channel.getSslSession(); } + @Override + public String getSecureProtocol() { + return channel.getSslSession().getProtocol(); + } + //Suppress incorrect resource leak warning. @SuppressWarnings("resource") public void renegotiateBufferRequest(HttpServerExchange exchange, SslClientAuthMode newAuthMode) throws IOException { diff --git a/core/src/main/java/io/undertow/server/Connectors.java b/core/src/main/java/io/undertow/server/Connectors.java index 7dea9b0295..3da1338d28 100644 --- a/core/src/main/java/io/undertow/server/Connectors.java +++ b/core/src/main/java/io/undertow/server/Connectors.java @@ -41,6 +41,9 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Date; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; @@ -57,6 +60,7 @@ public class Connectors { private static final boolean[] ALLOWED_TOKEN_CHARACTERS = new boolean[256]; private static final boolean[] ALLOWED_SCHEME_CHARACTERS = new boolean[256]; + private static final Set KNOWN_ATTRIBUTE_NAMES = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); static { for(int i = 0; i < ALLOWED_TOKEN_CHARACTERS.length; ++i) { @@ -108,6 +112,16 @@ 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"); } /** * Flattens the exchange cookie map into the response header map. This should be called by a @@ -224,9 +238,6 @@ private static String addRfc6265ResponseCookieToExchange(final Cookie cookie) { header.append("; Domain="); header.append(cookie.getDomain()); } - if (cookie.isDiscard()) { - header.append("; Discard"); - } if (cookie.isSecure()) { header.append("; Secure"); } @@ -234,7 +245,10 @@ private static String addRfc6265ResponseCookieToExchange(final Cookie cookie) { header.append("; HttpOnly"); } if (cookie.getMaxAge() != null) { - if (cookie.getMaxAge() >= 0) { + // TODO (jrp) Per the TCK test "RFC 6265 - server should only send +ve values for Max-Age" + // TODO (jrp) This is possibly per https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2, however + // TODO (jrp) I'm not sure not adding the value is correct. + if (cookie.getMaxAge() > 0) { header.append("; Max-Age="); header.append(cookie.getMaxAge()); } @@ -270,6 +284,7 @@ private static String addRfc6265ResponseCookieToExchange(final Cookie cookie) { header.append(cookie.getSameSiteMode()); } } + appendAttributes(cookie, header); return header.toString(); } @@ -298,7 +313,10 @@ private static String addVersion0ResponseCookieToExchange(final Cookie cookie) { header.append("; Expires="); header.append(DateUtils.toOldCookieDateString(cookie.getExpires())); } else if (cookie.getMaxAge() != null) { - if (cookie.getMaxAge() >= 0) { + // TODO (jrp) Per the TCK test "RFC 6265 - server should only send +ve values for Max-Age" + // TODO (jrp) This is possibly per https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2, however + // TODO (jrp) I'm not sure not adding the value is correct. + if (cookie.getMaxAge() > 0) { header.append("; Max-Age="); header.append(cookie.getMaxAge()); } @@ -320,6 +338,7 @@ private static String addVersion0ResponseCookieToExchange(final Cookie cookie) { header.append(cookie.getSameSiteMode()); } } + appendAttributes(cookie, header); return header.toString(); } @@ -350,7 +369,10 @@ private static String addVersion1ResponseCookieToExchange(final Cookie cookie) { header.append("; HttpOnly"); } if (cookie.getMaxAge() != null) { - if (cookie.getMaxAge() >= 0) { + // TODO (jrp) Per the TCK test "RFC 6265 - server should only send +ve values for Max-Age" + // TODO (jrp) This is possibly per https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2, however + // TODO (jrp) I'm not sure not adding the value is correct. + if (cookie.getMaxAge() > 0) { header.append("; Max-Age="); header.append(cookie.getMaxAge()); } @@ -386,6 +408,7 @@ private static String addVersion1ResponseCookieToExchange(final Cookie cookie) { header.append(cookie.getSameSiteMode()); } } + appendAttributes(cookie, header); return header.toString(); } @@ -656,4 +679,18 @@ public static boolean areRequestHeadersValid(HeaderMap headers) { } return true; } + + private static void appendAttributes(final Cookie cookie, final StringBuilder header) { + for (Map.Entry entry : cookie.getAttributes().entrySet()) { + if (KNOWN_ATTRIBUTE_NAMES.contains(entry.getKey())) { + continue; + } + header.append("; ") + .append(entry.getKey()); + if (!entry.getValue().isBlank()) { + header.append('=') + .append(entry.getValue()); + } + } + } } diff --git a/core/src/main/java/io/undertow/server/HttpServerExchange.java b/core/src/main/java/io/undertow/server/HttpServerExchange.java index 3515907dd1..e73514e025 100644 --- a/core/src/main/java/io/undertow/server/HttpServerExchange.java +++ b/core/src/main/java/io/undertow/server/HttpServerExchange.java @@ -1210,7 +1210,7 @@ public Iterable requestCookies() { Cookies.parseRequestCookies( getConnection().getUndertowOptions().get(UndertowOptions.MAX_COOKIES, UndertowOptions.DEFAULT_MAX_COOKIES), getConnection().getUndertowOptions().get(UndertowOptions.ALLOW_EQUALS_IN_COOKIE_VALUE, false), - requestHeaders.get(Headers.COOKIE), requestCookiesParam); + requestHeaders.get(Headers.COOKIE), requestCookiesParam, getConnection().getUndertowOptions().get(UndertowOptions.DISABLE_RFC6265_COOKIE_PARSING, UndertowOptions.DEFAULT_DISABLE_RFC6265_COOKIE_PARSING)); } return requestCookies; } diff --git a/core/src/main/java/io/undertow/server/SSLSessionInfo.java b/core/src/main/java/io/undertow/server/SSLSessionInfo.java index bf11bcf320..95d228f3cc 100644 --- a/core/src/main/java/io/undertow/server/SSLSessionInfo.java +++ b/core/src/main/java/io/undertow/server/SSLSessionInfo.java @@ -18,10 +18,10 @@ package io.undertow.server; -import org.xnio.SslClientAuthMode; - -import javax.net.ssl.SSLSession; import java.io.IOException; +import javax.net.ssl.SSLSession; + +import org.xnio.SslClientAuthMode; /** * SSL session information. @@ -126,4 +126,13 @@ default int getKeySize() { */ SSLSession getSSLSession(); + /** + * Returns the {@linkplain SSLSession#getProtocol() secure protocol}, if applicable, for the curren session. + * + * @return the secure protocol or {@code null} if one could not be found + */ + default String getSecureProtocol() { + return getSSLSession() == null ? null : getSSLSession().getProtocol(); + } + } 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 e1a4773024..a0655703f6 100644 --- a/core/src/main/java/io/undertow/server/handlers/Cookie.java +++ b/core/src/main/java/io/undertow/server/handlers/Cookie.java @@ -19,6 +19,7 @@ package io.undertow.server.handlers; import java.util.Date; +import java.util.Map; /** * A HTTP cookie. @@ -86,6 +87,39 @@ default Cookie setSameSiteMode(final String mode) { throw new UnsupportedOperationException("Not implemented"); } + /** + * Returns the attribute associated with the name or {@code null} if no attribute is associated with the name. + * + * @param name the name of the attribute + * + * @return the value or {@code null} if not found + */ + default String getAttribute(final String name) { + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * 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. + * + * @param name the name of the attribute + * @param value the value of the attribute or {@code null} to remove it + * + * @return this cookie + */ + default Cookie setAttribute(final String name, final String value) { + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Returns an unmodifiable map of the attributes associated with this cookie. + * + * @return an unmodifiable map of the attributes + */ + default Map getAttributes() { + throw new UnsupportedOperationException("Not implemented"); + } + @Override default int compareTo(final Object other) { final Cookie o = (Cookie) other; 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 488655e9cd..1e6ba2cc70 100644 --- a/core/src/main/java/io/undertow/server/handlers/CookieImpl.java +++ b/core/src/main/java/io/undertow/server/handlers/CookieImpl.java @@ -20,6 +20,8 @@ import java.util.Arrays; import java.util.Date; +import java.util.Map; +import java.util.TreeMap; import io.undertow.UndertowLogger; import io.undertow.UndertowMessages; @@ -43,14 +45,17 @@ public class CookieImpl implements Cookie { private String comment; private boolean sameSite; private String sameSiteMode; + private final Map attributes; public CookieImpl(final String name, final String value) { this.name = name; this.value = value; + this.attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } public CookieImpl(final String name) { this.name = name; + this.attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } public String getName() { @@ -176,6 +181,26 @@ public Cookie setSameSiteMode(final String mode) { return this; } + @Override + public String getAttribute(final String name) { + return attributes.get(name); + } + + @Override + public Cookie setAttribute(final String name, final String value) { + if (value != null) { + attributes.put(name, value); + } else { + attributes.remove(name); + } + return this; + } + + @Override + public Map getAttributes() { + return Map.copyOf(attributes); + } + @Override public final int hashCode() { int result = 17; diff --git a/core/src/main/java/io/undertow/server/handlers/SSLHeaderHandler.java b/core/src/main/java/io/undertow/server/handlers/SSLHeaderHandler.java index a144ac6058..9c22c90285 100644 --- a/core/src/main/java/io/undertow/server/handlers/SSLHeaderHandler.java +++ b/core/src/main/java/io/undertow/server/handlers/SSLHeaderHandler.java @@ -21,6 +21,7 @@ import static io.undertow.util.Headers.SSL_CIPHER; import static io.undertow.util.Headers.SSL_CIPHER_USEKEYSIZE; import static io.undertow.util.Headers.SSL_CLIENT_CERT; +import static io.undertow.util.Headers.SECURE_PROTOCOL; import static io.undertow.util.Headers.SSL_SESSION_ID; import java.util.Collections; @@ -83,6 +84,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { final String cipher = requestHeaders.getFirst(SSL_CIPHER); String clientCert = requestHeaders.getFirst(SSL_CLIENT_CERT); String keySizeStr = requestHeaders.getFirst(SSL_CIPHER_USEKEYSIZE); + final String sslProtocol = requestHeaders.getFirst(SECURE_PROTOCOL); Integer keySize = null; if (keySizeStr != null) { try { @@ -108,7 +110,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { } } try { - SSLSessionInfo info = new BasicSSLSessionInfo(sessionId, cipher, clientCert, keySize); + SSLSessionInfo info = new BasicSSLSessionInfo(sessionId, cipher, clientCert, keySize, sslProtocol); exchange.setRequestScheme(HTTPS); exchange.getConnection().setSslSessionInfo(info); exchange.addExchangeCompleteListener(CLEAR_SSL_LISTENER); diff --git a/core/src/main/java/io/undertow/server/protocol/http2/Http2SslSessionInfo.java b/core/src/main/java/io/undertow/server/protocol/http2/Http2SslSessionInfo.java index d70c930f6f..251540c0dc 100644 --- a/core/src/main/java/io/undertow/server/protocol/http2/Http2SslSessionInfo.java +++ b/core/src/main/java/io/undertow/server/protocol/http2/Http2SslSessionInfo.java @@ -97,4 +97,9 @@ public void renegotiate(HttpServerExchange exchange, SslClientAuthMode sslClient public SSLSession getSSLSession() { return channel.getSslSession(); } + + @Override + public String getSecureProtocol() { + return channel.getSslSession().getProtocol(); + } } diff --git a/core/src/main/java/io/undertow/util/Cookies.java b/core/src/main/java/io/undertow/util/Cookies.java index 5a7b465204..53cfe6a92f 100644 --- a/core/src/main/java/io/undertow/util/Cookies.java +++ b/core/src/main/java/io/undertow/util/Cookies.java @@ -175,6 +175,8 @@ private static void handleValue(CookieImpl cookie, String key, String value) { } else if (key.equalsIgnoreCase("samesite")) { cookie.setSameSite(true); cookie.setSameSiteMode(value); + } else { + cookie.setAttribute(key, value); } //otherwise ignore this key-value pair } @@ -211,8 +213,13 @@ public static Map parseRequestCookies(int maxCookies, boolean al return parseRequestCookies(maxCookies, allowEqualInValue, cookies, LegacyCookieSupport.COMMA_IS_SEPARATOR); } + @Deprecated(since = "2.4.0", forRemoval=true) public static void parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies, Set parsedCookies) { - parseRequestCookies(maxCookies, allowEqualInValue, cookies, parsedCookies, LegacyCookieSupport.COMMA_IS_SEPARATOR); + parseRequestCookies(maxCookies, allowEqualInValue, cookies, parsedCookies, LegacyCookieSupport.COMMA_IS_SEPARATOR, LegacyCookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0, false); + } + + public static void parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies, Set parsedCookies, boolean rfc6265ParsingDisabled) { + parseRequestCookies(maxCookies, allowEqualInValue, cookies, parsedCookies, LegacyCookieSupport.COMMA_IS_SEPARATOR, LegacyCookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0, rfc6265ParsingDisabled); } @Deprecated @@ -220,17 +227,13 @@ static Map parseRequestCookies(int maxCookies, boolean allowEqua return parseRequestCookies(maxCookies, allowEqualInValue, cookies, commaIsSeperator, LegacyCookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0); } - static void parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies, Set parsedCookies, boolean commaIsSeperator) { - parseRequestCookies(maxCookies, allowEqualInValue, cookies, parsedCookies, commaIsSeperator, LegacyCookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0); - } - static Map parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies, boolean commaIsSeperator, boolean allowHttpSepartorsV0) { if (cookies == null) { return new TreeMap<>(); } final Set parsedCookies = new HashSet<>(); for (String cookie : cookies) { - parseCookie(cookie, parsedCookies, maxCookies, allowEqualInValue, commaIsSeperator, allowHttpSepartorsV0); + parseCookie(cookie, parsedCookies, maxCookies, allowEqualInValue, commaIsSeperator, allowHttpSepartorsV0, true); } final Map retVal = new TreeMap<>(); @@ -240,19 +243,20 @@ static Map parseRequestCookies(int maxCookies, boolean allowEqua return retVal; } - static void parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies, Set parsedCookies, boolean commaIsSeperator, boolean allowHttpSepartorsV0) { + static void parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies, Set parsedCookies, boolean commaIsSeperator, boolean allowHttpSepartorsV0, boolean rfc6265ParsingDisabled) { if (cookies != null) { for (String cookie : cookies) { - parseCookie(cookie, parsedCookies, maxCookies, allowEqualInValue, commaIsSeperator, allowHttpSepartorsV0); + parseCookie(cookie, parsedCookies, maxCookies, allowEqualInValue, commaIsSeperator, allowHttpSepartorsV0, rfc6265ParsingDisabled); } } } - private static void parseCookie(final String cookie, final Set parsedCookies, int maxCookies, boolean allowEqualInValue, boolean commaIsSeperator, boolean allowHttpSepartorsV0) { + private static void parseCookie(final String cookie, final Set parsedCookies, int maxCookies, boolean allowEqualInValue, boolean commaIsSeperator, boolean allowHttpSepartorsV0, boolean rfc6265ParsingDisabled) { int state = 0; String name = null; int start = 0; boolean containsEscapedQuotes = false; + boolean inQuotes = false; int cookieCount = parsedCookies.size(); final Map cookies = new HashMap<>(); final Map additional = new HashMap<>(); @@ -293,6 +297,7 @@ private static void parseCookie(final String cookie, final Set parsedCoo start = i + 1; } else if (c == '"' && start == i) { //only process the " if it is the first character containsEscapedQuotes = false; + inQuotes = true; state = 3; start = i + 1; } else if (c == '=') { @@ -315,7 +320,14 @@ private static void parseCookie(final String cookie, final Set parsedCoo case 3: { //extract quoted value if (c == '"') { - cookieCount = createCookie(name, containsEscapedQuotes ? unescapeDoubleQuotes(cookie.substring(start, i)) : cookie.substring(start, i), maxCookies, cookieCount, cookies, additional); + if (!rfc6265ParsingDisabled && inQuotes) { + start = start - 1; + inQuotes = false; + i++; + cookieCount = createCookie(name, containsEscapedQuotes ? unescapeDoubleQuotes(cookie.substring(start, i)) : cookie.substring(start, i), maxCookies, cookieCount, cookies, additional); + } else { + cookieCount = createCookie(name, containsEscapedQuotes ? unescapeDoubleQuotes(cookie.substring(start, i)) : cookie.substring(start, i), maxCookies, cookieCount, cookies, additional); + } state = 0; start = i + 1; } else if (c == ';' || (commaIsSeperator && c == ',')) { @@ -335,6 +347,7 @@ private static void parseCookie(final String cookie, final Set parsedCoo // Skip the next double quote char ('"' behind '\') in the cookie value i++; containsEscapedQuotes = true; + inQuotes = false; } break; } diff --git a/core/src/main/java/io/undertow/util/Headers.java b/core/src/main/java/io/undertow/util/Headers.java index bae0cb64aa..12a46b27cb 100644 --- a/core/src/main/java/io/undertow/util/Headers.java +++ b/core/src/main/java/io/undertow/util/Headers.java @@ -95,6 +95,7 @@ private Headers() { public static final String SEC_WEB_SOCKET_ORIGIN_STRING = "Sec-WebSocket-Origin"; public static final String SEC_WEB_SOCKET_PROTOCOL_STRING = "Sec-WebSocket-Protocol"; public static final String SEC_WEB_SOCKET_VERSION_STRING = "Sec-WebSocket-Version"; + public static final String SECURE_PROTOCOL_STRING = "SECURE_PROTOCOL"; public static final String SERVER_STRING = "Server"; public static final String SERVLET_ENGINE_STRING = "Servlet-Engine"; public static final String SET_COOKIE_STRING = "Set-Cookie"; @@ -181,34 +182,35 @@ private Headers() { public static final HttpString SEC_WEB_SOCKET_ORIGIN = new HttpString(SEC_WEB_SOCKET_ORIGIN_STRING, 53); public static final HttpString SEC_WEB_SOCKET_PROTOCOL = new HttpString(SEC_WEB_SOCKET_PROTOCOL_STRING, 54); public static final HttpString SEC_WEB_SOCKET_VERSION = new HttpString(SEC_WEB_SOCKET_VERSION_STRING, 55); - public static final HttpString SERVER = new HttpString(SERVER_STRING, 56); - public static final HttpString SERVLET_ENGINE = new HttpString(SERVLET_ENGINE_STRING, 57); - public static final HttpString SET_COOKIE = new HttpString(SET_COOKIE_STRING, 58); - public static final HttpString SET_COOKIE2 = new HttpString(SET_COOKIE2_STRING, 59); - public static final HttpString SSL_CIPHER = new HttpString(SSL_CIPHER_STRING, 60); - public static final HttpString SSL_CIPHER_USEKEYSIZE = new HttpString(SSL_CIPHER_USEKEYSIZE_STRING, 61); - public static final HttpString SSL_CLIENT_CERT = new HttpString(SSL_CLIENT_CERT_STRING, 62); - public static final HttpString SSL_SESSION_ID = new HttpString(SSL_SESSION_ID_STRING, 63); - public static final HttpString STATUS = new HttpString(STATUS_STRING, 64); - public static final HttpString STRICT_TRANSPORT_SECURITY = new HttpString(STRICT_TRANSPORT_SECURITY_STRING, 65); - public static final HttpString TE = new HttpString(TE_STRING, 66); - public static final HttpString TRAILER = new HttpString(TRAILER_STRING, 67); - public static final HttpString TRANSFER_ENCODING = new HttpString(TRANSFER_ENCODING_STRING, 68); - public static final HttpString UPGRADE = new HttpString(UPGRADE_STRING, 69); - public static final HttpString USER_AGENT = new HttpString(USER_AGENT_STRING, 70); - public static final HttpString VARY = new HttpString(VARY_STRING, 71); - public static final HttpString VIA = new HttpString(VIA_STRING, 72); - public static final HttpString WARNING = new HttpString(WARNING_STRING, 73); - public static final HttpString WWW_AUTHENTICATE = new HttpString(WWW_AUTHENTICATE_STRING, 74); - public static final HttpString X_CONTENT_TYPE_OPTIONS = new HttpString(X_CONTENT_TYPE_OPTIONS_STRING, 75); - public static final HttpString X_DISABLE_PUSH = new HttpString(X_DISABLE_PUSH_STRING, 76); - public static final HttpString X_FORWARDED_FOR = new HttpString(X_FORWARDED_FOR_STRING, 77); - public static final HttpString X_FORWARDED_HOST = new HttpString(X_FORWARDED_HOST_STRING, 78); - public static final HttpString X_FORWARDED_PORT = new HttpString(X_FORWARDED_PORT_STRING, 79); - public static final HttpString X_FORWARDED_PROTO = new HttpString(X_FORWARDED_PROTO_STRING, 80); - public static final HttpString X_FORWARDED_SERVER = new HttpString(X_FORWARDED_SERVER_STRING, 81); - public static final HttpString X_FRAME_OPTIONS = new HttpString(X_FRAME_OPTIONS_STRING, 82); - public static final HttpString X_XSS_PROTECTION = new HttpString(X_XSS_PROTECTION_STRING, 83); + public static final HttpString SECURE_PROTOCOL = new HttpString(SECURE_PROTOCOL_STRING, 56); + public static final HttpString SERVER = new HttpString(SERVER_STRING, 57); + public static final HttpString SERVLET_ENGINE = new HttpString(SERVLET_ENGINE_STRING, 58); + public static final HttpString SET_COOKIE = new HttpString(SET_COOKIE_STRING, 59); + public static final HttpString SET_COOKIE2 = new HttpString(SET_COOKIE2_STRING, 60); + public static final HttpString SSL_CIPHER = new HttpString(SSL_CIPHER_STRING, 61); + public static final HttpString SSL_CIPHER_USEKEYSIZE = new HttpString(SSL_CIPHER_USEKEYSIZE_STRING, 62); + public static final HttpString SSL_CLIENT_CERT = new HttpString(SSL_CLIENT_CERT_STRING, 63); + public static final HttpString SSL_SESSION_ID = new HttpString(SSL_SESSION_ID_STRING, 64); + public static final HttpString STATUS = new HttpString(STATUS_STRING, 65); + public static final HttpString STRICT_TRANSPORT_SECURITY = new HttpString(STRICT_TRANSPORT_SECURITY_STRING, 66); + public static final HttpString TE = new HttpString(TE_STRING, 67); + public static final HttpString TRAILER = new HttpString(TRAILER_STRING, 68); + public static final HttpString TRANSFER_ENCODING = new HttpString(TRANSFER_ENCODING_STRING, 69); + public static final HttpString UPGRADE = new HttpString(UPGRADE_STRING, 70); + public static final HttpString USER_AGENT = new HttpString(USER_AGENT_STRING, 71); + public static final HttpString VARY = new HttpString(VARY_STRING, 72); + public static final HttpString VIA = new HttpString(VIA_STRING, 73); + public static final HttpString WARNING = new HttpString(WARNING_STRING, 74); + public static final HttpString WWW_AUTHENTICATE = new HttpString(WWW_AUTHENTICATE_STRING, 75); + public static final HttpString X_CONTENT_TYPE_OPTIONS = new HttpString(X_CONTENT_TYPE_OPTIONS_STRING, 76); + public static final HttpString X_DISABLE_PUSH = new HttpString(X_DISABLE_PUSH_STRING, 77); + public static final HttpString X_FORWARDED_FOR = new HttpString(X_FORWARDED_FOR_STRING, 78); + public static final HttpString X_FORWARDED_HOST = new HttpString(X_FORWARDED_HOST_STRING, 79); + public static final HttpString X_FORWARDED_PORT = new HttpString(X_FORWARDED_PORT_STRING, 80); + public static final HttpString X_FORWARDED_PROTO = new HttpString(X_FORWARDED_PROTO_STRING, 81); + public static final HttpString X_FORWARDED_SERVER = new HttpString(X_FORWARDED_SERVER_STRING, 82); + public static final HttpString X_FRAME_OPTIONS = new HttpString(X_FRAME_OPTIONS_STRING, 83); + public static final HttpString X_XSS_PROTECTION = new HttpString(X_XSS_PROTECTION_STRING, 84); // Content codings public static final HttpString COMPRESS = new HttpString("compress"); diff --git a/core/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder b/core/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder index eea0057862..18ec2a2aa7 100644 --- a/core/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder +++ b/core/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder @@ -23,6 +23,7 @@ io.undertow.attribute.PredicateContextAttribute$Builder io.undertow.attribute.QueryParameterAttribute$Builder io.undertow.attribute.SslClientCertAttribute$Builder io.undertow.attribute.SslCipherAttribute$Builder +io.undertow.attribute.SecureProtocolAttribute$Builder io.undertow.attribute.SslSessionIdAttribute$Builder io.undertow.attribute.ResponseTimeAttribute$Builder io.undertow.attribute.PathParameterAttribute$Builder diff --git a/core/src/test/java/io/undertow/server/handlers/SameSiteCookieHandlerTestCase.java b/core/src/test/java/io/undertow/server/handlers/SameSiteCookieHandlerTestCase.java index c079a2964d..5b96c757aa 100644 --- a/core/src/test/java/io/undertow/server/handlers/SameSiteCookieHandlerTestCase.java +++ b/core/src/test/java/io/undertow/server/handlers/SameSiteCookieHandlerTestCase.java @@ -108,7 +108,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { HttpResponse result = client.execute(get); Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); Header header = result.getFirstHeader("set-cookie"); - Assert.assertEquals("foo=bar; secure; SameSite=None", header.getValue()); + Assert.assertEquals("foo=bar; Secure; SameSite=None", header.getValue()); FileUtils.readFile(result.getEntity().getContent()); } finally { client.getConnectionManager().shutdown(); @@ -338,7 +338,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { HttpResponse result = client.execute(get); Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); Header header = result.getFirstHeader("set-cookie"); - Assert.assertEquals("foo=bar; secure; SameSite=None", header.getValue()); + Assert.assertEquals("foo=bar; Secure; SameSite=None", header.getValue()); FileUtils.readFile(result.getEntity().getContent()); } finally { client.getConnectionManager().shutdown(); @@ -365,7 +365,7 @@ public void handleRequest(final HttpServerExchange exchange) { HttpResponse result = client.execute(get); Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); Header header = result.getFirstHeader("set-cookie"); - Assert.assertEquals("foo=bar; secure; SameSite=None", header.getValue()); + Assert.assertEquals("foo=bar; Secure; SameSite=None", header.getValue()); FileUtils.readFile(result.getEntity().getContent()); } finally { client.getConnectionManager().shutdown(); @@ -400,7 +400,7 @@ protected HttpParams createHttpParams() { HttpResponse result = client.execute(get); Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); Header header = result.getFirstHeader("set-cookie"); - Assert.assertEquals("foo=bar; secure; SameSite=None", header.getValue()); + Assert.assertEquals("foo=bar; Secure; SameSite=None", header.getValue()); FileUtils.readFile(result.getEntity().getContent()); } finally { client.getConnectionManager().shutdown(); diff --git a/core/src/test/java/io/undertow/server/handlers/SecureCookieHandlerTestCase.java b/core/src/test/java/io/undertow/server/handlers/SecureCookieHandlerTestCase.java index 5dd5c38238..51291059a3 100644 --- a/core/src/test/java/io/undertow/server/handlers/SecureCookieHandlerTestCase.java +++ b/core/src/test/java/io/undertow/server/handlers/SecureCookieHandlerTestCase.java @@ -61,7 +61,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { HttpResponse result = client.execute(get); Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); Header header = result.getFirstHeader("set-cookie"); - Assert.assertEquals("foo=bar; secure", header.getValue()); + Assert.assertEquals("foo=bar; Secure", header.getValue()); FileUtils.readFile(result.getEntity().getContent()); get = new HttpGet(DefaultServer.getDefaultServerURL()); @@ -94,7 +94,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); Header header = result.getFirstHeader("set-cookie"); - Assert.assertEquals("cookie=value; secure", header.getValue()); + Assert.assertEquals("cookie=value; Secure", header.getValue()); FileUtils.readFile(result.getEntity().getContent()); } finally { client.getConnectionManager().shutdown(); diff --git a/core/src/test/java/io/undertow/testutils/DefaultServer.java b/core/src/test/java/io/undertow/testutils/DefaultServer.java index 75525d48c3..7555a70bc1 100644 --- a/core/src/test/java/io/undertow/testutils/DefaultServer.java +++ b/core/src/test/java/io/undertow/testutils/DefaultServer.java @@ -318,6 +318,7 @@ public DefaultServer(Class klass) throws InitializationError { @SuppressWarnings("deprecation") public static void setupProxyHandlerForSSL(ProxyHandler proxyHandler) { + proxyHandler.addRequestHeader(Headers.SECURE_PROTOCOL, "%{SECURE_PROTOCOL}", DefaultServer.class.getClassLoader()); proxyHandler.addRequestHeader(Headers.SSL_CLIENT_CERT, "%{SSL_CLIENT_CERT}", DefaultServer.class.getClassLoader()); proxyHandler.addRequestHeader(Headers.SSL_CIPHER, "%{SSL_CIPHER}", DefaultServer.class.getClassLoader()); proxyHandler.addRequestHeader(Headers.SSL_SESSION_ID, "%{SSL_SESSION_ID}", DefaultServer.class.getClassLoader()); diff --git a/pom.xml b/pom.xml index d4abd52d60..8f9024634d 100644 --- a/pom.xml +++ b/pom.xml @@ -63,7 +63,7 @@ 2.1.214 4.3 2.1.1 - 6.0.0 + 6.1.0 2.1.0 4.13.2 4.1.82.Final @@ -112,6 +112,7 @@ true false false + true @@ -256,7 +257,6 @@ ${version.surefire.plugin} ${test.categories} - true diff --git a/servlet/src/main/java/io/undertow/servlet/api/DeploymentInfo.java b/servlet/src/main/java/io/undertow/servlet/api/DeploymentInfo.java index 77c87a7d33..59312965a6 100644 --- a/servlet/src/main/java/io/undertow/servlet/api/DeploymentInfo.java +++ b/servlet/src/main/java/io/undertow/servlet/api/DeploymentInfo.java @@ -74,7 +74,7 @@ public class DeploymentInfo implements Cloneable { private int majorVersion = DEFAULT_MAJOR_VERSION; private int minorVersion = 0; private int containerMajorVersion = DEFAULT_MAJOR_VERSION; - private int containerMinorVersion = 0; + private int containerMinorVersion = 1; private Executor executor; private Executor asyncExecutor; private Path tempDir; diff --git a/servlet/src/main/java/io/undertow/servlet/handlers/security/SSLInformationAssociationHandler.java b/servlet/src/main/java/io/undertow/servlet/handlers/security/SSLInformationAssociationHandler.java index de3819c3c8..424a9b7eb0 100644 --- a/servlet/src/main/java/io/undertow/servlet/handlers/security/SSLInformationAssociationHandler.java +++ b/servlet/src/main/java/io/undertow/servlet/handlers/security/SSLInformationAssociationHandler.java @@ -105,6 +105,10 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { if (ssl != null) { String cipherSuite = ssl.getCipherSuite(); byte[] sessionId = ssl.getSessionId(); + // Required since Jakarta Servlet 6.1 + if (ssl.getSecureProtocol() != null) { + request.setAttribute("jakarta.servlet.request.secure_protocol", ssl.getSecureProtocol()); + } request.setAttribute("jakarta.servlet.request.cipher_suite", cipherSuite); request.setAttribute("jakarta.servlet.request.key_size", ssl.getKeySize()); request.setAttribute("jakarta.servlet.request.ssl_session_id", sessionId != null? HexConverter.convertToHexString(sessionId) : null); diff --git a/servlet/src/main/java/io/undertow/servlet/spec/HttpServletResponseImpl.java b/servlet/src/main/java/io/undertow/servlet/spec/HttpServletResponseImpl.java index 94adee52c4..a1e7f85bab 100644 --- a/servlet/src/main/java/io/undertow/servlet/spec/HttpServletResponseImpl.java +++ b/servlet/src/main/java/io/undertow/servlet/spec/HttpServletResponseImpl.java @@ -189,12 +189,20 @@ public void sendError(final int sc) throws IOException { @Override public void sendRedirect(final String location) throws IOException { + sendRedirect(location, StatusCodes.FOUND, true); + } + + @Override + public void sendRedirect(final String location, final int sc, final boolean clearBuffer) throws IOException { if (responseStarted()) { throw UndertowServletMessages.MESSAGES.responseAlreadyCommitted(); } - resetBuffer(); + if (clearBuffer) { + resetBuffer(); + } + // TODO (jrp) should this only be cleared if clearBuffer is set to false? I somewhat think no. exchange.getResponseHeaders().remove(Headers.CONTENT_LENGTH); - setStatus(StatusCodes.FOUND); + setStatus(sc); String realPath; if (isAbsoluteUrl(location)) {//absolute url exchange.getResponseHeaders().put(Headers.LOCATION, location); diff --git a/servlet/src/main/java/io/undertow/servlet/spec/ServletCookieAdaptor.java b/servlet/src/main/java/io/undertow/servlet/spec/ServletCookieAdaptor.java index 68a1f0b368..591b5656ef 100644 --- a/servlet/src/main/java/io/undertow/servlet/spec/ServletCookieAdaptor.java +++ b/servlet/src/main/java/io/undertow/servlet/spec/ServletCookieAdaptor.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Date; +import java.util.Map; import io.undertow.UndertowMessages; import io.undertow.server.handlers.Cookie; @@ -34,12 +35,10 @@ * @author Richard Opalka */ public class ServletCookieAdaptor implements Cookie { + private static final String SAME_SITE = "SameSite"; private final jakarta.servlet.http.Cookie cookie; - private boolean sameSite; - private String sameSiteMode; - public ServletCookieAdaptor(final jakarta.servlet.http.Cookie cookie) { this.cookie = cookie; } @@ -159,18 +158,18 @@ public Cookie setComment(final String comment) { @Override public boolean isSameSite() { - return sameSite; + return cookie.getAttribute(SAME_SITE) != null; } @Override public Cookie setSameSite(final boolean sameSite) { - this.sameSite = sameSite; + cookie.setAttribute(SAME_SITE, CookieSameSiteMode.LAX.toString()); return this; } @Override public String getSameSiteMode() { - return sameSiteMode; + return cookie.getAttribute(SAME_SITE); } @Override @@ -178,14 +177,29 @@ public Cookie setSameSiteMode(final String mode) { final String m = CookieSameSiteMode.lookupModeString(mode); if (m != null) { UndertowServletLogger.REQUEST_LOGGER.tracef("Setting SameSite mode to [%s] for cookie [%s]", m, this.getName()); - this.sameSiteMode = m; - this.setSameSite(true); + cookie.setAttribute(SAME_SITE, m); } else { UndertowServletLogger.REQUEST_LOGGER.warnf(UndertowMessages.MESSAGES.invalidSameSiteMode(mode, Arrays.toString(CookieSameSiteMode.values())), "Ignoring specified SameSite mode [%s] for cookie [%s]", mode, this.getName()); } return this; } + @Override + public String getAttribute(final String name) { + return cookie.getAttribute(name); + } + + @Override + public Cookie setAttribute(final String name, final String value) { + cookie.setAttribute(name, value); + return this; + } + + @Override + public Map getAttributes() { + return cookie.getAttributes(); + } + @Override public final int hashCode() { int result = 17; diff --git a/servlet/src/main/java/io/undertow/servlet/util/DispatchUtils.java b/servlet/src/main/java/io/undertow/servlet/util/DispatchUtils.java index 5fdd927a2e..0f49626a86 100644 --- a/servlet/src/main/java/io/undertow/servlet/util/DispatchUtils.java +++ b/servlet/src/main/java/io/undertow/servlet/util/DispatchUtils.java @@ -39,6 +39,8 @@ import static jakarta.servlet.RequestDispatcher.ERROR_EXCEPTION; import static jakarta.servlet.RequestDispatcher.ERROR_EXCEPTION_TYPE; import static jakarta.servlet.RequestDispatcher.ERROR_MESSAGE; +import static jakarta.servlet.RequestDispatcher.ERROR_METHOD; +import static jakarta.servlet.RequestDispatcher.ERROR_QUERY_STRING; import static jakarta.servlet.RequestDispatcher.ERROR_REQUEST_URI; import static jakarta.servlet.RequestDispatcher.ERROR_SERVLET_NAME; import static jakarta.servlet.RequestDispatcher.ERROR_STATUS_CODE; @@ -161,6 +163,8 @@ public static ServletPathMatch dispatchError(final String path, final String ser } // specific attributes for error requestImpl.setAttribute(ERROR_REQUEST_URI, requestImpl.getRequestURI()); + // Required since Jakarta Servlet 6.1 + requestImpl.setAttribute(ERROR_QUERY_STRING, requestImpl.getQueryString()); requestImpl.setAttribute(ERROR_SERVLET_NAME, servletName); if (exception != null) { if (exception instanceof ServletException && ((ServletException)exception).getRootCause() != null) { @@ -172,6 +176,8 @@ public static ServletPathMatch dispatchError(final String path, final String ser } } requestImpl.setAttribute(ERROR_MESSAGE, message); + // Required since Jakarta Servlet 6.1 + requestImpl.setAttribute(ERROR_METHOD, requestImpl.getMethod()); requestImpl.setAttribute(ERROR_STATUS_CODE, responseImpl.getStatus()); final String newRequestPath = assignRequestPath(path, requestImpl, servletContext, false); diff --git a/servlet/src/test/java/io/undertow/servlet/test/handlers/MarkSecureHandlerTestCase.java b/servlet/src/test/java/io/undertow/servlet/test/handlers/MarkSecureHandlerTestCase.java index c16a485f8a..59768f9aa4 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/handlers/MarkSecureHandlerTestCase.java +++ b/servlet/src/test/java/io/undertow/servlet/test/handlers/MarkSecureHandlerTestCase.java @@ -129,7 +129,7 @@ public void testMarkSecureHandlerWithSecureCookieHandler() throws IOException, G Assert.assertEquals("true", result.getHeaders("issecure")[0].getValue()); // When SecureCookieHandler is enabled with MarkSecureHandler, secure cookie is enabled as this channel is treated as secure Header header = result.getFirstHeader("set-cookie"); - Assert.assertEquals("foo=bar; secure", header.getValue()); + Assert.assertEquals("foo=bar; Secure", header.getValue()); final String response = HttpClientUtils.readResponse(result); Assert.assertEquals(HELLO_WORLD, response); } finally { diff --git a/servlet/src/test/java/io/undertow/servlet/test/mock/MockRequestTestCase.java b/servlet/src/test/java/io/undertow/servlet/test/mock/MockRequestTestCase.java index 8e9f361803..0c60c4c870 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/mock/MockRequestTestCase.java +++ b/servlet/src/test/java/io/undertow/servlet/test/mock/MockRequestTestCase.java @@ -531,6 +531,10 @@ public void setDateHeader(String name, long date) { public void addDateHeader(String name, long date) { } + @Override + public void sendRedirect(final String location, final int sc, final boolean clearBuffer) throws IOException { + } + @Override public void setHeader(String name, String value) { } diff --git a/servlet/src/test/java/io/undertow/servlet/test/response/cookies/ResponseCookiesTestCase.java b/servlet/src/test/java/io/undertow/servlet/test/response/cookies/ResponseCookiesTestCase.java index ed950fbaca..2858b57746 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/response/cookies/ResponseCookiesTestCase.java +++ b/servlet/src/test/java/io/undertow/servlet/test/response/cookies/ResponseCookiesTestCase.java @@ -122,7 +122,7 @@ public void overwriteCookies() throws Exception { final Header[] setCookieHeaders = result.getHeaders("Set-Cookie"); assertEquals(5, setCookieHeaders.length); Arrays.sort(setCookieHeaders, Comparator.comparing(Object::toString)); - assertTrue(setCookieHeadersMatchesValue("JSESSIONID=.*; path=/servletContext", setCookieHeaders)); + assertTrue(setCookieHeadersMatchesValue("JSESSIONID=.*; Path=/servletContext", setCookieHeaders)); assertTrue(setCookieHeadersContainsValue("test=test10; domain=www.domain.com", setCookieHeaders)); assertTrue(setCookieHeadersContainsValue("test=test2; path=/test", setCookieHeaders)); assertTrue(setCookieHeadersContainsValue("test=test5", setCookieHeaders)); @@ -146,9 +146,9 @@ public void jsessionIdCookies() throws Exception { final Header[] setCookieHeaders = result.getHeaders("Set-Cookie"); assertEquals(4, setCookieHeaders.length); - assertTrue(setCookieHeadersContainsValueStartingWithPrefix("JSESSIONID=_bug_fix; path=/path3; Max-Age=500; Expires=", setCookieHeaders)); - assertTrue(setCookieHeadersContainsValueStartingWithPrefix("JSESSIONID=_bug_fix; path=/path4; Max-Age=1000; Expires=", setCookieHeaders)); - assertTrue(setCookieHeadersMatchesValue("JSESSIONID=.*; path=/servletContext", setCookieHeaders)); + assertTrue(setCookieHeadersContainsValueStartingWithPrefix("JSESSIONID=_bug_fix; Path=/path3; Max-Age=500; Expires=", setCookieHeaders)); + assertTrue(setCookieHeadersContainsValueStartingWithPrefix("JSESSIONID=_bug_fix; Path=/path4; Max-Age=1000; Expires=", setCookieHeaders)); + assertTrue(setCookieHeadersMatchesValue("JSESSIONID=.*; Path=/servletContext", setCookieHeaders)); } finally { client.getConnectionManager().shutdown(); } @@ -157,7 +157,7 @@ public void jsessionIdCookies() throws Exception { private static boolean setCookieHeadersContainsValue(final String value, final Header[] setCookieHeaders) { if (setCookieHeaders == null) return false; for (Header h : setCookieHeaders) { - if (value.equals(h.getValue())) return true; + if (value.equalsIgnoreCase(h.getValue())) return true; } return false; } diff --git a/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLAttributesServlet.java b/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLAttributesServlet.java index 70ed94c22a..4bb510cc91 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLAttributesServlet.java +++ b/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLAttributesServlet.java @@ -44,6 +44,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se pw.write(req.getAttribute("jakarta.servlet.request.key_size").toString()); } else if (req.getServletPath().equals("/cipher-suite")) { pw.write(req.getAttribute("jakarta.servlet.request.cipher_suite").toString()); + } else if (req.getServletPath().equals("/secure-protocol")) { + pw.write(req.getAttribute("jakarta.servlet.request.secure_protocol").toString()); } else if (req.getServletPath().equals("/cert")) { final X509Certificate[] attribute = (X509Certificate[]) req.getAttribute("jakarta.servlet.request.X509Certificate"); if (attribute!=null){ diff --git a/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLMetaDataProxyTestCase.java b/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLMetaDataProxyTestCase.java index 58f7b03020..3dd993c67c 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLMetaDataProxyTestCase.java +++ b/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLMetaDataProxyTestCase.java @@ -70,11 +70,13 @@ public void testWithHeaders() throws Exception { String id = "1633d36df6f28e1325912b46f7d214f97370c39a6b3fc24ee374a76b3f9b0fba"; String cipher = "ECDHE-RSA-AES128-GCM-SHA256"; String keySize = "128"; + final String sslProtocol = "TLSv1.2"; Header[] headers = { new BasicHeader(Headers.SSL_SESSION_ID_STRING, id), new BasicHeader(Headers.SSL_CLIENT_CERT_STRING, cert), new BasicHeader(Headers.SSL_CIPHER_STRING, cipher), - new BasicHeader(Headers.SSL_CIPHER_USEKEYSIZE_STRING, keySize) + new BasicHeader(Headers.SSL_CIPHER_USEKEYSIZE_STRING, keySize), + new BasicHeader(Headers.SECURE_PROTOCOL_STRING, sslProtocol), }; String response = internalTest("/cert-dn", headers); Assert.assertEquals(dummyCertificate.getSubjectDN().toString(), response); @@ -82,6 +84,8 @@ public void testWithHeaders() throws Exception { Assert.assertEquals(id, response); response = internalTest("/cipher-suite", headers); Assert.assertEquals(cipher, response); + response = internalTest("/secure-protocol", headers); + Assert.assertEquals(sslProtocol, response); response = internalTest("/key-size", headers); Assert.assertEquals(keySize, response); } diff --git a/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLMetaDataTestCase.java b/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLMetaDataTestCase.java index 621fb8097a..aae78f2315 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLMetaDataTestCase.java +++ b/servlet/src/test/java/io/undertow/servlet/test/security/ssl/SSLMetaDataTestCase.java @@ -60,6 +60,7 @@ protected static PathHandler setupPathHandler() throws Exception { .addMapping("/cert") .addMapping("/cert-dn") .addMapping("/key-size") + .addMapping("/secure-protocol") .addMapping("/cipher-suite"); DeploymentInfo info = new DeploymentInfo() diff --git a/servlet/src/test/java/io/undertow/servlet/test/wrapper/NonStandardResponseWrapper.java b/servlet/src/test/java/io/undertow/servlet/test/wrapper/NonStandardResponseWrapper.java index 1b6e5127c5..548ef84ee1 100644 --- a/servlet/src/test/java/io/undertow/servlet/test/wrapper/NonStandardResponseWrapper.java +++ b/servlet/src/test/java/io/undertow/servlet/test/wrapper/NonStandardResponseWrapper.java @@ -331,6 +331,21 @@ public void sendRedirect(String location) throws IOException { this._getHttpServletResponse().sendRedirect(location); } + @Override + public void sendRedirect(final String location, final boolean clearBuffer) throws IOException { + this._getHttpServletResponse().sendRedirect(location, clearBuffer); + } + + @Override + public void sendRedirect(final String location, final int sc) throws IOException { + this._getHttpServletResponse().sendRedirect(location, sc); + } + + @Override + public void sendRedirect(final String location, final int sc, final boolean clearBuffer) throws IOException { + this._getHttpServletResponse().sendRedirect(location, sc, clearBuffer); + } + /** * The default behavior of this method is to call setDateHeader(String name, long date) * on the wrapped response object. diff --git a/websockets-jsr/pom.xml b/websockets-jsr/pom.xml index 47d90344e9..e7304b474d 100644 --- a/websockets-jsr/pom.xml +++ b/websockets-jsr/pom.xml @@ -170,7 +170,6 @@ true reversealphabetical ${skipWebSocketTests} - true true ${proxy}