diff --git a/bin/catalina.bat b/bin/catalina.bat index 70601b6799b2..0b7d8ba8bd94 100755 --- a/bin/catalina.bat +++ b/bin/catalina.bat @@ -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 diff --git a/bin/catalina.sh b/bin/catalina.sh index ee679ad0c6e0..0e6f33cce98e 100755 --- a/bin/catalina.sh +++ b/bin/catalina.sh @@ -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/*\"" \ + -Dcatalina.home="\"$CATALINA_HOME\"" \ + -Dcatalina.base="\"$CATALINA_BASE\"" \ org.apache.catalina.util.ServerInfo else diff --git a/java/org/apache/catalina/core/AprLifecycleListener.java b/java/org/apache/catalina/core/AprLifecycleListener.java index bf047c362d73..f21c7c21ecb8 100644 --- a/java/org/apache/catalina/core/AprLifecycleListener.java +++ b/java/org/apache/catalina/core/AprLifecycleListener.java @@ -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 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); } diff --git a/java/org/apache/catalina/core/OpenSSLLifecycleListener.java b/java/org/apache/catalina/core/OpenSSLLifecycleListener.java index 1e314ba87b9f..ba8c082b5395 100644 --- a/java/org/apache/catalina/core/OpenSSLLifecycleListener.java +++ b/java/org/apache/catalina/core/OpenSSLLifecycleListener.java @@ -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"); + 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); } diff --git a/java/org/apache/catalina/util/ServerInfo.java b/java/org/apache/catalina/util/ServerInfo.java index ae74d176dabe..556f0db07f85 100644 --- a/java/org/apache/catalina/util/ServerInfo.java +++ b/java/org/apache/catalina/util/ServerInfo.java @@ -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; @@ -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()); @@ -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 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; } } diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java index 7272db9cf2d1..2d71ab6d113d 100644 --- a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java +++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java @@ -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 findCiphers(String ciphers) { ArrayList ciphersList = new ArrayList<>(); try (var localArena = Arena.ofConfined()) { diff --git a/test/org/apache/catalina/util/TestServerInfo.java b/test/org/apache/catalina/util/TestServerInfo.java index 3a617017302d..e5d129b1dbe3 100644 --- a/test/org/apache/catalina/util/TestServerInfo.java +++ b/test/org/apache/catalina/util/TestServerInfo.java @@ -16,8 +16,24 @@ */ package org.apache.catalina.util; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.function.Consumer; +import java.util.jar.Attributes; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; +import org.apache.catalina.core.AprLifecycleListener; +import org.apache.catalina.core.OpenSSLLifecycleListener; +import org.apache.tomcat.util.compat.JreCompat; + public class TestServerInfo { /** @@ -27,4 +43,381 @@ public class TestServerInfo { public void testServerInfo() { ServerInfo.main(new String[0]); } + + /** + * Test that ServerInfo.main() outputs expected basic information. + */ + @Test + public void testServerInfoOutput() throws Exception { + String output = captureServerInfoOutput(); + + // Check for expected output lines + Assert.assertTrue("Should contain server version", output.contains("Server version:")); + Assert.assertTrue("Should contain server built", output.contains("Server built:")); + Assert.assertTrue("Should contain server number", output.contains("Server number:")); + Assert.assertTrue("Should contain OS Name", output.contains("OS Name:")); + Assert.assertTrue("Should contain JVM Version", output.contains("JVM Version:")); + Assert.assertTrue("Should contain APR loaded status", output.contains("APR loaded:")); + } + + /** + * Test isTomcatCoreJar() with Tomcat core JAR (Bundle-SymbolicName pattern). + */ + @Test + public void testIsTomcatCoreJarWithBundleSymbolicName() throws Exception { + withTestJar("test-tomcat-core.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue("Bundle-SymbolicName", "org.apache.tomcat-test"); + }, jar -> Assert.assertTrue("Should identify org.apache.tomcat-* as core JAR", + invokeIsTomcatCoreJar(jar))); + } + + /** + * Test isTomcatCoreJar() with Catalina core JAR (Bundle-SymbolicName pattern). + */ + @Test + public void testIsTomcatCoreJarWithCatalinaSymbolicName() throws Exception { + withTestJar("test-catalina-core.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue("Bundle-SymbolicName", "org.apache.catalina-ha"); + }, jar -> Assert.assertTrue("Should identify org.apache.catalina-* as core JAR", + invokeIsTomcatCoreJar(jar))); + } + + /** + * Test isTomcatCoreJar() with Jakarta API JAR (Bundle-SymbolicName pattern). + */ + @Test + public void testIsTomcatCoreJarWithJakartaSymbolicName() throws Exception { + withTestJar("test-jakarta-api.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue("Bundle-SymbolicName", "jakarta.servlet.api"); + }, jar -> Assert.assertTrue("Should identify jakarta.* as core JAR", + invokeIsTomcatCoreJar(jar))); + } + + /** + * Test isTomcatCoreJar() with Tomcat core JAR (Implementation-Vendor fallback). + */ + @Test + public void testIsTomcatCoreJarWithImplementationVendor() throws Exception { + withTestJar("test-tomcat-i18n.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_VENDOR, "Apache Software Foundation"); + manifest.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_TITLE, "Apache Tomcat"); + }, jar -> Assert.assertTrue("Should identify ASF/Tomcat as core JAR", + invokeIsTomcatCoreJar(jar))); + } + + /** + * Test isTomcatCoreJar() with third-party JAR. + */ + @Test + public void testIsTomcatCoreJarWithThirdParty() throws Exception { + withTestJar("test-third-party.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue("Bundle-SymbolicName", "com.example.library"); + manifest.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_VENDOR, "Example Corp"); + }, jar -> Assert.assertFalse("Should not identify third-party JAR as core", + invokeIsTomcatCoreJar(jar))); + } + + /** + * Test getJarVersion() with Bundle-Version. + */ + @Test + public void testGetJarVersionWithBundleVersion() throws Exception { + withTestJar("test-bundle-version.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue("Bundle-Version", "1.2.3"); + }, jar -> Assert.assertEquals("Should read Bundle-Version", "1.2.3", + invokeGetJarVersion(jar))); + } + + /** + * Test getJarVersion() with Implementation-Version. + */ + @Test + public void testGetJarVersionWithImplementationVersion() throws Exception { + withTestJar("test-impl-version.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_VERSION, "2.3.4"); + }, jar -> Assert.assertEquals("Should read Implementation-Version", "2.3.4", + invokeGetJarVersion(jar))); + } + + /** + * Test getJarVersion() with Specification-Version. + */ + @Test + public void testGetJarVersionWithSpecificationVersion() throws Exception { + withTestJar("test-spec-version.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.SPECIFICATION_VERSION, "3.4.5"); + }, jar -> Assert.assertEquals("Should read Specification-Version", "3.4.5", + invokeGetJarVersion(jar))); + } + + /** + * Test getJarVersion() priority: Bundle-Version takes precedence. + */ + @Test + public void testGetJarVersionPriority() throws Exception { + withTestJar("test-version-priority.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue("Bundle-Version", "1.0.0"); + manifest.getMainAttributes().put(Attributes.Name.IMPLEMENTATION_VERSION, "2.0.0"); + manifest.getMainAttributes().put(Attributes.Name.SPECIFICATION_VERSION, "3.0.0"); + }, jar -> Assert.assertEquals("Should prioritize Bundle-Version", "1.0.0", + invokeGetJarVersion(jar))); + } + + /** + * Test getJarVersion() with no version information. + */ + @Test + public void testGetJarVersionWithNoVersion() throws Exception { + withTestJar("test-no-version.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + }, jar -> Assert.assertNull("Should return null when no version found", + invokeGetJarVersion(jar))); + } + + /** + * Test ServerInfo.main() output with APR/Tomcat Native when available. + */ + @Test + public void testServerInfoOutputWithApr() throws Exception { + // Only run this test if APR is available + Assume.assumeTrue("APR not available", AprLifecycleListener.isAprAvailable()); + + String output = captureServerInfoOutput(); + + // Check for APR-specific output + Assert.assertTrue("Should contain 'APR loaded: true'", output.contains("APR loaded: true")); + Assert.assertTrue("Should contain APR Version", output.contains("APR Version:")); + Assert.assertTrue("Should contain Tomcat Native version", output.contains("Tomcat Native:")); + // OpenSSL via APR should be present if SSL is initialized + // Note: May not always be present depending on initialization state + } + + /** + * Test ServerInfo.main() output with FFM OpenSSL when available. + */ + @Test + public void testServerInfoOutputWithFFM() throws Exception { + // Only run this test if JRE 22+ is available + Assume.assumeTrue("JRE 22+ not available", JreCompat.isJre22Available()); + + // Initialize FFM OpenSSL + boolean ffmAvailable = OpenSSLLifecycleListener.isAvailable(); + Assume.assumeTrue("FFM OpenSSL not available", ffmAvailable); + + String output = captureServerInfoOutput(); + + // Check for FFM OpenSSL output + Assert.assertTrue("Should contain OpenSSL (FFM) information", output.contains("OpenSSL (FFM):")); + } + + /** + * Test ServerInfo.main() output when neither APR nor FFM is available. + */ + @Test + public void testServerInfoOutputWithoutNativeLibraries() throws Exception { + // Skip if APR or FFM is available + boolean aprAvailable = AprLifecycleListener.isAprAvailable(); + boolean ffmAvailable = JreCompat.isJre22Available() && OpenSSLLifecycleListener.isAvailable(); + + // Only run if neither is available (or force the test by not initializing them) + // This test validates the "not available" code path + if (!aprAvailable && !ffmAvailable) { + String output = captureServerInfoOutput(); + + // When no native libraries are available, should show APR loaded: false + Assert.assertTrue("Should contain 'APR loaded: false'", output.contains("APR loaded: false")); + // Should NOT contain FFM or APR version information + Assert.assertFalse("Should not contain APR Version", output.contains("APR Version:")); + Assert.assertFalse("Should not contain Tomcat Native", output.contains("Tomcat Native:")); + } + } + + /** + * Test that APR version info is displayed correctly. + */ + @Test + public void testAprVersionInfo() throws Exception { + // Only run if APR is available + Assume.assumeTrue("APR not available", AprLifecycleListener.isAprAvailable()); + + String output = captureServerInfoOutput(); + + // Verify version info format (should contain version numbers) + String[] lines = output.split("\n"); + boolean foundAprVersion = false; + boolean foundTcnVersion = false; + + for (String line : lines) { + if (line.contains("APR Version:")) { + foundAprVersion = true; + // APR version should be in format like "1.7.0" + Assert.assertTrue("APR Version line should contain version number", + line.matches(".*APR Version:\\s+\\d+\\.\\d+.*")); + } + if (line.contains("Tomcat Native:")) { + foundTcnVersion = true; + // Tomcat Native version should be in format like "2.0.5" + Assert.assertTrue("Tomcat Native line should contain version number", + line.matches(".*Tomcat Native:\\s+\\d+\\.\\d+.*")); + } + } + + Assert.assertTrue("Should have found APR Version line", foundAprVersion); + Assert.assertTrue("Should have found Tomcat Native line", foundTcnVersion); + } + + /** + * Test that version warning is returned when APR is available but outdated. + * This tests the real version check using the installed APR library. + */ + @Test + public void testTomcatNativeVersionWarningWithRealVersion() throws Exception { + // Only run if APR is available + Assume.assumeTrue("APR not available", AprLifecycleListener.isAprAvailable()); + + // If APR is available, getTcnVersionWarning() should return non-null if version is old, + // or null if version is current. We can't predict which, so just verify the method works. + String warning = AprLifecycleListener.getTcnVersionWarning(); + + // The warning should either be null (version is OK) or contain expected text + if (warning != null) { + Assert.assertTrue("Warning should mention 'WARNING'", warning.contains("WARNING")); + Assert.assertTrue("Warning should mention version", warning.matches(".*\\d+\\.\\d+\\.\\d+.*")); + } + // If warning is null, that's also valid (version is current) + } + + /** + * Test that FFM OpenSSL version info is displayed correctly. + */ + @Test + public void testFFMVersionInfo() throws Exception { + // Only run if JRE 22+ and FFM OpenSSL are available + Assume.assumeTrue("JRE 22+ not available", JreCompat.isJre22Available()); + + boolean ffmAvailable = OpenSSLLifecycleListener.isAvailable(); + Assume.assumeTrue("FFM OpenSSL not available", ffmAvailable); + + String output = captureServerInfoOutput(); + + // Verify FFM OpenSSL version info format + String[] lines = output.split("\n"); + boolean foundFFMVersion = false; + + for (String line : lines) { + if (line.contains("OpenSSL (FFM):")) { + foundFFMVersion = true; + // Should contain either version string or library name + Assert.assertTrue("OpenSSL (FFM) line should not be empty", + line.length() > "OpenSSL (FFM): ".length()); + } + } + + Assert.assertTrue("Should have found OpenSSL (FFM) line", foundFFMVersion); + } + + /** + * Test that OpenSSLLibrary.getVersionString() returns the native version string. + * This ensures FFM output format matches APR output format. + */ + @Test + public void testOpenSSLLibraryVersionString() throws Exception { + // Only run if JRE 22+ and FFM OpenSSL are available + Assume.assumeTrue("JRE 22+ not available", JreCompat.isJre22Available()); + + boolean ffmAvailable = OpenSSLLifecycleListener.isAvailable(); + Assume.assumeTrue("FFM OpenSSL not available", ffmAvailable); + + // Call OpenSSLLibrary.getVersionString() via reflection + Class openSSLLibraryClass = Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary"); + String versionString = (String) openSSLLibraryClass.getMethod("getVersionString").invoke(null); + + // Verify the version string is in the expected format + Assert.assertNotNull("Version string should not be null", versionString); + Assert.assertTrue("Version string should start with 'OpenSSL' or library name", + versionString.matches("^(OpenSSL|LibreSSL|BoringSSL).*")); + Assert.assertTrue("Version string should contain version number", + versionString.matches(".*\\d+\\.\\d+.*")); + } + + /** + * Functional interface for test logic that can throw exceptions. + */ + @FunctionalInterface + private interface TestWithJar { + void test(File jarFile) throws Exception; + } + + /** + * Helper method to run a test with a JAR file and ensure cleanup. + */ + private void withTestJar(String filename, Consumer customizer, TestWithJar test) throws Exception { + File testJar = createTestJar(filename, customizer); + try { + test.test(testJar); + } finally { + testJar.delete(); + } + } + + /** + * Helper method to invoke the private isTomcatCoreJar() method via reflection. + */ + private boolean invokeIsTomcatCoreJar(File jarFile) throws Exception { + Method method = ServerInfo.class.getDeclaredMethod("isTomcatCoreJar", File.class); + method.setAccessible(true); + return (Boolean) method.invoke(null, jarFile); + } + + /** + * Helper method to invoke the private getJarVersion() method via reflection. + */ + private String invokeGetJarVersion(File jarFile) throws Exception { + Method method = ServerInfo.class.getDeclaredMethod("getJarVersion", File.class); + method.setAccessible(true); + return (String) method.invoke(null, jarFile); + } + + /** + * Helper method to capture ServerInfo.main() output. + */ + private String captureServerInfoOutput() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + PrintStream oldOut = System.out; + try { + System.setOut(ps); + ServerInfo.main(new String[0]); + } finally { + System.setOut(oldOut); + } + return baos.toString(); + } + + /** + * Helper method to create a test JAR file with custom manifest. + */ + private File createTestJar(String filename, Consumer customizer) throws Exception { + File tempDir = new File(System.getProperty("java.io.tmpdir")); + File jarFile = new File(tempDir, filename); + + Manifest manifest = new Manifest(); + customizer.accept(manifest); + + try (FileOutputStream fos = new FileOutputStream(jarFile); + JarOutputStream jos = new JarOutputStream(fos, manifest)) { + // Empty JAR with just manifest + } + + return jarFile; + } } diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 30cbe951245a..e868bd98c237 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -185,6 +185,12 @@ of MessageDigestCredentialHandler from false to true. (markt) + + Enhance version.sh and version.bat to display + APR, Tomcat Native, and OpenSSL version information (both APR and FFM + implementations), along with version compatibility warnings and + third-party library version information. (csutherl) + When generating the class path in the Loader, re-order the check on