Skip to content

Commit badf1ff

Browse files
authored
Merge pull request #1823 from fl4via/backport-features_2.4.x_3
[UNDERTOW-2325][UNDERTOW-2523][UNDERTOW-2539] Port features and api changes to 2.4.x
2 parents d42d152 + eb97b10 commit badf1ff

35 files changed

+468
-86
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010

1111
jobs:
1212
build-all:
13-
name: Compile (no tests) with JDK 11
13+
name: Compile (no tests) with JDK 17
1414
runs-on: ubuntu-latest
1515
steps:
1616
- uses: n1hility/cancel-previous-runs@v3
@@ -23,11 +23,11 @@ jobs:
2323
restore-keys: |
2424
m2-
2525
- uses: actions/checkout@v4
26-
- name: Set up JDK 11
26+
- name: Set up JDK 17
2727
uses: actions/setup-java@v4
2828
with:
2929
distribution: temurin
30-
java-version: 11
30+
java-version: 17
3131
- name: Generate settings.xml for Maven Builds
3232
uses: whelk-io/maven-settings-xml-action@v21
3333
with:
@@ -60,7 +60,7 @@ jobs:
6060
matrix:
6161
os: [ubuntu-latest, windows-latest, macos-latest]
6262
module: [core]
63-
jdk: [11, 17, 21]
63+
jdk: [17, 21]
6464
openjdk_impl: [ temurin ]
6565
steps:
6666
- name: Update hosts - linux
@@ -122,7 +122,7 @@ jobs:
122122
os: [ubuntu-latest]
123123
module: [core, servlet, websockets-jsr]
124124
proxy: ['-Pproxy', '']
125-
jdk: [11]
125+
jdk: [17]
126126
steps:
127127
- name: Update hosts - linux
128128
if: matrix.os == 'ubuntu-latest'

