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
2 changes: 1 addition & 1 deletion bin/catalina.bat
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ set CATALINA_OPTS=
goto execCmd

:doVersion
%_EXECJAVA% %JAVA_OPTS% -classpath "%CATALINA_HOME%\lib\catalina.jar" org.apache.catalina.util.ServerInfo
%_EXECJAVA% %JAVA_OPTS% -classpath "%CATALINA_HOME%\bin\tomcat-juli.jar;%CATALINA_HOME%\lib\*" -Dcatalina.home="%CATALINA_HOME%" -Dcatalina.base="%CATALINA_BASE%" org.apache.catalina.util.ServerInfo
goto end


Expand Down
4 changes: 3 additions & 1 deletion bin/catalina.sh
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,9 @@ elif [ "$1" = "configtest" ] ; then
elif [ "$1" = "version" ] ; then

eval "\"$_RUNJAVA\"" "$JAVA_OPTS" \
-classpath "\"$CATALINA_HOME/lib/catalina.jar\"" \
-classpath "\"$CATALINA_HOME/bin/tomcat-juli.jar:$CATALINA_HOME/lib/*\"" \
Comment thread
csutherl marked this conversation as resolved.
-Dcatalina.home="\"$CATALINA_HOME\"" \
-Dcatalina.base="\"$CATALINA_BASE\"" \
org.apache.catalina.util.ServerInfo

else
Expand Down
90 changes: 90 additions & 0 deletions java/org/apache/catalina/core/AprLifecycleListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,96 @@ public static boolean isAprAvailable() {
return org.apache.tomcat.jni.AprStatus.isAprAvailable();
}

/**
* Helper method to safely get a version string from APR/TCN.
* Checks APR availability and handles exceptions.
*
* @param versionSupplier supplier that returns the version string
* @return the version string, or null if APR is not available or an error occurs
*/
private static String getVersionString(java.util.function.Supplier<String> versionSupplier) {
if (!isAprAvailable()) {
return null;
}

try {
return versionSupplier.get();
} catch (Exception e) {
return null;
}
}

/**
* Get the installed Tomcat Native version string, if available.
*
* @return the version string, or null if APR is not available
*/
public static String getInstalledTcnVersion() {
return getVersionString(org.apache.tomcat.jni.Library::versionString);
}

/**
* Get the installed APR version string, if available.
*
* @return the APR version string, or null if APR is not available
*/
public static String getInstalledAprVersion() {
return getVersionString(org.apache.tomcat.jni.Library::aprVersionString);
}

/**
* Get the installed OpenSSL version string (via APR), if available.
*
* @return the OpenSSL version string, or null if not available
*/
public static String getInstalledOpenSslVersion() {
return getVersionString(org.apache.tomcat.jni.SSL::versionString);
}

/**
* Helper method to convert version components to a comparable integer.
*
* @param major major version number
* @param minor minor version number
* @param patch patch version number
*
* @return comparable version integer
*/
private static int versionToInt(int major, int minor, int patch) {
return major * 1000 + minor * 100 + patch;
}

/**
* Get a warning message if the installed Tomcat Native version is older than recommended.
* This performs the same version check used during Tomcat startup.
*
* @return a warning message if the installed version is outdated, or null if the version
* is acceptable or APR is not available
*/
public static String getTcnVersionWarning() {
if (!isAprAvailable()) {
return null;
}

try {
int installedVersion = versionToInt(
org.apache.tomcat.jni.Library.TCN_MAJOR_VERSION,
org.apache.tomcat.jni.Library.TCN_MINOR_VERSION,
org.apache.tomcat.jni.Library.TCN_PATCH_VERSION);
int recommendedVersion = versionToInt(
TCN_RECOMMENDED_MAJOR,
TCN_RECOMMENDED_MINOR,
TCN_RECOMMENDED_PV);
if (installedVersion < recommendedVersion) {
return "WARNING: Tomcat recommends a minimum version of " +
TCN_RECOMMENDED_MAJOR + "." + TCN_RECOMMENDED_MINOR + "." + TCN_RECOMMENDED_PV;
}
return null;
} catch (Exception e) {
Comment thread
csutherl marked this conversation as resolved.
return null;
}
}

public AprLifecycleListener() {
org.apache.tomcat.jni.AprStatus.setInstanceCreated(true);
}
Expand Down
23 changes: 23 additions & 0 deletions java/org/apache/catalina/core/OpenSSLLifecycleListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,29 @@ public static boolean isAvailable() {
return OpenSSLStatus.isAvailable();
}

