diff --git a/core/pom.xml b/core/pom.xml index de72eb5220..11cba2cf5a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -102,7 +102,7 @@ junit test - + org.apache.directory.server apacheds-test-framework @@ -530,6 +530,11 @@ + + + true + + 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..d002bc8d3f --- /dev/null +++ b/core/src/main/java/io/undertow/attribute/SecureProtocolAttribute.java @@ -0,0 +1,85 @@ +/* + * 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 javax.net.ssl.SSLSession; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.SSLSessionInfo; +import io.undertow.util.HeaderValues; + +public class SecureProtocolAttribute implements ExchangeAttribute { + + public static final SecureProtocolAttribute INSTANCE = new SecureProtocolAttribute(); + + @Override + public String readAttribute(HttpServerExchange exchange) { + String secureProtocol = 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()) { + secureProtocol = headerValues.getFirst(); + } + } else { + SSLSessionInfo ssl = exchange.getConnection().getSslSessionInfo(); + if (ssl == null) { + return null; + } + SSLSession session = ssl.getSSLSession(); + if (session != null) { + secureProtocol = session.getProtocol(); + } + } + + return secureProtocol; + } + + @Override + public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Secure 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/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..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/ssl/SecureProtocolAttributeTestCase.java b/core/src/test/java/io/undertow/server/ssl/SecureProtocolAttributeTestCase.java new file mode 100644 index 0000000000..ea1bad717c --- /dev/null +++ b/core/src/test/java/io/undertow/server/ssl/SecureProtocolAttributeTestCase.java @@ -0,0 +1,91 @@ +/* + * 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.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.Executor; +import javax.net.ssl.SSLContext; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributes; +import io.undertow.attribute.SubstituteEmptyWrapper; +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.AssumptionViolatedException; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(DefaultServer.class) +public class SecureProtocolAttributeTestCase { + private static final Path logDirectory = Paths.get(System.getProperty("java.io.tmpdir"), "logs"); + + @Test + public void testTlsRequestViaLogging() throws IOException { + if (System.getProperty("undertow.proxied") != null) { + throw new AssumptionViolatedException("This test makes no sense in a proxy environment"); + } + LocalAccessLogReceiver logReceiver + = new LocalAccessLogReceiver(DefaultServer.getWorker(), logDirectory, "server", ".log"); + + final String formatString = "Secure Protocol is %{SECURE_PROTOCOL}."; + CompletionLatchHandler latchHandler = new CompletionLatchHandler( + exchange -> { + ExchangeAttribute tokens = ExchangeAttributes.parser(SecureProtocolAttributeTestCase.class.getClassLoader(), + new SubstituteEmptyWrapper("-")).parse(formatString); + exchange.getResponseSender().send(tokens.readAttribute(exchange)); + }); + + DefaultServer.setRootHandler(latchHandler); + + try (TestHttpClient client = new TestHttpClient()) { + DefaultServer.startSSLServer(); + SSLContext sslContext = DefaultServer.getClientSSLContext(); + client.setSSLContext(sslContext); + + HttpResponse result = client.execute(new HttpGet(DefaultServer.getDefaultServerSSLAddress() + "/path")); + Assert.assertEquals(StatusCodes.OK, result.getStatusLine().getStatusCode()); + Assert.assertEquals(formatString.replaceAll("%\\{SECURE_PROTOCOL}", sslContext.getProtocol()), + HttpClientUtils.readResponse(result)); + } finally { + DefaultServer.stopSSLServer(); + } + } + + private static class LocalAccessLogReceiver extends DefaultAccessLogReceiver { + LocalAccessLogReceiver(final Executor logWriteExecutor, + final Path outputDirectory, + final String logBaseName, + final String logNameSuffix) { + super(logWriteExecutor, outputDirectory, logBaseName, logNameSuffix, true); + } + + protected void awaitWrittenForTest() throws InterruptedException { + super.awaitWrittenForTest(); + } + } + +}