Skip to content
Open
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
9 changes: 9 additions & 0 deletions java/org/apache/catalina/Globals.java
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,15 @@ public Globals() {
public static final boolean STRICT_SERVLET_COMPLIANCE =
Boolean.getBoolean("org.apache.catalina.STRICT_SERVLET_COMPLIANCE");

/**
* If {@code true}, every ServerInfo.properties on the class path is merged
* (bundled catalina.jar copy first as defaults, override files on top) so an
* override file can replace individual properties. Otherwise only the first
* ServerInfo.properties found is used.
*/
public static final boolean LOAD_SERVER_INFO_OVERRIDE =
Boolean.getBoolean("org.apache.catalina.util.LOAD_SERVER_INFO_OVERRIDE");


/**
* Default domain for MBeans if none can be determined
Expand Down
44 changes: 38 additions & 6 deletions java/org/apache/catalina/util/ServerInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@

import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import org.apache.catalina.Globals;
import org.apache.tomcat.util.ExceptionUtils;


Expand Down Expand Up @@ -71,15 +74,44 @@ public ServerInfo() {
String number = null;

Properties props = new Properties();
try (InputStream is = ServerInfo.class.getResourceAsStream("/org/apache/catalina/util/ServerInfo.properties")) {
props.load(is);
info = props.getProperty("server.info");
built = props.getProperty("server.built");
builtIso = props.getProperty("server.built.iso");
number = props.getProperty("server.number");

try {
if (Globals.LOAD_SERVER_INFO_OVERRIDE) {
// Merge every ServerInfo.properties on the class path in reverse
// order, so the bundled catalina.jar copy provides the defaults
// and an override file (e.g. in $CATALINA_BASE/lib) replaces
// only the properties it sets.
List<URL> urls = Collections.list(ServerInfo.class.getClassLoader()
.getResources("org/apache/catalina/util/ServerInfo.properties"));
Collections.reverse(urls);
for (URL url : urls) {
try (InputStream is = url.openStream()) {
props.load(is);
} catch (Throwable t) {
// Catch Throwable so one unreadable or malformed resource
// does not stop the others loading; narrowing to
// IOException was rejected on apache/tomcat PR #366.
ExceptionUtils.handleThrowable(t);
}
}
} else {
// Original behavior: load only the first resource found
try (InputStream is = ServerInfo.class.getResourceAsStream(
"/org/apache/catalina/util/ServerInfo.properties")) {
if (is != null) {
props.load(is);
}
}
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}

info = props.getProperty("server.info");
built = props.getProperty("server.built");
builtIso = props.getProperty("server.built.iso");
number = props.getProperty("server.number");

if (info == null || info.equals("Apache Tomcat/@VERSION@")) {
info = "Apache Tomcat/12.0.x-dev";
}
Expand Down
158 changes: 158 additions & 0 deletions test/org/apache/catalina/util/TestServerInfoOverride.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* 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.catalina.util;

import java.io.File;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

/**
* Tests the opt-in {@code ServerInfo.properties} merge behaviour enabled by the
* {@code org.apache.catalina.util.LOAD_SERVER_INFO_OVERRIDE} system property.
* <p>
* The default behaviour (property unset) is unchanged and is covered by
* {@code TestServerInfo}: only the first {@code ServerInfo.properties} on the
* class path is read, with no merging. When the property is set to {@code true}
* every {@code ServerInfo.properties} on the class path is merged so an override
* file can replace individual properties while the rest keep their bundled
* values.
* <p>
* {@link ServerInfo} reads its values in a static initializer that runs once per
* class loader, and {@link org.apache.catalina.Globals} reads the controlling
* system property once when it is initialized. To exercise the real code this
* test loads a fresh copy of both classes in an isolated {@link URLClassLoader}
* whose class path places an override {@code ServerInfo.properties} ahead of a
* bundled one.
*/
public class TestServerInfoOverride {

private static final String PROP = "org.apache.catalina.util.LOAD_SERVER_INFO_OVERRIDE";
private static final String RESOURCE = "org/apache/catalina/util/ServerInfo.properties";

private static final String BUNDLED =
"server.info=Apache Tomcat/99.0.0\nserver.number=99.0.0\n" +
"server.built=Jan 1 2026\nserver.built.iso=2026-01-01\n";

@Rule
public final TemporaryFolder tmp = new TemporaryFolder();

/**
* With the feature enabled, values present in the override file win while
* values it does not set fall back to the bundled defaults.
*
* @throws Exception if the test experiences an unexpected error
*/
@Test
public void testPartialOverrideMergedWhenEnabled() throws Exception {
File overrideDir = writeProps("override",
"server.info=Custom Tomcat/1.2.3\nserver.number=1.2.3\n");
File bundledDir = writeProps("bundled", BUNDLED);

Properties result = loadServerInfo(true, overrideDir, bundledDir);

// Set by the override file - the override wins.
Assert.assertEquals("Custom Tomcat/1.2.3", result.getProperty("server.info"));
Assert.assertEquals("1.2.3", result.getProperty("server.number"));
// Not set by the override file - the bundled defaults are retained.
Assert.assertEquals("Jan 1 2026", result.getProperty("server.built"));
Assert.assertEquals("2026-01-01", result.getProperty("server.built.iso"));
}

/**
* Create {@code <name>/org/apache/catalina/util/ServerInfo.properties} under
* the temporary folder with the supplied content and return the root
* directory so it can be added to a class path.
*/
private File writeProps(String name, String content) throws Exception {
File root = tmp.newFolder(name);
File file = new File(root, RESOURCE);
Files.createDirectories(file.getParentFile().toPath());
try (OutputStream os = Files.newOutputStream(file.toPath())) {
os.write(content.getBytes(StandardCharsets.UTF_8));
}
return root;
}

/**
* Load a fresh {@link ServerInfo} in an isolated class loader with the given
* override state and class path prefix, and return its resolved values.
* <p>
* The supplied directories are placed at the front of the class path in
* order, so - once {@link ServerInfo} reverses the discovered resources when
* the feature is enabled - the later directories act as defaults that the
* earlier ones override.
*/
private Properties loadServerInfo(boolean overrideEnabled, File... classpathPrefix)
throws Exception {

List<URL> urls = new ArrayList<>();
for (File dir : classpathPrefix) {
urls.add(dir.toURI().toURL());
}
// Include the current class path so the isolated loader can resolve the
// Tomcat classes (and their dependencies) it needs to load fresh.
for (String entry : System.getProperty("java.class.path").split(File.pathSeparator)) {
urls.add(new File(entry).toURI().toURL());
}

String previous = System.getProperty(PROP);
if (overrideEnabled) {
System.setProperty(PROP, "true");
} else {
System.clearProperty(PROP);
}

// Parent is the platform class loader so that ServerInfo and Globals are
// loaded fresh from our URLs (and thus re-run their static initializers)
// rather than being delegated to the application class loader.
try (URLClassLoader loader = new URLClassLoader(urls.toArray(new URL[0]),
ClassLoader.getPlatformClassLoader())) {

Class<?> clazz = Class.forName("org.apache.catalina.util.ServerInfo", true, loader);

Properties result = new Properties();
result.setProperty("server.info", invoke(clazz, "getServerInfo"));
result.setProperty("server.number", invoke(clazz, "getServerNumber"));
result.setProperty("server.built", invoke(clazz, "getServerBuilt"));
result.setProperty("server.built.iso", invoke(clazz, "getServerBuiltISO"));
return result;
} finally {
if (previous == null) {
System.clearProperty(PROP);
} else {
System.setProperty(PROP, previous);
}
}
}

private static String invoke(Class<?> clazz, String method) throws Exception {
Method m = clazz.getMethod(method);
return (String) m.invoke(null);
}
}
9 changes: 9 additions & 0 deletions webapps/docs/changelog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@
</subsection>
<subsection name="Catalina">
<changelog>
<add>
Add an opt-in system property
<code>org.apache.catalina.util.LOAD_SERVER_INFO_OVERRIDE</code> that, when
set to <code>true</code>, merges every <code>ServerInfo.properties</code>
on the class path so a file placed in <code>$CATALINA_BASE/lib</code> can
override individual server version strings while the properties it does
not set retain their bundled values. The default behavior is unchanged.
(csutherl)
</add>
<fix>
Lower the log level to debug when OpenSSL initialization fails in
<code>OpenSSLLifecycleListener</code> to avoid stack traces
Expand Down
11 changes: 11 additions & 0 deletions webapps/docs/config/systemprops.xml
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,17 @@
(100 MiB) will be used.</p>
</property>

<property name="org.apache.catalina.util.LOAD_SERVER_INFO_OVERRIDE">
<p>If <code>true</code>, every <code>ServerInfo.properties</code> on the
class path is merged: the copy bundled in <code>catalina.jar</code>
provides the defaults and an override file (for example in
<code>$CATALINA_BASE/lib/</code>) replaces only the properties it sets.
This customizes the reported server version strings without modifying
<code>catalina.jar</code>.</p>
<p>If not specified, the default value of <code>false</code> is used and
only the first <code>ServerInfo.properties</code> found is loaded.</p>
</property>

</properties>

</section>
Expand Down
10 changes: 10 additions & 0 deletions webapps/docs/security-howto.xml
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,11 @@
number reported in some of the management tools and may make it harder to
determine the real version installed. The CATALINA_HOME/bin/version.bat|sh
script will still report the correct version number.</p>
<p>By default only the first <code>ServerInfo.properties</code> on the class
path is used, so any property it omits falls back to a generic default. To
override only specific properties and keep the real values for the rest, set
<code>org.apache.catalina.util.LOAD_SERVER_INFO_OVERRIDE</code> to
<code>true</code> so the bundled values are loaded first as defaults.</p>

<p>The default ErrorReportValve can display stack traces and/or JSP
source code to clients when an error occurs. To avoid this, custom error
Expand Down Expand Up @@ -555,6 +560,11 @@
determine the real version installed. The CATALINA_HOME/bin/version.bat|sh
script will still report the correct version number.
</p>
<p>By default only the first <code>ServerInfo.properties</code> on the class
path is used, so any property it omits falls back to a generic default. To
override only specific properties and keep the real values for the rest, set
<code>org.apache.catalina.util.LOAD_SERVER_INFO_OVERRIDE</code> to
<code>true</code> so the bundled values are loaded first as defaults.</p>

<p>The CGI Servlet is disabled by default. If enabled, the debug
initialisation parameter should not be set to <code>10</code> or higher on a
Expand Down
Loading