/**
* Get the installed OpenSSL version string (via FFM), if available.
*
* @return the OpenSSL version string (e.g., "OpenSSL 3.2.6 30 Sep 2025"), or null if not available
*/
public static String getInstalledOpenSslVersion() {
if (!isAvailable()) {
return null;
}

if (JreCompat.isJre22Available()) {
try {
Class<?> openSSLLibraryClass =
Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary");
Comment thread
csutherl marked this conversation as resolved.
return (String) openSSLLibraryClass.getMethod("getVersionString").invoke(null);
} catch (Throwable t) {
Throwable throwable = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(throwable);
}
}
return null;
}

public OpenSSLLifecycleListener() {
OpenSSLStatus.setInstanceCreated(true);
}
Expand Down
215 changes: 215 additions & 0 deletions java/org/apache/catalina/util/ServerInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@
package org.apache.catalina.util;


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

import org.apache.tomcat.util.ExceptionUtils;

Expand Down Expand Up @@ -121,6 +126,10 @@ public static String getServerNumber() {
}

public static void main(String[] args) {
// Suppress INFO logging from library initialization
java.util.logging.Logger.getLogger("org.apache.tomcat.util.net.openssl.panama").setLevel(java.util.logging.Level.WARNING);
java.util.logging.Logger.getLogger("org.apache.catalina.core").setLevel(java.util.logging.Level.WARNING);

System.out.println("Server version: " + getServerInfo());
System.out.println("Server built: " + getServerBuilt());
System.out.println("Server number: " + getServerNumber());
Expand All @@ -129,6 +138,212 @@ public static void main(String[] args) {
System.out.println("Architecture: " + System.getProperty("os.arch"));
System.out.println("JVM Version: " + System.getProperty("java.runtime.version"));
System.out.println("JVM Vendor: " + System.getProperty("java.vm.vendor"));

// Get CATALINA_HOME for library scanning (already displayed in catalina script output preface)
String catalinaHome = System.getProperty("catalina.home");

// Display APR/Tomcat Native information if available
boolean aprLoaded = false;
try {
// Try to initialize APR by creating an instance and calling isAprAvailable()
// Creating an instance sets the instance flag which allows initialization
Class<?> aprLifecycleListenerClass = Class.forName("org.apache.catalina.core.AprLifecycleListener");
aprLifecycleListenerClass.getConstructor().newInstance();
Boolean aprAvailable = (Boolean) aprLifecycleListenerClass.getMethod("isAprAvailable").invoke(null);
if (aprAvailable != null && aprAvailable.booleanValue()) {
// APR is available, get version information using public methods
String tcnVersion = (String) aprLifecycleListenerClass.getMethod("getInstalledTcnVersion").invoke(null);
String aprVersion = (String) aprLifecycleListenerClass.getMethod("getInstalledAprVersion").invoke(null);

System.out.println("APR loaded: true");
System.out.println("APR Version: " + aprVersion);
System.out.println("Tomcat Native: " + tcnVersion);
aprLoaded = true;

// Check if installed version is older than recommended
try {
String warning = (String) aprLifecycleListenerClass.getMethod("getTcnVersionWarning").invoke(null);

if (warning != null) {
System.out.println(" " + warning);
}
} catch (Exception e) {
// Failed to check version - ignore
}

// Display OpenSSL version if available
try {
String openSSLVersion = (String) aprLifecycleListenerClass.getMethod("getInstalledOpenSslVersion").invoke(null);

if (openSSLVersion != null && !openSSLVersion.isEmpty()) {
System.out.println("OpenSSL (APR): " + openSSLVersion);
}
} catch (Exception e) {
// SSL not initialized or not available
}
}
} catch (ClassNotFoundException | NoClassDefFoundError e) {
// APR/Tomcat Native classes not available on classpath
} catch (Exception e) {
// Error checking APR status
}

if (!aprLoaded) {
System.out.println("APR loaded: false");
}

// Display FFM OpenSSL information if available
try {
// Try to initialize FFM OpenSSL by creating an instance and calling isAvailable()
// Creating an instance sets the instance flag which allows initialization
Class<?> openSSLLifecycleListenerClass = Class.forName("org.apache.catalina.core.OpenSSLLifecycleListener");
openSSLLifecycleListenerClass.getConstructor().newInstance();
Boolean ffmAvailable = (Boolean) openSSLLifecycleListenerClass.getMethod("isAvailable").invoke(null);

if (ffmAvailable != null && ffmAvailable.booleanValue()) {
// FFM OpenSSL is available, get version information using public method
String versionString = (String) openSSLLifecycleListenerClass.getMethod("getInstalledOpenSslVersion").invoke(null);

if (versionString != null && !versionString.isEmpty()) {
System.out.println("OpenSSL (FFM): " + versionString);
}
}
} catch (ClassNotFoundException | NoClassDefFoundError e) {
// FFM OpenSSL classes not available on classpath
} catch (Exception e) {
// Error checking FFM OpenSSL status
}

// Display third-party libraries in CATALINA_HOME/lib
if (catalinaHome != null) {
File libDir = new File(catalinaHome, "lib");
if (libDir.exists() && libDir.isDirectory()) {
File[] allJars = libDir.listFiles((dir, name) -> name.endsWith(".jar"));

if (allJars != null && allJars.length > 0) {
// First pass: collect third-party JARs and find longest name
List<File> thirdPartyJars = new ArrayList<>();
int maxNameLength = 0;
for (File jar : allJars) {
if (!isTomcatCoreJar(jar)) {
thirdPartyJars.add(jar);
maxNameLength = Math.max(maxNameLength, jar.getName().length());
}
}

// Second pass: print with aligned formatting
if (!thirdPartyJars.isEmpty()) {
System.out.println();
System.out.println("Third-party libraries:");
for (File jar : thirdPartyJars) {
String version = getJarVersion(jar);
String jarName = jar.getName();
// Colon right after name, then pad to align version numbers
String nameWithColon = jarName + ":";
String paddedName = String.format("%-" + (maxNameLength + 1) + "s", nameWithColon);
if (version != null) {
System.out.println(" " + paddedName + " " + version);
} else {
System.out.println(" " + paddedName + " (unknown)");
}
}
}
}
}
}
}

private static boolean isTomcatCoreJar(File jarFile) {
try (JarFile jar = new JarFile(jarFile)) {
Manifest manifest = jar.getManifest();

if (manifest != null) {
// Check Bundle-SymbolicName to identify Tomcat core JARs
String bundleName = manifest.getMainAttributes().getValue("Bundle-SymbolicName");
if (bundleName != null) {
// Tomcat core JARs have Bundle-SymbolicName starting with org.apache.tomcat,
// org.apache.catalina, or jakarta.
if (bundleName.startsWith("org.apache.tomcat") ||
bundleName.startsWith("org.apache.catalina") ||
bundleName.startsWith("jakarta.")) {
return true;
}
}

// Fallback: Check Implementation-Vendor and Implementation-Title
String implVendor = manifest.getMainAttributes().getValue("Implementation-Vendor");
String implTitle = manifest.getMainAttributes().getValue("Implementation-Title");

if ("Apache Software Foundation".equals(implVendor) && "Apache Tomcat".equals(implTitle)) {
return true;
}
}
} catch (Exception e) {
// Ignore errors reading JAR manifest
}

return false;
}

private static String getJarVersion(File jarFile) {
Comment thread
csutherl marked this conversation as resolved.
// First try manifest attributes
try (JarFile jar = new JarFile(jarFile)) {
Manifest manifest = jar.getManifest();

if (manifest != null) {
// Try different common version attributes
String[] versionAttrs = {"Bundle-Version", "Implementation-Version", "Specification-Version"};
for (String attr : versionAttrs) {
String version = manifest.getMainAttributes().getValue(attr);
if (version != null) {
return version;
}
}
}
} catch (Exception e) {
// Ignore errors reading JAR manifest
}

// Fallback: try to parse version from filename
return parseVersionFromFilename(jarFile.getName());
}

/**
* Attempt to extract a version number from a JAR filename.
* Common patterns include:
* - name-version.jar (e.g., commons-logging-1.2.jar)
* - name_version.jar (e.g., library_2.3.4.jar)
* - name-version-SNAPSHOT.jar (e.g., mylib-1.0.0-SNAPSHOT.jar)
*
* @param filename the JAR filename
* @return the extracted version string, or null if no version pattern is found
*/
private static String parseVersionFromFilename(String filename) {
if (filename == null || !filename.endsWith(".jar")) {
return null;
}

// Remove .jar extension
String nameWithoutExt = filename.substring(0, filename.length() - 4);

// Try to find version pattern by looking for the first separator followed by a digit
// Search from right to left to find the start of the version string
String[] separators = {"-", "_"};
for (String sep : separators) {
// Find all occurrences of the separator
int index = nameWithoutExt.indexOf(sep);
while (index >= 0 && index < nameWithoutExt.length() - 1) {
String candidate = nameWithoutExt.substring(index + 1);
// Check if this looks like a version number (starts with digit)
if (!candidate.isEmpty() && Character.isDigit(candidate.charAt(0))) {
return candidate;
}
// Move to next separator
index = nameWithoutExt.indexOf(sep, index + 1);
}
}

return null;
}

}
Loading