From 6d1dad2f962ef6ca9f17c0c69908e57b4b04f09f Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Mon, 9 Feb 2026 14:56:39 +0200 Subject: [PATCH 01/15] Add httpd properties in build.xml --- build.xml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/build.xml b/build.xml index 14d2960f0528..b08a6fbd4ca0 100644 --- a/build.xml +++ b/build.xml @@ -222,6 +222,10 @@ + + + + @@ -2119,6 +2123,22 @@ + + + + + + + + + + + + + + + + @@ -2176,6 +2196,7 @@ + From bdecff0dbec2bf5eeb0f9d79e7d41a8125ca0f1b Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Wed, 11 Feb 2026 13:17:14 +0200 Subject: [PATCH 02/15] Make access method getTemporaryDirectory() static --- test/org/apache/catalina/startup/LoggingBaseTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/org/apache/catalina/startup/LoggingBaseTest.java b/test/org/apache/catalina/startup/LoggingBaseTest.java index 8a96e414361b..66c5f745c7d3 100644 --- a/test/org/apache/catalina/startup/LoggingBaseTest.java +++ b/test/org/apache/catalina/startup/LoggingBaseTest.java @@ -89,7 +89,7 @@ public static File getBuildDirectory() { * that have to be deleted on cleanup, register them with * {@link #addDeleteOnTearDown(File)}. */ - public File getTemporaryDirectory() { + public static File getTemporaryDirectory() { return tempDir; } From ae48ca22590bbd4ab622e6780a94657dbbcb949d Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Tue, 17 Feb 2026 14:26:42 +0200 Subject: [PATCH 03/15] Document httpd integration tests in BUILDING.txt --- BUILDING.txt | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/BUILDING.txt b/BUILDING.txt index 8ee307952018..6a4a83ba0ca0 100644 --- a/BUILDING.txt +++ b/BUILDING.txt @@ -603,6 +603,73 @@ NOTE: Cobertura is licensed under GPL v2 with parts of it being under test.verbose=false +(7.5) Running httpd integration tests + + Tomcat includes integration tests that verify Tomcat's behavior when + running behind an Apache HTTP Server (httpd) reverse proxy. These tests + require a working httpd installation and are skipped when httpd is not + available. + + 1. Prerequisites + + An Apache HTTP Server (httpd) installation is required. + The following httpd modules must be available: + + - mod_proxy + - mod_proxy_http + - mod_headers + - mod_ssl + - mod_authz_core + - mod_unixd (Linux and macOS only) + - mod_mpm_event or mod_mpm_prefork (Linux and macOS) + + On most Linux distributions, httpd can be installed via the system + package manager: + + # Debian/Ubuntu + sudo apt install apache2 + + # RHEL/Fedora + sudo dnf install httpd + + On macOS, httpd is included with the system or can be installed via + Homebrew: + + brew install httpd + + 2. Configuration + + If the httpd binary is on the system PATH, the tests will find it + automatically. If httpd is installed in a non-standard location, set + the "test.httpd.path" property in your build.properties file to point + to the httpd binary: + + test.httpd.path=/usr/sbin/httpd + + 3. Running the tests + Integration tests are excluded from the default test run. To run + them, use the integration test profile: + + ant test -Dtest.profile=integration + + The httpd integration tests are located in: + + test/org/apache/tomcat/integration/httpd/ + + 4. How the tests work + + Each test starts an httpd instance in foreground mode (-X) with a + generated configuration that proxies requests to a Tomcat instance + managed by the test framework. A file lock (httpd-binary.lock) is + used to serialize test classes so that only one httpd instance runs + at a time. + + The base class (HttpdIntegrationBaseTest) handles platform-specific + httpd module loading automatically. Test subclasses only need to + provide the test-specific httpd directives (proxy rules, extra + modules, etc.). + + (8) Source code checks (8.1) Checkstyle From ce10a8d8491aa825f918719d4a0a5d7b08d4f960 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Tue, 17 Feb 2026 16:34:44 +0200 Subject: [PATCH 04/15] Add httpd integration testing infrastructure --- .../httpd/HttpdIntegrationBaseTest.java | 154 ++++++++++++++++++ .../tomcat/integration/httpd/TesterHttpd.java | 149 +++++++++++++++++ .../integration/httpd/httpd-binary.lock | 0 3 files changed, 303 insertions(+) create mode 100644 test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java create mode 100644 test/org/apache/tomcat/integration/httpd/TesterHttpd.java create mode 100644 test/org/apache/tomcat/integration/httpd/httpd-binary.lock diff --git a/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java b/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java new file mode 100644 index 000000000000..17930db98da2 --- /dev/null +++ b/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat.integration.httpd; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.nio.channels.FileLock; +import java.util.List; + +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Valve; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.compat.JrePlatform; + +/** + * Base class for httpd integration tests. + * Manages httpd and Tomcat process lifecycle. + */ +public abstract class HttpdIntegrationBaseTest extends TomcatBaseTest { + + private static final File lockFile = new File("test/org/apache/tomcat/integration/httpd/httpd-binary.lock"); + private static FileLock lock = null; + + private TesterHttpd httpd; + private int httpdPort; + protected File httpdConfDir; + + private int tomcatPort; + + protected abstract String getHttpdConfig(); + protected abstract List getValveConfig(); + + @BeforeClass + public static void obtainHttpdBinaryLock() throws IOException { + @SuppressWarnings("resource") + FileOutputStream fos = new FileOutputStream(lockFile); + lock = fos.getChannel().lock(); + } + + @AfterClass + public static void releaseHttpdBinaryLock() throws IOException { + // Should not be null be in case obtaining the lock fails, avoid a second error. + if (lock != null) { + lock.release(); + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + setUpTomcat(); + setUpHttpd(); + } + + @Override + public void tearDown() throws Exception { + if (httpd != null) { + httpd.stop(); + httpd = null; + } + super.tearDown(); + } + + private void setUpTomcat() throws LifecycleException { + Tomcat tomcat = getTomcatInstance(); + Context ctx = getProgrammaticRootContext(); + for (Valve valve : getValveConfig()) { + ctx.getPipeline().addValve(valve); + } + Tomcat.addServlet(ctx, "snoop", new SnoopServlet()); + ctx.addServletMappingDecoded("/snoop", "snoop"); + tomcat.start(); + tomcatPort = getPort(); + } + + private void setUpHttpd() throws IOException { + httpdPort = findFreePort(); + httpdConfDir = getTemporaryDirectory(); + generateHttpdConfig(getHttpdConfig()); + + httpd = new TesterHttpd(httpdConfDir, httpdPort); + try { + httpd.start(); + } catch (IOException | InterruptedException ioe) { + httpd = null; + } + } + + private static int findFreePort() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } + + public void generateHttpdConfig(String httpdConf) throws IOException { + + httpdConf = getPlatformHttpdConfig() + httpdConf; + + httpdConf = httpdConf.replace("%{HTTPD_PORT}", Integer.toString(httpdPort)) + .replace("%{TOMCAT_PORT}", Integer.toString(tomcatPort)) + .replace("%{CONF_DIR}", httpdConfDir.getAbsolutePath()); + + + try (PrintWriter writer = new PrintWriter(new File(httpdConfDir, "httpd.conf"))) { + writer.write(httpdConf); + } + + } + + private String getPlatformHttpdConfig() { + StringBuilder sb = new StringBuilder(); + sb.append("Listen %{HTTPD_PORT}\n"); + sb.append("PidFile %{CONF_DIR}/httpd.pid\n"); + sb.append("LoadModule authz_core_module modules/mod_authz_core.so\n"); + if (JrePlatform.IS_WINDOWS) { + sb.append("LoadModule mpm_winnt_module modules/mod_mpm_winnt.so\n"); + sb.append("ErrorLog \"|C:/Windows/System32/more.com\"\n"); + } else { + sb.append("LoadModule unixd_module modules/mod_unixd.so\n"); + sb.append("LoadModule mpm_event_module modules/mod_mpm_event.so\n"); + sb.append("ErrorLog /dev/stderr\n"); + } + sb.append("LogLevel warn\n"); + sb.append("ServerName localhost:%{HTTPD_PORT}\n"); + return sb.toString(); + } + + public int getHttpdPort() { + return httpdPort; + } +} diff --git a/test/org/apache/tomcat/integration/httpd/TesterHttpd.java b/test/org/apache/tomcat/integration/httpd/TesterHttpd.java new file mode 100644 index 000000000000..7502af76ad9d --- /dev/null +++ b/test/org/apache/tomcat/integration/httpd/TesterHttpd.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat.integration.httpd; + +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.Reader; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; + + +public class TesterHttpd { + + private final File httpdConfDir; + private final int httpdPort; + + private static final String HTTPD_PATH = "tomcat.test.httpd.path"; + + private Process p; + + public TesterHttpd(File httpdConfDir, int httpdPort) { + this.httpdConfDir = httpdConfDir; + this.httpdPort = httpdPort; + } + + public void start() throws IOException, InterruptedException { + start(false); + } + + public void start(boolean swallowOutput) throws IOException, InterruptedException { + if (p != null) { + throw new IllegalStateException("Already started"); + } + + String httpdPath = System.getProperty(HTTPD_PATH); + if (httpdPath == null) { + httpdPath = "httpd"; + } + + File httpdConfFile = new File(httpdConfDir, "httpd.conf"); + validateHttpdConfig(httpdPath, httpdConfFile.getAbsolutePath()); + + List cmd = new ArrayList<>(4); + cmd.add(httpdPath); + cmd.add("-f"); + cmd.add(httpdConfFile.getAbsolutePath()); + cmd.add("-X"); + + ProcessBuilder pb = new ProcessBuilder(cmd.toArray(new String[0])); + + p = pb.start(); + + redirect(p.inputReader(), System.out, swallowOutput); + redirect(p.errorReader(), System.err, swallowOutput); + + Assert.assertTrue(p.isAlive() && isHttpdReady()); + } + + public void stop() { + if (p == null) { + throw new IllegalStateException("Not started"); + } + p.destroy(); + + try { + if (!p.waitFor(30, TimeUnit.SECONDS)) { + throw new IllegalStateException("Failed to stop"); + } + } catch (InterruptedException e) { + throw new IllegalStateException("Interrupted while waiting to stop", e); + } + } + + private void redirect(final Reader r, final PrintStream os, final boolean swallow) { + /* + * InputStream will close when process ends. Thread will exit once stream closes. + */ + new Thread( () -> { + char[] cbuf = new char[1024]; + try { + int read; + while ((read = r.read(cbuf)) > 0) { + if (!swallow) { + os.print(new String(cbuf, 0, read)); + } + } + } catch (IOException ignore) { + // Ignore + } + + }).start(); + } + + private static void validateHttpdConfig(final String httpdPath, final String httpdConfPath) throws IOException, InterruptedException { + List cmd = new ArrayList<>(4); + + cmd.add(httpdPath); + cmd.add("-t"); + cmd.add("-f"); + cmd.add(httpdConfPath); + + ProcessBuilder pb = new ProcessBuilder(cmd.toArray(new String[0])); + pb.redirectErrorStream(true); + + Process p = pb.start(); + + String output = new String(p.getInputStream().readAllBytes()); + int exitCode = p.waitFor(); + + if (exitCode != 0) { + throw new IllegalStateException("Httpd configuration invalid. Output: " + output); + } + } + + @SuppressWarnings("BusyWait") + private boolean isHttpdReady() throws InterruptedException { + long deadline = System.currentTimeMillis() + 1000; + while (System.currentTimeMillis() < deadline) { + try (Socket ignored = new Socket("localhost", this.httpdPort)) { + return true; + } catch (IOException e) { + Thread.sleep(100); + } + } + throw new IllegalStateException("Httpd has not been started."); + } + + +} diff --git a/test/org/apache/tomcat/integration/httpd/httpd-binary.lock b/test/org/apache/tomcat/integration/httpd/httpd-binary.lock new file mode 100644 index 000000000000..e69de29bb2d1 From a5c995109310d703a1acc5aa6691388fda40270a Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Tue, 17 Feb 2026 16:34:56 +0200 Subject: [PATCH 05/15] Add TestBasicProxy --- .../integration/httpd/TestBasicProxy.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 test/org/apache/tomcat/integration/httpd/TestBasicProxy.java diff --git a/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java b/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java new file mode 100644 index 000000000000..d3eddbda16aa --- /dev/null +++ b/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat.integration.httpd; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Valve; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestBasicProxy extends HttpdIntegrationBaseTest { + + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + ProxyRequests Off + ProxyPreserveHost On + ProxyPass /snoop http://localhost:%{TOMCAT_PORT}/snoop + ProxyPassReverse /snoop http://localhost:%{TOMCAT_PORT}/snoop + RequestHeader set X-Forwarded-For 140.211.11.130 \s + RequestHeader set X-Forwarded-Proto "http" + """; + + @Override + protected List getValveConfig() { + return new ArrayList<>(); + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void testBasicProxying() throws Exception { + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getHttpdPort() + "/snoop", res, false); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + Assert.assertNotNull(requestDesc.getRequestInfo()); + Assert.assertEquals("127.0.0.1", requestDesc.getRequestInfo("REQUEST-REMOTE-ADDR")); + Assert.assertEquals(getHttpdPort(), Integer.valueOf(requestDesc.getRequestInfo("REQUEST-SERVER-PORT")).intValue()); + Assert.assertEquals(getPort(), Integer.valueOf(requestDesc.getRequestInfo("REQUEST-LOCAL-PORT")).intValue()); + Assert.assertEquals("http", requestDesc.getRequestInfo("REQUEST-SCHEME")); + Assert.assertEquals("false", requestDesc.getRequestInfo("REQUEST-IS-SECURE")); + Assert.assertNotNull(requestDesc.getHeaders()); + Assert.assertNotNull(requestDesc.getHeader("X-Forwarded-For")); + Assert.assertEquals("http", requestDesc.getHeader("X-Forwarded-Proto")); + } +} From 277a20aebe52f4851e93152ccf5dd4178fa33623 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Tue, 17 Feb 2026 16:35:04 +0200 Subject: [PATCH 06/15] Add TestRemoteIpValveWithProxy --- .../httpd/TestRemoteIpValveWithProxy.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java diff --git a/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java b/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java new file mode 100644 index 000000000000..85d805c3fe1e --- /dev/null +++ b/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat.integration.httpd; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Valve; +import org.apache.catalina.valves.RemoteIpValve; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestRemoteIpValveWithProxy extends HttpdIntegrationBaseTest { + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + ProxyRequests Off + ProxyPreserveHost On + ProxyPass /snoop http://localhost:%{TOMCAT_PORT}/snoop + ProxyPassReverse /snoop http://localhost:%{TOMCAT_PORT}/snoop + ProxyAddHeaders Off + RequestHeader set X-Forwarded-For 140.211.11.130 \s + RequestHeader set X-Forwarded-Proto https + RequestHeader set X-Forwarded-Host whoamI.tomcat + """; + + @Override + protected List getValveConfig() { + List valves = new ArrayList<>(); + + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setHostHeader("X-Forwarded-Host"); + valves.add(remoteIpValve); + + return valves; + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void testRemoteIpValveProxying() throws Exception { + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getHttpdPort() + "/snoop", res, false); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + Assert.assertNotNull(requestDesc.getRequestInfo()); + Assert.assertEquals("140.211.11.130", requestDesc.getRequestInfo("REQUEST-REMOTE-ADDR")); + Assert.assertEquals(443, Integer.valueOf(requestDesc.getRequestInfo("REQUEST-SERVER-PORT")).intValue()); + Assert.assertEquals(getPort(), Integer.valueOf(requestDesc.getRequestInfo("REQUEST-LOCAL-PORT")).intValue()); + Assert.assertEquals("https", requestDesc.getRequestInfo("REQUEST-SCHEME")); + Assert.assertEquals("true", requestDesc.getRequestInfo("REQUEST-IS-SECURE")); + Assert.assertEquals("whoamI.tomcat", requestDesc.getRequestInfo("REQUEST-SERVER-NAME")); + Assert.assertNotNull(requestDesc.getHeaders()); + Assert.assertNull(requestDesc.getHeader("X-Forwarded-For")); + Assert.assertEquals("https", requestDesc.getHeader("X-Forwarded-Proto")); + } +} From c828b74514245c2053a0265d9d198e1aefdc76a3 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Wed, 18 Feb 2026 16:54:06 +0200 Subject: [PATCH 07/15] Add ssl support for httpd integration tests --- .../httpd/HttpdIntegrationBaseTest.java | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java b/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java index 17930db98da2..160f5da401bb 100644 --- a/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java +++ b/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java @@ -26,6 +26,7 @@ import java.util.List; import org.junit.AfterClass; +import org.junit.Assume; import org.junit.BeforeClass; import org.apache.catalina.Context; @@ -34,6 +35,7 @@ import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; import org.apache.tomcat.util.compat.JrePlatform; +import org.apache.tomcat.util.net.TesterSupport; /** * Base class for httpd integration tests. @@ -41,11 +43,35 @@ */ public abstract class HttpdIntegrationBaseTest extends TomcatBaseTest { + private static final String HTTPD_CONFIG = + """ + Listen %{HTTPD_PORT} + PidFile %{CONF_DIR}/httpd.pid + LoadModule authz_core_module modules/mod_authz_core.so + """ + + (JrePlatform.IS_WINDOWS ? + """ + LoadModule mpm_winnt_module modules/mod_mpm_winnt.so + ErrorLog "|C:/Windows/System32/more.com" + """ + : + """ + LoadModule unixd_module modules/mod_unixd.so + LoadModule mpm_event_module modules/mod_mpm_event.so + ErrorLog /dev/stderr + """ + ) + + """ + LogLevel warn + ServerName localhost:%{HTTPD_PORT} + """; + private static final File lockFile = new File("test/org/apache/tomcat/integration/httpd/httpd-binary.lock"); private static FileLock lock = null; private TesterHttpd httpd; private int httpdPort; + private int httpdSslPort; protected File httpdConfDir; private int tomcatPort; @@ -98,6 +124,7 @@ private void setUpTomcat() throws LifecycleException { private void setUpHttpd() throws IOException { httpdPort = findFreePort(); + httpdSslPort = findFreePort(); httpdConfDir = getTemporaryDirectory(); generateHttpdConfig(getHttpdConfig()); @@ -106,6 +133,10 @@ private void setUpHttpd() throws IOException { httpd.start(); } catch (IOException | InterruptedException ioe) { httpd = null; + } catch (IllegalStateException ise) { + httpd = null; + Assume.assumeFalse("Required httpd module not available", ise.getMessage() != null && ise.getMessage().contains("Cannot load modules")); + throw ise; } } @@ -117,12 +148,15 @@ private static int findFreePort() throws IOException { public void generateHttpdConfig(String httpdConf) throws IOException { - httpdConf = getPlatformHttpdConfig() + httpdConf; + httpdConf = HTTPD_CONFIG + httpdConf; httpdConf = httpdConf.replace("%{HTTPD_PORT}", Integer.toString(httpdPort)) .replace("%{TOMCAT_PORT}", Integer.toString(tomcatPort)) - .replace("%{CONF_DIR}", httpdConfDir.getAbsolutePath()); - + .replace("%{CONF_DIR}", httpdConfDir.getAbsolutePath()) + .replace("%{HTTPD_SSL_PORT}", Integer.toString(httpdSslPort)) + .replace("%{SSL_CERT_FILE}", new File(TesterSupport.LOCALHOST_RSA_CERT_PEM).getAbsolutePath()) + .replace("%{SSL_KEY_FILE}", new File(TesterSupport.LOCALHOST_RSA_KEY_PEM).getAbsolutePath()) + .replace("%{SSL_CA_CERT_FILE}", new File(TesterSupport.CA_CERT_PEM).getAbsolutePath()); try (PrintWriter writer = new PrintWriter(new File(httpdConfDir, "httpd.conf"))) { writer.write(httpdConf); @@ -130,25 +164,11 @@ public void generateHttpdConfig(String httpdConf) throws IOException { } - private String getPlatformHttpdConfig() { - StringBuilder sb = new StringBuilder(); - sb.append("Listen %{HTTPD_PORT}\n"); - sb.append("PidFile %{CONF_DIR}/httpd.pid\n"); - sb.append("LoadModule authz_core_module modules/mod_authz_core.so\n"); - if (JrePlatform.IS_WINDOWS) { - sb.append("LoadModule mpm_winnt_module modules/mod_mpm_winnt.so\n"); - sb.append("ErrorLog \"|C:/Windows/System32/more.com\"\n"); - } else { - sb.append("LoadModule unixd_module modules/mod_unixd.so\n"); - sb.append("LoadModule mpm_event_module modules/mod_mpm_event.so\n"); - sb.append("ErrorLog /dev/stderr\n"); - } - sb.append("LogLevel warn\n"); - sb.append("ServerName localhost:%{HTTPD_PORT}\n"); - return sb.toString(); - } - public int getHttpdPort() { return httpdPort; } + + public int getHttpdSslPort() { + return httpdSslPort; + } } From 1ebe0c09def4a66c4a0d61a1373c586aafe1f11d Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Wed, 18 Feb 2026 16:54:17 +0200 Subject: [PATCH 08/15] Add TestSSLValveWithProxy --- .../httpd/TestSSLValveWithProxy01.java | 88 +++++++++++++++++++ .../httpd/TestSSLValveWithProxy02.java | 86 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java create mode 100644 test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java diff --git a/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java b/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java new file mode 100644 index 000000000000..5fa8dd8b1523 --- /dev/null +++ b/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat.integration.httpd; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Globals; +import org.apache.catalina.Valve; +import org.apache.catalina.valves.SSLValve; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.TesterSupport; + +public class TestSSLValveWithProxy01 extends HttpdIntegrationBaseTest { + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + LoadModule ssl_module modules/mod_ssl.so + SSLSessionCache none + Listen %{HTTPD_SSL_PORT} https + + ServerName localhost:%{HTTPD_SSL_PORT} + SSLEngine on + SSLCertificateFile "%{SSL_CERT_FILE}" + SSLCertificateKeyFile "%{SSL_KEY_FILE}" + ProxyRequests Off + ProxyPass /snoop http://localhost:%{TOMCAT_PORT}/snoop + ProxyPassReverse /snoop http://localhost:%{TOMCAT_PORT}/snoop + RequestHeader set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s" + RequestHeader set SSL_CIPHER "%{SSL_CIPHER}s" + RequestHeader set SSL_SESSION_ID "%{SSL_SESSION_ID}s" + RequestHeader set SSL_CIPHER_USEKEYSIZE "%{SSL_CIPHER_USEKEYSIZE}s" + + """; + + @Override + protected List getValveConfig() { + List valves = new ArrayList<>(); + + SSLValve sslValve = new SSLValve(); + valves.add(sslValve); + + return valves; + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void testSSLValveProxying() throws Exception { + TesterSupport.configureClientSsl(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("https://localhost:" + getHttpdSslPort() + "/snoop", res, false); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + Assert.assertNotNull(requestDesc.getAttributes()); + Assert.assertNotNull(requestDesc.getAttribute(Globals.CIPHER_SUITE_ATTR)); + Assert.assertNotNull(requestDesc.getAttribute(Globals.SSL_SESSION_ID_ATTR)); + Assert.assertNotNull(requestDesc.getAttribute(Globals.KEY_SIZE_ATTR)); + // No client certificate in this test, mod_ssl sends null which SSLValve correctly treats as absent. + Assert.assertNull(requestDesc.getAttribute(Globals.CERTIFICATES_ATTR)); + } +} diff --git a/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java b/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java new file mode 100644 index 000000000000..af9a7a24446a --- /dev/null +++ b/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat.integration.httpd; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Globals; +import org.apache.catalina.Valve; +import org.apache.catalina.valves.SSLValve; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.TesterSupport; + +public class TestSSLValveWithProxy02 extends HttpdIntegrationBaseTest { + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + LoadModule ssl_module modules/mod_ssl.so + SSLSessionCache none + Listen %{HTTPD_SSL_PORT} https + + ServerName localhost:%{HTTPD_SSL_PORT} + SSLEngine on + SSLCertificateFile "%{SSL_CERT_FILE}" + SSLCertificateKeyFile "%{SSL_KEY_FILE}" + ProxyRequests Off + ProxyPass /snoop http://localhost:%{TOMCAT_PORT}/snoop + ProxyPassReverse /snoop http://localhost:%{TOMCAT_PORT}/snoop + RequestHeader set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s" + RequestHeader set SSL_CIPHER "%{SSL_CIPHER}s" + RequestHeader set SSL_SESSION_ID "%{SSL_SESSION_ID}s" + RequestHeader set SSL_CIPHER_USEKEYSIZE "%{SSL_CIPHER_USEKEYSIZE}s" + SSLVerifyClient optional \s + SSLCACertificateFile "%{SSL_CA_CERT_FILE}" \s + SSLOptions +ExportCertData + + """; + + @Override + protected List getValveConfig() { + List valves = new ArrayList<>(); + + SSLValve sslValve = new SSLValve(); + valves.add(sslValve); + + return valves; + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void testSSLValveProxying() throws Exception { + TesterSupport.configureClientSsl(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("https://localhost:" + getHttpdSslPort() + "/snoop", res, false); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + Assert.assertNotNull(requestDesc.getAttribute(Globals.CERTIFICATES_ATTR)); + } +} From eccc3374c8c566b97e58c475dbecd2eb24db4e2e Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 19 Feb 2026 16:16:33 +0200 Subject: [PATCH 09/15] Make things more transparent --- .../integration/httpd/HttpdIntegrationBaseTest.java | 7 +++++-- .../tomcat/integration/httpd/TestBasicProxy.java | 13 +++++++------ .../httpd/TestRemoteIpValveWithProxy.java | 8 ++++---- .../integration/httpd/TestSSLValveWithProxy01.java | 6 +++--- .../integration/httpd/TestSSLValveWithProxy02.java | 10 +++++----- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java b/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java index 160f5da401bb..84b88562ba9f 100644 --- a/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java +++ b/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java @@ -66,6 +66,8 @@ public abstract class HttpdIntegrationBaseTest extends TomcatBaseTest { ServerName localhost:%{HTTPD_PORT} """; + private static final String SERVLET_NAME = "snoop"; + private static final File lockFile = new File("test/org/apache/tomcat/integration/httpd/httpd-binary.lock"); private static FileLock lock = null; @@ -116,8 +118,8 @@ private void setUpTomcat() throws LifecycleException { for (Valve valve : getValveConfig()) { ctx.getPipeline().addValve(valve); } - Tomcat.addServlet(ctx, "snoop", new SnoopServlet()); - ctx.addServletMappingDecoded("/snoop", "snoop"); + Tomcat.addServlet(ctx, SERVLET_NAME, new SnoopServlet()); + ctx.addServletMappingDecoded("/" + SERVLET_NAME, SERVLET_NAME); tomcat.start(); tomcatPort = getPort(); } @@ -152,6 +154,7 @@ public void generateHttpdConfig(String httpdConf) throws IOException { httpdConf = httpdConf.replace("%{HTTPD_PORT}", Integer.toString(httpdPort)) .replace("%{TOMCAT_PORT}", Integer.toString(tomcatPort)) + .replace("%{SERVLET_NAME}", SERVLET_NAME) .replace("%{CONF_DIR}", httpdConfDir.getAbsolutePath()) .replace("%{HTTPD_SSL_PORT}", Integer.toString(httpdSslPort)) .replace("%{SSL_CERT_FILE}", new File(TesterSupport.LOCALHOST_RSA_CERT_PEM).getAbsolutePath()) diff --git a/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java b/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java index d3eddbda16aa..e27c73b79dc0 100644 --- a/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java +++ b/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java @@ -36,10 +36,10 @@ public class TestBasicProxy extends HttpdIntegrationBaseTest { LoadModule headers_module modules/mod_headers.so ProxyRequests Off ProxyPreserveHost On - ProxyPass /snoop http://localhost:%{TOMCAT_PORT}/snoop - ProxyPassReverse /snoop http://localhost:%{TOMCAT_PORT}/snoop - RequestHeader set X-Forwarded-For 140.211.11.130 \s - RequestHeader set X-Forwarded-Proto "http" + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + RequestHeader set X-Forwarded-For 140.211.11.130 + RequestHeader set X-Forwarded-Proto "https" """; @Override @@ -55,7 +55,7 @@ protected String getHttpdConfig() { @Test public void testBasicProxying() throws Exception { ByteChunk res = new ByteChunk(); - int rc = getUrl("http://localhost:" + getHttpdPort() + "/snoop", res, false); + int rc = getUrl("http://localhost:" + getHttpdPort() + "/endpoint", res, false); Assert.assertEquals(HttpServletResponse.SC_OK, rc); RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); @@ -63,10 +63,11 @@ public void testBasicProxying() throws Exception { Assert.assertEquals("127.0.0.1", requestDesc.getRequestInfo("REQUEST-REMOTE-ADDR")); Assert.assertEquals(getHttpdPort(), Integer.valueOf(requestDesc.getRequestInfo("REQUEST-SERVER-PORT")).intValue()); Assert.assertEquals(getPort(), Integer.valueOf(requestDesc.getRequestInfo("REQUEST-LOCAL-PORT")).intValue()); + // httpd sets X-Forwarded-Proto: https, but without RemoteIpValve Tomcat does not process it. Assert.assertEquals("http", requestDesc.getRequestInfo("REQUEST-SCHEME")); Assert.assertEquals("false", requestDesc.getRequestInfo("REQUEST-IS-SECURE")); Assert.assertNotNull(requestDesc.getHeaders()); Assert.assertNotNull(requestDesc.getHeader("X-Forwarded-For")); - Assert.assertEquals("http", requestDesc.getHeader("X-Forwarded-Proto")); + Assert.assertEquals("https", requestDesc.getHeader("X-Forwarded-Proto")); } } diff --git a/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java b/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java index 85d805c3fe1e..24dfe2fc1c97 100644 --- a/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java +++ b/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java @@ -36,10 +36,10 @@ public class TestRemoteIpValveWithProxy extends HttpdIntegrationBaseTest { LoadModule headers_module modules/mod_headers.so ProxyRequests Off ProxyPreserveHost On - ProxyPass /snoop http://localhost:%{TOMCAT_PORT}/snoop - ProxyPassReverse /snoop http://localhost:%{TOMCAT_PORT}/snoop + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} ProxyAddHeaders Off - RequestHeader set X-Forwarded-For 140.211.11.130 \s + RequestHeader set X-Forwarded-For 140.211.11.130 RequestHeader set X-Forwarded-Proto https RequestHeader set X-Forwarded-Host whoamI.tomcat """; @@ -63,7 +63,7 @@ protected String getHttpdConfig() { @Test public void testRemoteIpValveProxying() throws Exception { ByteChunk res = new ByteChunk(); - int rc = getUrl("http://localhost:" + getHttpdPort() + "/snoop", res, false); + int rc = getUrl("http://localhost:" + getHttpdPort() + "/endpoint", res, false); Assert.assertEquals(HttpServletResponse.SC_OK, rc); RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); diff --git a/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java b/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java index 5fa8dd8b1523..65e2b883d979 100644 --- a/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java +++ b/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java @@ -45,8 +45,8 @@ public class TestSSLValveWithProxy01 extends HttpdIntegrationBaseTest { SSLCertificateFile "%{SSL_CERT_FILE}" SSLCertificateKeyFile "%{SSL_KEY_FILE}" ProxyRequests Off - ProxyPass /snoop http://localhost:%{TOMCAT_PORT}/snoop - ProxyPassReverse /snoop http://localhost:%{TOMCAT_PORT}/snoop + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} RequestHeader set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s" RequestHeader set SSL_CIPHER "%{SSL_CIPHER}s" RequestHeader set SSL_SESSION_ID "%{SSL_SESSION_ID}s" @@ -74,7 +74,7 @@ public void testSSLValveProxying() throws Exception { TesterSupport.configureClientSsl(); ByteChunk res = new ByteChunk(); - int rc = getUrl("https://localhost:" + getHttpdSslPort() + "/snoop", res, false); + int rc = getUrl("https://localhost:" + getHttpdSslPort() + "/endpoint", res, false); Assert.assertEquals(HttpServletResponse.SC_OK, rc); RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); diff --git a/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java b/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java index af9a7a24446a..b1ee23803f4f 100644 --- a/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java +++ b/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java @@ -45,14 +45,14 @@ public class TestSSLValveWithProxy02 extends HttpdIntegrationBaseTest { SSLCertificateFile "%{SSL_CERT_FILE}" SSLCertificateKeyFile "%{SSL_KEY_FILE}" ProxyRequests Off - ProxyPass /snoop http://localhost:%{TOMCAT_PORT}/snoop - ProxyPassReverse /snoop http://localhost:%{TOMCAT_PORT}/snoop + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} RequestHeader set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s" RequestHeader set SSL_CIPHER "%{SSL_CIPHER}s" RequestHeader set SSL_SESSION_ID "%{SSL_SESSION_ID}s" RequestHeader set SSL_CIPHER_USEKEYSIZE "%{SSL_CIPHER_USEKEYSIZE}s" - SSLVerifyClient optional \s - SSLCACertificateFile "%{SSL_CA_CERT_FILE}" \s + SSLVerifyClient optional + SSLCACertificateFile "%{SSL_CA_CERT_FILE}" SSLOptions +ExportCertData """; @@ -77,7 +77,7 @@ public void testSSLValveProxying() throws Exception { TesterSupport.configureClientSsl(); ByteChunk res = new ByteChunk(); - int rc = getUrl("https://localhost:" + getHttpdSslPort() + "/snoop", res, false); + int rc = getUrl("https://localhost:" + getHttpdSslPort() + "/endpoint", res, false); Assert.assertEquals(HttpServletResponse.SC_OK, rc); RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); From 17e799056b7affc39cd3ac473cef4c4634e07586 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 19 Feb 2026 16:29:31 +0200 Subject: [PATCH 10/15] Add TestFullReverseProxy --- .../httpd/TestFullReverseProxy.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java diff --git a/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java b/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java new file mode 100644 index 000000000000..829bd500cce9 --- /dev/null +++ b/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat.integration.httpd; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Globals; +import org.apache.catalina.Valve; +import org.apache.catalina.valves.RemoteIpValve; +import org.apache.catalina.valves.SSLValve; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.TesterSupport; + +public class TestFullReverseProxy extends HttpdIntegrationBaseTest { + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + LoadModule ssl_module modules/mod_ssl.so + SSLSessionCache none + Listen %{HTTPD_SSL_PORT} https + + ServerName localhost:%{HTTPD_SSL_PORT} + SSLEngine on + SSLCertificateFile "%{SSL_CERT_FILE}" + SSLCertificateKeyFile "%{SSL_KEY_FILE}" + ProxyRequests Off + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + RequestHeader set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s" + RequestHeader set SSL_CIPHER "%{SSL_CIPHER}s" + RequestHeader set SSL_SESSION_ID "%{SSL_SESSION_ID}s" + RequestHeader set SSL_CIPHER_USEKEYSIZE "%{SSL_CIPHER_USEKEYSIZE}s" + SSLVerifyClient optional + SSLCACertificateFile "%{SSL_CA_CERT_FILE}" + SSLOptions +ExportCertData + ProxyAddHeaders Off + RequestHeader set X-Forwarded-For 140.211.11.130 + RequestHeader set X-Forwarded-Proto https + RequestHeader set X-Forwarded-Host whoamI.tomcat + + """; + + @Override + protected List getValveConfig() { + List valves = new ArrayList<>(); + + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setHostHeader("X-Forwarded-Host"); + valves.add(remoteIpValve); + + SSLValve sslValve = new SSLValve(); + valves.add(sslValve); + + return valves; + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void testFullReverseProxying() throws Exception { + TesterSupport.configureClientSsl(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("https://localhost:" + getHttpdSslPort() + "/endpoint", res, false); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + Assert.assertNotNull(requestDesc.getRequestInfo()); + Assert.assertEquals("140.211.11.130", requestDesc.getRequestInfo("REQUEST-REMOTE-ADDR")); + Assert.assertEquals(443, Integer.valueOf(requestDesc.getRequestInfo("REQUEST-SERVER-PORT")).intValue()); + Assert.assertEquals("https", requestDesc.getRequestInfo("REQUEST-SCHEME")); + Assert.assertEquals("true", requestDesc.getRequestInfo("REQUEST-IS-SECURE")); + Assert.assertEquals("whoamI.tomcat", requestDesc.getRequestInfo("REQUEST-SERVER-NAME")); + + Assert.assertNotNull(requestDesc.getHeaders()); + Assert.assertNull(requestDesc.getHeader("X-Forwarded-For")); + Assert.assertEquals("https", requestDesc.getHeader("X-Forwarded-Proto")); + + + Assert.assertNotNull(requestDesc.getAttributes()); + Assert.assertNotNull(requestDesc.getAttribute(Globals.CIPHER_SUITE_ATTR)); + Assert.assertNotNull(requestDesc.getAttribute(Globals.SSL_SESSION_ID_ATTR)); + Assert.assertNotNull(requestDesc.getAttribute(Globals.KEY_SIZE_ATTR)); + Assert.assertNotNull(requestDesc.getAttribute(Globals.CERTIFICATES_ATTR)); + } +} From 33ca8be062a6ca4b1193d7b4fb5f035bbd8c3058 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 19 Feb 2026 17:29:40 +0200 Subject: [PATCH 11/15] Add integration test profile and exclude them from default test run --- build.xml | 10 ++++++++++ test-profiles.properties.default | 4 ++++ .../apache/tomcat/integration/httpd/TesterHttpd.java | 2 +- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/build.xml b/build.xml index b08a6fbd4ca0..31fe59cb9a45 100644 --- a/build.xml +++ b/build.xml @@ -2012,6 +2012,9 @@ + + + @@ -2041,6 +2044,11 @@ + + + + + @@ -2226,6 +2234,8 @@ + + diff --git a/test-profiles.properties.default b/test-profiles.properties.default index da10ee9f2e7d..4920030801c6 100644 --- a/test-profiles.properties.default +++ b/test-profiles.properties.default @@ -98,3 +98,7 @@ test.profile.tribes=**/tribes/**/*Test*.java # Build utility test profile: Tests for build tools (normally excluded) # Note: These tests depend on classes not in output JARs and are excluded by default test.profile.buildutil=**/buildutil/**/*Test*.java + +# Integration test profile: Tests requiring external processes (e.g. httpd) +# Note: These tests are excluded by default as they require external binaries +test.profile.integration=**/integration/**/Test*.java \ No newline at end of file diff --git a/test/org/apache/tomcat/integration/httpd/TesterHttpd.java b/test/org/apache/tomcat/integration/httpd/TesterHttpd.java index 7502af76ad9d..691dd428cd11 100644 --- a/test/org/apache/tomcat/integration/httpd/TesterHttpd.java +++ b/test/org/apache/tomcat/integration/httpd/TesterHttpd.java @@ -53,7 +53,7 @@ public void start(boolean swallowOutput) throws IOException, InterruptedExceptio } String httpdPath = System.getProperty(HTTPD_PATH); - if (httpdPath == null) { + if (httpdPath == null || httpdPath.isEmpty()) { httpdPath = "httpd"; } From 8b7ef271dde1f68c9bbb63edbb03ce4cc3a85996 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 19 Feb 2026 19:07:07 +0200 Subject: [PATCH 12/15] Add TestSessionWithProxy --- .../catalina/startup/TomcatBaseTest.java | 6 +- .../httpd/TestSessionWithProxy.java | 117 ++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java diff --git a/test/org/apache/catalina/startup/TomcatBaseTest.java b/test/org/apache/catalina/startup/TomcatBaseTest.java index 21f744647a5c..eb1e320a86e5 100644 --- a/test/org/apache/catalina/startup/TomcatBaseTest.java +++ b/test/org/apache/catalina/startup/TomcatBaseTest.java @@ -22,6 +22,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; +import java.io.Serial; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.URI; @@ -466,6 +467,7 @@ public static RequestDescriptor parse(String body) { */ public static final class SnoopServlet extends HttpServlet { + @Serial private static final long serialVersionUID = 1L; @Override @@ -481,7 +483,7 @@ public void service(HttpServletRequest request, response.setCharacterEncoding("UTF-8"); ServletContext ctx = this.getServletContext(); - HttpSession session = request.getSession(false); + HttpSession session = request.getSession("true".equals(request.getParameter("createSession"))); PrintWriter out = response.getWriter(); out.println("CONTEXT-NAME: " + ctx.getServletContextName()); @@ -558,7 +560,7 @@ public void service(HttpServletRequest request, e.hasMoreElements();) { name = e.nextElement(); value = new StringBuilder(); - String values[] = request.getParameterValues(name); + String[] values = request.getParameterValues(name); int m = values.length; for (int j = 0; j < m; j++) { value.append(values[j]); diff --git a/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java b/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java new file mode 100644 index 000000000000..a021b741a2c9 --- /dev/null +++ b/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat.integration.httpd; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Valve; +import org.apache.catalina.valves.RemoteIpValve; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.TesterSupport; + +public class TestSessionWithProxy extends HttpdIntegrationBaseTest { + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + LoadModule ssl_module modules/mod_ssl.so + SSLSessionCache none + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + Listen %{HTTPD_SSL_PORT} https + + ServerName localhost:%{HTTPD_SSL_PORT} + SSLEngine on + SSLCertificateFile "%{SSL_CERT_FILE}" + SSLCertificateKeyFile "%{SSL_KEY_FILE}" + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + RequestHeader set X-Forwarded-Proto https + + """; + + @Override + protected List getValveConfig() { + List valves = new ArrayList<>(); + + RemoteIpValve remoteIpValve = new RemoteIpValve(); + valves.add(remoteIpValve); + + return valves; + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + /** + * Verify that a session created through httpd can be retrieved + * on a subsequent request using the session cookie. + */ + @Test + public void testSessionCookieSetAndRetrieved() throws Exception { + // Create a session + ByteChunk res = new ByteChunk(); + Map> resHead = new HashMap<>(); + int rc = getUrl("http://localhost:" + getHttpdPort() + "/endpoint?createSession=true", res, null, resHead); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + Assert.assertNotNull(requestDesc.getRequestInfo()); + String sessionId = requestDesc.getRequestInfo("SESSION-ID"); + Assert.assertNotNull(sessionId); + Assert.assertEquals("true", requestDesc.getRequestInfo("SESSION-IS-NEW")); + + String setCookie = resHead.get("Set-Cookie").getFirst(); + Assert.assertTrue(setCookie.contains("JSESSIONID")); + + // Send the session cookie back + Map> reqHead = new HashMap<>(); + reqHead.put("Cookie", List.of("JSESSIONID=" + sessionId)); + rc = getUrl("http://localhost:" + getHttpdPort() + "/endpoint", res, reqHead, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + requestDesc = SnoopResult.parse(res.toString()); + Assert.assertNotNull(requestDesc.getRequestInfo()); + Assert.assertEquals(sessionId, requestDesc.getRequestInfo("SESSION-ID")); + Assert.assertEquals("false", requestDesc.getRequestInfo("SESSION-IS-NEW")); + } + + /** + * Verify that when SSL is used at httpd, but not Tomcat, and RemoteIpValve + * sets the scheme to https, session cookies have the Secure flag. + */ + @Test + public void testSecureCookieWithSslTermination() throws Exception { + TesterSupport.configureClientSsl(); + ByteChunk res = new ByteChunk(); + Map> resHead = new HashMap<>(); + getUrl("https://localhost:" + getHttpdSslPort() + "/endpoint?createSession=true", res, null, resHead); + Assert.assertTrue("Session cookie should have Secure flag", resHead.get("Set-Cookie").getFirst().contains("Secure")); + } + +} From cc86e1d9604288b917aabb1ed3692fe34eae4a38 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Thu, 19 Feb 2026 20:45:24 +0200 Subject: [PATCH 13/15] Add TestLargePayloadWithProxy and TestChunkedTransferEncodingWithProxy --- .../catalina/startup/TomcatBaseTest.java | 2 +- .../TestChunkedTransferEncodingWithProxy.java | 96 +++++++++++++++ .../httpd/TestLargePayloadWithProxy.java | 111 ++++++++++++++++++ 3 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java create mode 100644 test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java diff --git a/test/org/apache/catalina/startup/TomcatBaseTest.java b/test/org/apache/catalina/startup/TomcatBaseTest.java index eb1e320a86e5..10631c5af69c 100644 --- a/test/org/apache/catalina/startup/TomcatBaseTest.java +++ b/test/org/apache/catalina/startup/TomcatBaseTest.java @@ -599,7 +599,7 @@ public void service(HttpServletRequest request, } int bodySize = 0; - if (Method.PUT.equals(request.getMethod())) { + if (Method.PUT.equals(request.getMethod()) || Method.POST.equals(request.getMethod())) { InputStream is = request.getInputStream(); int read = 0; byte[] buffer = new byte[8192]; diff --git a/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java b/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java new file mode 100644 index 000000000000..f051a4ad2c0d --- /dev/null +++ b/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat.integration.httpd; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Valve; +import org.apache.catalina.startup.BytesStreamer; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestChunkedTransferEncodingWithProxy extends HttpdIntegrationBaseTest { + + private static final int PAYLOAD_SIZE = 10 * 1024 * 1024 * 100; + + private static final String HTTPD_CONFIG = """ + LoadModule env_module modules/mod_env.so \s + SetEnv proxy-sendchunked 1 + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + """; + + @Override + protected List getValveConfig() { + return new ArrayList<>(); + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + /** + * Verify that chunked transfer encoding works correctly through the httpd reverse proxy + * which sets proxy-sendchunked to minimize resource usage by using chunked encoding. + */ + @Test + public void testChunkedTransferEncoding() throws Exception { + byte[] payload = new byte[PAYLOAD_SIZE]; + Arrays.fill(payload, (byte) 'A'); + + BytesStreamer streamer = new BytesStreamer() { + private boolean sent = false; + + @Override + public int getLength() { + return -1; + } + + @Override + public int available() { + return sent ? 0 : payload.length; + } + + @Override + public byte[] next() { + sent = true; + return payload; + } + }; + + ByteChunk res = new ByteChunk(); + Map> reqHead = new HashMap<>(); + reqHead.put("Content-Type", List.of("application/octet-stream")); + int rc = postUrl(true, streamer, "http://localhost:" + getHttpdPort() + "/endpoint", res, reqHead, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + Assert.assertEquals(String.valueOf(PAYLOAD_SIZE), requestDesc.getRequestInfo("REQUEST-BODY-SIZE")); + } +} diff --git a/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java b/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java new file mode 100644 index 000000000000..992d0ce6dd2c --- /dev/null +++ b/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat.integration.httpd; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Valve; +import org.apache.catalina.startup.BytesStreamer; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestLargePayloadWithProxy extends HttpdIntegrationBaseTest { + + private static final int PAYLOAD_SIZE = 10 * 1024 * 1024; + + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + """; + + @Override + protected List getValveConfig() { + return new ArrayList<>(); + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + /** + * Verify that a large POST body passes through the httpd reverse proxy + */ + @Test + public void testLargePostBody() throws Exception { + byte[] payload = new byte[PAYLOAD_SIZE]; + Arrays.fill(payload, (byte) 'A'); + + ByteChunk res = new ByteChunk(); + Map> reqHead = new HashMap<>(); + reqHead.put("Content-Type", List.of("application/octet-stream")); + int rc = postUrl(payload, "http://localhost:" + getHttpdPort() + "/endpoint", res, reqHead, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + Assert.assertEquals(String.valueOf(PAYLOAD_SIZE), requestDesc.getRequestInfo("REQUEST-BODY-SIZE")); + } + + /** + * Verify that chunked transfer encoding works correctly through the httpd reverse proxy. + */ + @Test + public void testChunkedTransferEncoding() throws Exception { + byte[] payload = new byte[PAYLOAD_SIZE]; + Arrays.fill(payload, (byte) 'A'); + + BytesStreamer streamer = new BytesStreamer() { + private boolean sent = false; + + @Override + public int getLength() { + return -1; + } + + @Override + public int available() { + return sent ? 0 : payload.length; + } + + @Override + public byte[] next() { + sent = true; + return payload; + } + }; + + ByteChunk res = new ByteChunk(); + Map> reqHead = new HashMap<>(); + reqHead.put("Content-Type", List.of("application/octet-stream")); + int rc = postUrl(true, streamer, "http://localhost:" + getHttpdPort() + "/endpoint", res, reqHead, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + Assert.assertEquals(String.valueOf(PAYLOAD_SIZE), requestDesc.getRequestInfo("REQUEST-BODY-SIZE")); + } +} From efe20a582ad8a7886c747d7667832ca757f0f635 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Tue, 24 Feb 2026 14:19:22 +0200 Subject: [PATCH 14/15] Add TestErrorHandling --- .../integration/httpd/TestErrorHandling.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 test/org/apache/tomcat/integration/httpd/TestErrorHandling.java diff --git a/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java b/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java new file mode 100644 index 000000000000..f6601ccc05ee --- /dev/null +++ b/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.tomcat.integration.httpd; + +import java.io.Serial; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Valve; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestErrorHandling extends HttpdIntegrationBaseTest { + + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + ProxyRequests Off + ProxyPreserveHost On + ProxyPass / http://localhost:%{TOMCAT_PORT}/ + ProxyPassReverse / http://localhost:%{TOMCAT_PORT}/ + """; + + @Override + protected List getValveConfig() { + return new ArrayList<>(); + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void test404NotFound() throws Exception { + int rc = getUrl("http://localhost:" + getHttpdPort() + "/nonexistent", new ByteChunk(), false); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + } + + @Test + public void test500InternalError() throws Exception { + Context ctx = (Context) getTomcatInstance().getHost().findChildren()[0]; + Tomcat.addServlet(ctx, "error", new HttpServlet() { + @Serial + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException { + throw new ServletException("Expected error"); + } + }); + ctx.addServletMappingDecoded("/error", "error"); + int rc = getUrl("http://localhost:" + getHttpdPort() + "/error", new ByteChunk(), false); + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + +} From d5695632ba2c967fd88fc86d8d2bb088def57d26 Mon Sep 17 00:00:00 2001 From: Dimitris Soumis Date: Tue, 24 Feb 2026 16:01:50 +0200 Subject: [PATCH 15/15] Remove redundant module from Windows httpd.conf --- .../tomcat/integration/httpd/HttpdIntegrationBaseTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java b/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java index 84b88562ba9f..f54c2d7cee9f 100644 --- a/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java +++ b/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java @@ -51,7 +51,6 @@ public abstract class HttpdIntegrationBaseTest extends TomcatBaseTest { """ + (JrePlatform.IS_WINDOWS ? """ - LoadModule mpm_winnt_module modules/mod_mpm_winnt.so ErrorLog "|C:/Windows/System32/more.com" """ :