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/*\"" \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes also the boot classpath...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on why you think that? The change was made in the version command's classpath which doesn't affect tomcat startup since the start command uses $CLASSPATH. Am I missing something?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you didn't miss anything. I expect that this change was deliberate. Ideally that would inlude rather the classpath from catalina.properties to be consistent. But if you think this is enough, I am fine with that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I could use that I guess. I only added what was needed for the new logic though. Let's leave this open and see if others have an opinion since you're neutral. Thanks for the feedback.

-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 @@ -119,6 +119,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) {
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");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I consider making the forName and getMethod calls use a variable rather than a string to make it easier to update if the classes or packages change? Or are these internal APIs generally not changed?

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
175 changes: 175 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,172 @@ 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) {
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
}

return null;
}

}
12 changes: 12 additions & 0 deletions java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,18 @@ public static boolean isFIPSModeActive() {
return fipsModeActive;
}

public static String getVersionString() {
if (!OpenSSLStatus.isAvailable()) {
return null;
}

try {
return OpenSSL_version(0).getString(0);
} catch (Exception e) {
return null;
}
}

public static List<String> findCiphers(String ciphers) {
ArrayList<String> ciphersList = new ArrayList<>();
try (var localArena = Arena.ofConfined()) {
Expand Down
Loading