Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UNDERTOW-2523] Prepare for Jakarta Servlet 6.1 #1702

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
13 changes: 12 additions & 1 deletion core/src/main/java/io/undertow/UndertowOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,25 @@ public class UndertowOptions {
*/
public static final Option<Boolean> 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.
* <p>
* default is {@code false}
* </p>
*/
public static final Option<Boolean> 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.
*
* default is false
*/
public static final Option<Boolean> 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="mailto:[email protected]">James R. Perkins</a>
*/
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;
}
}
}
35 changes: 35 additions & 0 deletions core/src/main/java/io/undertow/server/BasicSSLSessionInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
*
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
49 changes: 43 additions & 6 deletions core/src/main/java/io/undertow/server/Connectors.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<String> KNOWN_ATTRIBUTE_NAMES = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

static {
for(int i = 0; i < ALLOWED_TOKEN_CHARACTERS.length; ++i) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -224,17 +238,17 @@ 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");
}
if (cookie.isHttpOnly()) {
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());
}
Expand Down Expand Up @@ -270,6 +284,7 @@ private static String addRfc6265ResponseCookieToExchange(final Cookie cookie) {
header.append(cookie.getSameSiteMode());
}
}
appendAttributes(cookie, header);
return header.toString();
}

Expand Down Expand Up @@ -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());
}
Expand All @@ -320,6 +338,7 @@ private static String addVersion0ResponseCookieToExchange(final Cookie cookie) {
header.append(cookie.getSameSiteMode());
}
}
appendAttributes(cookie, header);
return header.toString();

}
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -386,6 +408,7 @@ private static String addVersion1ResponseCookieToExchange(final Cookie cookie) {
header.append(cookie.getSameSiteMode());
}
}
appendAttributes(cookie, header);
return header.toString();
}

Expand Down Expand Up @@ -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<String, String> 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());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1210,7 +1210,7 @@ public Iterable<Cookie> 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;
}
Expand Down
15 changes: 12 additions & 3 deletions core/src/main/java/io/undertow/server/SSLSessionInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}

}
Loading
Loading