From f8e3988f88a12d007cdc15d8b38911da07263c79 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 25 Nov 2024 10:58:41 -0600 Subject: [PATCH] [UNDERTOW-1881] - Add a new exchange attribute for SSL/TLS protocol version Add and register new ExchangeAttribute implementation Add support for AJP and TLS Add test case --- .../attribute/SslProtocolAttribute.java | 80 ++++++++++++++++ .../accesslog/DefaultAccessLogReceiver.java | 2 +- ...ndertow.attribute.ExchangeAttributeBuilder | 1 + .../ssl/SslProtocolAttributeTestCase.java | 95 +++++++++++++++++++ 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/io/undertow/attribute/SslProtocolAttribute.java create mode 100644 core/src/test/java/io/undertow/server/ssl/SslProtocolAttributeTestCase.java diff --git a/core/src/main/java/io/undertow/attribute/SslProtocolAttribute.java b/core/src/main/java/io/undertow/attribute/SslProtocolAttribute.java new file mode 100644 index 0000000000..acbcb5161e --- /dev/null +++ b/core/src/main/java/io/undertow/attribute/SslProtocolAttribute.java @@ -0,0 +1,80 @@ +/* + * 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; +import io.undertow.util.HeaderValues; + +public class SslProtocolAttribute implements ExchangeAttribute { + + public static final SslProtocolAttribute INSTANCE = new SslProtocolAttribute(); + + @Override + public String readAttribute(HttpServerExchange exchange) { + String sslProtocol = null; + String transportProtocol = exchange.getConnection().getTransportProtocol(); + if ("ajp".equals(transportProtocol)) { + // TODO: wrong + HeaderValues headerValues = exchange.getRequestHeaders().get("AJP_SSL_PROTOCOL"); + if (headerValues != null && !headerValues.isEmpty()) { + sslProtocol = headerValues.getFirst(); + } + } else { + SSLSessionInfo ssl = exchange.getConnection().getSslSessionInfo(); + if (ssl == null) { + return null; + } + sslProtocol = ssl.getSSLSession().getProtocol(); + } + + return sslProtocol; + } + + @Override + public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("SSL Protocol", newValue); + } + + @Override + public String toString() { + return "%{SSL_PROTOCOL}"; + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "SSL Protocol"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals("%{SSL_PROTOCOL}")) { + return INSTANCE; + } + return null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/core/src/main/java/io/undertow/server/handlers/accesslog/DefaultAccessLogReceiver.java b/core/src/main/java/io/undertow/server/handlers/accesslog/DefaultAccessLogReceiver.java index d6f093c645..d048ae26fb 100644 --- a/core/src/main/java/io/undertow/server/handlers/accesslog/DefaultAccessLogReceiver.java +++ b/core/src/main/java/io/undertow/server/handlers/accesslog/DefaultAccessLogReceiver.java @@ -234,7 +234,7 @@ public void run() { *

* DO NOT USE THIS OUTSIDE OF A TEST */ - void awaitWrittenForTest() throws InterruptedException { + protected void awaitWrittenForTest() throws InterruptedException { while (!pendingMessages.isEmpty() || forceLogRotation) { Thread.sleep(10); } 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..29096dc801 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.SslProtocolAttribute$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/ssl/SslProtocolAttributeTestCase.java b/core/src/test/java/io/undertow/server/ssl/SslProtocolAttributeTestCase.java new file mode 100644 index 0000000000..af232e1d0d --- /dev/null +++ b/core/src/test/java/io/undertow/server/ssl/SslProtocolAttributeTestCase.java @@ -0,0 +1,95 @@ +/* + * 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.server.ssl; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.Executor; +import javax.net.ssl.SSLContext; + +import io.undertow.server.handlers.accesslog.AccessLogFileTestCase; +import io.undertow.server.handlers.accesslog.AccessLogHandler; +import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver; +import io.undertow.testutils.DefaultServer; +import io.undertow.testutils.HttpClientUtils; +import io.undertow.testutils.TestHttpClient; +import io.undertow.util.CompletionLatchHandler; +import io.undertow.util.StatusCodes; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(DefaultServer.class) +public class SslProtocolAttributeTestCase { + private static final Path logDirectory = Paths.get(System.getProperty("java.io.tmpdir")); + + @Test + public void testTlsRequestViaLogging() throws IOException, InterruptedException { + logDirectory.toFile().mkdirs(); + Path logFileName = logDirectory.resolve("server1.log"); + File logFile = logFileName.toFile(); + logFile.createNewFile(); + logFile.deleteOnExit(); + + AccessLogReceiver logReceiver = new AccessLogReceiver(DefaultServer.getWorker(), logDirectory, + "server1.", "log"); + + String formatString = "SSL Protocol is %{SSL_PROTOCOL}."; + CompletionLatchHandler latchHandler= new CompletionLatchHandler( + new AccessLogHandler(exchange -> exchange.getResponseSender().send("ping"), + logReceiver, formatString, + AccessLogFileTestCase.class.getClassLoader())); + DefaultServer.setRootHandler(latchHandler); + + try(TestHttpClient client = new TestHttpClient()) { + DefaultServer.startSSLServer(); + SSLContext sslContext = DefaultServer.getClientSSLContext(); + client.setSSLContext(sslContext); + + HttpGet get = new HttpGet(DefaultServer.getDefaultServerSSLAddress() + "/path"); + HttpResponse result = client.execute(get); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + Assert.assertEquals("ping", HttpClientUtils.readResponse(result)); + latchHandler.await(); + logReceiver.awaitWrittenForTest(); + Assert.assertEquals(formatString.replaceAll("%\\{SSL_PROTOCOL}", sslContext.getProtocol()) + System.lineSeparator(), + new String(Files.readAllBytes(logFileName))); + } finally { + DefaultServer.stopSSLServer(); + } + } + + private static class AccessLogReceiver extends DefaultAccessLogReceiver { + AccessLogReceiver(final Executor logWriteExecutor, + final Path outputDirectory, + final String logBaseName, + final String logNameSuffix) { + super(logWriteExecutor, outputDirectory, logBaseName, logNameSuffix, true); + } + + public void awaitWrittenForTest() throws InterruptedException { + super.awaitWrittenForTest(); + } + } + +}