Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions BUILDING.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@
<!-- The OpenSSL tests cases be disabled by specifying an invalid path here -->
<property name="test.openssl.path" value="" />

<!-- Location of Httpd binary (file name, not directory) -->
<!-- The Httpd tests cases be disabled by specifying an invalid path here -->
<property name="test.httpd.path" value="" />

<!-- Include .gitignore in src distributions. -->
<!-- .git and .gitignore are in defaultexcludes since Ant 1.8.2 -->
<defaultexcludes add="**/.git" />
Expand Down Expand Up @@ -2008,6 +2012,9 @@
<condition property="test.name" value="${test.profile.buildutil}">
<equals arg1="@{profile}" arg2="buildutil"/>
</condition>
<condition property="test.name" value="${test.profile.integration}">
<equals arg1="@{profile}" arg2="integration"/>
</condition>
</sequential>
</macrodef>

Expand Down Expand Up @@ -2037,6 +2044,11 @@
<equals arg1="${test.profile}" arg2="tribes"/>
</condition>

<!-- Special handling for integration profile: include integration tests -->
<condition property="test.includeIntegration" value="true">
<equals arg1="${test.profile}" arg2="integration"/>
</condition>

<echo message="Test profile: ${test.profile}"/>
</target>

Expand Down Expand Up @@ -2119,6 +2131,22 @@
</condition>
</target>

<target name="test-httpd-exists" description="Checks for the httpd binary">
<property environment="env" />
<condition property="test.httpd.exists">
<or>
<and>
<length string="${test.httpd.path}" trim="true" length="0" when="gt"/>
<available file="${test.httpd.path}" property="test.httpd.exists"/>
</and>
<and>
<length string="${test.httpd.path}" trim="true" length="0" when="eq"/>
<available file="httpd" filepath="${env.PATH}" property="test.httpd.exists"/>
</and>
</or>
</condition>
</target>

<!-- Set native specific properties -->
<property name="native.nativeaccess" value="--enable-native-access=ALL-UNNAMED"/>
<property name="runtests.librarypath" value="-Djava.library.path=${test.apr.loc}${path.separator}${java.library.path}"/>
Expand Down Expand Up @@ -2176,6 +2204,7 @@
<sysproperty key="tomcat.test.accesslog" value="${test.accesslog}" />
<sysproperty key="tomcat.test.reports" value="${test.reports}" />
<sysproperty key="tomcat.test.openssl.path" value="${test.openssl.path}" />
<sysproperty key="tomcat.test.httpd.path" value="${test.httpd.path}" />
<sysproperty key="tomcat.test.openssl.unimplemented" value="${test.openssl.unimplemented}" />
<sysproperty key="tomcat.test.relaxTiming" value="${test.relaxTiming}" />
<sysproperty key="tomcat.test.sslImplementation" value="${test.sslImplementation}" />
Expand Down Expand Up @@ -2205,6 +2234,8 @@
<exclude name="**/*Performance.java" if="${test.excludePerformance}" />
<!-- Exclude tests that Gump can't compile (unless explicitly requested via buildutil profile) -->
<exclude name="org/apache/tomcat/buildutil/**" unless="test.includeBuildutil" />
<!-- Exclude integration tests requiring external binaries (unless explicitly requested via integration profile) -->
<exclude name="**/integration/**" unless="test.includeIntegration" />
<!-- Exclude tests that require large heaps -->
<exclude name="**/*LargeHeap.java" unless="${test.includeLargeHeap}" />
</fileset>
Expand Down
4 changes: 4 additions & 0 deletions test-profiles.properties.default
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion test/org/apache/catalina/startup/LoggingBaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
8 changes: 5 additions & 3 deletions test/org/apache/catalina/startup/TomcatBaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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());
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -597,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];
Expand Down
176 changes: 176 additions & 0 deletions test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* 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.Assume;
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;
import org.apache.tomcat.util.net.TesterSupport;

/**
* Base class for httpd integration tests.
* Manages httpd and Tomcat process lifecycle.
*/
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 ?
"""
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 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;

private TesterHttpd httpd;
private int httpdPort;
private int httpdSslPort;
protected File httpdConfDir;

private int tomcatPort;

protected abstract String getHttpdConfig();
protected abstract List<Valve> 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, SERVLET_NAME, new SnoopServlet());
ctx.addServletMappingDecoded("/" + SERVLET_NAME, SERVLET_NAME);
tomcat.start();
tomcatPort = getPort();
}

private void setUpHttpd() throws IOException {
httpdPort = findFreePort();
httpdSslPort = findFreePort();
httpdConfDir = getTemporaryDirectory();
generateHttpdConfig(getHttpdConfig());

httpd = new TesterHttpd(httpdConfDir, httpdPort);
try {
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;
}
}

private static int findFreePort() throws IOException {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
}
}

public void generateHttpdConfig(String httpdConf) throws IOException {

httpdConf = HTTPD_CONFIG + httpdConf;

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())
.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);
}

}

public int getHttpdPort() {
return httpdPort;
}

public int getHttpdSslPort() {
return httpdSslPort;
}
}
Loading