core/src/main/java/io/undertow/UndertowOptions.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,14 +204,25 @@ public class UndertowOptions {
204204
*/
205205
public static final Option<Boolean> ALLOW_EQUALS_IN_COOKIE_VALUE = Option.simple(UndertowOptions.class, "ALLOW_EQUALS_IN_COOKIE_VALUE", Boolean.class);
206206

207+
/**
208+
* If this is true then Undertow will disable RFC6265 compliant cookie parsing for Set-Cookie header instead of legacy backward compatible behavior.
209+
* <p>
210+
* default is {@code false}
211+
* </p>
212+
*/
213+
public static final Option<Boolean> DISABLE_RFC6265_COOKIE_PARSING = Option.simple(UndertowOptions.class, "DISABLE_RFC6265_COOKIE_PARSING", Boolean.class);
214+
215+
public static final boolean DEFAULT_DISABLE_RFC6265_COOKIE_PARSING = false;
216+
207217
/**
208218
* If this is true then Undertow will enable RFC6265 compliant cookie validation for Set-Cookie header instead of legacy backward compatible behavior.
209219
*
210220
* default is false
211221
*/
212222
public static final Option<Boolean> ENABLE_RFC6265_COOKIE_VALIDATION = Option.simple(UndertowOptions.class, "ENABLE_RFC6265_COOKIE_VALIDATION", Boolean.class);
213223

214-
public static final boolean DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATION = false;
224+
// As of Jakarta 6.1, RFC 6265 is used for the cookie specification https://github.com/jakartaee/servlet/issues/37
225+
public static final boolean DEFAULT_ENABLE_RFC6265_COOKIE_VALIDATION = true;
215226

216227
/**
217228
* If we should attempt to use SPDY for HTTPS connections.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* JBoss, Home of Professional Open Source.
3+
*
4+
* Copyright 2024 Red Hat, Inc., and individual contributors
5+
* as indicated by the @author tags.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package io.undertow.attribute;
21+
22+
import io.undertow.server.HttpServerExchange;
23+
import io.undertow.server.SSLSessionInfo;
24+
25+
/**
26+
* An attribute which describes the secure protocol. This is the protocol resolved from the {@link SSLSessionInfo#getSecureProtocol()}.
27+
* @author <a href="mailto:[email protected]">James R. Perkins</a>
28+
*/
29+
public class SecureProtocolAttribute implements ExchangeAttribute {
30+
31+
public static final SecureProtocolAttribute INSTANCE = new SecureProtocolAttribute();
32+
33+
@Override
34+
public String readAttribute(final HttpServerExchange exchange) {
35+
final SSLSessionInfo ssl = exchange.getConnection().getSslSessionInfo();
36+
if (ssl == null || ssl.getSecureProtocol() == null) {
37+
return null;
38+
}
39+
return ssl.getSecureProtocol();
40+
}
41+
42+
@Override
43+
public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException {
44+
throw new ReadOnlyAttributeException("Secure Protocol", newValue);
45+
}
46+
47+
@Override
48+
public String toString() {
49+
return "%{SECURE_PROTOCOL}";
50+
}
51+
52+
public static final class Builder implements ExchangeAttributeBuilder {
53+
54+
@Override
55+
public String name() {
56+
return "Secure Protocol";
57+
}
58+
59+
@Override
60+
public ExchangeAttribute build(final String token) {
61+
if (token.equals("%{SECURE_PROTOCOL}")) {
62+
return INSTANCE;
63+
}
64+
return null;
65+
}
66+
67+
@Override
68+
public int priority() {
69+
return 0;
70+
}
71+
}
72+
}

core/src/main/java/io/undertow/server/BasicSSLSessionInfo.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public class BasicSSLSessionInfo implements SSLSessionInfo {
4343
private final java.security.cert.Certificate[] peerCertificate;
4444
private final X509Certificate[] certificate;
4545
private final Integer keySize;
46+
private final String secureProtocol;
4647

4748
/**
4849
*
@@ -54,9 +55,24 @@ public class BasicSSLSessionInfo implements SSLSessionInfo {
5455
* @throws CertificateException If the client cert could not be decoded
5556
*/
5657
public BasicSSLSessionInfo(byte[] sessionId, String cypherSuite, String certificate, Integer keySize) throws java.security.cert.CertificateException, CertificateException {
58+
this(sessionId, cypherSuite, certificate, keySize, null);
59+
}
60+
61+
/**
62+
*
63+
* @param sessionId The SSL session ID
64+
* @param cypherSuite The cypher suite name
65+
* @param certificate A string representation of the client certificate
66+
* @param keySize The key-size used by the cypher
67+
* @param secureProtocol the secure protocol, example {@code TLSv1.2}
68+
* @throws java.security.cert.CertificateException If the client cert could not be decoded
69+
* @throws CertificateException If the client cert could not be decoded
70+
*/
71+
public BasicSSLSessionInfo(byte[] sessionId, String cypherSuite, String certificate, Integer keySize, String secureProtocol) throws java.security.cert.CertificateException, CertificateException {
5772
this.sessionId = sessionId;
5873
this.cypherSuite = cypherSuite;
5974
this.keySize = keySize;
75+
this.secureProtocol = secureProtocol;
6076
if (certificate != null) {
6177
java.security.cert.CertificateFactory cf = java.security.cert.CertificateFactory.getInstance("X.509");
6278
byte[] certificateBytes = certificate.getBytes(StandardCharsets.US_ASCII);
@@ -123,6 +139,20 @@ public BasicSSLSessionInfo(String sessionId, String cypherSuite, String certific
123139
this(sessionId == null ? null : fromHex(sessionId), cypherSuite, certificate, keySize);
124140
}
125141

142+
/**
143+
*
144+
* @param sessionId The encoded SSL session ID
145+
* @param cypherSuite The cypher suite name
146+
* @param certificate A string representation of the client certificate
147+
* @param keySize The key-size used by the cypher
148+
* @param secureProtocol the secure protocol, example {@code TLSv1.2}
149+
* @throws java.security.cert.CertificateException If the client cert could not be decoded
150+
* @throws CertificateException If the client cert could not be decoded
151+
*/
152+
public BasicSSLSessionInfo(String sessionId, String cypherSuite, String certificate, Integer keySize, String secureProtocol) throws java.security.cert.CertificateException, CertificateException {
153+
this(sessionId == null ? null : fromHex(sessionId), cypherSuite, certificate, keySize, secureProtocol);
154+
}
155+
126156
@Override
127157
public byte[] getSessionId() {
128158
if(sessionId == null) {
@@ -174,6 +204,11 @@ public SSLSession getSSLSession() {
174204
return null;
175205
}
176206

207+
@Override
208+
public String getSecureProtocol() {
209+
return secureProtocol;
210+
}
211+
177212
private static byte[] fromHex(String sessionId) {
178213
try {
179214
return HexConverter.convertFromHex(sessionId);

core/src/main/java/io/undertow/server/ConnectionSSLSessionInfo.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ public SSLSession getSSLSession() {
145145
return channel.getSslSession();
146146
}
147147

148+
@Override
149+
public String getSecureProtocol() {
150+
return channel.getSslSession().getProtocol();
151+
}
152+
148153
//Suppress incorrect resource leak warning.
149154
@SuppressWarnings("resource")
150155
public void renegotiateBufferRequest(HttpServerExchange exchange, SslClientAuthMode newAuthMode) throws IOException {

core/src/main/java/io/undertow/server/Connectors.java

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
import java.io.IOException;
4242
import java.nio.charset.StandardCharsets;
4343
import java.util.Date;
44+
import java.util.Map;
45+
import java.util.Set;
46+
import java.util.TreeSet;
4447
import java.util.concurrent.Executor;
4548
import java.util.concurrent.RejectedExecutionException;
4649

@@ -57,6 +60,7 @@ public class Connectors {
5760

5861
private static final boolean[] ALLOWED_TOKEN_CHARACTERS = new boolean[256];
5962
private static final boolean[] ALLOWED_SCHEME_CHARACTERS = new boolean[256];
63+
private static final Set<String> KNOWN_ATTRIBUTE_NAMES = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
6064

6165
static {
6266
for(int i = 0; i < ALLOWED_TOKEN_CHARACTERS.length; ++i) {
@@ -108,6 +112,16 @@ public class Connectors {
108112
}
109113
}
110114
}
115+
116+
KNOWN_ATTRIBUTE_NAMES.add("Path");
117+
KNOWN_ATTRIBUTE_NAMES.add("Domain");
118+
KNOWN_ATTRIBUTE_NAMES.add("Discard");
119+
KNOWN_ATTRIBUTE_NAMES.add("Secure");
120+
KNOWN_ATTRIBUTE_NAMES.add("HttpOnly");
121+
KNOWN_ATTRIBUTE_NAMES.add("Max-Age");
122+
KNOWN_ATTRIBUTE_NAMES.add("Expires");
123+
KNOWN_ATTRIBUTE_NAMES.add("Comment");
124+
KNOWN_ATTRIBUTE_NAMES.add("SameSite");
111125
}
112126
/**
113127
* Flattens the exchange cookie map into the response header map. This should be called by a
@@ -224,17 +238,17 @@ private static String addRfc6265ResponseCookieToExchange(final Cookie cookie) {
224238
header.append("; Domain=");
225239
header.append(cookie.getDomain());
226240
}
227-
if (cookie.isDiscard()) {
228-
header.append("; Discard");
229-
}
230241
if (cookie.isSecure()) {
231242
header.append("; Secure");
232243
}
233244
if (cookie.isHttpOnly()) {
234245
header.append("; HttpOnly");
235246
}
236247
if (cookie.getMaxAge() != null) {
237-
if (cookie.getMaxAge() >= 0) {
248+
// TODO (jrp) Per the TCK test "RFC 6265 - server should only send +ve values for Max-Age"
249+
// TODO (jrp) This is possibly per https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2, however
250+
// TODO (jrp) I'm not sure not adding the value is correct.
251+
if (cookie.getMaxAge() > 0) {
238252
header.append("; Max-Age=");
239253
header.append(cookie.getMaxAge());
240254
}
@@ -270,6 +284,7 @@ private static String addRfc6265ResponseCookieToExchange(final Cookie cookie) {
270284
header.append(cookie.getSameSiteMode());
271285
}
272286
}
287+
appendAttributes(cookie, header);
273288
return header.toString();
274289
}
275290

@@ -298,7 +313,10 @@ private static String addVersion0ResponseCookieToExchange(final Cookie cookie) {
298313
header.append("; Expires=");
299314
header.append(DateUtils.toOldCookieDateString(cookie.getExpires()));
300315
} else if (cookie.getMaxAge() != null) {
301-
if (cookie.getMaxAge() >= 0) {
316+
// TODO (jrp) Per the TCK test "RFC 6265 - server should only send +ve values for Max-Age"
317+
// TODO (jrp) This is possibly per https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2, however
318+
// TODO (jrp) I'm not sure not adding the value is correct.
319+
if (cookie.getMaxAge() > 0) {
302320
header.append("; Max-Age=");
303321
header.append(cookie.getMaxAge());
304322
}
@@ -320,6 +338,7 @@ private static String addVersion0ResponseCookieToExchange(final Cookie cookie) {
320338
header.append(cookie.getSameSiteMode());
321339
}
322340
}
341+
appendAttributes(cookie, header);
323342
return header.toString();
324343

325344
}
@@ -350,7 +369,10 @@ private static String addVersion1ResponseCookieToExchange(final Cookie cookie) {
350369
header.append("; HttpOnly");
351370
}
352371
if (cookie.getMaxAge() != null) {
353-
if (cookie.getMaxAge() >= 0) {
372+
// TODO (jrp) Per the TCK test "RFC 6265 - server should only send +ve values for Max-Age"
373+
// TODO (jrp) This is possibly per https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2, however
374+
// TODO (jrp) I'm not sure not adding the value is correct.
375+
if (cookie.getMaxAge() > 0) {
354376
header.append("; Max-Age=");
355377
header.append(cookie.getMaxAge());
356378
}
@@ -386,6 +408,7 @@ private static String addVersion1ResponseCookieToExchange(final Cookie cookie) {
386408
header.append(cookie.getSameSiteMode());
387409
}
388410
}
411+
appendAttributes(cookie, header);
389412
return header.toString();
390413
}
391414

@@ -665,4 +688,18 @@ public static boolean areRequestHeadersValid(HeaderMap headers) {
665688
}
666689
return true;
667690
}
691+
692+
private static void appendAttributes(final Cookie cookie, final StringBuilder header) {
693+
for (Map.Entry<String, String> entry : cookie.getAttributes().entrySet()) {
694+
if (KNOWN_ATTRIBUTE_NAMES.contains(entry.getKey())) {
695+
continue;
696+
}
697+
header.append("; ")
698+
.append(entry.getKey());
699+
if (!entry.getValue().isBlank()) {
700+
header.append('=')
701+
.append(entry.getValue());
702+
}
703+
}
704+
}
668705
}

core/src/main/java/io/undertow/server/HttpServerExchange.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1249,7 +1249,7 @@ public Iterable<Cookie> requestCookies() {
12491249
Cookies.parseRequestCookies(
12501250
getConnection().getUndertowOptions().get(UndertowOptions.MAX_COOKIES, UndertowOptions.DEFAULT_MAX_COOKIES),
12511251
getConnection().getUndertowOptions().get(UndertowOptions.ALLOW_EQUALS_IN_COOKIE_VALUE, false),
1252-
requestHeaders.get(Headers.COOKIE), requestCookiesParam);
1252+
requestHeaders.get(Headers.COOKIE), requestCookiesParam, getConnection().getUndertowOptions().get(UndertowOptions.DISABLE_RFC6265_COOKIE_PARSING, UndertowOptions.DEFAULT_DISABLE_RFC6265_COOKIE_PARSING));
12531253
}
12541254
return requestCookies;
12551255
}

core/src/main/java/io/undertow/server/SSLSessionInfo.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919
package io.undertow.server;
2020

21-
import org.xnio.SslClientAuthMode;
22-
23-
import javax.net.ssl.SSLSession;
2421
import java.io.IOException;
22+
import javax.net.ssl.SSLSession;
23+
24+
import org.xnio.SslClientAuthMode;
2525

2626
/**
2727
* SSL session information.
@@ -126,4 +126,13 @@ default int getKeySize() {
126126
*/
127127
SSLSession getSSLSession();
128128

129+
/**
130+
* Returns the {@linkplain SSLSession#getProtocol() secure protocol}, if applicable, for the curren session.
131+
*
132+
* @return the secure protocol or {@code null} if one could not be found
133+
*/
134+
default String getSecureProtocol() {
135+
return getSSLSession() == null ? null : getSSLSession().getProtocol();
136+
}
137+
129138
}

0 commit comments

Comments
 (0)