Version in base suite: 11.0.15-1~deb13u1 Base version: tomcat11_11.0.15-1~deb13u1 Target version: tomcat11_11.0.22-1~deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/t/tomcat11/tomcat11_11.0.15-1~deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/t/tomcat11/tomcat11_11.0.22-1~deb13u1.dsc /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/res/install-win/Uninstall.exe.sig |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/res/install-win/tomcat-installer.exe.sig |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ca.jks |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-crl-rsa.jks |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-ec.jks |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-rsa-copy1.jks |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-rsa.jks |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/client-keystore.p12 |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/ocsp-client-good.der |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/ocsp-client-revoked.der |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/ocsp-good.der |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/ocsp-revoked.der |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/trustStore.p12 |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/user1.jks |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/user2-crl.jks |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/user3-crl-long.jks |binary /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/webapp/WEB-INF/lib/bug69623-lib.jar |binary tomcat11-11.0.22/.github/workflows/ci.yml | 2 tomcat11-11.0.22/.gitignore | 1 tomcat11-11.0.22/BUILDING.txt | 115 + tomcat11-11.0.22/CONTRIBUTING.md | 61 tomcat11-11.0.22/MERGE.txt | 6 tomcat11-11.0.22/NOTICE | 2 tomcat11-11.0.22/README.md | 10 tomcat11-11.0.22/bin/catalina.bat | 2 tomcat11-11.0.22/bin/catalina.sh | 4 tomcat11-11.0.22/build.properties.default | 86 - tomcat11-11.0.22/build.properties.release | 10 tomcat11-11.0.22/build.xml | 258 +++ tomcat11-11.0.22/debian/ant.properties | 3 tomcat11-11.0.22/debian/changelog | 45 tomcat11-11.0.22/debian/control | 5 tomcat11-11.0.22/debian/copyright | 4 tomcat11-11.0.22/debian/patches/0004-split-deploy-webapps-target-from-deploy-target.patch | 14 tomcat11-11.0.22/debian/patches/0005-skip-test-failures.patch | 12 tomcat11-11.0.22/debian/patches/0010-debianize-build-xml.patch | 14 tomcat11-11.0.22/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch | 17 tomcat11-11.0.22/debian/patches/0018-fix-manager-webapp.patch | 18 tomcat11-11.0.22/debian/patches/0019-add-distribution-to-error-page.patch | 16 tomcat11-11.0.22/debian/patches/0021-dont-test-unsupported-ciphers.patch | 20 tomcat11-11.0.22/debian/patches/0023-disable-shutdown-by-socket.patch | 12 tomcat11-11.0.22/debian/patches/0024-systemd-log-formatter.patch | 12 tomcat11-11.0.22/debian/patches/0025-invalid-configuration-exit-status.patch | 15 tomcat11-11.0.22/debian/patches/0026-easymock4-compatibility.patch | 34 tomcat11-11.0.22/debian/patches/0030-eclipse-jdt-classpath.patch | 12 tomcat11-11.0.22/debian/patches/0031-eclipse-jdt-compatibility.patch | 24 tomcat11-11.0.22/debian/patches/disable-jacoco.patch | 20 tomcat11-11.0.22/debian/patches/exclude-TestJNDIRealmIntegration.patch | 20 tomcat11-11.0.22/debian/patches/series | 4 tomcat11-11.0.22/debian/tomcat11-common.links | 2 tomcat11-11.0.22/java/jakarta/el/ImportHandler.java | 3 tomcat11-11.0.22/java/org/apache/catalina/AccessLog.java | 2 tomcat11-11.0.22/java/org/apache/catalina/Context.java | 1 tomcat11-11.0.22/java/org/apache/catalina/JmxEnabled.java | 4 tomcat11-11.0.22/java/org/apache/catalina/WebResource.java | 40 tomcat11-11.0.22/java/org/apache/catalina/WebResourceRoot.java | 12 tomcat11-11.0.22/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java | 23 tomcat11-11.0.22/java/org/apache/catalina/authenticator/AuthenticatorBase.java | 115 + tomcat11-11.0.22/java/org/apache/catalina/authenticator/DigestAuthenticator.java | 5 tomcat11-11.0.22/java/org/apache/catalina/authenticator/FormAuthenticator.java | 38 tomcat11-11.0.22/java/org/apache/catalina/authenticator/SSLAuthenticator.java | 2 tomcat11-11.0.22/java/org/apache/catalina/authenticator/SingleSignOn.java | 9 tomcat11-11.0.22/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java | 194 -- tomcat11-11.0.22/java/org/apache/catalina/connector/CoyoteAdapter.java | 11 tomcat11-11.0.22/java/org/apache/catalina/connector/Request.java | 3 tomcat11-11.0.22/java/org/apache/catalina/connector/Response.java | 48 tomcat11-11.0.22/java/org/apache/catalina/core/AprLifecycleListener.java | 133 + tomcat11-11.0.22/java/org/apache/catalina/core/LocalStrings.properties | 3 tomcat11-11.0.22/java/org/apache/catalina/core/LocalStrings_fr.properties | 3 tomcat11-11.0.22/java/org/apache/catalina/core/LocalStrings_ja.properties | 3 tomcat11-11.0.22/java/org/apache/catalina/core/OpenSSLLifecycleListener.java | 23 tomcat11-11.0.22/java/org/apache/catalina/core/StandardContext.java | 16 tomcat11-11.0.22/java/org/apache/catalina/filters/CsrfPreventionFilter.java | 83 + tomcat11-11.0.22/java/org/apache/catalina/ha/context/ReplicatedContext.java | 10 tomcat11-11.0.22/java/org/apache/catalina/ha/tcp/ReplicationValve.java | 19 tomcat11-11.0.22/java/org/apache/catalina/ha/tcp/SendMessageData.java | 2 tomcat11-11.0.22/java/org/apache/catalina/manager/Constants.java | 2 tomcat11-11.0.22/java/org/apache/catalina/manager/HTMLManagerServlet.java | 26 tomcat11-11.0.22/java/org/apache/catalina/manager/host/Constants.java | 2 tomcat11-11.0.22/java/org/apache/catalina/manager/host/HostManagerServlet.java | 3 tomcat11-11.0.22/java/org/apache/catalina/mapper/LocalStrings.properties | 1 tomcat11-11.0.22/java/org/apache/catalina/mapper/LocalStrings_fr.properties | 1 tomcat11-11.0.22/java/org/apache/catalina/mapper/LocalStrings_ja.properties | 1 tomcat11-11.0.22/java/org/apache/catalina/mapper/Mapper.java | 2 tomcat11-11.0.22/java/org/apache/catalina/mapper/MappingData.java | 6 tomcat11-11.0.22/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java | 42 tomcat11-11.0.22/java/org/apache/catalina/realm/LockOutRealm.java | 25 tomcat11-11.0.22/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java | 10 tomcat11-11.0.22/java/org/apache/catalina/realm/RealmBase.java | 32 tomcat11-11.0.22/java/org/apache/catalina/servlets/WebdavServlet.java | 111 + tomcat11-11.0.22/java/org/apache/catalina/startup/LocalStrings_zh_CN.properties | 4 tomcat11-11.0.22/java/org/apache/catalina/storeconfig/OpenSSLConfSF.java | 13 tomcat11-11.0.22/java/org/apache/catalina/tribes/group/AbsoluteOrder.java | 67 tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java | 61 tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties | 5 tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties | 3 tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties | 3 tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties | 1 tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties | 1 tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java | 2 tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java | 10 tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties | 4 tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java | 2 tomcat11-11.0.22/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java | 2 tomcat11-11.0.22/java/org/apache/catalina/util/ServerInfo.java | 215 +++ tomcat11-11.0.22/java/org/apache/catalina/util/URLEncoder.java | 14 tomcat11-11.0.22/java/org/apache/catalina/valves/AbstractAccessLogValve.java | 480 ++++++- tomcat11-11.0.22/java/org/apache/catalina/valves/ExtendedAccessLogValve.java | 19 tomcat11-11.0.22/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java | 30 tomcat11-11.0.22/java/org/apache/catalina/valves/LocalStrings_fr.properties | 2 tomcat11-11.0.22/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties | 3 tomcat11-11.0.22/java/org/apache/catalina/valves/rewrite/RewriteValve.java | 1 tomcat11-11.0.22/java/org/apache/catalina/webresources/AbstractArchiveResource.java | 45 tomcat11-11.0.22/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java | 47 tomcat11-11.0.22/java/org/apache/catalina/webresources/AbstractResource.java | 5 tomcat11-11.0.22/java/org/apache/catalina/webresources/CachedResource.java | 5 tomcat11-11.0.22/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties | 1 tomcat11-11.0.22/java/org/apache/coyote/AbstractProtocol.java | 435 ++++++ tomcat11-11.0.22/java/org/apache/coyote/CompressionConfig.java | 34 tomcat11-11.0.22/java/org/apache/coyote/LocalStrings.properties | 1 tomcat11-11.0.22/java/org/apache/coyote/Request.java | 4 tomcat11-11.0.22/java/org/apache/coyote/Response.java | 9 tomcat11-11.0.22/java/org/apache/coyote/UpgradeProtocol.java | 10 tomcat11-11.0.22/java/org/apache/coyote/ajp/AbstractAjpProtocol.java | 120 + tomcat11-11.0.22/java/org/apache/coyote/ajp/AjpProcessor.java | 3 tomcat11-11.0.22/java/org/apache/coyote/http11/AbstractHttp11Protocol.java | 9 tomcat11-11.0.22/java/org/apache/coyote/http11/LocalStrings_fr.properties | 1 tomcat11-11.0.22/java/org/apache/coyote/http11/LocalStrings_ja.properties | 1 tomcat11-11.0.22/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java | 115 + tomcat11-11.0.22/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java | 57 tomcat11-11.0.22/java/org/apache/coyote/http11/filters/GzipOutputFilter.java | 6 tomcat11-11.0.22/java/org/apache/coyote/http2/HPackHuffman.java | 105 + tomcat11-11.0.22/java/org/apache/coyote/http2/Hpack.java | 7 tomcat11-11.0.22/java/org/apache/coyote/http2/HpackDecoder.java | 54 tomcat11-11.0.22/java/org/apache/coyote/http2/HpackEncoder.java | 34 tomcat11-11.0.22/java/org/apache/coyote/http2/Http2Parser.java | 38 tomcat11-11.0.22/java/org/apache/coyote/http2/Http2Protocol.java | 23 tomcat11-11.0.22/java/org/apache/coyote/http2/Http2UpgradeHandler.java | 9 tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings.properties | 11 tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_fr.properties | 2 tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_ja.properties | 2 tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_ko.properties | 1 tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties | 1 tomcat11-11.0.22/java/org/apache/coyote/http2/Stream.java | 206 +-- tomcat11-11.0.22/java/org/apache/coyote/http2/StreamException.java | 6 tomcat11-11.0.22/java/org/apache/coyote/http2/StreamProcessor.java | 15 tomcat11-11.0.22/java/org/apache/el/ExpressionFactoryImpl.java | 2 tomcat11-11.0.22/java/org/apache/el/parser/AstValue.java | 9 tomcat11-11.0.22/java/org/apache/jasper/compiler/Compiler.java | 31 tomcat11-11.0.22/java/org/apache/jasper/compiler/Generator.java | 21 tomcat11-11.0.22/java/org/apache/jasper/compiler/JDTCompiler.java | 22 tomcat11-11.0.22/java/org/apache/jasper/compiler/PageInfo.java | 3 tomcat11-11.0.22/java/org/apache/jasper/compiler/TagFileProcessor.java | 25 tomcat11-11.0.22/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java | 37 tomcat11-11.0.22/java/org/apache/jasper/compiler/TextOptimizer.java | 1 tomcat11-11.0.22/java/org/apache/jasper/runtime/JspWriterImpl.java | 2 tomcat11-11.0.22/java/org/apache/naming/LocalStrings_zh_CN.properties | 2 tomcat11-11.0.22/java/org/apache/tomcat/Jar.java | 26 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/AbandonedTrace.java | 14 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSource.java | 46 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceFactory.java | 81 - tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceMXBean.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactory.java | 8 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactoryFactory.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/Constants.java | 12 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DataSourceConnectionFactory.java | 12 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DataSourceMXBean.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingCallableStatement.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingConnection.java | 33 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingDatabaseMetaData.java | 13 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingPreparedStatement.java | 9 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingResultSet.java | 21 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java | 47 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DriverConnectionFactory.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DriverFactory.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DriverManagerConnectionFactory.java | 16 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/Jdbc41Bridge.java | 12 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/LifetimeExceededException.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ListException.java | 5 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ObjectNameWrapper.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PStmtKey.java | 196 +- tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableCallableStatement.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnection.java | 44 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionFactory.java | 56 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionMXBean.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolablePreparedStatement.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolingConnection.java | 40 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolingDataSource.java | 5 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolingDriver.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/SQLExceptionList.java | 6 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/SwallowedExceptionLogger.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/Utils.java | 6 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/ConnectionImpl.java | 67 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/DriverAdapterCPDS.java | 56 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PStmtKeyCPDS.java | 26 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PooledConnectionImpl.java | 157 +- tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/package-info.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/AbstractConnectionFactory.java | 147 ++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/CPDSConnectionFactory.java | 221 --- tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/CharArray.java | 26 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSource.java | 208 +-- tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSourceFactory.java | 10 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/KeyedCPDSConnectionFactory.java | 182 -- tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSource.java | 9 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PoolKey.java | 7 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionAndInfo.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionManager.java | 31 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSource.java | 7 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSourceFactory.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/UserPassKey.java | 20 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/package-info.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java | 6 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/DataSourceXAConnectionFactory.java | 38 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/LocalXAConnectionFactory.java | 20 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedConnection.java | 34 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedDataSource.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnection.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnectionFactory.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/SynchronizationAdapter.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContext.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContextListener.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionRegistry.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/XAConnectionFactory.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/package-info.java | 3 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/package-info.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/BaseObject.java | 9 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/BaseObjectPool.java | 9 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/DestroyMode.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/KeyedObjectPool.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/KeyedPooledObjectFactory.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/ObjectPool.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PoolUtils.java | 50 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PooledObject.java | 35 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PooledObjectFactory.java | 164 +- tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PooledObjectState.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/SwallowedExceptionListener.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/TrackedUse.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/UsageTracking.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/AbandonedConfig.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/BaseGenericObjectPool.java | 126 + tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java | 63 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/CallStack.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultEvictionPolicy.java | 11 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObject.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfo.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfoMBean.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/EvictionConfig.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/EvictionPolicy.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/EvictionTimer.java | 35 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPool.java | 257 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolConfig.java | 82 + tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolMXBean.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPool.java | 233 +-- tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolConfig.java | 9 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolMXBean.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/InterruptibleReentrantLock.java | 8 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/LinkedBlockingDeque.java | 70 - tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/NoOpCallStack.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/PoolImplUtils.java | 34 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/PooledSoftReference.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/SoftReferenceObjectPool.java | 14 tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/ThrowableCallStack.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/jni/SSL.java | 28 tomcat11-11.0.22/java/org/apache/tomcat/jni/SSLConf.java | 14 tomcat11-11.0.22/java/org/apache/tomcat/jni/SSLContext.java | 28 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/Const.java | 138 +- tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java | 5 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java | 8 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java | 24 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/Constant.java | 8 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/package-info.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/util/buf/Asn1Parser.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/http/HeaderUtil.java | 30 tomcat11-11.0.22/java/org/apache/tomcat/util/http/RequestUtil.java | 10 tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/ChunkExtension.java | 130 + tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/EntityTag.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/HttpParser.java | 92 + tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/LocalStrings.properties | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/Upgrade.java | 6 tomcat11-11.0.22/java/org/apache/tomcat/util/net/AbstractEndpoint.java | 11 tomcat11-11.0.22/java/org/apache/tomcat/util/net/LocalStrings.properties | 6 tomcat11-11.0.22/java/org/apache/tomcat/util/net/LocalStrings_fr.properties | 5 tomcat11-11.0.22/java/org/apache/tomcat/util/net/LocalStrings_ja.properties | 5 tomcat11-11.0.22/java/org/apache/tomcat/util/net/Nio2Endpoint.java | 1 tomcat11-11.0.22/java/org/apache/tomcat/util/net/NioChannel.java | 6 tomcat11-11.0.22/java/org/apache/tomcat/util/net/NioEndpoint.java | 23 tomcat11-11.0.22/java/org/apache/tomcat/util/net/SSLHostConfig.java | 285 +++- tomcat11-11.0.22/java/org/apache/tomcat/util/net/SSLSupport.java | 3 tomcat11-11.0.22/java/org/apache/tomcat/util/net/SSLUtilBase.java | 30 tomcat11-11.0.22/java/org/apache/tomcat/util/net/SecureNio2Channel.java | 19 tomcat11-11.0.22/java/org/apache/tomcat/util/net/SecureNioChannel.java | 27 tomcat11-11.0.22/java/org/apache/tomcat/util/net/SocketWrapperBase.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java | 5 tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties | 1 tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties | 1 tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties | 1 tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLConfCmd.java | 14 tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java | 30 tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java | 34 tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java | 45 tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/ciphers/Group.java | 6 tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java | 85 - tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/ciphers/SignatureScheme.java | 21 tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java | 114 + tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java | 151 +- tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java | 31 tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java | 2 tomcat11-11.0.22/java/org/apache/tomcat/util/openssl/openssl_h.java | 479 ++++++- tomcat11-11.0.22/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java | 66 tomcat11-11.0.22/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java | 18 tomcat11-11.0.22/java/org/apache/tomcat/util/security/ConstantTime.java | 142 ++ tomcat11-11.0.22/java/org/apache/tomcat/websocket/Authenticator.java | 30 tomcat11-11.0.22/java/org/apache/tomcat/websocket/BasicAuthenticator.java | 4 tomcat11-11.0.22/java/org/apache/tomcat/websocket/DigestAuthenticator.java | 20 tomcat11-11.0.22/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java | 32 tomcat11-11.0.22/java/org/apache/tomcat/websocket/WsWebSocketContainer.java | 93 - tomcat11-11.0.22/modules/cxf/pom.xml | 4 tomcat11-11.0.22/modules/jdbc-pool/NOTICE | 2 tomcat11-11.0.22/res/bnd/jasper.jar.tmp.bnd | 1 tomcat11-11.0.22/res/bnd/tomcat-embed-jasper.jar.tmp.bnd | 1 tomcat11-11.0.22/res/checkstyle/header-al2.txt | 2 tomcat11-11.0.22/res/ide-support/eclipse/eclipse.classpath | 3 tomcat11-11.0.22/res/ide-support/eclipse/java-compiler-errors-warnings.txt | 3 tomcat11-11.0.22/res/ide-support/idea/tomcat.iml | 27 tomcat11-11.0.22/res/ide-support/netbeans/nb-tomcat-build.properties | 2 tomcat11-11.0.22/res/ide-support/netbeans/project.xml | 2 tomcat11-11.0.22/res/maven/mvn.properties.default | 2 tomcat11-11.0.22/res/maven/mvn.properties.release | 2 tomcat11-11.0.22/res/maven/tomcat-embed-jasper.pom | 2 tomcat11-11.0.22/res/maven/tomcat-jasper.pom | 2 tomcat11-11.0.22/res/maven/tomcat-servlet-api.pom | 2 tomcat11-11.0.22/res/openssl/openssl-tomcat.conf | 23 tomcat11-11.0.22/res/rat/rat-excludes.txt | 9 tomcat11-11.0.22/test-profiles.properties.default | 104 + tomcat11-11.0.22/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java | 6 tomcat11-11.0.22/test/org/apache/catalina/authenticator/TestDigestAuthenticatorB.java | 135 + tomcat11-11.0.22/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java | 40 tomcat11-11.0.22/test/org/apache/catalina/connector/TestResponse.java | 45 tomcat11-11.0.22/test/org/apache/catalina/connector/TestValidateClientSessionId.java | 89 + tomcat11-11.0.22/test/org/apache/catalina/core/TestStandardContext.java | 54 tomcat11-11.0.22/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java | 102 + tomcat11-11.0.22/test/org/apache/catalina/filters/TestHttpHeaderSecurityFilter.java | 191 ++ tomcat11-11.0.22/test/org/apache/catalina/filters/TestRateLimitFilter.java | 94 - tomcat11-11.0.22/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java | 115 - tomcat11-11.0.22/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java | 11 tomcat11-11.0.22/test/org/apache/catalina/filters/TesterHttpServletRequest.java | 12 tomcat11-11.0.22/test/org/apache/catalina/filters/TesterRateLimitClientBase.java | 73 + tomcat11-11.0.22/test/org/apache/catalina/loader/TestVirtualWebappLoader.java | 4 tomcat11-11.0.22/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java | 2 tomcat11-11.0.22/test/org/apache/catalina/realm/TestLockoutRealm.java | 117 + tomcat11-11.0.22/test/org/apache/catalina/realm/TestRealmBase.java | 82 + tomcat11-11.0.22/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java | 121 + tomcat11-11.0.22/test/org/apache/catalina/servlets/TestWebdavServlet.java | 83 + tomcat11-11.0.22/test/org/apache/catalina/session/FileStoreTest.java | 100 - tomcat11-11.0.22/test/org/apache/catalina/session/TestFileStore.java | 130 + tomcat11-11.0.22/test/org/apache/catalina/startup/LoggingBaseTest.java | 2 tomcat11-11.0.22/test/org/apache/catalina/startup/TomcatBaseTest.java | 8 tomcat11-11.0.22/test/org/apache/catalina/storeconfig/TestStoreConfig.java | 18 tomcat11-11.0.22/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java | 58 tomcat11-11.0.22/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorAlgorithms.java | 162 ++ tomcat11-11.0.22/test/org/apache/catalina/util/TestServerInfo.java | 497 +++++++ tomcat11-11.0.22/test/org/apache/catalina/util/TestURLEncoder.java | 28 tomcat11-11.0.22/test/org/apache/catalina/valves/TestAccessLogValve.java | 12 tomcat11-11.0.22/test/org/apache/catalina/valves/TestAccessLogValveFile.java | 273 +++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestFilterValve.java | 229 +++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestJsonErrorReportValve.java | 412 ++++++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java | 28 tomcat11-11.0.22/test/org/apache/catalina/valves/TestPersistentValve.java | 134 + tomcat11-11.0.22/test/org/apache/catalina/valves/TestProxyErrorReportValve.java | 277 ++++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestSemaphoreValve.java | 440 ++++++ tomcat11-11.0.22/test/org/apache/catalina/webresources/TestCachedResource.java | 35 tomcat11-11.0.22/test/org/apache/coyote/TestCompressionConfig.java | 60 tomcat11-11.0.22/test/org/apache/coyote/http11/TestHttp11Processor.java | 30 tomcat11-11.0.22/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java | 95 + tomcat11-11.0.22/test/org/apache/coyote/http11/filters/TestChunkedOutputFilter.java | 123 + tomcat11-11.0.22/test/org/apache/coyote/http2/Http2TestBase.java | 31 tomcat11-11.0.22/test/org/apache/coyote/http2/TestHPackHuffman.java | 45 tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2RequestParameters.java | 52 tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_6_2.java | 36 tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_1.java | 54 tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_2.java | 121 + tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_3.java | 74 + tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_5.java | 84 + tomcat11-11.0.22/test/org/apache/coyote/http2/TestLargeUpload.java | 7 tomcat11-11.0.22/test/org/apache/coyote/http2/TestStreamProcessor.java | 6 tomcat11-11.0.22/test/org/apache/el/TestValueExpressionImpl.java | 29 tomcat11-11.0.22/test/org/apache/jasper/compiler/TestGenerator.java | 43 tomcat11-11.0.22/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java | 120 + tomcat11-11.0.22/test/org/apache/juli/TestFileHandlerNonRotatable.java | 12 tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java | 176 ++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java | 73 + tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java | 96 + tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java | 81 + tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java | 111 + tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java | 111 + tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java | 81 + tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java | 88 + tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java | 86 + tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java | 117 + tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TesterHttpd.java | 149 ++ tomcat11-11.0.22/test/org/apache/tomcat/jni/TesterLibraryLoad.java | 37 tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2017.java | 43 tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java | 113 + tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2018.java | 79 + tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2019.java | 185 ++ tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2023.java | 99 + tomcat11-11.0.22/test/org/apache/tomcat/unittest/TesterRequest.java | 10 tomcat11-11.0.22/test/org/apache/tomcat/unittest/TesterResponseWithStatus.java | 36 tomcat11-11.0.22/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java | 3 tomcat11-11.0.22/test/org/apache/tomcat/util/http/parser/TestChunkExtension.java | 186 ++ tomcat11-11.0.22/test/org/apache/tomcat/util/http/parser/TestMediaType.java | 6 tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestAlpnFallback.java | 52 tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestLargeClientHello.java | 91 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSSLHostConfig.java | 91 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSSLHostConfigCipher.java | 161 ++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSSLHostConfigProtocol.java | 153 ++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSsl.java | 4 tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSslHandshakeFailure.java | 83 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java | 103 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/TesterSupport.java | 113 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/ca-cert.pem | 74 - tomcat11-11.0.22/test/org/apache/tomcat/util/net/index.db | 7 tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-crl-rsa-cert.pem | 108 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-crl-rsa-key.pem | 28 tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-ec-cert.pem | 130 - tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-ec-key.pem | 13 tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-rsa-cert.pem | 165 +- tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-rsa-key.pem | 52 tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp-responder-rsa-cert.pem | 105 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp-responder-rsa-key.pem | 28 tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/OcspBaseTest.java | 186 ++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java | 121 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspIntegration.java | 432 ------ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFail.java | 57 tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailInternalError.java | 111 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailTryLater.java | 111 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspTimeout.java | 73 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java | 112 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderNoResponse.java | 113 + tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java | 271 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/ca-cert.pem | 19 tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/client-password | 1 tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/generate-ocsp-test-artifacts.sh | 172 -- tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/server-cert.pem | 86 - tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/server-key.pem | 28 tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/trust-password | 1 tomcat11-11.0.22/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java | 14 tomcat11-11.0.22/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java | 143 +- tomcat11-11.0.22/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java | 27 tomcat11-11.0.22/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java | 57 tomcat11-11.0.22/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java | 61 tomcat11-11.0.22/test/org/apache/tomcat/websocket/server/TestClose.java | 19 tomcat11-11.0.22/test/webapp/WEB-INF/classes/bug69623-a.mdd | 16 tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order-a.jspf | 17 tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order-b.jspf | 17 tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order-c.jspf | 17 tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order.jsp | 19 tomcat11-11.0.22/test/webapp/jsp/generator/external-taglib.jsp | 19 tomcat11-11.0.22/webapps/docs/changelog.xml | 683 +++++++++- tomcat11-11.0.22/webapps/docs/cluster-howto.xml | 10 tomcat11-11.0.22/webapps/docs/config/ajp.xml | 1 tomcat11-11.0.22/webapps/docs/config/cluster-interceptor.xml | 19 tomcat11-11.0.22/webapps/docs/config/cluster-manager.xml | 47 tomcat11-11.0.22/webapps/docs/config/cluster-valve.xml | 46 tomcat11-11.0.22/webapps/docs/config/cluster.xml | 97 - tomcat11-11.0.22/webapps/docs/config/filter.xml | 128 + tomcat11-11.0.22/webapps/docs/config/http.xml | 126 + tomcat11-11.0.22/webapps/docs/config/http2.xml | 21 tomcat11-11.0.22/webapps/docs/config/listeners.xml | 31 tomcat11-11.0.22/webapps/docs/config/realm.xml | 6 tomcat11-11.0.22/webapps/docs/config/server.xml | 2 tomcat11-11.0.22/webapps/docs/config/valve.xml | 184 ++ tomcat11-11.0.22/webapps/docs/ssl-howto.xml | 37 tomcat11-11.0.22/webapps/docs/windows-auth-howto.xml | 17 tomcat11-11.0.22/webapps/examples/WEB-INF/classes/LocalStrings_zh_CN.properties | 2 tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/connectorCerts.jsp | 2 tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/connectorCiphers.jsp | 2 tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/connectorTrustedCerts.jsp | 2 tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/sessionDetail.jsp | 2 tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/sessionsList.jsp | 2 491 files changed, 18428 insertions(+), 4495 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmps7zhhhk8/tomcat11_11.0.15-1~deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmps7zhhhk8/tomcat11_11.0.22-1~deb13u1.dsc: no acceptable signature found diff -Nru tomcat11-11.0.15/.github/workflows/ci.yml tomcat11-11.0.22/.github/workflows/ci.yml --- tomcat11-11.0.15/.github/workflows/ci.yml 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/.github/workflows/ci.yml 2026-05-01 18:56:05.000000000 +0000 @@ -46,7 +46,7 @@ run: | ant -noinput echoproperties deploy embed test-nio test-status env: - ANT_OPTS: -Dtest.openssl.exists=false -Dtest.excludePerformance=true -Dtest.exclude=jakarta/servlet/http/TestHttpServletResponseSendError.java,org/apache/catalina/authenticator/TestFormAuthenticatorA.java,org/apache/catalina/authenticator/TestFormAuthenticatorB.java,org/apache/catalina/authenticator/TestFormAuthenticatorC.java,org/apache/catalina/authenticator/TestSSOnonLoginAndBasicAuthenticator.java,org/apache/catalina/authenticator/TestSSOnonLoginAndDigestAuthenticator.java,org/apache/catalina/core/TestAsyncContextImpl.java,org/apache/catalina/core/TestAsyncContextStateChanges.java,org/apache/catalina/core/TestStandardContextResources.java,org/apache/catalina/core/TestStandardWrapper.java,org/apache/catalina/loader/TestVirtualContext.java,org/apache/catalina/mapper/TestMapperWebapps.java,org/apache/catalina/nonblocking/TestNonBlockingAPI.java,org/apache/catalina/servlets/TestDefaultServletEncodingPassThroughBom.java,org/apache/catalina/servlets/TestDefaultServletEncodingWithBom.java,org/apache/catalina/servlets/TestDefaultServletEncodingWithoutBom.java,org/apache/catalina/servlets/TestDefaultServletIfMatchRequests.java,org/apache/catalina/servlets/TestDefaultServlet.java,org/apache/catalina/servlets/TestDefaultServletOptions.java,org/apache/catalina/servlets/TestWebdavServletOptionsFile.java,org/apache/catalina/startup/TestContextConfig.java,org/apache/catalina/startup/TestHostConfigAutomaticDeploymentA.java,org/apache/catalina/startup/TestHostConfigAutomaticDeploymentB.java,org/apache/catalina/startup/TestHostConfigAutomaticDeploymentC.java,org/apache/catalina/valves/rewrite/TestRewriteValve.java,org/apache/catalina/valves/TestStuckThreadDetectionValve.java,org/apache/coyote/ajp/TestAbstractAjpProcessor.java,org/apache/coyote/http11/filters/TestChunkedInputFilter.java,org/apache/coyote/http11/TestHttp11InputBufferCRLF.java,org/apache/coyote/http11/TestHttp11InputBuffer.java,org/apache/coyote/http11/TestHttp11Processor.java,org/apache/coyote/http2/TestAsync.java,org/apache/coyote/http2/TestHttp2ConnectionTimeouts.java,org/apache/coyote/http2/TestHttp2Limits.java,org/apache/coyote/http2/TestHttp2Section_6_8.java,org/apache/coyote/http2/TestHttp2Timeouts.java,org/apache/coyote/http2/TestStreamQueryString.java,org/apache/el/TestELInJsp.java,org/apache/jasper/compiler/TestCompiler.java,org/apache/jasper/compiler/TestEncodingDetector.java,org/apache/jasper/compiler/TestGenerator.java,org/apache/jasper/compiler/TestJspConfig.java,org/apache/jasper/compiler/TestJspDocumentParser.java,org/apache/jasper/compiler/TestValidator.java,org/apache/jasper/optimizations/TestELInterpreterTagSetters.java,org/apache/jasper/optimizations/TestStringInterpreterTagSetters.java,org/apache/jasper/runtime/TestCustomHttpJspPage.java,org/apache/jasper/runtime/TestJspContextWrapper.java,org/apache/jasper/runtime/TestJspRuntimeLibrary.java,org/apache/jasper/runtime/TestPageContextImpl.java,org/apache/jasper/servlet/TestTldScanner.java,org/apache/naming/resources/TestWarDirContext.java,org/apache/naming/TestEnvEntry.java,org/apache/tomcat/util/net/TestClientCert.java,org/apache/tomcat/util/net/TestCustomSslTrustManager.java,org/apache/tomcat/util/net/TestSSLHostConfigCompat.java,org/apache/tomcat/util/net/TestSsl.java,org/apache/tomcat/websocket/server/TestSlowClient.java,org/apache/tomcat/websocket/server/TestWsRemoteEndpointImplServerDeadlock.java,org/apache/tomcat/websocket/TestWebSocketFrameClientSSL.java,org/apache/tomcat/websocket/TestWsWebSocketContainer.java,org/apache/tomcat/websocket/TestWsWebSocketContainerTimeoutServer.java,org/apache/catalina/tribes/group/interceptors/TestTcpFailureDetector.java,org/apache/catalina/filters/TestRateLimitFilter.java,jakarta/servlet/http/TestHttpServletDoHeadInvalidWrite*.java,jakarta/servlet/http/TestHttpServletDoHeadValidWrite*.java + ANT_OPTS: -Dtest.openssl.exists=false -Dtest.excludePerformance=true -Dtest.profile=smoke - name: Upload logs if: ${{ !cancelled() }} diff -Nru tomcat11-11.0.15/.gitignore tomcat11-11.0.22/.gitignore --- tomcat11-11.0.15/.gitignore 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/.gitignore 2026-05-01 18:56:05.000000000 +0000 @@ -52,4 +52,3 @@ modules/jdbc-pool/bin modules/jdbc-pool/includes webapps/docs/jdbc-pool.xml -/test/org/apache/tomcat/util/net/ocsp/ocsp-work/ diff -Nru tomcat11-11.0.15/BUILDING.txt tomcat11-11.0.22/BUILDING.txt --- tomcat11-11.0.15/BUILDING.txt 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/BUILDING.txt 2026-05-01 18:56:05.000000000 +0000 @@ -54,7 +54,7 @@ listed above and that compilation will fail if a later version of Java is used. - See Apache Commons DBCP 2 project web site for more details on + See Apache Commons DBCP 2 project website for more details on available versions of the library and its requirements, https://commons.apache.org/dbcp/ @@ -140,7 +140,7 @@ The base.path property specifies the place where Tomcat dependencies required by the build are downloaded. It is recommended to place this - directory outside of the source tree, so that you do not waste your + directory outside the source tree, so that you do not waste your time re-downloading the libraries. * NOTE: The default value of the base.path property configures the build script @@ -252,10 +252,10 @@ (5.4) Building the Windows installer The Windows installer uses the NSIS installer framework. -It can be build on Windows, on any other platform which provides +It can be built on Windows, on any other platform which provides the Wine Windows emulator or the NSIS binary "makensis". -Linux and MacOS are platforms, on which you can install Wine or +Linux and macOS are platforms, on which you can install Wine or "makensis". Selecting between Wine and makensis on non-Windows platforms can @@ -279,7 +279,7 @@ For details see below. Provided that Wine or "makensis" is available on non-Windows platforms, - a full release build may be made on Windows, Linux or MacOS. + a full release build may be made on Windows, Linux or macOS. If you do not want to build the Windows installer, the skip.installer property may be set to skip the creation of the Windows installer. @@ -293,14 +293,15 @@ # Location of GPG executable (used only for releases) gpg.exec=/path/to/gpg - You do not need it if you do not plan to sign the release. - - If "gpg.exec" property does not point to an existing file, it will be - ignored and this feature will be deactivated. - You will be prompted for the GPG passphrase when the release build starts, unless "gpg.passphrase" property is set. + The ant target "verify-release" also uses the property "gpg.exec" to + call the GPG executable. If you want to use "verify-release" but you + do not want to sign the release artefacts, set "gpg.exec" and also + "gpg.sign.files=false". + + 3. If building the Windows installer on Windows If running the build in a UAC enabled environment, building the Windows @@ -367,7 +368,7 @@ the NSIS binary distribution that "ant download-dist" installed. When using "NSIS_CONFIG_CONST_DATA_PATH=no" the binary is independent of the value of "PREFIX". - In addition set the ant property "nsis.tool" to "makensis" in + In addition, set the ant property "nsis.tool" to "makensis" in build.properties. 6. Build the release: @@ -533,13 +534,22 @@ junit.formatter.usefile=false - 5. It is possible to speed up testing by letting JUnit to run several + 5. It is possible to speed up testing by letting JUnit run several tests in parallel. This is configured by setting "test.threads" property. The recommended value is one thread per core. - 6. Optional support is provided for the Cobertura code coverage tool. + 6. Test output can be suppressed for cleaner console output, especially useful + when running tests with multiple threads: + + ant test -Dtest.silent=true + + This suppresses JUnit console output while still saving all test results to + individual log files in output/build/logs. The test-status target will still + display a summary of any failures or skipped tests. + + 7. Optional support is provided for the Cobertura code coverage tool. NOTE: Cobertura is licensed under GPL v2 with parts of it being under Apache License v1.1. See https://cobertura.github.io/cobertura/ for details. @@ -558,7 +568,7 @@ output/coverage - 7. The performance tests are written to run on reasonably powerful machines + 8. The performance tests are written to run on reasonably powerful machines (such as a developer may use day to day) assuming no other resource hungry processes are running. @@ -570,7 +580,7 @@ Where there is no benefit in running an absolute performance test as part of a standard test run, the test will be excluded by naming it - Tester*Performance.java. + Test*Performance.java. The relative tests are included as part of a standard test run however, where the assumptions made about host capabilities are not true (e.g. on @@ -579,30 +589,97 @@ test.excludePerformance=true - 8. Some tests are require large heaps (e.g. 8GB). The CI systems used by the + 9. Some tests are require large heaps (e.g. 8GB). The CI systems used by the project either cannot support heaps of this size or do not support them by default. These tests are therefore disabled by default and may be enabled by using the following property: test.includeLargeHeap=true - 9. Some tests include checks that the access log valve entries are as expected. + 10. Some tests include checks that the access log valve entries are as expected. These checks include timings. On slower / loaded systems these checks will often fail. The checks may be relaxed by using the following property: test.relaxTiming=true - 10. It is known that some platforms (e.g. OSX El Capitan) require IPv4 to + 11. It is known that some platforms (e.g. OSX El Capitan) require IPv4 to be the default for the multicast tests to work. This is configured by the following property: java.net.preferIPv4Stack=true - 11. By default the output of unit tests is sent to the console and can be + 12. By default, the output of unit tests is sent to the console and can be quite verbose. The output can be deactivated by setting the property: 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 diff -Nru tomcat11-11.0.15/CONTRIBUTING.md tomcat11-11.0.22/CONTRIBUTING.md --- tomcat11-11.0.15/CONTRIBUTING.md 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/CONTRIBUTING.md 2026-05-01 18:56:05.000000000 +0000 @@ -157,6 +157,67 @@ * Java source: { at end of line, 4 space indents * XML source: 2 space indents +## Changelog Entries + +Every code change that affects functionality, behavior, or user-visible features should have a corresponding entry in `webapps/docs/changelog.xml`. + +### When Changelog Entries Are Needed + +Add a changelog entry for: +- New features or APIs +- Bug fixes +- Changes to defaults or existing behavior +- Removals or deprecations +- Significant refactoring that affects performance or behavior +- Documentation updates to public APIs + +Skip changelog entries for: +- Test-only changes (unless fixing a test infrastructure issue) +- Build script maintenance +- Internal code cleanup with no user impact + +### Entry Types + +Use the appropriate XML element based on the nature of your change: + +- `` - New features, APIs, or capabilities. Start with "Add" or "Implement". + Example: `Add support for Jakarta EE 12 XML schemas.` + +- `` - Changes to existing features, defaults, or removals. Start with "Change", "Remove", or "Update". + Example: `Change the default for encodedSolidusHandling from decode to reject.` + +- `` - Bug fixes. Start with "Fix". Include `NUMBER` if applicable. + Example: `70000: Fix duplication of special headers in the response after commit.` + +- `` - Internal refactoring or code structure changes. Explain the benefit, not just the change. + Example: `Refactor generation of the remote user element in the access log to remove unnecessary code.` + +- `` - Documentation-only updates (Javadoc, configuration docs). + Example: `Add Javadoc for the Common Annotations API implementation.` + +### Style and Formatting + +- Write in present tense, imperative mood ("Add support" not "Added support") +- Keep entries concise (1-3 sentences) +- Use `` tags for class names, method names, and configuration attributes +- Use `NUMBER` for Bugzilla references +- For default changes, show before -> after: "from `false` to `true`" +- End with attribution in parentheses: `(username)` +- For community patches: "Patch submitted by Name. (committer)" + +### Subsection Placement + +Place entries in the appropriate subsection following this order: +General, Catalina, Coyote, Jasper, Cluster, WebSocket, Web applications, Extras, Tribes, jdbc-pool, Other + +### Common Pitfalls to Avoid + +- Don't use past tense ("Fixed" -> use "Fix") +- Don't omit `` tags for technical terms +- Don't forget attribution at the end +- Don't be vague ("Fix bug" -> "Fix NPE when processing empty request headers") +- Don't include implementation details unless relevant to users + ## Did we miss something? Have you reviewed this guide and found it lacking? Or are you confused about diff -Nru tomcat11-11.0.15/MERGE.txt tomcat11-11.0.22/MERGE.txt --- tomcat11-11.0.15/MERGE.txt 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/MERGE.txt 2026-05-01 18:56:05.000000000 +0000 @@ -37,7 +37,7 @@ Sub-tree: src/main/java/org/apache/bcel The SHA1 ID / tag for the most recent commit to be merged to Tomcat is: -rel/commons-bcel-6.10.0 (2024-07-23) +rel/commons-bcel-6.12.0 (2026-01-22) FileUpload ---------- @@ -58,7 +58,7 @@ Sub-tree src/main/java/org/apache/commons/pool2 The SHA1 ID / tag for the most recent commit to be merged to Tomcat is: -rel/commons-pool-2.12.1 (2025-01-27) +rel/commons-pool-2.13.1 (2025-12-30) DBCP2 No unused code removed @@ -66,4 +66,4 @@ src/main/java/org/apache/commons/dbcp2 src/main/resources/org/apache/commons/dbcp2 The SHA1 ID / tag for the most recent commit to be merged to Tomcat is: -rel/commons-dbcp-2.13.0 (2024-12-02) +rel/commons-dbcp-2.14.0 (2025-12-16) diff -Nru tomcat11-11.0.15/NOTICE tomcat11-11.0.22/NOTICE --- tomcat11-11.0.15/NOTICE 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/NOTICE 2026-05-01 18:56:05.000000000 +0000 @@ -1,5 +1,5 @@ Apache Tomcat -Copyright 1999-2025 The Apache Software Foundation +Copyright 1999-2026 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (https://www.apache.org/). diff -Nru tomcat11-11.0.15/README.md tomcat11-11.0.22/README.md --- tomcat11-11.0.15/README.md 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/README.md 2026-05-01 18:56:05.000000000 +0000 @@ -48,18 +48,16 @@ ### Installation -Please see [RUNNING.txt](RUNNING.txt) for more info. +Please see [RUNNING.txt](RUNNING.txt) for more information. ### Licensing -Please see [LICENSE](LICENSE) for more info. +Please see [LICENSE](LICENSE) for more information. ### Support and Mailing List Information * Free community support is available through the -[tomcat-users](https://tomcat.apache.org/lists.html#tomcat-users) email list and -a dedicated [IRC channel](https://tomcat.apache.org/irc.html) (#tomcat on -Freenode). +[tomcat-users](https://tomcat.apache.org/lists.html#tomcat-users) email list. * If you want freely available support for running Apache Tomcat, please see the resources page [here](https://tomcat.apache.org/findhelp.html). @@ -76,4 +74,4 @@ ### Contributing -Please see [CONTRIBUTING](CONTRIBUTING.md) for more info. +Please see [CONTRIBUTING](CONTRIBUTING.md) for more information. diff -Nru tomcat11-11.0.15/bin/catalina.bat tomcat11-11.0.22/bin/catalina.bat --- tomcat11-11.0.15/bin/catalina.bat 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/bin/catalina.bat 2026-05-01 18:56:05.000000000 +0000 @@ -306,7 +306,7 @@ 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 -Nru tomcat11-11.0.15/bin/catalina.sh tomcat11-11.0.22/bin/catalina.sh --- tomcat11-11.0.15/bin/catalina.sh 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/bin/catalina.sh 2026-05-01 18:56:05.000000000 +0000 @@ -565,7 +565,9 @@ 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 -Nru tomcat11-11.0.15/build.properties.default tomcat11-11.0.22/build.properties.default --- tomcat11-11.0.15/build.properties.default 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/build.properties.default 2026-05-01 18:56:05.000000000 +0000 @@ -31,7 +31,7 @@ # ----- Version Control Flags ----- version.major=11 version.minor=0 -version.build=15 +version.build=22 version.patch=0 version.suffix= version.dev=-dev @@ -76,6 +76,8 @@ test.accesslog=false # Display the tests output on the console test.verbose=true +# Suppress JUnit console output (useful for parallel testing) +test.silent=false # Number of parallel threads to use for testing. The recommended value is one # thread per core. @@ -100,6 +102,8 @@ # ----- Release build settings ----- # Location of GPG executable gpg.exec=/path/to/gpg +# Release artefact signing with gpg +gpg.sign.files=true # Code signing of Windows installer # See https://infra.apache.org/digicert-use.html for setup instructions @@ -141,15 +145,11 @@ # ----- Eclipse JDT, version 4.7 or later -----# # See https://cwiki.apache.org/confluence/display/TOMCAT/Managing+Tomcat%27s+Dependency+on+the+Eclipse+JDT+Core+Batch+Compiler # -# Checksum is from "SHA512 Checksums for 4.36" link at -# https://download.eclipse.org/eclipse/downloads/drops4/R-4.36-202505281830/ -# https://download.eclipse.org/eclipse/downloads/drops4/R-4.36-202505281830/checksum/eclipse-4.36-SUMSSHA512 -# -jdt.version=4.37 -jdt.release=R-4.37-202509050730 +jdt.version=4.39 +jdt.release=R-4.39-202602260420 jdt.checksum.enabled=true jdt.checksum.algorithm=SHA-512 -jdt.checksum.value=06d8974af46d81f7a03c257b54fb5854a9b9ba8ab2733db4946745142e07e7dc3e69ea864d4b011230ffb8f92243503d7ffb6ac2ddf9b747c1bbc155c55fb583 +jdt.checksum.value=af25493d4a429fd24256f69a085a618afffe23bb09b284976fb9e82f651715a0455b704ee800432b43dd409f74874765089cd65fc148e2175d251f9ef830ce0a jdt.home=${base.path}/ecj-${jdt.version} jdt.jar=${jdt.home}/ecj-${jdt.version}.jar # The download will be moved to the archive area eventually. We are taking care of that in advance. @@ -157,14 +157,14 @@ jdt.loc.2=https://download.eclipse.org/eclipse/downloads/drops4/${jdt.release}/ecj-${jdt.version}.jar # ----- Tomcat native library ----- -tomcat-native.version=2.0.9 -tomcat-native-openssl.version=3.5.0 +tomcat-native.version=2.0.14 +tomcat-native-openssl.version=3.5.5 tomcat-native.src.checksum.enabled=true tomcat-native.src.checksum.algorithm=SHA-512 -tomcat-native.src.checksum.value=c8eb81de1cf7316174c36038c2133b013fd18ba11df09c41edb927ff33fef46863ef706b6193487ecde1eed7055d4c47fa23fc29d5a8d53f0c4b6d69b0ce9b33 +tomcat-native.src.checksum.value=33d626fab35cbfa7398ca90cabd99950c6362ab4e19637012850fd84ecc78184e4c6c975ece92dc8d6461b6a8c2f83221cbc7374ff154422e7722606a4a144c7 tomcat-native.win.checksum.enabled=true tomcat-native.win.checksum.algorithm=SHA-512 -tomcat-native.win.checksum.value=8e8a580425671025913259659b61c497354d682735481663730e800c8f8b4d16d3322d9c75037146f901af22341a903fa9700d18d8d2cd874745a18563cde0d5 +tomcat-native.win.checksum.value=82c46733be9f84f11bcbf97cc1db3b9c9b861c32f30b9dee3fb3ebe1d400325587eb6b77c384622515583d455f170017201cfac62498f0a0886211839bdfa56f tomcat-native.home=${base.path}/tomcat-native-${tomcat-native.version} tomcat-native.tar.gz=${tomcat-native.home}/tomcat-native.tar.gz tomcat-native.loc.1=${base-tomcat.loc.1}/tomcat-connectors/native/${tomcat-native.version}/source/tomcat-native-${tomcat-native.version}-src.tar.gz @@ -173,33 +173,33 @@ tomcat-native.win.2=${base-tomcat.loc.2}/tomcat-connectors/native/${tomcat-native.version}/binaries/tomcat-native-${tomcat-native.version}-openssl-${tomcat-native-openssl.version}-win32-bin.zip # ----- NSIS, version 3.0 or later ----- -nsis.version=3.11 +nsis.version=3.12 nsis.checksum.enabled=true nsis.bin.checksum.algorithm=MD5|SHA-1 -nsis.bin.checksum.value=b7c063bee3afc8127dca0fd64c4e22ce|ef7ff767e5cbd9edd22add3a32c9b8f4500bb10d +nsis.bin.checksum.value=757c22153dd8b90f5e297310d9966997|364fd795b0cafc1fbff3e966f103a8f8fc8fb7f1 nsis.bin.home=${base.path}/nsis-${nsis.version} nsis.executable.windows=${nsis.bin.home}/makensis.exe nsis.bin.loc=${base-sf.loc}/nsis/nsis-${nsis.version}.zip nsis.src.checksum.algorithm=MD5|SHA-1 -nsis.src.checksum.value=63bdc1b7676c96486532c98c0b4d2fb5|63ddba57fe46b1e0a4810ece2a7980f069c031b6 +nsis.src.checksum.value=8ec7c3e1228ac4eb96e5e421610b4aae|432e99150881c061c7e313eb1aac45763d951572 nsis.src.home=${base.path}/nsis-${nsis.version}-src nsis.src.loc=${base-sf.loc}/nsis/nsis-${nsis.version}-src.tar.bz2 # ----- Commons Daemon, version 1.2.0 or later ----- -commons-daemon.version=1.5.0 +commons-daemon.version=1.5.1 -# checksum for commons-daemon-1.5.0-bin.tar.gz +# checksum for commons-daemon-1.5.1-bin.tar.gz commons-daemon.bin.checksum.enabled=true commons-daemon.bin.checksum.algorithm=SHA-512 -commons-daemon.bin.checksum.value=2b6365d3301b2bdf71851aa1be8b09f46b082ad93a564fbd1e64d69ad7f607807c45813fad182138150ea69fd231cd3b4812601ce011f42bd145979c005dca1b +commons-daemon.bin.checksum.value=740b832b8a7b5df8f2aa8029cf6ee88b8be6f4f5e42e34587a70bbf2cbe5b7e7dd75da375d2f3f1f7f2e6a5d89b5ceba5433499773852df67e8bbeb88b466786 -# checksums for commons-daemon-1.5.0-native-src.tar.gz, commons-daemon-1.5.0-bin-windows.zip +# checksums for commons-daemon-1.5.1-native-src.tar.gz, commons-daemon-1.5.1-bin-windows.zip commons-daemon.native.src.checksum.enabled=true commons-daemon.native.src.checksum.algorithm=SHA-512 -commons-daemon.native.src.checksum.value=8b3c22912b2fb87e5b8452f62e40d9ca328d33e5ea4db66e32139c341772e8147de300958d2e32b7a6289f0e2f3eb1e2114f10f6dc5c74232cd9f6fa2177a1f8 +commons-daemon.native.src.checksum.value=ced2238b4fb47a208e7c9aa435e7981b5d42fd9e07d11d7f72da2b7239c388a3b7d6b7ce3424624aecfbe7a8471ee316cdd46a040fadb129096705fa3325129b commons-daemon.native.win.checksum.enabled=true commons-daemon.native.win.checksum.algorithm=SHA-512 -commons-daemon.native.win.checksum.value=e5d4d94e350d44ca2c9f34f65439bbc663b9d4df6edda10c83d3c8a76522bd3881cb4523152739369c89ec9bdc672c9b456d46ed72e12c366d6be8ba4619575d +commons-daemon.native.win.checksum.value=26cef55a0237aab449d2133e56815393729c3965c6fd063326bccdb569be91ddfe500b247ba3df3e9baac511452825d3d0333d165abd24932717a372a294a463 commons-daemon.home=${base.path}/commons-daemon-${commons-daemon.version} commons-daemon.jar=${commons-daemon.home}/commons-daemon-${commons-daemon.version}.jar @@ -242,37 +242,37 @@ easymock.loc=${base-maven.loc}/org/easymock/easymock/${easymock.version}/easymock-${easymock.version}.jar # ----- objenesis, used by EasyMock, version 3.3 or later ----- -objenesis.version=3.4 +objenesis.version=3.5 objenesis.checksum.enabled=true -objenesis.checksum.algorithm=MD5|SHA-1 -objenesis.checksum.value=51242320cb2bb25a3f36e2e21fa87de0|675cbe121a68019235d27f6c34b4f0ac30e07418 +objenesis.checksum.algorithm=SHA-512 +objenesis.checksum.value=7587fabe1dd4a639e869e4478a097665d34686de8c1ec5794356a5ebc27501fdad42a365e0f000bdf30b0b0f73d6c02523346fc6cfb9109239e5b2f7876e981e objenesis.home=${base.path}/objenesis-${objenesis.version} objenesis.jar=${objenesis.home}/objenesis-${objenesis.version}.jar objenesis.loc=${base-maven.loc}/org/objenesis/objenesis/${objenesis.version}/objenesis-${objenesis.version}.jar # ----- byte-buddy, used by EasyMock, version 1.12.18 or later ----- -bytebuddy.version=1.18.2 +bytebuddy.version=1.18.8 bytebuddy.checksum.enabled=true bytebuddy.checksum.algorithm=SHA-512 -bytebuddy.checksum.value=fc5395040b177a382cff96fda197d624d3d914510e19d8ec68de9b472dc80ae48fcdb7283547774d4e4b3b74ff92107e8258c498d527c46846b5246fc9b09db9 +bytebuddy.checksum.value=0e50f1c029c91831fada246eb57a3898e327aed0e9b8b5052c1d66ff58b0d1a8b9a1c255b60da286a30cca60c1a964377c40e5fb7988bf4839f8c8ab1289b5f6 bytebuddy.home=${base.path}/byte-buddy-${bytebuddy.version} bytebuddy.jar=${bytebuddy.home}/byte-buddy-${bytebuddy.version}.jar bytebuddy.loc=${base-maven.loc}/net/bytebuddy/byte-buddy/${bytebuddy.version}/byte-buddy-${bytebuddy.version}.jar # ----- UnboundID, used by unit tests, version 5.1.4 or later ----- -unboundid.version=7.0.3 +unboundid.version=7.0.4 unboundid.checksum.enabled=true -unboundid.checksum.algorithm=SHA-512 -unboundid.checksum.value=25fef7f28e06bedfa54672370e0e4cfb5ef8041f1bd5abf2826ad022e5131bf1d3791314888a939f3fe25edbc28d28afffdd7125cfae16876ead0bf50c68351a +unboundid.checksum.algorithm=MD5|SHA-1 +unboundid.checksum.value=7006a217741934517b1cdd6aa12c6d9f|2fe2d5461a87a58aee97f836e3af63ef8ce7b29e unboundid.home=${base.path}/unboundid-${unboundid.version} unboundid.jar=${unboundid.home}/unboundid-ldapsdk-${unboundid.version}.jar unboundid.loc=${base-maven.loc}/com/unboundid/unboundid-ldapsdk/${unboundid.version}/unboundid-ldapsdk-${unboundid.version}.jar # ----- Checkstyle, version 6.16 or later ----- -checkstyle.version=12.2.0 +checkstyle.version=12.3.1 checkstyle.checksum.enabled=true checkstyle.checksum.algorithm=SHA-512 -checkstyle.checksum.value=ddf947de9dbf6be0c69171d49415b1b0cc268f47d1a8fab04551b08086fb0100c87623cde9d99fca1cbfc92e33eb4afcea66d38a3c70155ec141c1f014453990 +checkstyle.checksum.value=8406977982d6e5eb9dba561ccb319daa9d98fc7172217f0c36644d5830121124181752c425dcf34fb7579232983d923e430a5d6b119272ab8615e7b1e4f10bf1 checkstyle.home=${base.path}/checkstyle-${checkstyle.version} checkstyle.jar=${checkstyle.home}/checkstyle-${checkstyle.version}-all.jar checkstyle.loc=${base-gh.loc}/checkstyle/checkstyle/releases/download/checkstyle-${checkstyle.version}/checkstyle-${checkstyle.version}-all.jar @@ -297,10 +297,10 @@ # ----- bnd, version 6.3.0 or later ----- # ----- provides OSGI metadata for JARs ----- -bnd.version=7.1.0 +bnd.version=7.2.3 bnd.checksum.enabled=true bnd.checksum.algorithm=MD5|SHA-1 -bnd.checksum.value=9cee533d5f3973d6135e557934160180|49e4ebe633c608c498cbfc7d7a4e9dda5fefa2fc +bnd.checksum.value=56f0d8c7d872b88cfc5411f6caa96379|8eaec893e3ed1058acf5b780ce354db5b8226ade bnd.home=${base.path}/bnd-${bnd.version} bnd.jar=${bnd.home}/biz.aQute.bnd-${bnd.version}.jar @@ -347,3 +347,23 @@ derby-shared.loc=${base-maven.loc}/org/apache/derby/derbyshared/${derby.version}/derbyshared-${derby.version}.jar derby-tools.jar=${derby.home}/derby-tools-${derby.version}.jar derby-tools.loc=${base-maven.loc}/org/apache/derby/derbytools/${derby.version}/derbytools-${derby.version}.jar + +# ----- Bouncy Castle, used by unit tests ---- +bouncycastle.version=1.84 +bouncycastle-provider.checksum.enabled=true +bouncycastle-provider.checksum.algorithm=SHA-512 +bouncycastle-provider.checksum.value=4b7e5696830023bad1594d9f9766898f78018ec7d6ec34de23f2a6683b02803b92ffe8ab5d21f1a717eb4fafa8e22dcf3a4b6dd275bd86c7cb02609987fe92f5 +bouncycastle-pkix.checksum.enabled=true +bouncycastle-pkix.checksum.algorithm=SHA-512 +bouncycastle-pkix.checksum.value=01644d7e0c6041ea8c8629f6ad73f7206efa2797d954da1507dd43c5d262a161194ed84e960234cf5c53787033f1181aa4965e0d6ebfe82ca040de13ca307565 +bouncycastle-util.checksum.enabled=true +bouncycastle-util.checksum.algorithm=SHA-512 +bouncycastle-util.checksum.value=e001b244723fb3c4d1e06862bb857512015a92d7e18650ae3447a3d258274ec8ed37b8cba958397b00f8dd73463943e9a9489dd5dbddbe24b24cd6fae5ca8a62 + +bouncycastle.home=${base.path}/bouncycastle-${bouncycastle.version} +bouncycastle-provider.jar=${bouncycastle.home}/bouncycastle-provider-${bouncycastle.version}.jar +bouncycastle-provider.loc=${base-maven.loc}/org/bouncycastle/bcprov-jdk18on/${bouncycastle.version}/bcprov-jdk18on-${bouncycastle.version}.jar +bouncycastle-pkix.jar=${bouncycastle.home}/bouncycastle-pkix-${bouncycastle.version}.jar +bouncycastle-pkix.loc=${base-maven.loc}/org/bouncycastle/bcpkix-jdk18on/${bouncycastle.version}/bcpkix-jdk18on-${bouncycastle.version}.jar +bouncycastle-util.jar=${bouncycastle.home}/bouncycastle-util-${bouncycastle.version}.jar +bouncycastle-util.loc=${base-maven.loc}/org/bouncycastle/bcutil-jdk18on/${bouncycastle.version}/bcutil-jdk18on-${bouncycastle.version}.jar diff -Nru tomcat11-11.0.15/build.properties.release tomcat11-11.0.22/build.properties.release --- tomcat11-11.0.15/build.properties.release 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/build.properties.release 2026-05-01 18:56:05.000000000 +0000 @@ -24,7 +24,7 @@ version.dev= # Ensure consistent timestamps for reproducible builds. -ant.tstamp.now.iso=2025-12-02T16:32:48Z +ant.tstamp.now.iso=2026-05-01T18:29:05Z # Enable insertion of detached signatures into the Windows installer. do.codesigning=true @@ -37,11 +37,11 @@ # build since this data is embedded in various files, particularly JAR file # manifests, as part of the build process. # -# Apache Ant: Apache Ant(TM) version 1.10.15 compiled on August 25 2024 +# Apache Ant: Apache Ant(TM) version 1.10.17 compiled on April 6 2026 # # Java Name: OpenJDK 64-Bit Server VM # Java Vendor: Eclipse Adoptium -# Java Version: 25.0.1+8-LTS +# Java Version: 25.0.3+9-LTS # The following is provided for information only. Builds will be repeatable # whether or not the build environment is consistent with this information. @@ -50,5 +50,5 @@ # File encoding: UTF-8 # # Release Manager: markt -release-java-version=25.0.1+8-LTS -release-ant-version=1.10.15 +release-java-version=25.0.3+9-LTS +release-ant-version=1.10.17 diff -Nru tomcat11-11.0.15/build.xml tomcat11-11.0.22/build.xml --- tomcat11-11.0.15/build.xml 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/build.xml 2026-05-01 18:56:05.000000000 +0000 @@ -198,7 +198,7 @@ - + @@ -222,6 +222,10 @@ + + + + @@ -253,6 +257,9 @@ + + + @@ -269,6 +276,9 @@ + + + @@ -308,6 +318,8 @@ + + @@ -635,7 +647,6 @@ - @@ -920,6 +931,8 @@ + + @@ -927,6 +940,8 @@ + + @@ -955,6 +970,8 @@ + + @@ -1925,7 +1942,7 @@ - + @@ -1942,14 +1959,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + depends="-test-name-default,test-nio,test-nio2,coverage-report,test-status" /> + depends="clean-classes,-test-name-default,test-nio,test-nio2,coverage-report,test-status" /> + depends="-test-name-default,test-only-nio,test-only-nio2,test-status" /> @@ -1987,13 +2110,13 @@ + depends="-test-name-default,setup-jacoco,test-compile,deploy,test-openssl-exists,test-tcnative-exists" if="${execute.test.nio}"> + depends="-test-name-default,setup-jacoco,test-openssl-exists,test-tcnative-exists" if="${execute.test.nio}"> @@ -2024,6 +2147,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2051,7 +2232,7 @@ enabled="${test.coverage}" destfile="${coverage.datafile}" > - + @@ -2104,14 +2286,16 @@ - - + + - - + + + + @@ -3012,26 +3196,31 @@ - + - + - -gpg.exec.available=${gpg.exec.available} -gpg.exec=${gpg.exec} - Enter GPG passphrase - + + + + + + + + gpg.exec.available=${gpg.exec.available} + gpg.exec=${gpg.exec} + Enter GPG passphrase: - + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tue, 03 Feb 2026 12:58:13 +0100 + -- Markus Koschany Thu, 04 Jun 2026 13:23:48 +0200 + +tomcat11 (11.0.22-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + * Install jakartaee-migration-shaded.jar into the Tomcat lib directory + (Closes: #1132599) + * Standards-Version updated to 4.7.4 + + -- Emmanuel Bourg Mon, 11 May 2026 17:13:08 +0200 + +tomcat11 (11.0.21-1) unstable; urgency=medium + + * New upstream release + - Refreshed the patches + - New build dependency on Bouncy Castle + + -- Emmanuel Bourg Tue, 14 Apr 2026 22:21:26 +0200 + +tomcat11 (11.0.18-1) unstable; urgency=medium + + * New upstream version 11.0.18. + * Refresh all patches. + * Declare compliance with Debian Policy 4.7.3. + + -- Markus Koschany Tue, 03 Feb 2026 12:37:50 +0100 tomcat11 (11.0.15-1) unstable; urgency=medium diff -Nru tomcat11-11.0.15/debian/control tomcat11-11.0.22/debian/control --- tomcat11-11.0.15/debian/control 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/control 2026-06-04 11:23:48.000000000 +0000 @@ -13,6 +13,8 @@ default-jdk (>= 2:1.17), javahelper, junit4 (>= 4.11), + libbcpkix-java, + libbcprov-java, libbyte-buddy-java, libderby-java, libeasymock-java (>= 3.0), @@ -28,7 +30,7 @@ lsb-release, maven-repo-helper, tomcat-jakartaee-migration (>= 1.0.7-2~) -Standards-Version: 4.7.2 +Standards-Version: 4.7.4 Vcs-Git: https://salsa.debian.org/java-team/tomcat11.git Vcs-Browser: https://salsa.debian.org/java-team/tomcat11 Homepage: http://tomcat.apache.org @@ -38,6 +40,7 @@ Depends: default-jre-headless (>= 2:1.17) | java17-runtime-headless | java17-runtime, libtomcat11-java (>= ${source:Version}), + tomcat-jakartaee-migration, ${misc:Depends} Description: Apache Tomcat 11 - Servlet and JSP engine -- common files Apache Tomcat implements the Java Servlet and the JavaServer Pages (JSP) diff -Nru tomcat11-11.0.15/debian/copyright tomcat11-11.0.22/debian/copyright --- tomcat11-11.0.15/debian/copyright 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/copyright 2026-06-04 11:23:48.000000000 +0000 @@ -4,7 +4,7 @@ Files-Excluded: */taglibs-standard-*.jar Files: * -Copyright: 2000-2025, The Apache Software Foundation. +Copyright: 2000-2026, The Apache Software Foundation. 2002, International Business Machines Corporation. License: Apache-2.0 @@ -49,7 +49,7 @@ 2013, Jakub Adam 2013-2014, Gianfranco Costamagna 2013-2025, Emmanuel Bourg - 2016-2025, Markus Koschany + 2016-2026, Markus Koschany License: Apache-2.0 License: Apache-2.0 diff -Nru tomcat11-11.0.15/debian/patches/0004-split-deploy-webapps-target-from-deploy-target.patch tomcat11-11.0.22/debian/patches/0004-split-deploy-webapps-target-from-deploy-target.patch --- tomcat11-11.0.15/debian/patches/0004-split-deploy-webapps-target-from-deploy-target.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0004-split-deploy-webapps-target-from-deploy-target.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,9 +1,15 @@ -Description: Split deploy-webapps target from deploy target -Author: Debian Java Maintainers +From: Debian Java Maintainers +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: Split deploy-webapps target from deploy target + Forwarded: not-needed +--- + build.xml | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + --- a/build.xml +++ b/build.xml -@@ -1513,7 +1513,7 @@ +@@ -1530,7 +1530,7 @@ @@ -12,7 +18,7 @@ description="Default. Builds a working Tomcat instance"> -@@ -1547,6 +1547,10 @@ +@@ -1564,6 +1564,10 @@ diff -Nru tomcat11-11.0.15/debian/patches/0005-skip-test-failures.patch tomcat11-11.0.22/debian/patches/0005-skip-test-failures.patch --- tomcat11-11.0.15/debian/patches/0005-skip-test-failures.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0005-skip-test-failures.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,9 +1,15 @@ -Description: Ignore the failing tests -Author: Emmanuel Bourg +From: Emmanuel Bourg +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: Ignore the failing tests + Forwarded: not-needed +--- + build.xml | 2 ++ + 1 file changed, 2 insertions(+) + --- a/build.xml +++ b/build.xml -@@ -1986,8 +1986,10 @@ +@@ -2109,8 +2109,10 @@ diff -Nru tomcat11-11.0.15/debian/patches/0010-debianize-build-xml.patch tomcat11-11.0.22/debian/patches/0010-debianize-build-xml.patch --- tomcat11-11.0.15/debian/patches/0010-debianize-build-xml.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0010-debianize-build-xml.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,9 +1,15 @@ -Description: Disable usage of embedded library copies -Author: James Pages +From: James Pages +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: Disable usage of embedded library copies + Forwarded: no +--- + build.xml | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + --- a/build.xml +++ b/build.xml -@@ -1016,7 +1016,7 @@ +@@ -1033,7 +1033,7 @@ @@ -12,7 +18,7 @@ diff -Nru tomcat11-11.0.15/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch tomcat11-11.0.22/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch --- tomcat11-11.0.15/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,11 +1,18 @@ -Description: Don't look for build.properties in the user home directory. - This directory doesn't exist on the builders and the attempt to load - a property file there causes a build failure. -Author: Jakub Adam +From: Jakub Adam +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: Don't look for build.properties in the user home directory. + Forwarded: not-needed + +This directory doesn't exist on the builders and the attempt to load +a property file there causes a build failure. +--- + build.xml | 1 - + 1 file changed, 1 deletion(-) + --- a/build.xml +++ b/build.xml -@@ -859,7 +859,6 @@ +@@ -870,7 +870,6 @@ diff -Nru tomcat11-11.0.15/debian/patches/0018-fix-manager-webapp.patch tomcat11-11.0.22/debian/patches/0018-fix-manager-webapp.patch --- tomcat11-11.0.15/debian/patches/0018-fix-manager-webapp.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0018-fix-manager-webapp.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,10 +1,20 @@ -Description: This patch changes the manager path from webapps/manager to ../tomcat11-admin/manager -Author: "ubuntu@iam.tj" +From: "ubuntu@iam.tj" +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: This patch changes the manager path from webapps/manager to + ../tomcat11-admin/manager + Reviewed-By: Gianfranco Costamagna Bug-Ubuntu: https://bugs.launchpad.net/bugs/1128067 +--- + webapps/docs/manager-howto.xml | 2 +- + webapps/host-manager/WEB-INF/manager.xml | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/webapps/docs/manager-howto.xml b/webapps/docs/manager-howto.xml +index 3bd65b9..f3f5946 100644 --- a/webapps/docs/manager-howto.xml +++ b/webapps/docs/manager-howto.xml -@@ -73,7 +73,7 @@ +@@ -73,7 +73,7 @@ configuration file in the $CATALINA_BASE/conf/[enginename]/[hostname] folder. Here is an example:

+From: Yolanda Robla +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: Adds the name of the distribution to the version of Tomcat + Forwarded: not-needed Bug-Debian: http://bugs.debian.org/729840 + +reported on the error pages (i.e. 'Apache Tomcat/8.0.x (Debian)') +--- + build.xml | 1 + + java/org/apache/catalina/util/ServerInfo.properties | 2 +- + 2 files changed, 2 insertions(+), 1 deletion(-) + --- a/build.xml +++ b/build.xml -@@ -285,6 +285,7 @@ +@@ -295,6 +295,7 @@ diff -Nru tomcat11-11.0.15/debian/patches/0021-dont-test-unsupported-ciphers.patch tomcat11-11.0.22/debian/patches/0021-dont-test-unsupported-ciphers.patch --- tomcat11-11.0.15/debian/patches/0021-dont-test-unsupported-ciphers.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0021-dont-test-unsupported-ciphers.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,7 +1,17 @@ -Description: Don't check the IDEA cipher during the tests since it is disabled in Debian (see #327739) - Also ignore ARIA which is disabled by default in OpenSSL. -Author: Markus Koschany +From: Markus Koschany +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: Don't check the IDEA cipher during the tests since it is disabled in + Debian (see #327739) + Forwarded: not-needed + +Also ignore ARIA which is disabled by default in OpenSSL. +--- + .../tomcat/util/net/openssl/ciphers/TestCipher.java | 2 +- + .../ciphers/TestOpenSSLCipherConfigurationParser.java | 2 +- + .../tomcat/util/net/openssl/ciphers/TesterOpenSSL.java | 18 ++++++++++++++++++ + 3 files changed, 20 insertions(+), 2 deletions(-) + --- a/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java +++ b/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java @@ -76,7 +76,7 @@ @@ -15,7 +25,7 @@ for (Cipher cipher : Cipher.values()) { --- a/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java +++ b/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java -@@ -573,7 +573,7 @@ +@@ -608,7 +608,7 @@ private void testSpecification(String specification) throws Exception { // Filter out cipher suites that OpenSSL does not implement @@ -26,7 +36,7 @@ List jsseCipherListFromParser = --- a/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java +++ b/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java -@@ -105,6 +105,24 @@ +@@ -107,6 +107,24 @@ unimplemented.add(Cipher.SSL2_RC4_128_EXPORT40_WITH_MD5); unimplemented.add(Cipher.SSL2_IDEA_128_CBC_WITH_MD5); unimplemented.add(Cipher.SSL2_DES_192_EDE3_CBC_WITH_MD5); diff -Nru tomcat11-11.0.15/debian/patches/0023-disable-shutdown-by-socket.patch tomcat11-11.0.22/debian/patches/0023-disable-shutdown-by-socket.patch --- tomcat11-11.0.15/debian/patches/0023-disable-shutdown-by-socket.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0023-disable-shutdown-by-socket.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,6 +1,14 @@ -Description: Disables the shutdown port (8005) by default -Author: Emmanuel Bourg +From: Emmanuel Bourg +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: Disables the shutdown port (8005) by default + Forwarded: no +--- + conf/server.xml | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/conf/server.xml b/conf/server.xml +index aa75d33..874b286 100644 --- a/conf/server.xml +++ b/conf/server.xml @@ -19,7 +19,7 @@ diff -Nru tomcat11-11.0.15/debian/patches/0024-systemd-log-formatter.patch tomcat11-11.0.22/debian/patches/0024-systemd-log-formatter.patch --- tomcat11-11.0.15/debian/patches/0024-systemd-log-formatter.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0024-systemd-log-formatter.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,6 +1,16 @@ -Description: Adds a log formatter suitable for systemd From: Emmanuel Bourg +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: Adds a log formatter suitable for systemd + Forwarded: no +--- + java/org/apache/juli/SystemdFormatter.java | 109 +++++++++++++++++++++++++++++ + 1 file changed, 109 insertions(+) + create mode 100644 java/org/apache/juli/SystemdFormatter.java + +diff --git a/java/org/apache/juli/SystemdFormatter.java b/java/org/apache/juli/SystemdFormatter.java +new file mode 100644 +index 0000000..014a193 --- /dev/null +++ b/java/org/apache/juli/SystemdFormatter.java @@ -0,0 +1,109 @@ diff -Nru tomcat11-11.0.15/debian/patches/0025-invalid-configuration-exit-status.patch tomcat11-11.0.22/debian/patches/0025-invalid-configuration-exit-status.patch --- tomcat11-11.0.15/debian/patches/0025-invalid-configuration-exit-status.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0025-invalid-configuration-exit-status.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,9 +1,18 @@ -Description: Fix the exit status when Tomcat terminates because the configuration is invalid -Author: Emmanuel Bourg +From: Emmanuel Bourg +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: Fix the exit status when Tomcat terminates because the configuration + is invalid + Bug: https://bz.apache.org/bugzilla/show_bug.cgi?id=62607 +--- + java/org/apache/catalina/startup/Bootstrap.java | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/java/org/apache/catalina/startup/Bootstrap.java b/java/org/apache/catalina/startup/Bootstrap.java +index 6d169f0..9e83911 100644 --- a/java/org/apache/catalina/startup/Bootstrap.java +++ b/java/org/apache/catalina/startup/Bootstrap.java -@@ -467,6 +467,10 @@ +@@ -467,6 +467,10 @@ public final class Bootstrap { case "start": daemon.setAwait(true); daemon.load(args); diff -Nru tomcat11-11.0.15/debian/patches/0026-easymock4-compatibility.patch tomcat11-11.0.22/debian/patches/0026-easymock4-compatibility.patch --- tomcat11-11.0.15/debian/patches/0026-easymock4-compatibility.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0026-easymock4-compatibility.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,9 +1,17 @@ -Description: Fixes the compatibility with the version of Easymock in Debian -Author: Emmanuel Bourg +From: Emmanuel Bourg +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: Fixes the compatibility with the version of Easymock in Debian + Forwarded: no +--- + .../valves/TestCrawlerSessionManagerValve.java | 28 +++++++++++----------- + 1 file changed, 14 insertions(+), 14 deletions(-) + +diff --git a/test/org/apache/catalina/valves/TestCrawlerSessionManagerValve.java b/test/org/apache/catalina/valves/TestCrawlerSessionManagerValve.java +index c813156..484ebf8 100644 --- a/test/org/apache/catalina/valves/TestCrawlerSessionManagerValve.java +++ b/test/org/apache/catalina/valves/TestCrawlerSessionManagerValve.java -@@ -57,13 +57,13 @@ +@@ -57,13 +57,13 @@ public class TestCrawlerSessionManagerValve { CrawlerSessionManagerValve valve = new CrawlerSessionManagerValve(); valve.setCrawlerIps("216\\.58\\.206\\.174"); valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); @@ -19,7 +27,7 @@ EasyMock.verify(request, session); } -@@ -73,13 +73,13 @@ +@@ -73,13 +73,13 @@ public class TestCrawlerSessionManagerValve { CrawlerSessionManagerValve valve = new CrawlerSessionManagerValve(); valve.setCrawlerIps("216\\.58\\.206\\.174"); valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); @@ -35,7 +43,7 @@ EasyMock.verify(request, session); } -@@ -90,7 +90,7 @@ +@@ -90,7 +90,7 @@ public class TestCrawlerSessionManagerValve { valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); valve.setHostAware(true); valve.setContextAware(true); @@ -44,7 +52,7 @@ verifyCrawlingLocalhost(valve, "localhost"); verifyCrawlingLocalhost(valve, "example.invalid"); -@@ -102,7 +102,7 @@ +@@ -102,7 +102,7 @@ public class TestCrawlerSessionManagerValve { valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); valve.setHostAware(true); valve.setContextAware(true); @@ -53,7 +61,7 @@ verifyCrawlingContext(valve, "/examples"); verifyCrawlingContext(valve, null); -@@ -113,7 +113,7 @@ +@@ -113,7 +113,7 @@ public class TestCrawlerSessionManagerValve { CrawlerSessionManagerValve valve = new CrawlerSessionManagerValve(); valve.setCrawlerIps("216\\.58\\.206\\.174"); valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); @@ -62,7 +70,7 @@ valve.setSessionInactiveInterval(0); StandardSession session = new StandardSession(TEST_MANAGER); session.setId("id"); -@@ -123,7 +123,7 @@ +@@ -123,7 +123,7 @@ public class TestCrawlerSessionManagerValve { EasyMock.replay(request); @@ -71,7 +79,7 @@ EasyMock.verify(request); -@@ -142,7 +142,7 @@ +@@ -142,7 +142,7 @@ public class TestCrawlerSessionManagerValve { EasyMock.replay(request, session); @@ -80,7 +88,7 @@ EasyMock.verify(request, session); } -@@ -156,14 +156,14 @@ +@@ -156,14 +156,14 @@ public class TestCrawlerSessionManagerValve { EasyMock.replay(request, session); @@ -97,7 +105,7 @@ if (isBot) { EasyMock.expect(session.getId()).andReturn("id").times(1); session.setAttribute(EasyMock.eq(valve.getClass().getName()), -@@ -182,7 +182,7 @@ +@@ -182,7 +182,7 @@ public class TestCrawlerSessionManagerValve { private Request createRequestExpectations(String ip, HttpSession session, boolean isBot, String hostname, String contextPath, String userAgent) { @@ -106,7 +114,7 @@ EasyMock.expect(request.getRemoteAddr()).andReturn(ip); EasyMock.expect(request.getRemoteAddr()).andReturn(ip); EasyMock.expect(request.getHost()).andReturn(simpleHostWithName(hostname)); -@@ -198,7 +198,7 @@ +@@ -198,7 +198,7 @@ public class TestCrawlerSessionManagerValve { } private Host simpleHostWithName(String hostname) { @@ -115,7 +123,7 @@ EasyMock.expect(host.getName()).andReturn(hostname); EasyMock.replay(host); return host; -@@ -208,7 +208,7 @@ +@@ -208,7 +208,7 @@ public class TestCrawlerSessionManagerValve { if (contextPath == null) { return null; } diff -Nru tomcat11-11.0.15/debian/patches/0030-eclipse-jdt-classpath.patch tomcat11-11.0.22/debian/patches/0030-eclipse-jdt-classpath.patch --- tomcat11-11.0.15/debian/patches/0030-eclipse-jdt-classpath.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0030-eclipse-jdt-classpath.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,9 +1,15 @@ -Description: Updates the Eclipse Compiler classpath -Author: Emmanuel Bourg +From: Emmanuel Bourg +Date: Tue, 3 Feb 2026 12:31:37 +0100 +Subject: Updates the Eclipse Compiler classpath + Forwarded: no +--- + build.xml | 1 + + 1 file changed, 1 insertion(+) + --- a/build.xml +++ b/build.xml -@@ -233,6 +233,7 @@ +@@ -237,6 +237,7 @@ diff -Nru tomcat11-11.0.15/debian/patches/0031-eclipse-jdt-compatibility.patch tomcat11-11.0.22/debian/patches/0031-eclipse-jdt-compatibility.patch --- tomcat11-11.0.15/debian/patches/0031-eclipse-jdt-compatibility.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/0031-eclipse-jdt-compatibility.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,9 +1,17 @@ -Description: Fixes the compatibility with the version of the Eclipse compiler in Debian -Author: Emmanuel Bourg +From: Emmanuel Bourg +Date: Tue, 3 Feb 2026 12:31:56 +0100 +Subject: 0031-eclipse-jdt-compatibility + +Fixes the compatibility with the version of the Eclipse compiler in Debian + Forwarded: not-needed +--- + java/org/apache/jasper/compiler/JDTCompiler.java | 30 ++++++++++++------------ + 1 file changed, 15 insertions(+), 15 deletions(-) + --- a/java/org/apache/jasper/compiler/JDTCompiler.java +++ b/java/org/apache/jasper/compiler/JDTCompiler.java -@@ -326,13 +326,13 @@ +@@ -326,15 +326,15 @@ } else if (opt.equals("20")) { settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_20); } else if (opt.equals("21")) { @@ -19,9 +27,12 @@ - settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_24); + settings.put(CompilerOptions.OPTION_Source, "24"); } else if (opt.equals("25")) { +- settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_25); ++ settings.put(CompilerOptions.OPTION_Source, "25"); + } else if (opt.equals("26")) { // Constant not available in latest ECJ version shipped with // Tomcat. May be supported in a snapshot build. -@@ -409,17 +409,17 @@ +@@ -416,20 +416,20 @@ settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_20); settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_20); } else if (opt.equals("21")) { @@ -45,5 +56,10 @@ + settings.put(CompilerOptions.OPTION_TargetPlatform, "24"); + settings.put(CompilerOptions.OPTION_Compliance, "24"); } else if (opt.equals("25")) { +- settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_25); +- settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_25); ++ settings.put(CompilerOptions.OPTION_TargetPlatform, "25"); ++ settings.put(CompilerOptions.OPTION_Compliance, "25"); + } else if (opt.equals("26")) { // Constant not available in latest ECJ version shipped with // Tomcat. May be supported in a snapshot build. diff -Nru tomcat11-11.0.15/debian/patches/disable-jacoco.patch tomcat11-11.0.22/debian/patches/disable-jacoco.patch --- tomcat11-11.0.15/debian/patches/disable-jacoco.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/disable-jacoco.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,9 +1,17 @@ -Description: Disable jacoco test coverage -Author: Markus Koschany +From: Markus Koschany +Date: Tue, 3 Feb 2026 12:32:45 +0100 +Subject: disable jacoco + +Disable jacoco test coverage. + Forwarded: not-needed +--- + build.xml | 12 ------------ + 1 file changed, 12 deletions(-) + --- a/build.xml +++ b/build.xml -@@ -2054,10 +2054,6 @@ +@@ -2235,10 +2235,6 @@ @@ -11,10 +19,10 @@ - enabled="${test.coverage}" - destfile="${coverage.datafile}" - > - @@ -22,7 +30,7 @@ -@@ -3653,15 +3648,8 @@ +@@ -3868,15 +3863,8 @@ diff -Nru tomcat11-11.0.15/debian/patches/exclude-TestJNDIRealmIntegration.patch tomcat11-11.0.22/debian/patches/exclude-TestJNDIRealmIntegration.patch --- tomcat11-11.0.15/debian/patches/exclude-TestJNDIRealmIntegration.patch 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/exclude-TestJNDIRealmIntegration.patch 2026-06-04 11:23:48.000000000 +0000 @@ -1,18 +1,26 @@ -Description: Exclude TestJNDIRealmIntegration because it fails to build due to missing dependencies -Author: Markus Koschany +From: Markus Koschany +Date: Tue, 3 Feb 2026 12:33:51 +0100 +Subject: exclude-TestJNDIRealmIntegration + +Exclude TestJNDIRealmIntegration because it fails to build due to missing dependencies + Forwarded: not-needed +--- + build.xml | 2 ++ + 1 file changed, 2 insertions(+) + --- a/build.xml +++ b/build.xml -@@ -1930,6 +1930,7 @@ +@@ -1948,6 +1948,7 @@ - + +
-@@ -2120,6 +2121,7 @@ - +@@ -2301,6 +2302,7 @@ + + diff -Nru tomcat11-11.0.15/debian/patches/series tomcat11-11.0.22/debian/patches/series --- tomcat11-11.0.15/debian/patches/series 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/patches/series 2026-06-04 11:23:48.000000000 +0000 @@ -9,7 +9,7 @@ 0025-invalid-configuration-exit-status.patch 0026-easymock4-compatibility.patch 0021-dont-test-unsupported-ciphers.patch -exclude-TestJNDIRealmIntegration.patch -disable-jacoco.patch 0030-eclipse-jdt-classpath.patch 0031-eclipse-jdt-compatibility.patch +disable-jacoco.patch +exclude-TestJNDIRealmIntegration.patch diff -Nru tomcat11-11.0.15/debian/tomcat11-common.links tomcat11-11.0.22/debian/tomcat11-common.links --- tomcat11-11.0.15/debian/tomcat11-common.links 2026-02-03 11:58:13.000000000 +0000 +++ tomcat11-11.0.22/debian/tomcat11-common.links 2026-06-04 11:23:48.000000000 +0000 @@ -25,3 +25,5 @@ /usr/share/java/tomcat11-websocket-api.jar /usr/share/tomcat11/lib/websocket-api.jar /usr/share/java/tomcat11-websocket-client-api.jar /usr/share/tomcat11/lib/websocket-client-api.jar /usr/share/java/tomcat11-websocket.jar /usr/share/tomcat11/lib/tomcat-websocket.jar + +/usr/share/tomcat-jakartaee-migration/lib/jakartaee-migration-shaded.jar /usr/share/tomcat11/lib/jakartaee-migration-shaded.jar diff -Nru tomcat11-11.0.15/java/jakarta/el/ImportHandler.java tomcat11-11.0.22/java/jakarta/el/ImportHandler.java --- tomcat11-11.0.15/java/jakarta/el/ImportHandler.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/jakarta/el/ImportHandler.java 2026-05-01 18:56:05.000000000 +0000 @@ -138,7 +138,7 @@ standardPackages.put("jakarta.servlet.jsp", servletJspClassNames); Set javaLangClassNames = new HashSet<>(); - // Based on Java 25 EA16 + // Based on Java 26 EA26 // Interfaces javaLangClassNames.add("Appendable"); javaLangClassNames.add("AutoCloseable"); @@ -146,6 +146,7 @@ javaLangClassNames.add("Cloneable"); javaLangClassNames.add("Comparable"); javaLangClassNames.add("Iterable"); + javaLangClassNames.add("LazyConstant"); javaLangClassNames.add("ProcessHandle"); javaLangClassNames.add("ProcessHandle.Info"); javaLangClassNames.add("Readable"); diff -Nru tomcat11-11.0.15/java/org/apache/catalina/AccessLog.java tomcat11-11.0.22/java/org/apache/catalina/AccessLog.java --- tomcat11-11.0.15/java/org/apache/catalina/AccessLog.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/AccessLog.java 2026-05-01 18:56:05.000000000 +0000 @@ -82,6 +82,8 @@ void setRequestAttributesEnabled(boolean requestAttributesEnabled); /** + * Checks if request attributes will be logged. + * * @see #setRequestAttributesEnabled(boolean) * * @return true if the attributes will be logged, otherwise false diff -Nru tomcat11-11.0.15/java/org/apache/catalina/Context.java tomcat11-11.0.22/java/org/apache/catalina/Context.java --- tomcat11-11.0.15/java/org/apache/catalina/Context.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/Context.java 2026-05-01 18:56:05.000000000 +0000 @@ -60,7 +60,6 @@ *

* The child containers attached to a Context are generally implementations of Wrapper (representing individual servlet * definitions). - *

*/ public interface Context extends Container, ContextBind { diff -Nru tomcat11-11.0.15/java/org/apache/catalina/JmxEnabled.java tomcat11-11.0.22/java/org/apache/catalina/JmxEnabled.java --- tomcat11-11.0.15/java/org/apache/catalina/JmxEnabled.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/JmxEnabled.java 2026-05-01 18:56:05.000000000 +0000 @@ -27,6 +27,8 @@ public interface JmxEnabled extends MBeanRegistration { /** + * Returns the domain under which this component will be / has been registered. + * * @return the domain under which this component will be / has been registered. */ String getDomain(); @@ -42,6 +44,8 @@ /** + * Returns the name under which this component has been registered with JMX. + * * @return the name under which this component has been registered with JMX. */ ObjectName getObjectName(); diff -Nru tomcat11-11.0.15/java/org/apache/catalina/WebResource.java tomcat11-11.0.22/java/org/apache/catalina/WebResource.java --- tomcat11-11.0.15/java/org/apache/catalina/WebResource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/WebResource.java 2026-05-01 18:56:05.000000000 +0000 @@ -26,17 +26,23 @@ */ public interface WebResource { /** + * Returns the last modified time. + * * @return {@link java.io.File#lastModified()}. */ long getLastModified(); /** + * Returns the last modified time in HTTP format. + * * @return the last modified time of this resource in the correct format for the HTTP Last-Modified header as * specified by RFC 2616. */ String getLastModifiedHttp(); /** + * Checks if this resource exists. + * * @return {@link java.io.File#exists()}. */ boolean exists(); @@ -51,48 +57,64 @@ boolean isVirtual(); /** + * Checks if this resource is a directory. + * * @return {@link java.io.File#isDirectory()}. */ boolean isDirectory(); /** + * Checks if this resource is a file. + * * @return {@link java.io.File#isFile()}. */ boolean isFile(); /** + * Deletes this resource. + * * @return {@link java.io.File#delete()}. */ boolean delete(); /** + * Returns the name of this resource. + * * @return {@link java.io.File#getName()}. */ String getName(); /** + * Returns the content length of this resource. + * * @return {@link java.io.File#length()}. */ long getContentLength(); /** + * Returns the canonical path of this resource. + * * @return {@link java.io.File#getCanonicalPath()}. */ String getCanonicalPath(); /** + * Checks if this resource can be read. + * * @return {@link java.io.File#canRead()}. */ boolean canRead(); /** + * Returns the webapp path of this resource. + * * @return The path of this resource relative to the web application root. If the resource is a directory, the * return value will end in '/'. */ String getWebappPath(); /** - * Return the weak ETag calculated from the content length and last modified. + * Returns the weak ETag calculated from the content length and last modified. * * @return The ETag for this resource */ @@ -116,12 +138,14 @@ void setMimeType(String mimeType); /** + * Returns the MIME type for this Resource. + * * @return the MIME type for this Resource. */ String getMimeType(); /** - * Obtain an InputStream based on the contents of this resource. + * Obtains an InputStream based on the contents of this resource. * * @return An InputStream based on the contents of this resource or null if the resource does not exist * or does not represent a file @@ -129,18 +153,24 @@ InputStream getInputStream(); /** + * Returns the binary content of this resource. + * * @return the binary content of this resource or {@code null} if it is not available in a byte[] because, for * example, it is too big. */ byte[] getContent(); /** + * Returns the creation time of this resource. + * * @return The time the file was created. If not available, the result of {@link #getLastModified()} will be * returned. */ long getCreation(); /** + * Returns a URL to access this resource. + * * @return a URL to access the resource or null if no such URL is available or if the resource does not * exist. */ @@ -160,11 +190,15 @@ } /** + * Returns a reference to the WebResourceRoot of which this WebResource is a part. + * * @return a reference to the WebResourceRoot of which this WebResource is a part. */ WebResourceRoot getWebResourceRoot(); /** + * Returns the certificates that were used to sign this resource to verify it. + * * @return the certificates that were used to sign this resource to verify it or @null if none. * * @see java.util.jar.JarEntry#getCertificates() @@ -172,6 +206,8 @@ Certificate[] getCertificates(); /** + * Returns the manifest associated with this resource. + * * @return the manifest associated with this resource or @null if none. * * @see java.util.jar.JarFile#getManifest() diff -Nru tomcat11-11.0.15/java/org/apache/catalina/WebResourceRoot.java tomcat11-11.0.22/java/org/apache/catalina/WebResourceRoot.java --- tomcat11-11.0.15/java/org/apache/catalina/WebResourceRoot.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/WebResourceRoot.java 2026-05-01 18:56:05.000000000 +0000 @@ -207,6 +207,8 @@ void addPreResources(WebResourceSet webResourceSet); /** + * Returns the array of WebResourceSet configured to this web application as a 'Pre' resource. + * * @return the array of WebResourceSet configured to this web application as a 'Pre' resource. */ WebResourceSet[] getPreResources(); @@ -219,6 +221,8 @@ void addJarResources(WebResourceSet webResourceSet); /** + * Returns the array of WebResourceSet configured to this web application as a 'Jar' resource. + * * @return the array of WebResourceSet configured to this web application as a 'Jar' resource. */ WebResourceSet[] getJarResources(); @@ -231,11 +235,15 @@ void addPostResources(WebResourceSet webResourceSet); /** + * Returns the array of WebResourceSet configured to this web application as a 'Post' resource. + * * @return the array of WebResourceSet configured to this web application as a 'Post' resource. */ WebResourceSet[] getPostResources(); /** + * Returns the web application this WebResourceRoot is associated with. + * * @return the web application this WebResourceRoot is associated with. */ Context getContext(); @@ -271,6 +279,8 @@ void setCachingAllowed(boolean cachingAllowed); /** + * Checks if caching is permitted for this web application. + * * @return true if caching is permitted for this web application. */ boolean isCachingAllowed(); @@ -428,6 +438,8 @@ } /** + * Checks if the main resources are read only. + * * @return {@code true} if the main resources are read only, otherwise {@code false}. The default implementation * returns {@code false}. */ diff -Nru tomcat11-11.0.15/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java tomcat11-11.0.22/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java --- tomcat11-11.0.15/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java 2026-05-01 18:56:05.000000000 +0000 @@ -21,6 +21,9 @@ import org.apache.tools.ant.BuildException; +/** + * Abstract task for Catalina commands. + */ public abstract class AbstractCatalinaCommandTask extends AbstractCatalinaTask { /** @@ -28,10 +31,20 @@ */ protected String path = null; + /** + * Returns the path. + * + * @return the path + */ public String getPath() { return this.path; } + /** + * Sets the path. + * + * @param path The path to set + */ public void setPath(String path) { this.path = path; } @@ -41,10 +54,20 @@ */ protected String version = null; + /** + * Returns the version. + * + * @return the version + */ public String getVersion() { return this.version; } + /** + * Sets the version. + * + * @param version The version to set + */ public void setVersion(String version) { this.version = version; } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/authenticator/AuthenticatorBase.java tomcat11-11.0.22/java/org/apache/catalina/authenticator/AuthenticatorBase.java --- tomcat11-11.0.15/java/org/apache/catalina/authenticator/AuthenticatorBase.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/authenticator/AuthenticatorBase.java 2026-05-01 18:56:05.000000000 +0000 @@ -210,7 +210,6 @@ * {@code remote-user} and {@code auth-type} to a reverse proxy. This is useful, e.g., for access log consistency or * other decisions to make. */ - protected boolean sendAuthInfoResponseHeaders = false; protected SessionIdGeneratorBase sessionIdGenerator = null; @@ -220,6 +219,8 @@ */ protected SingleSignOn sso = null; + private SsoReauthenticationMode ssoReauthenticationMode = SsoReauthenticationMode.DEFAULT; + private AllowCorsPreflight allowCorsPreflight = AllowCorsPreflight.NEVER; private volatile String jaspicAppContextID = null; @@ -229,6 +230,15 @@ // ------------------------------------------------------------- Properties + public String getSsoReauthenticationMode() { + return ssoReauthenticationMode.name().toLowerCase(Locale.ENGLISH); + } + + public void setSsoReauthenticationMode(String ssoReauthenticationMode) { + this.ssoReauthenticationMode = + SsoReauthenticationMode.valueOf(ssoReauthenticationMode.trim().toUpperCase(Locale.ENGLISH)); + } + public String getAllowCorsPreflight() { return allowCorsPreflight.name().toLowerCase(Locale.ENGLISH); } @@ -519,9 +529,11 @@ if (constraints != null) { hasAuthConstraint = true; for (int i = 0; i < constraints.length && hasAuthConstraint; i++) { - if (!constraints[i].getAuthConstraint()) { + if (constraints[i].getAllRoles() || constraints[i].getAuthenticatedUsers()) { + // NO-OP - has hasAuthConstraint + } else if (!constraints[i].getAuthConstraint()) { hasAuthConstraint = false; - } else if (!constraints[i].getAllRoles() && !constraints[i].getAuthenticatedUsers()) { + } else { String[] roles = constraints[i].findAuthRoles(); if (roles == null || roles.length == 0) { hasAuthConstraint = false; @@ -878,40 +890,87 @@ * Check to see if the user has already been authenticated earlier in the processing chain or if there is enough * information available to authenticate the user without requiring further user interaction. * - * @param request The current request - * @param response The current response - * @param useSSO Should information available from SSO be used to attempt to authenticate the current user? + * @param request The current request + * @param response The current response + * @param useSsoCachedUserAndPassword Should the user and password available from SSO be used to attempt to + * authenticate the current user? * * @return true if the user was authenticated via the cache, otherwise false */ - protected boolean checkForCachedAuthentication(Request request, HttpServletResponse response, boolean useSSO) { + protected boolean checkForCachedAuthentication(Request request, HttpServletResponse response, + boolean useSsoCachedUserAndPassword) { - // Has the user already been authenticated? - Principal principal = request.getUserPrincipal(); + /* + * There are two methods for authentication caching implemented by the SSO Valve. The first caches the + * authenticated Principal returned by the Realm. The second caches the user name and password passed to the + * Realm that were used for authentication. + * + * If cached authentication is not available or fails for any reason, the Authenticator will attempt the normal + * authentication process for the Authenticator. + * + * Which cached authentication methods are used depends on the configuration of the SSO Valve and/or the + * Authenticator. + * + * If the SSO Valve is configured to require re-authentication, any cached Principal will not be used unless the + * Authenticator is explicitly configured (via ssoReauthenticationMode) to use it. + * + * If the SSO Valve is configured to require re-authentication, whether the cached user name and password can be + * used will be determined by the calling Authenticator type unless the Authenticator's ssoReauthenticationMode + * is explicitly configured. + */ + + // Determine which - if any - checks for cached authentication will be made. + boolean checkPrincipal = false; + boolean checkPassword = false; + + // Will be null if SSO is not configured or there is no current SSO session String ssoId = (String) request.getNote(Constants.REQ_SSOID_NOTE); - if (principal != null) { - if (log.isDebugEnabled()) { - log.debug(sm.getString("authenticator.check.found", principal.getName())); - } - // Associate the session with any existing SSO session. Even if - // useSSO is false, this will ensure coordinated session - // invalidation at log out. + + if (sso == null) { + // There is no SSO - check in case some other component has set the Principal + checkPrincipal = true; + } else if (ssoReauthenticationMode == SsoReauthenticationMode.DEFAULT && !sso.getRequireReauthentication() || + ssoReauthenticationMode == SsoReauthenticationMode.PRINCIPAL) { + checkPrincipal = true; + // If checkPrincipal is enabled then checkPassword is enabled if there is an SSO session if (ssoId != null) { - associate(ssoId, request.getSessionInternal(true)); + checkPassword = true; } - return true; + } else if (ssoId != null && (ssoReauthenticationMode == SsoReauthenticationMode.PASSWORD || + sso.getRequireReauthentication() && useSsoCachedUserAndPassword)) { + checkPassword = true; } - // Is there an SSO session against which we can try to reauthenticate? - if (useSSO && ssoId != null) { + // Check for a cached Principal. Most likely from SSO but could be another component. + if (checkPrincipal) { + if (ssoId != null && sso != null && sso.getRequireReauthentication()) { + // There is a valid SSO session but SSO Valve won't have cached the Principal. + sso.populateRequestFromSsoEntry(request, ssoId); + } + + // Has the user already been authenticated? + Principal principal = request.getUserPrincipal(); + if (principal != null) { + if (log.isDebugEnabled()) { + log.debug(sm.getString("authenticator.check.found", principal.getName())); + } + // Associate the session with any existing SSO session. Even if + // useSSO is false, this will ensure coordinated session + // invalidation at log out. + if (ssoId != null) { + associate(ssoId, request.getSessionInternal(true)); + } + return true; + } + } + + // Check for a user and password cached by SSO + if (checkPassword) { if (log.isDebugEnabled()) { log.debug(sm.getString("authenticator.check.sso", ssoId)); } /* - * Try to reauthenticate using data cached by SSO. If this fails, either the original SSO logon was of - * DIGEST or SSL (which we can't reauthenticate ourselves because there is no cached username and password), - * or the realm denied the user's reauthentication for some reason. In either case we have to prompt the - * user for a logon + * Try to reauthenticate using data cached by SSO. If this fails we have to prompt the user for credentials. */ if (reauthenticateFromSSO(ssoId, request)) { return true; @@ -1294,4 +1353,12 @@ FILTER, ALWAYS } + + + protected enum SsoReauthenticationMode { + DEFAULT, + PRINCIPAL, + PASSWORD, + FULL + } } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/authenticator/DigestAuthenticator.java tomcat11-11.0.22/java/org/apache/catalina/authenticator/DigestAuthenticator.java --- tomcat11-11.0.15/java/org/apache/catalina/authenticator/DigestAuthenticator.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/authenticator/DigestAuthenticator.java 2026-05-01 18:56:05.000000000 +0000 @@ -43,6 +43,7 @@ import org.apache.tomcat.util.buf.StringUtils; import org.apache.tomcat.util.http.parser.Authorization; import org.apache.tomcat.util.security.ConcurrentMessageDigest; +import org.apache.tomcat.util.security.ConstantTime; /** @@ -273,8 +274,6 @@ * authentication. Reauthenticating with the cached user name and password should be sufficient for DIGEST in * that scenario. However, the original behaviour to reauthenticate has been retained in case of any (very * unlikely) backwards compatibility issues. - * - * TODO: Make the reauthentication behaviour configurable per authenticator. */ if (checkForCachedAuthentication(request, response, false)) { return true; @@ -602,7 +601,7 @@ byte[] buffer = ConcurrentMessageDigest.digest(NONCE_DIGEST, serverIpTimeKey.getBytes(StandardCharsets.ISO_8859_1)); String digestServerIpTimeKey = HexUtils.toHexString(buffer); - if (!digestServerIpTimeKey.equals(digestclientIpTimeKey)) { + if (!ConstantTime.equals(digestServerIpTimeKey, digestclientIpTimeKey, true)) { return false; } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/authenticator/FormAuthenticator.java tomcat11-11.0.22/java/org/apache/catalina/authenticator/FormAuthenticator.java --- tomcat11-11.0.15/java/org/apache/catalina/authenticator/FormAuthenticator.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/authenticator/FormAuthenticator.java 2026-05-01 18:56:05.000000000 +0000 @@ -357,15 +357,7 @@ // a resource is protected for some HTTP methods but not protected for // GET which is used after authentication when redirecting to the // protected resource. - // TODO: This is similar to the FormAuthenticator.matchRequest() logic - // Is there a way to remove the duplication? - Session session = request.getSessionInternal(false); - if (session != null) { - SavedRequest savedRequest = (SavedRequest) session.getNote(Constants.FORM_REQUEST_NOTE); - return savedRequest != null && decodedRequestURI.equals(savedRequest.getDecodedRequestURI()); - } - - return false; + return matchRequest(request, false); } @@ -499,15 +491,21 @@ } + protected boolean matchRequest(Request request) { + return matchRequest(request, true); + } + /** * Does this request match the saved one (so that it must be the redirect we signaled after successful * authentication?) * * @param request The request to be verified + * @param strict true to check for a valid Principal and valid Session ID, false to only + * check for a valid saved request and matching URI * * @return true if the requests matched the saved one */ - protected boolean matchRequest(Request request) { + protected boolean matchRequest(Request request, boolean strict) { // Has a session been created? Session session = request.getSessionInternal(false); if (session == null) { @@ -520,17 +518,19 @@ return false; } - // Is there a saved principal? - if (cache && session.getPrincipal() == null || !cache && request.getPrincipal() == null) { - return false; - } - - // Does session id match? - if (getChangeSessionIdOnAuthentication()) { - String expectedSessionId = (String) session.getNote(Constants.SESSION_ID_NOTE); - if (expectedSessionId == null || !expectedSessionId.equals(request.getRequestedSessionId())) { + if (strict) { + // Is there a saved principal? + if (cache && session.getPrincipal() == null || !cache && request.getPrincipal() == null) { return false; } + + // Does session id match? + if (getChangeSessionIdOnAuthentication()) { + String expectedSessionId = (String) session.getNote(Constants.SESSION_ID_NOTE); + if (expectedSessionId == null || !expectedSessionId.equals(request.getRequestedSessionId())) { + return false; + } + } } // Does the request URI match? diff -Nru tomcat11-11.0.15/java/org/apache/catalina/authenticator/SSLAuthenticator.java tomcat11-11.0.22/java/org/apache/catalina/authenticator/SSLAuthenticator.java --- tomcat11-11.0.15/java/org/apache/catalina/authenticator/SSLAuthenticator.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/authenticator/SSLAuthenticator.java 2026-05-01 18:56:05.000000000 +0000 @@ -69,8 +69,6 @@ * since it will not make any TLS information (client certificate etc) available that a web application may * depend on. Therefore, the reauthentication behaviour for CLIENT-CERT is to perform a normal CLIENT-CERT * authentication. - * - * TODO: Make the reauthentication behaviour configurable per authenticator. */ if (checkForCachedAuthentication(request, response, false)) { return true; diff -Nru tomcat11-11.0.15/java/org/apache/catalina/authenticator/SingleSignOn.java tomcat11-11.0.22/java/org/apache/catalina/authenticator/SingleSignOn.java --- tomcat11-11.0.15/java/org/apache/catalina/authenticator/SingleSignOn.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/authenticator/SingleSignOn.java 2026-05-01 18:56:05.000000000 +0000 @@ -506,6 +506,15 @@ } + protected void populateRequestFromSsoEntry(Request request, String ssoId) { + SingleSignOnEntry entry = cache.get(ssoId); + if (entry != null) { + request.setAuthType(entry.getAuthType()); + request.setUserPrincipal(entry.getPrincipal()); + } + } + + /** * Register the specified Principal as being associated with the specified value for the single sign on identifier. * diff -Nru tomcat11-11.0.15/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java tomcat11-11.0.22/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java --- tomcat11-11.0.15/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java 2026-05-01 18:56:05.000000000 +0000 @@ -20,7 +20,6 @@ import java.io.IOException; import java.security.Principal; import java.util.Base64; -import java.util.LinkedHashMap; import java.util.concurrent.CompletionException; import java.util.regex.Pattern; @@ -93,14 +92,29 @@ } } - private boolean applyJava8u40Fix = true; - + /** + * This attribute is now hard-coded to {@code false} as the work-around this attribute enabled is no longer + * required. + * + * @return Always {@code false} + * + * @deprecated This method will be removed from Tomcat 12 onwards. + */ + @Deprecated public boolean getApplyJava8u40Fix() { - return applyJava8u40Fix; + return false; } + /** + * This method is now a NO-OP as the work-around this attribute enabled is no longer required. + * + * @param applyJava8u40Fix Ignored + * + * @deprecated This method will be removed from Tomcat 12 onwards. + */ + @Deprecated public void setApplyJava8u40Fix(boolean applyJava8u40Fix) { - this.applyJava8u40Fix = applyJava8u40Fix; + // NO-OP } @@ -144,8 +158,6 @@ * Reauthenticating with the cached user name and password may not be sufficient for SPNEGO since it will not * make the delegated credentials available that a web application may depend on. Therefore, the * reauthentication behaviour for SPNEGO is to perform a normal SPNEGO authentication. - * - * TODO: Make the reauthentication behaviour configurable per authenticator. */ if (checkForCachedAuthentication(request, response, false)) { return true; @@ -181,10 +193,6 @@ authorizationBC.getLength()); byte[] decoded = Base64.getDecoder().decode(encoded); - if (getApplyJava8u40Fix()) { - SpnegoTokenFixer.fix(decoded); - } - if (decoded.length == 0) { if (log.isDebugEnabled()) { log.debug(sm.getString("spnegoAuthenticator.authHeaderNoToken")); @@ -306,168 +314,4 @@ MessageBytes authorizationHeader = request.getCoyoteRequest().getMimeHeaders().getValue("authorization"); return authorizationHeader != null && authorizationHeader.startsWithIgnoreCase("negotiate ", 0); } - - - /** - * This class implements a hack around an incompatibility between the SPNEGO implementation in Windows and the - * SPNEGO implementation in Java 8 update 40 onwards. It was introduced by the change to fix this bug: - * JDK-8048194 (note: the change applied is not the - * one suggested in the bug report) - *

- * It is not clear to me if Windows, Java or Tomcat is at fault here. I think it is Java, but I could be wrong. - *

- * This hack works by re-ordering the list of mechTypes in the NegTokenInit token. - */ - public static class SpnegoTokenFixer { - - public static void fix(byte[] token) { - SpnegoTokenFixer fixer = new SpnegoTokenFixer(token); - fixer.fix(); - } - - - private final byte[] token; - private int pos = 0; - - - private SpnegoTokenFixer(byte[] token) { - this.token = token; - } - - - // Fixes the token in-place - private void fix() { - /* - * Useful references: http://tools.ietf.org/html/rfc4121#page-5 http://tools.ietf.org/html/rfc2743#page-81 - * https://msdn.microsoft.com/en-us/library/ms995330.aspx - */ - - // Scan until we find the mech types list. If we find anything - // unexpected, abort the fix process. - if (!tag(0x60)) { - return; - } - if (!length()) { - return; - } - if (!oid("1.3.6.1.5.5.2")) { - return; - } - if (!tag(0xa0)) { - return; - } - if (!length()) { - return; - } - if (!tag(0x30)) { - return; - } - if (!length()) { - return; - } - if (!tag(0xa0)) { - return; - } - lengthAsInt(); - if (!tag(0x30)) { - return; - } - // Now at the start of the mechType list. - // Read the mechTypes into an ordered set - int mechTypesLen = lengthAsInt(); - int mechTypesStart = pos; - LinkedHashMap mechTypeEntries = new LinkedHashMap<>(); - while (pos < mechTypesStart + mechTypesLen) { - int[] value = new int[2]; - value[0] = pos; - String key = oidAsString(); - value[1] = pos - value[0]; - mechTypeEntries.put(key, value); - } - // Now construct the re-ordered mechType list - byte[] replacement = new byte[mechTypesLen]; - int replacementPos = 0; - - int[] first = mechTypeEntries.remove("1.2.840.113554.1.2.2"); - if (first != null) { - System.arraycopy(token, first[0], replacement, replacementPos, first[1]); - replacementPos += first[1]; - } - for (int[] markers : mechTypeEntries.values()) { - System.arraycopy(token, markers[0], replacement, replacementPos, markers[1]); - replacementPos += markers[1]; - } - - // Finally, replace the original mechType list with the re-ordered - // one. - System.arraycopy(replacement, 0, token, mechTypesStart, mechTypesLen); - } - - - private boolean tag(int expected) { - return (token[pos++] & 0xFF) == expected; - } - - - private boolean length() { - // No need to retain the length - just need to consume it and make - // sure it is valid. - int len = lengthAsInt(); - return pos + len == token.length; - } - - - private int lengthAsInt() { - int len = token[pos++] & 0xFF; - if (len > 127) { - int bytes = len - 128; - len = 0; - for (int i = 0; i < bytes; i++) { - len = len << 8; - len = len + (token[pos++] & 0xff); - } - } - return len; - } - - - private boolean oid(String expected) { - return expected.equals(oidAsString()); - } - - - private String oidAsString() { - if (!tag(0x06)) { - return null; - } - StringBuilder result = new StringBuilder(); - int len = lengthAsInt(); - // First byte is special case - int v = token[pos++] & 0xFF; - int c2 = v % 40; - int c1 = (v - c2) / 40; - result.append(c1); - result.append('.'); - result.append(c2); - int c = 0; - boolean write = false; - for (int i = 1; i < len; i++) { - int b = token[pos++] & 0xFF; - if (b > 127) { - b -= 128; - } else { - write = true; - } - c = c << 7; - c += b; - if (write) { - result.append('.'); - result.append(c); - c = 0; - write = false; - } - } - return result.toString(); - } - } } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/connector/CoyoteAdapter.java tomcat11-11.0.22/java/org/apache/catalina/connector/CoyoteAdapter.java --- tomcat11-11.0.15/java/org/apache/catalina/connector/CoyoteAdapter.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/connector/CoyoteAdapter.java 2026-05-01 18:56:05.000000000 +0000 @@ -555,9 +555,10 @@ protected boolean postParseRequest(org.apache.coyote.Request req, Request request, org.apache.coyote.Response res, Response response) throws IOException, ServletException { - // If the processor has set the scheme (AJP does this, HTTP does this if - // SSL is enabled) use this to set the secure flag as well. If the - // processor hasn't set it, use the settings from the connector + /* + * If the processor has set the scheme (AJP and HTTP/2 do this, HTTP/1.x does this if SSL is enabled), use this + * to set the secure flag as well. If the processor hasn't set it, use the settings from the connector. + */ if (req.scheme().isNull()) { // Use connector scheme and secure configuration, (defaults to // "http" and false respectively) @@ -1109,6 +1110,8 @@ */ public static boolean normalize(MessageBytes uriMB, boolean allowBackslash) { + // Keep behaviour aligned with RequestUtil.normalize() + ByteChunk uriBC = uriMB.getByteChunk(); final byte[] b = uriBC.getBytes(); final int start = uriBC.getStart(); @@ -1283,7 +1286,7 @@ byte b2 = bytes[pos + 2]; pos += 3; int decoded = (HexUtils.getDec(b1) << 4) + HexUtils.getDec(b2); - if (decoded < 20 || decoded == 0x7F || decoded == 0x2F) { + if (decoded < 0x20 || decoded == 0x7F || decoded == 0x2F) { return true; } } else { diff -Nru tomcat11-11.0.15/java/org/apache/catalina/connector/Request.java tomcat11-11.0.22/java/org/apache/catalina/connector/Request.java --- tomcat11-11.0.15/java/org/apache/catalina/connector/Request.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/connector/Request.java 2026-05-01 18:56:05.000000000 +0000 @@ -2984,7 +2984,8 @@ return; } parameters.processParameters(formData, 0, len); - } else if ("chunked".equalsIgnoreCase(coyoteRequest.getHeader("transfer-encoding"))) { + } else if (coyoteRequest.protocol().equals("HTTP/2.0") + || "chunked".equalsIgnoreCase(coyoteRequest.getHeader("transfer-encoding"))) { byte[] formData = null; try { formData = readChunkedPostBody(); diff -Nru tomcat11-11.0.15/java/org/apache/catalina/connector/Response.java tomcat11-11.0.22/java/org/apache/catalina/connector/Response.java --- tomcat11-11.0.15/java/org/apache/catalina/connector/Response.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/connector/Response.java 2026-05-01 18:56:05.000000000 +0000 @@ -749,6 +749,23 @@ @Override public String getHeader(String name) { + // Need special handling for Content-Type and Content-Length due to + // special handling of these in coyoteResponse + char cc = name.charAt(0); + if (cc == 'C' || cc == 'c') { + if (name.equalsIgnoreCase("Content-Type")) { + // Will return null if this has not been set + return getCoyoteResponse().getContentType(); + } + if (name.equalsIgnoreCase("Content-Length")) { + // -1 means not known and is not sent to client + if (getCoyoteResponse().getContentLengthLong() != -1) { + return String.valueOf(getCoyoteResponse().getContentLengthLong()); + } else { + return null; + } + } + } return getCoyoteResponse().getMimeHeaders().getHeader(name); } @@ -761,13 +778,42 @@ for (int i = 0; i < n; i++) { result.add(headers.getName(i).toString()); } + if (!getCoyoteResponse().isCommitted()) { + if (getCoyoteResponse().getContentType() != null) { + result.add("Content-Type"); + } + if (getCoyoteResponse().getContentLengthLong() != -1) { + result.add("Content-Length"); + } + } return result; - } @Override public Collection getHeaders(String name) { + // Need special handling for Content-Type and Content-Length due to + // special handling of these in coyoteResponse + char cc = name.charAt(0); + if (cc == 'C' || cc == 'c') { + if (name.equalsIgnoreCase("Content-Type")) { + // Will return null if this has not been set + String contentType = getCoyoteResponse().getContentType(); + if (contentType != null) { + return Set.of(contentType); + } else { + return Set.of(); + } + } + if (name.equalsIgnoreCase("Content-Length")) { + // -1 means not known and is not sent to client + if (getCoyoteResponse().getContentLengthLong() != -1) { + return Set.of(String.valueOf(getCoyoteResponse().getContentLengthLong())); + } else { + return Set.of(); + } + } + } Enumeration enumeration = getCoyoteResponse().getMimeHeaders().values(name); Set result = new LinkedHashSet<>(); while (enumeration.hasMoreElements()) { diff -Nru tomcat11-11.0.15/java/org/apache/catalina/core/AprLifecycleListener.java tomcat11-11.0.22/java/org/apache/catalina/core/AprLifecycleListener.java --- tomcat11-11.0.15/java/org/apache/catalina/core/AprLifecycleListener.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/core/AprLifecycleListener.java 2026-05-01 18:56:05.000000000 +0000 @@ -64,12 +64,15 @@ // ---------------------------------------------- Constants - protected static final int TCN_REQUIRED_MAJOR = 1; - protected static final int TCN_REQUIRED_MINOR = 2; - protected static final int TCN_REQUIRED_PATCH = 34; + private static final int TCN_1_REQUIRED_MINOR = 3; + private static final int TCN_1_REQUIRED_PATCH = 4; + + protected static final int TCN_REQUIRED_MAJOR = 2; + protected static final int TCN_REQUIRED_MINOR = 0; + protected static final int TCN_REQUIRED_PATCH = 12; protected static final int TCN_RECOMMENDED_MAJOR = 2; protected static final int TCN_RECOMMENDED_MINOR = 0; - protected static final int TCN_RECOMMENDED_PV = 5; + protected static final int TCN_RECOMMENDED_PV = 14; // ---------------------------------------------- Properties @@ -119,6 +122,96 @@ 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); } @@ -209,9 +302,6 @@ } private static void init() { - int rqver = TCN_REQUIRED_MAJOR * 1000 + TCN_REQUIRED_MINOR * 100 + TCN_REQUIRED_PATCH; - int rcver = TCN_RECOMMENDED_MAJOR * 1000 + TCN_RECOMMENDED_MINOR * 100 + TCN_RECOMMENDED_PV; - if (org.apache.tomcat.jni.AprStatus.isAprInitialized()) { return; } @@ -249,9 +339,34 @@ } return; } + + /* + * With parallel development of 1.x and 2.x there are now minimum and recommended versions for both branches. + * + * The minimum required version is increased when the Tomcat Native API is changed (typically extended) to + * include functionality that Tomcat expects to always be present. + * + * The minimum recommended version is increased when there is a change in Tomcat Native that while not required + * is recommended (such as bug fixes). + */ + int rqver; + int rcver; + if (tcnMajor == 1) { + rqver = 1000 + TCN_1_REQUIRED_MINOR * 100 + TCN_1_REQUIRED_PATCH; + rcver = TCN_RECOMMENDED_MAJOR * 1000 + TCN_RECOMMENDED_MINOR * 100 + TCN_RECOMMENDED_PV; + } else { + rqver = TCN_REQUIRED_MAJOR * 1000 + TCN_REQUIRED_MINOR * 100 + TCN_REQUIRED_PATCH; + rcver = TCN_RECOMMENDED_MAJOR * 1000 + TCN_RECOMMENDED_MINOR * 100 + TCN_RECOMMENDED_PV; + } + if (tcnVersion < rqver) { - log.error(sm.getString("aprListener.tcnInvalid", Library.versionString(), - TCN_REQUIRED_MAJOR + "." + TCN_REQUIRED_MINOR + "." + TCN_REQUIRED_PATCH)); + if (tcnMajor == 1) { + log.error(sm.getString("aprListener.tcnInvalid.1", Library.versionString(), + "1." + TCN_1_REQUIRED_MINOR + "." + TCN_1_REQUIRED_PATCH)); + } else { + log.error(sm.getString("aprListener.tcnInvalid", Library.versionString(), + TCN_REQUIRED_MAJOR + "." + TCN_REQUIRED_MINOR + "." + TCN_REQUIRED_PATCH)); + } try { // Terminate the APR in case the version // is below required. diff -Nru tomcat11-11.0.15/java/org/apache/catalina/core/LocalStrings.properties tomcat11-11.0.22/java/org/apache/catalina/core/LocalStrings.properties --- tomcat11-11.0.15/java/org/apache/catalina/core/LocalStrings.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/core/LocalStrings.properties 2026-05-01 18:56:05.000000000 +0000 @@ -87,7 +87,8 @@ aprListener.skipFIPSInitialization=Already in FIPS mode; skipping FIPS initialization. aprListener.sslInit=Failed to initialize the SSLEngine. aprListener.sslRequired=[{0}] is not a valid value for SSLEngine when using version [{1}] of the Tomcat Native library since SSL is required for version 2.x onwards. -aprListener.tcnInvalid=An incompatible version [{0}] of the Apache Tomcat Native library is installed, while Tomcat requires version [{1}] +aprListener.tcnInvalid=An incompatible version [{0}] of the Apache Tomcat Native library is installed, while Tomcat requires at least version [{1}] +aprListener.tcnInvalid.1=An incompatible version [{0}] of the Apache Tomcat Native library is installed, while Tomcat requires at least version [{1}] of Tomcat Native 1.x aprListener.tcnValid=Loaded Apache Tomcat Native library [{0}] using APR version [{1}]. aprListener.tcnVersion=An older version [{0}] of the Apache Tomcat Native library is installed, while Tomcat recommends a minimum version of [{1}] aprListener.tooLateForFIPSMode=Cannot setFIPSMode: SSL has already been initialized diff -Nru tomcat11-11.0.15/java/org/apache/catalina/core/LocalStrings_fr.properties tomcat11-11.0.22/java/org/apache/catalina/core/LocalStrings_fr.properties --- tomcat11-11.0.15/java/org/apache/catalina/core/LocalStrings_fr.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/core/LocalStrings_fr.properties 2026-05-01 18:56:05.000000000 +0000 @@ -88,6 +88,7 @@ aprListener.sslInit=Impossible d'initialiser le SSLEngine aprListener.sslRequired=[{0}] n''est pas une valeur valide pour SSLEngine quand la version [{1}] de la librairie Tomcat Native est utilisée car SSL est nécessaire à partir de la version 2.x aprListener.tcnInvalid=Une version incompatible [{0}] de la librairie Apache Tomcat Native basée sur APR est installée, alors que Tomcat nécessite la version [{1}] +aprListener.tcnInvalid.1=Une version incompatible [{0}] de la librairie Apache Tomcat Native est installée alors que Tomcat requiert au moins la version [{1}] de Tomcat Native 1.x aprListener.tcnValid=Chargement de la librairie Apache Tomcat Native [{0}] en utilisant APR version [{1}] aprListener.tcnVersion=Un version ancienne [{0}] de la bibliothèque Apache Tomcat Native basée sur APR est installée, alors que Tomcat recommande au minimum la version [{1}] aprListener.tooLateForFIPSMode=Ne peut pas passer en mode FIPS, SSL a déjà été initialisé @@ -269,7 +270,9 @@ standardHost.notContext=Le fils d'un hôte (child of a Host) doit être un contexte standardHost.nullName=Le nom d'hôte est requis standardHost.problematicAppBase=Utiliser une chaîne vide pour l''appBase de l''hôte [{0}] la fera correspondre à CATALINA_BASE, ce qui causera des problèmes +standardHost.problematicAppBaseParent=appBase de l''hôte [{0}] est un parent du répertoire CATALINA_BASE, ce qui est mauvais standardHost.problematicLegacyAppBase=L''utilisation d''une chaîne vide pour legacyAppBase de l''hôte [{0}] le fixera à CATALINA_BASE, ce qui n''est pas judicieux +standardHost.problematicLegacyAppBaseParent=legacyAppBase de l''hôte [{0}] est un parent du répertoire CATALINA_BASE, ce qui est mauvais standardHostValve.customStatusFailed=La page d''erreur personnalisée [{0}] n''a pu être redirigée correctement standardHostValve.exception=Exception lors du traitement de [{0}] diff -Nru tomcat11-11.0.15/java/org/apache/catalina/core/LocalStrings_ja.properties tomcat11-11.0.22/java/org/apache/catalina/core/LocalStrings_ja.properties --- tomcat11-11.0.15/java/org/apache/catalina/core/LocalStrings_ja.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/core/LocalStrings_ja.properties 2026-05-01 18:56:05.000000000 +0000 @@ -88,6 +88,7 @@ aprListener.sslInit=SSLEngineの初期化に失敗しました。 aprListener.sslRequired=バージョン 2.x以降ではSSLが必要なため、バージョン [{1}] のTomcat Native ライブラリを使用する場合、 [{0}] はSSLEngineの有効な値ではありません。 aprListener.tcnInvalid=APRベースのApache Tomcatネイティブライブラリの互換性のないバージョン[{0}]がインストールされていますが、Tomcatにはバージョン[{1}]が必要です。 +aprListener.tcnInvalid.1=互換性のないバージョンの Apache Tomcat Native ライブラリ [{0}] がインストールされていますが、Tomcat には少なくともバージョン [{1}] のTomcat Native 1.x が必要です aprListener.tcnValid=APRバージョン[{1}]を使用してAPRベースのApache Tomcatネイティブライブラリ[{0}]をロードしました。 aprListener.tcnVersion=インストールされた Apache Tomcat ネイティブライブラリの APR バージョンは [{0}] ですが、推奨する最小バージョンは [{1}] です。 aprListener.tooLateForFIPSMode=FIPSModeを設定できません:SSLは既に初期化されています。 @@ -269,7 +270,9 @@ standardHost.notContext=Host の子供はContextでなければいけません standardHost.nullName=ホスト名が必要です standardHost.problematicAppBase=ホスト [{0}] のappBaseに空の文字列を使用すると、CATALINA_BASEに設定されますが、これは悪い考えです +standardHost.problematicAppBaseParent=Host [{0}] の appBase は CATALINA_BASE の親フォルダですが、これは適切ではありません standardHost.problematicLegacyAppBase=ホスト [{0}] のlegacyAppBaseに空の文字列を使用すると、CATALINA_BASEに設定されます。これは悪い考えです +standardHost.problematicLegacyAppBaseParent=Host [{0}] の legacyAppBase は CATALINA_BASE の親フォルダですが、これは適切ではありません standardHostValve.customStatusFailed=カスタムエラーページ [{0}] を正しくディスパッチできませんでした standardHostValve.exception=例外処理 [{0}] diff -Nru tomcat11-11.0.15/java/org/apache/catalina/core/OpenSSLLifecycleListener.java tomcat11-11.0.22/java/org/apache/catalina/core/OpenSSLLifecycleListener.java --- tomcat11-11.0.15/java/org/apache/catalina/core/OpenSSLLifecycleListener.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/core/OpenSSLLifecycleListener.java 2026-05-01 18:56:05.000000000 +0000 @@ -67,6 +67,29 @@ 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 -Nru tomcat11-11.0.15/java/org/apache/catalina/core/StandardContext.java tomcat11-11.0.22/java/org/apache/catalina/core/StandardContext.java --- tomcat11-11.0.15/java/org/apache/catalina/core/StandardContext.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/core/StandardContext.java 2026-05-01 18:56:05.000000000 +0000 @@ -276,7 +276,7 @@ /** * The ServletContext implementation associated with this Context. */ - protected ApplicationContext context = null; + protected volatile ApplicationContext context = null; /** * The wrapped version of the associated ServletContext that is presented to listeners that are required to have @@ -2176,12 +2176,16 @@ @Override public ServletContext getServletContext() { - // This method is called multiple times during context start which is single threaded - // so there is no concurrency issue + // Outer check avoids locking when context already exists and + // inner check prevents duplicate creation when multiple threads race past the outer check. if (context == null) { - context = new ApplicationContext(this); - if (altDDName != null) { - context.setAttribute(Globals.ALT_DD_ATTR, altDDName); + synchronized (this) { + if (context == null) { + context = new ApplicationContext(this); + if (altDDName != null) { + context.setAttribute(Globals.ALT_DD_ATTR, altDDName); + } + } } } return context.getFacade(); diff -Nru tomcat11-11.0.15/java/org/apache/catalina/filters/CsrfPreventionFilter.java tomcat11-11.0.22/java/org/apache/catalina/filters/CsrfPreventionFilter.java --- tomcat11-11.0.15/java/org/apache/catalina/filters/CsrfPreventionFilter.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/filters/CsrfPreventionFilter.java 2026-05-01 18:56:05.000000000 +0000 @@ -544,6 +544,8 @@ @Override public String encodeRedirectURL(String url) { + url = removeQueryParameters(url, nonceRequestParameterName); + if (shouldAddNonce(url)) { return addNonce(super.encodeRedirectURL(url)); } else { @@ -553,6 +555,8 @@ @Override public String encodeURL(String url) { + url = removeQueryParameters(url, nonceRequestParameterName); + if (shouldAddNonce(url)) { return addNonce(super.encodeURL(url)); } else { @@ -574,6 +578,85 @@ return true; } + /** + * Removes zero or more query parameters from a URL. All instances of the query parameter and any associated + * values will be removed. + * + * @param url The URL whose query parameters should be removed. + * @param parameterName The name of the parameter to remove. + * + * @return The URL without any instances of the query parameter parameterName present. + */ + public static String removeQueryParameters(String url, String parameterName) { + if (null != parameterName) { + // Check for query string + int q = url.indexOf('?'); + if (q > -1) { + + // Look for parameter end + int start = q + 1; + int pos = url.indexOf('&', start); + + int iterations = 0; + + while (-1 < pos) { + // Process all parameters + if (++iterations > 100000) { + // Just in case things get out of control + throw new IllegalStateException("Way too many loop iterations"); + } + + int eq = url.indexOf('=', start); + int paramNameEnd; + if (-1 == eq || eq > pos) { + paramNameEnd = pos; + // Found no equal sign at all or past next & ; Parameter is all name. + } else { + // Found this param's equal sign + paramNameEnd = eq; + } + if (parameterName.equals(url.substring(start, paramNameEnd))) { + // Remove the parameter + url = url.substring(0, start) + url.substring(pos + 1); // +1 to consume the & + } else { + start = pos + 1; // Go to next parameter + } + pos = url.indexOf('&', start); + } + + // Check final parameter + String paramName; + pos = url.indexOf('=', start); + + if (-1 < pos) { + paramName = url.substring(start, pos); + } else { + paramName = url.substring(start); + // No value + } + if (paramName.equals(parameterName)) { + // Remove this parameter + + // Remove any trailing ? or & as well + char c = url.charAt(start - 1); + if ('?' == c || '&' == c) { + start--; + } + + url = url.substring(0, start); + } else { + // Remove trailing ? if it's there. Is this worth it? + int length = url.length(); + if (length == q + 1) { + url = url.substring(0, length - 1); + } + } + } + } + + return url; + } + /* * Return the specified URL with the nonce added to the query string. * diff -Nru tomcat11-11.0.15/java/org/apache/catalina/ha/context/ReplicatedContext.java tomcat11-11.0.22/java/org/apache/catalina/ha/context/ReplicatedContext.java --- tomcat11-11.0.15/java/org/apache/catalina/ha/context/ReplicatedContext.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/ha/context/ReplicatedContext.java 2026-05-01 18:56:05.000000000 +0000 @@ -118,9 +118,13 @@ @Override public ServletContext getServletContext() { if (context == null) { - context = new ReplApplContext(this); - if (getAltDDName() != null) { - context.setAttribute(Globals.ALT_DD_ATTR, getAltDDName()); + synchronized (this) { + if (context == null) { + context = new ReplApplContext(this); + if (getAltDDName() != null) { + context.setAttribute(Globals.ALT_DD_ATTR, getAltDDName()); + } + } } } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/ha/tcp/ReplicationValve.java tomcat11-11.0.22/java/org/apache/catalina/ha/tcp/ReplicationValve.java --- tomcat11-11.0.15/java/org/apache/catalina/ha/tcp/ReplicationValve.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/ha/tcp/ReplicationValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -362,16 +362,19 @@ start = System.currentTimeMillis(); } try { - // send invalid sessions sendInvalidSessions(clusterManager); - // send replication - sendSessionReplicationMessage(request, clusterManager); - if (isCrossContext) { - sendCrossContextSession(); + try { + sendSessionReplicationMessage(request, clusterManager); + } catch (Exception e) { + log.error(sm.getString("ReplicationValve.send.failure"), e); + } + try { + if (isCrossContext) { + sendCrossContextSession(); + } + } catch (Exception e) { + log.error(sm.getString("ReplicationValve.send.failure"), e); } - } catch (Exception e) { - // FIXME we have a lot of sends, but the trouble with one node stops the correct replication to other nodes! - log.error(sm.getString("ReplicationValve.send.failure"), e); } finally { if (doStatistics()) { updateStats(totalstart, start, isAsync); diff -Nru tomcat11-11.0.15/java/org/apache/catalina/ha/tcp/SendMessageData.java tomcat11-11.0.22/java/org/apache/catalina/ha/tcp/SendMessageData.java --- tomcat11-11.0.15/java/org/apache/catalina/ha/tcp/SendMessageData.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/ha/tcp/SendMessageData.java 2026-05-01 18:56:05.000000000 +0000 @@ -19,6 +19,8 @@ import org.apache.catalina.tribes.Member; /** + * Data about a message send operation. + * * @param message The message that was sent * @param destination The destination of the message * @param exception The exception, if any, when attempting to send the message diff -Nru tomcat11-11.0.15/java/org/apache/catalina/manager/Constants.java tomcat11-11.0.22/java/org/apache/catalina/manager/Constants.java --- tomcat11-11.0.15/java/org/apache/catalina/manager/Constants.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/manager/Constants.java 2026-05-01 18:56:05.000000000 +0000 @@ -131,7 +131,7 @@ HTML_TAIL_SECTION = "


\n" + "
\n" + - " Copyright © 1999-2025, Apache Software Foundation" + + " Copyright © 1999-2026, Apache Software Foundation" + "
\n" + "\n" + "\n" + diff -Nru tomcat11-11.0.15/java/org/apache/catalina/manager/HTMLManagerServlet.java tomcat11-11.0.22/java/org/apache/catalina/manager/HTMLManagerServlet.java --- tomcat11-11.0.15/java/org/apache/catalina/manager/HTMLManagerServlet.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/manager/HTMLManagerServlet.java 2026-05-01 18:56:05.000000000 +0000 @@ -730,7 +730,7 @@ @Override public String getServletInfo() { - return "HTMLManagerServlet, Copyright (c) 1999-2025, The Apache Software Foundation"; + return "HTMLManagerServlet, Copyright (c) 1999-2026, The Apache Software Foundation"; } @Override @@ -1172,34 +1172,34 @@ "\n" + "\n" + " \n" + " \n" + "\n" + "\n" + " \n" + " \n" + "\n" + "\n" + " \n" + " \n" + "\n" + "\n" + " \n" + " \n" + "\n" + "\n" + @@ -1226,10 +1226,10 @@ "
\n" + - " {3}\n" + + " \n" + " \n" + - " \n" + + " \n" + "
\n" + - " {4}\n" + + " \n" + " \n" + - " \n" + + " \n" + "
\n" + - " {5}\n" + + " \n" + " \n" + - " \n" + + " \n" + "
\n" + - " {6}\n" + + " \n" + " \n" + - " \n" + + " \n" + "
\n" + "\n" + " \n" + " \n" + "\n" + "\n" + @@ -1262,10 +1262,10 @@ "
\n" + - " {2}\n" + + " \n" + " \n" + - " \n" + + " \n" + "
\n" + "\n" + " \n" + " \n" + "\n" + "\n" + diff -Nru tomcat11-11.0.15/java/org/apache/catalina/manager/host/Constants.java tomcat11-11.0.22/java/org/apache/catalina/manager/host/Constants.java --- tomcat11-11.0.15/java/org/apache/catalina/manager/host/Constants.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/manager/host/Constants.java 2026-05-01 18:56:05.000000000 +0000 @@ -80,7 +80,7 @@ public static final String HTML_TAIL_SECTION = "
\n" + "
\n" + - " Copyright © 1999-2025, Apache Software Foundation" + + " Copyright © 1999-2026, Apache Software Foundation" + "
\n" + "\n" + "\n" + diff -Nru tomcat11-11.0.15/java/org/apache/catalina/manager/host/HostManagerServlet.java tomcat11-11.0.22/java/org/apache/catalina/manager/host/HostManagerServlet.java --- tomcat11-11.0.15/java/org/apache/catalina/manager/host/HostManagerServlet.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/manager/host/HostManagerServlet.java 2026-05-01 18:56:05.000000000 +0000 @@ -67,6 +67,9 @@ * host-name#host-aliases. *
  • /start?name={host-name} - Start the virtual host.
  • *
  • /stop?name={host-name} - Stop the virtual host.
  • + *
  • /persist - Persist the current server configuration to {@code server.xml}. Requires the + * {@link org.apache.catalina.storeconfig.StoreConfigLifecycleListener StoreConfigLifecycleListener} to + * be configured.
  • * *

    * NOTE - Attempting to stop or remove the host containing this servlet itself will not succeed. Therefore, this diff -Nru tomcat11-11.0.15/java/org/apache/catalina/mapper/LocalStrings.properties tomcat11-11.0.22/java/org/apache/catalina/mapper/LocalStrings.properties --- tomcat11-11.0.15/java/org/apache/catalina/mapper/LocalStrings.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/mapper/LocalStrings.properties 2026-05-01 18:56:05.000000000 +0000 @@ -22,6 +22,7 @@ mapper.addHost.success=Registered host [{0}] mapper.addHostAlias.sameHost=Duplicate registration of alias [{0}] for the same host [{1}]. Ignored. mapper.addHostAlias.success=Registered alias [{0}] for host [{1}] +mapper.alreadyDone=Mapping was already done on this request on [{0}] mapper.duplicateHost=Duplicate Host [{0}]. The name is already used by Host [{1}]. This Host will be ignored. mapper.duplicateHostAlias=Duplicate host Alias [{0}] in Host [{1}]. The name is already used by Host [{2}]. This Alias will be ignored. mapper.findContext.noContext=No context found [{0}] diff -Nru tomcat11-11.0.15/java/org/apache/catalina/mapper/LocalStrings_fr.properties tomcat11-11.0.22/java/org/apache/catalina/mapper/LocalStrings_fr.properties --- tomcat11-11.0.15/java/org/apache/catalina/mapper/LocalStrings_fr.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/mapper/LocalStrings_fr.properties 2026-05-01 18:56:05.000000000 +0000 @@ -22,6 +22,7 @@ mapper.addHost.success=Enregistré l''hôte [{0}] mapper.addHostAlias.sameHost=L''enregistrement en double de l''alias [{0}] de l''hôte [{1}] est ignoré mapper.addHostAlias.success=L''alias [{0}] pour le hôte [{1}] a été enregistré +mapper.alreadyDone=Le mapping a déjà été effectué pour cette requête vers [{0}] mapper.duplicateHost=L''hôte [{0}] est en double et sera ignoré, le nom est déjà utilisé par l''hôte [{1}] mapper.duplicateHostAlias=L''alias [{0}] de l''hôte [{1}] est en double et sera ignoré, le nom est déjà utilisé par l''hôte [{2}] mapper.findContext.noContext=Pas de contexte trouvé [{0}] diff -Nru tomcat11-11.0.15/java/org/apache/catalina/mapper/LocalStrings_ja.properties tomcat11-11.0.22/java/org/apache/catalina/mapper/LocalStrings_ja.properties --- tomcat11-11.0.15/java/org/apache/catalina/mapper/LocalStrings_ja.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/mapper/LocalStrings_ja.properties 2026-05-01 18:56:05.000000000 +0000 @@ -22,6 +22,7 @@ mapper.addHost.success=登録されたホスト [{0}] mapper.addHostAlias.sameHost=ホスト [{1}] にエイリアス [{0}] が重複して登録されました。無視します。 mapper.addHostAlias.success=ホスト [{1}] のエイリアス [{0}] を登録しました。 +mapper.alreadyDone=このリクエストのマッピングは、既に [{0}] で完了しています mapper.duplicateHost=Host名の重複 [{0}]。この名前はHost[{1}] で使用されているため無視されます。 mapper.duplicateHostAlias=Host [{1}] のホストエイリアス [{0}] が重複しています。 名前はすでにHost [{2}] によって使用されています。 このエイリアスは無視されます。 mapper.findContext.noContext=コンテキストが見つかりません [{0}] diff -Nru tomcat11-11.0.15/java/org/apache/catalina/mapper/Mapper.java tomcat11-11.0.22/java/org/apache/catalina/mapper/Mapper.java --- tomcat11-11.0.15/java/org/apache/catalina/mapper/Mapper.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/mapper/Mapper.java 2026-05-01 18:56:05.000000000 +0000 @@ -710,7 +710,7 @@ // skipped all mapping work in this case. That behaviour has a risk // of returning an inconsistent result. // I do not see a valid use case for it. - throw new AssertionError(); + throw new IllegalStateException(sm.getString("mapper.alreadyDone", mappingData)); } // Virtual host mapping diff -Nru tomcat11-11.0.15/java/org/apache/catalina/mapper/MappingData.java tomcat11-11.0.22/java/org/apache/catalina/mapper/MappingData.java --- tomcat11-11.0.15/java/org/apache/catalina/mapper/MappingData.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/mapper/MappingData.java 2026-05-01 18:56:05.000000000 +0000 @@ -57,4 +57,10 @@ redirectPath.recycle(); matchType = null; } + + @Override + public final String toString() { + return "MappingData[" + host + ":" + context + ":" + wrapper + "]"; + } + } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java tomcat11-11.0.22/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java --- tomcat11-11.0.15/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java 2026-05-01 18:56:05.000000000 +0000 @@ -25,6 +25,7 @@ import org.apache.juli.logging.Log; import org.apache.tomcat.util.buf.HexUtils; import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.ConstantTime; /** * Base implementation for the Tomcat provided {@link CredentialHandler}s. @@ -191,7 +192,7 @@ return false; } - return DigestCredentialHandlerBase.equals(storedHexEncoded, inputHexEncoded, true); + return ConstantTime.equals(storedHexEncoded, inputHexEncoded, true); } @@ -286,38 +287,12 @@ * here is only guaranteed to work with plain ASCII characters. * * @return true if the strings are equal to each other, false otherwise. + * + * @deprecated Use {@link ConstantTime#equals(String, String, boolean)}. This method will be removed in Tomcat 12. */ + @Deprecated public static boolean equals(final String s1, final String s2, final boolean ignoreCase) { - if (s1 == s2) { - return true; - } - if (s1 == null || s2 == null) { - return false; - } - - final int len1 = s1.length(); - final int len2 = s2.length(); - - if (len2 == 0) { - return len1 == 0; - } - - int result = 0; - result |= len1 - len2; - - // time-constant comparison - for (int i = 0; i < len1; i++) { - // If i >= len2, index2 is 0; otherwise, i. - final int index2 = ((i - len2) >>> 31) * i; - char c1 = s1.charAt(i); - char c2 = s2.charAt(index2); - if (ignoreCase) { - c1 = Character.toLowerCase(c1); - c2 = Character.toLowerCase(c2); - } - result |= c1 ^ c2; - } - return result == 0; + return ConstantTime.equals(s1, s2, ignoreCase); } /** @@ -333,8 +308,11 @@ * @param b2 The second array to compare. * * @return true if the arrays are equal to each other, false otherwise. + * + * @deprecated Use {@link ConstantTime#equals(byte[], byte[])}. This method will be removed in Tomcat 12. */ + @Deprecated public static boolean equals(final byte[] b1, final byte[] b2) { - return MessageDigest.isEqual(b1, b2); + return ConstantTime.equals(b1, b2); } } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/realm/LockOutRealm.java tomcat11-11.0.22/java/org/apache/catalina/realm/LockOutRealm.java --- tomcat11-11.0.15/java/org/apache/catalina/realm/LockOutRealm.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/realm/LockOutRealm.java 2026-05-01 18:56:05.000000000 +0000 @@ -20,6 +20,7 @@ import java.security.Principal; import java.security.cert.X509Certificate; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -72,6 +73,11 @@ */ protected Map failedUsers = null; + /* + * Should user names (the keys) in the failedUsers Map be treated in a case sensitive manner. The default is false. + */ + private boolean caseSensitive = false; + @Override protected void startInternal() throws LifecycleException { @@ -207,6 +213,9 @@ * time will be recorded and any attempt to authenticate a locked user will log a warning. */ public boolean isLocked(String username) { + if (!getCaseSensitive()) { + username = username.toLowerCase(Locale.ROOT); + } LockRecord lockRecord; synchronized (this) { lockRecord = failedUsers.get(username); @@ -229,6 +238,9 @@ * After successful authentication, any record of previous authentication failure is removed. */ private synchronized void registerAuthSuccess(String username) { + if (!getCaseSensitive()) { + username = username.toLowerCase(Locale.ROOT); + } // Successful authentication means removal from the list of failed users failedUsers.remove(username); } @@ -238,6 +250,9 @@ * After a failed authentication, add the record of the failed authentication. */ private void registerAuthFailure(String username) { + if (!getCaseSensitive()) { + username = username.toLowerCase(Locale.ROOT); + } LockRecord lockRecord; synchronized (this) { if (!failedUsers.containsKey(username)) { @@ -339,6 +354,16 @@ } + public boolean getCaseSensitive() { + return caseSensitive; + } + + + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + protected static class LockRecord { private final AtomicInteger failures = new AtomicInteger(0); private long lastFailureTime = 0; diff -Nru tomcat11-11.0.15/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java tomcat11-11.0.22/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java --- tomcat11-11.0.15/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java 2026-05-01 18:56:05.000000000 +0000 @@ -20,7 +20,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; -import java.util.Arrays; import java.util.Base64; import org.apache.juli.logging.Log; @@ -28,6 +27,7 @@ import org.apache.tomcat.util.buf.B2CConverter; import org.apache.tomcat.util.buf.HexUtils; import org.apache.tomcat.util.security.ConcurrentMessageDigest; +import org.apache.tomcat.util.security.ConstantTime; /** * This credential handler supports the following forms of stored passwords: @@ -110,7 +110,7 @@ if (getAlgorithm() == null) { // No digests, compare directly - return DigestCredentialHandlerBase.equals(inputCredentials, storedCredentials, false); + return ConstantTime.equals(inputCredentials, storedCredentials, false); } else { // Some directories and databases prefix the password with the hash // type. The string is in a format compatible with Base64.encode not @@ -123,7 +123,7 @@ inputCredentials.getBytes(StandardCharsets.ISO_8859_1)); String base64UserDigest = Base64.getEncoder().encodeToString(userDigest); - return DigestCredentialHandlerBase.equals(base64UserDigest, base64ServerDigest, false); + return ConstantTime.equals(base64UserDigest, base64ServerDigest, false); } else if (storedCredentials.startsWith("{SSHA}")) { // "{SSHA}" // Need to convert the salt to bytes to apply it to the user's @@ -146,7 +146,7 @@ byte[] userDigestBytes = ConcurrentMessageDigest.digest(getAlgorithm(), inputCredentials.getBytes(StandardCharsets.ISO_8859_1), serverSaltBytes); - return Arrays.equals(userDigestBytes, serverDigestBytes); + return ConstantTime.equals(userDigestBytes, serverDigestBytes); } else if (storedCredentials.indexOf('$') > -1) { return matchesSaltIterationsEncoded(inputCredentials, storedCredentials); } else { @@ -157,7 +157,7 @@ // Root cause should be logged by mutate() return false; } - return storedCredentials.equalsIgnoreCase(userDigest); + return ConstantTime.equals(storedCredentials, userDigest, true); } } } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/realm/RealmBase.java tomcat11-11.0.22/java/org/apache/catalina/realm/RealmBase.java --- tomcat11-11.0.15/java/org/apache/catalina/realm/RealmBase.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/realm/RealmBase.java 2026-05-01 18:56:05.000000000 +0000 @@ -61,6 +61,7 @@ import org.apache.tomcat.util.descriptor.web.SecurityConstraint; import org.apache.tomcat.util.res.StringManager; import org.apache.tomcat.util.security.ConcurrentMessageDigest; +import org.apache.tomcat.util.security.ConstantTime; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; @@ -416,7 +417,7 @@ "digestA2:" + digestA2 + " Server digest:" + serverDigest); } - if (serverDigest.equals(clientDigest)) { + if (ConstantTime.equals(serverDigest, clientDigest, true)) { return getPrincipal(username); } @@ -658,8 +659,6 @@ constraints[i].included(uri, method)); } - boolean matched = false; - int pos = -1; for (int j = 0; j < collection.length; j++) { String[] patterns = collection[j].findPatterns(); @@ -669,6 +668,7 @@ continue; } + boolean matched = false; for (int k = 0; k < patterns.length && !matched; k++) { String pattern = patterns[k]; if (pattern.startsWith("*.")) { @@ -678,19 +678,18 @@ uri.length() - dot == pattern.length() - 1) { if (pattern.regionMatches(1, uri, dot, uri.length() - dot)) { matched = true; - pos = j; } } } } - } - if (matched) { - found = true; - if (collection[pos].findMethod(method)) { - if (results == null) { - results = new ArrayList<>(); + if (matched) { + found = true; + if (collection[j].findMethod(method)) { + if (results == null) { + results = new ArrayList<>(); + } + results.add(constraints[i]); } - results.add(constraints[i]); } } } @@ -1121,12 +1120,19 @@ * @return the digest for the specified user */ protected String getDigest(String username, String realmName, String algorithm) { + String password = getPassword(username); + + // Short-cut null password case + if (password == null) { + return null; + } + if (hasMessageDigest(algorithm)) { // Use pre-generated digest - return getPassword(username); + return password; } - String digestValue = username + ":" + realmName + ":" + getPassword(username); + String digestValue = username + ":" + realmName + ":" + password; byte[] valueBytes; try { diff -Nru tomcat11-11.0.15/java/org/apache/catalina/servlets/WebdavServlet.java tomcat11-11.0.22/java/org/apache/catalina/servlets/WebdavServlet.java --- tomcat11-11.0.15/java/org/apache/catalina/servlets/WebdavServlet.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/servlets/WebdavServlet.java 2026-05-01 18:56:05.000000000 +0000 @@ -175,6 +175,8 @@ * </init-param> * *

    + * By default, WebDAV request bodies for LOCK and PROPFIND are limited to 4096 bytes. To change this limit, set the + * maxRequestBodySize init-param for the WebDAV servlet. * * @see RFC 4918 */ @@ -202,6 +204,12 @@ private static final int MAX_DEPTH = 3; + /* + * Default max request body size. + */ + private static final int DEFAULT_MAX_REQUEST_BODY_SIZE = 4096; + + /** * Default namespace. */ @@ -215,6 +223,7 @@ "\n \n" + " \n"; + /** * Simple date format for the creation date ISO representation (partial). */ @@ -227,6 +236,7 @@ */ protected static final String LOCK_SCHEME = "urn:uuid:"; + // ----------------------------------------------------- Instance Variables /** @@ -247,6 +257,9 @@ private int maxDepth = MAX_DEPTH; + private int maxRequestBodySize = DEFAULT_MAX_REQUEST_BODY_SIZE; + + /** * Is access allowed via WebDAV to the special paths (/WEB-INF and /META-INF)? */ @@ -293,6 +306,10 @@ maxDepth = Integer.parseInt(getServletConfig().getInitParameter("maxDepth")); } + if (getServletConfig().getInitParameter("maxRequestBodySize") != null) { + maxRequestBodySize = Integer.parseInt(getServletConfig().getInitParameter("maxRequestBodySize")); + } + if (getServletConfig().getInitParameter("allowSpecialPaths") != null) { allowSpecialPaths = Boolean.parseBoolean(getServletConfig().getInitParameter("allowSpecialPaths")); } @@ -829,12 +846,8 @@ } } - byte[] body; - try (InputStream is = req.getInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream()) { - IOTools.flow(is, os); - body = os.toByteArray(); - } catch (IOException ioe) { - resp.sendError(WebdavStatus.SC_BAD_REQUEST); + byte[] body = readRequestBody(req, resp); + if (body == null) { return; } if (body.length > 0) { @@ -995,6 +1008,41 @@ /** + * Read request body + * + * @param req The request + * @param resp The response + * + * @return {@code null} if the body could not be read and an error status code has been set, otherwise the request + * body as a byte array + * + * @throws IOException if the reading the body fails and a response status code cannot be set + */ + private byte[] readRequestBody(HttpServletRequest req, HttpServletResponse resp) throws IOException { + // Short-cut if client provided a content length + if (req.getContentLengthLong() > maxRequestBodySize) { + resp.sendError(WebdavStatus.SC_REQUEST_TOO_LONG); + return null; + } + + byte[] body; + try (InputStream is = req.getInputStream(); + BoundedByteArrayOutputStream os = new BoundedByteArrayOutputStream(maxRequestBodySize)) { + IOTools.flow(is, os); + body = os.toByteArray(); + } catch (IOException ioe) { + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return null; + } catch (ArrayIndexOutOfBoundsException e) { + resp.sendError(WebdavStatus.SC_REQUEST_TOO_LONG); + return null; + } + + return body; + } + + + /** * PROPPATCH Method. Dead properties support is a SHOULD in the specification and are not implemented. * * @param req The Servlet request @@ -1235,6 +1283,7 @@ WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); + return; } deleteResource(path, req, resp, true); } @@ -1388,14 +1437,11 @@ Node lockInfoNode = null; - byte[] body; - try (InputStream is = req.getInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream()) { - IOTools.flow(is, os); - body = os.toByteArray(); - } catch (IOException ioe) { - resp.sendError(WebdavStatus.SC_BAD_REQUEST); + byte[] body = readRequestBody(req, resp); + if (body == null) { return; } + if (body.length > 0) { DocumentBuilder documentBuilder = getDocumentBuilder(); @@ -1882,6 +1928,9 @@ } String hrefPath = hrefUri.getPath(); + if (hrefPath == null) { + return null; + } // Avoid path traversals if (!hrefPath.equals(RequestUtil.normalize(hrefPath))) { @@ -2044,6 +2093,10 @@ } String destinationPath = destinationUri.getPath(); + if (destinationPath == null) { + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return false; + } // Destination isn't allowed to use '.' or '..' segments if (!destinationPath.equals(RequestUtil.normalize(destinationPath))) { @@ -3012,6 +3065,40 @@ } + static class BoundedByteArrayOutputStream extends ByteArrayOutputStream { + + private final int sizeLimit; + private int size; + + BoundedByteArrayOutputStream(int sizeLimit) { + super(); + this.sizeLimit = sizeLimit; + } + + @Override + public synchronized void write(int b) { + size++; + if (size > sizeLimit) { + throw new ArrayIndexOutOfBoundsException(); + } + super.write(b); + } + + @Override + public synchronized void write(byte[] b, int off, int len) { + size += len; + if (size > sizeLimit) { + throw new ArrayIndexOutOfBoundsException(); + } + super.write(b, off, len); + } + + @Override + public synchronized void reset() { + size = 0; + super.reset(); + } + } } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/startup/LocalStrings_zh_CN.properties tomcat11-11.0.22/java/org/apache/catalina/startup/LocalStrings_zh_CN.properties --- tomcat11-11.0.15/java/org/apache/catalina/startup/LocalStrings_zh_CN.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/startup/LocalStrings_zh_CN.properties 2026-05-01 18:56:05.000000000 +0000 @@ -127,7 +127,7 @@ hostConfig.deployDir.finished=Web应用程序目录[{0}]的部署已在[{1}]毫秒内完成 hostConfig.deployDir.threaded.error=等待目录的多线程部署完成时出错 hostConfig.deployWar=正在部署web应用程序存档文件[{0}] -hostConfig.deployWar.error=部署 Web 应用程序 archive [{0}] 时出错 +hostConfig.deployWar.error=部署 Web 应用程序存档 [{0}] 时出错 hostConfig.deployWar.finished=web应用程序存档文件[{0}]的部署已在[{1}]ms内完成 hostConfig.deployWar.hiddenDir=将忽略目录[{0}],因为WAR [{1}]优先,unpackWAR为false hostConfig.deployWar.threaded.error=等待WAR文件的多线程部署完成时出错 @@ -163,7 +163,7 @@ tomcat.noContextClass=无法为主机[{1}]和url[{2}]实例化上下文类[{0}] tomcat.noContextXml=不能找到web 应用的context.xml [{0}] -userConfig.database=加载用户数据库异常 +userConfig.database=用户数据库加载异常 userConfig.deploy=正在为用户[{0}]部署web应用程序 userConfig.deploy.threaded.error=等待用户目录的多线程部署完成时出错 userConfig.deploying=正在部署用户 web 应用程序 diff -Nru tomcat11-11.0.15/java/org/apache/catalina/storeconfig/OpenSSLConfSF.java tomcat11-11.0.22/java/org/apache/catalina/storeconfig/OpenSSLConfSF.java --- tomcat11-11.0.15/java/org/apache/catalina/storeconfig/OpenSSLConfSF.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/storeconfig/OpenSSLConfSF.java 2026-05-01 18:56:05.000000000 +0000 @@ -17,6 +17,7 @@ package org.apache.catalina.storeconfig; import java.io.PrintWriter; +import java.util.Set; import org.apache.tomcat.util.net.openssl.OpenSSLConf; import org.apache.tomcat.util.net.openssl.OpenSSLConfCmd; @@ -26,6 +27,8 @@ */ public class OpenSSLConfSF extends StoreFactoryBase { + private static final Set INTERNAL_COMMANDS = Set.of(OpenSSLConfCmd.NO_OCSP_CHECK); + /** * Store nested OpenSSLConfCmd elements. *

    @@ -36,9 +39,13 @@ throws Exception { if (aOpenSSLConf instanceof OpenSSLConf openSslConf) { // Store nested elements - OpenSSLConfCmd[] openSSLConfCmds = openSslConf.getCommands().toArray(new OpenSSLConfCmd[0]); - storeElementArray(aWriter, indent + 2, openSSLConfCmds); + for (OpenSSLConfCmd openSSLConfCmd : openSslConf.getCommands()) { + // Don't store the internal commands that are created from other SslHostConfig attributes. + if (INTERNAL_COMMANDS.contains(openSSLConfCmd.getName())) { + continue; + } + storeElement(aWriter, indent + 2, openSSLConfCmd); + } } } - } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/group/AbsoluteOrder.java tomcat11-11.0.22/java/org/apache/catalina/tribes/group/AbsoluteOrder.java --- tomcat11-11.0.15/java/org/apache/catalina/tribes/group/AbsoluteOrder.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/group/AbsoluteOrder.java 2026-05-01 18:56:05.000000000 +0000 @@ -46,13 +46,24 @@ * @see org.apache.catalina.tribes.Member */ public class AbsoluteOrder { + /** + * The comparator for absolute order. + */ public static final AbsoluteComparator comp = new AbsoluteComparator(); + /** + * Protected constructor. + */ protected AbsoluteOrder() { super(); } + /** + * Sort the members in absolute order. + * + * @param members The members to sort + */ public static void absoluteOrder(Member[] members) { if (members == null || members.length <= 1) { return; @@ -60,6 +71,11 @@ Arrays.sort(members, comp); } + /** + * Sort the members in absolute order. + * + * @param members The members to sort + */ public static void absoluteOrder(List members) { if (members == null || members.size() <= 1) { return; @@ -67,11 +83,20 @@ members.sort(comp); } + /** + * A comparator for absolute ordering of members. + */ public static class AbsoluteComparator implements Comparator, Serializable { @Serial private static final long serialVersionUID = 1L; + /** + * Default constructor. + */ + public AbsoluteComparator() { + } + @Override public int compare(Member m1, Member m2) { int result = compareIps(m1, m2); @@ -84,18 +109,46 @@ return result; } + /** + * Compare the IP addresses of two members. + * + * @param m1 First member + * @param m2 Second member + * @return comparison result + */ public int compareIps(Member m1, Member m2) { return compareBytes(m1.getHost(), m2.getHost()); } + /** + * Compare the ports of two members. + * + * @param m1 First member + * @param m2 Second member + * @return comparison result + */ public int comparePorts(Member m1, Member m2) { return compareInts(m1.getPort(), m2.getPort()); } + /** + * Compare the unique IDs of two members. + * + * @param m1 First member + * @param m2 Second member + * @return comparison result + */ public int compareIds(Member m1, Member m2) { return compareBytes(m1.getUniqueId(), m2.getUniqueId()); } + /** + * Compare two byte arrays. + * + * @param d1 First byte array + * @param d2 Second byte array + * @return comparison result + */ protected int compareBytes(byte[] d1, byte[] d2) { int result = 0; if (d1.length == d2.length) { @@ -110,10 +163,24 @@ return result; } + /** + * Compare two bytes. + * + * @param b1 First byte + * @param b2 Second byte + * @return comparison result + */ protected int compareBytes(byte b1, byte b2) { return compareInts(b1, b2); } + /** + * Compare two integers. + * + * @param b1 First integer + * @param b2 Second integer + * @return comparison result + */ protected int compareInts(int b1, int b2) { return Integer.compare(b1, b2); } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java --- tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java 2026-05-01 18:56:05.000000000 +0000 @@ -22,6 +22,7 @@ import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; +import java.util.Locale; import java.util.concurrent.ConcurrentLinkedQueue; import javax.crypto.Cipher; @@ -42,7 +43,6 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; - /** * Adds encryption using a pre-shared key. The length of the key (in bytes) must be acceptable for the encryption * algorithm being used. For example, for AES, you must use a key of either 16 bytes (128 bits, 24 bytes 192 bits), or @@ -150,7 +150,8 @@ * Sets the encryption algorithm to be used for encrypting and decrypting channel messages. You must specify the * algorithm/mode/padding. Information on standard algorithm names may be found in the * Java - * documentation. Default is AES/CBC/PKCS5Padding. + * documentation. Default is AES/CBC/PKCS5Padding for backwards compatibility but it is recommended + * that AES/GCM/NoPadding is used. * * @param algorithm The algorithm to use. */ @@ -314,33 +315,67 @@ String algorithmName; String algorithmMode; + String algorithmPadding; - // We need to break-apart the algorithm name e.g. AES/CBC/PKCS5Padding + // We need to break-apart the algorithm name e.g. AES/GCM/NoPadding // take just the algorithm part. int pos = algorithm.indexOf('/'); if (pos >= 0) { - algorithmName = algorithm.substring(0, pos); + algorithmName = algorithm.substring(0, pos).toUpperCase(Locale.ENGLISH); int pos2 = algorithm.indexOf('/', pos + 1); if (pos2 >= 0) { - algorithmMode = algorithm.substring(pos + 1, pos2); + algorithmMode = algorithm.substring(pos + 1, pos2).toUpperCase(Locale.ENGLISH); + algorithmPadding = algorithm.substring(pos2 + 1).toUpperCase(Locale.ENGLISH); } else { - algorithmMode = "CBC"; + algorithmMode = "GCM"; + algorithmPadding = "NOPADDING"; } } else { algorithmName = algorithm; - algorithmMode = "CBC"; + algorithmMode = "GCM"; + algorithmPadding = "NOPADDING"; } - if ("GCM".equalsIgnoreCase(algorithmMode)) { + /* + * Limit the cipher algorithm modes available. The limits are based on the cipher algorithm modes listed in the + * Java Standard Names documentation. Those modes that are not appropriate or provide no protection are blocked. + * Where there are performance or security concerns regarding a mode, a warning is logged. Unrecognised modes, + * such as those provided by custom JCA providers are allowed but will be rejected if there is no JCA provider + * to support them. + */ + if ("NONE".equals(algorithmMode) || "ECB".equals(algorithmMode) || "PCBC".equals(algorithmMode) || + "CTS".equals(algorithmMode) || "KW".equals(algorithmMode) || "KWP".equals(algorithmMode) || + "CTR".equals(algorithmMode) || + ("CBC".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding)) || + ("CFB".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding)) || + ("GCM".equals(algorithmMode) && "PKCS5PADDING".equals(algorithmPadding)) || + ("OFB".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding))) { + // Insecure, unsuitable or unsupported + throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.unsupported", algorithm)); + + } else if (("CBC".equals(algorithmMode) && "PKCS5PADDING".equals(algorithmPadding)) || + ("CFB".equals(algorithmMode) && "PKCS5PADDING".equals(algorithmPadding)) || + ("OFB".equals(algorithmMode) && "PKCS5PADDING".equals(algorithmPadding))) { + // Supported but not recommended as more secure modes are available + log.warn(sm.getString("encryptInterceptor.algorithm.switch", algorithm)); + + } else if (algorithmMode.startsWith("CFB") || algorithmMode.startsWith("OFB")) { + // Using a non-default block size. Not supported as insecure and/or inefficient. + throw new IllegalArgumentException( + sm.getString("encryptInterceptor.algorithm.unsupported", algorithm)); + + } else if ("GCM".equals(algorithmMode) && "NOPADDING".equals(algorithmPadding)) { + // Needs a specialised encryption manager to handle the differences between GCM and other modes return new GCMEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName); - } else if ("CBC".equalsIgnoreCase(algorithmMode) || "OFB".equalsIgnoreCase(algorithmMode) || - "CFB".equalsIgnoreCase(algorithmMode)) { + } + + // Use the default encryption manager + try { return new BaseEncryptionManager(algorithm, new SecretKeySpec(encryptionKey, algorithmName), providerName); - } else { - throw new IllegalArgumentException( - sm.getString("encryptInterceptor.algorithm.unsupported-mode", algorithmMode)); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | NoSuchProviderException ex) { + throw new IllegalArgumentException(sm.getString("encryptInterceptor.algorithm.unsupported", algorithm), ex); } } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties --- tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties 2026-05-01 18:56:05.000000000 +0000 @@ -19,8 +19,9 @@ domainFilterInterceptor.member.refused=Member [{0}] was refused to join cluster domainFilterInterceptor.message.refused=Received message from cluster[{0}] was refused. -encryptInterceptor.algorithm.required=Encryption algorithm is required, fully-specified e.g. AES/CBC/PKCS5Padding -encryptInterceptor.algorithm.unsupported-mode=EncryptInterceptor does not support block cipher mode [{0}] +encryptInterceptor.algorithm.required=Encryption algorithm is required, fully-specified e.g. AES/GCM/NoPadding +encryptInterceptor.algorithm.switch=The EncryptInterceptor is using the algorithm [{0}]. It is recommended to switch to using AES/GCM/NoPadding. +encryptInterceptor.algorithm.unsupported=EncryptInterceptor does not support algorithm [{0}] encryptInterceptor.decrypt.error.short-message=Failed to decrypt message: premature end-of-message encryptInterceptor.decrypt.failed=Failed to decrypt message encryptInterceptor.encrypt.failed=Failed to encrypt message diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties --- tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties 2026-05-01 18:56:05.000000000 +0000 @@ -20,7 +20,8 @@ domainFilterInterceptor.message.refused=Le message reçu du cluster [{0}] a été refusé encryptInterceptor.algorithm.required=Un algorithme de cryptage est requis, avec une spécification complète telle que AES/CBC/PKCS5Padding -encryptInterceptor.algorithm.unsupported-mode=L''EncryptInterceptor ne supporte pas le mode de chiffrage de bloc [{0}] +encryptInterceptor.algorithm.switch=EncryptInterceptor utilise l''algorithme [{0}], il est recommandé de changer en AES/GCM/NoPadding +encryptInterceptor.algorithm.unsupported=EncryptInterceptor ne supporte pas l''algorithme [{0}] encryptInterceptor.decrypt.error.short-message=Echec du décryptage du message : fin de message prématuré encryptInterceptor.decrypt.failed=Echec de décryptage du message encryptInterceptor.encrypt.failed=Erreur de cryptage du message diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties --- tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties 2026-05-01 18:56:05.000000000 +0000 @@ -20,7 +20,8 @@ domainFilterInterceptor.message.refused=クラスター [{0}] から受信したメッセージは拒否されました。 encryptInterceptor.algorithm.required=暗号化アルゴリズムが必要です。完全指定。 AES/CBC/PKCS5Padding -encryptInterceptor.algorithm.unsupported-mode=EncryptInterceptorはブロック暗号モード [{0}]をサポートしていません。 +encryptInterceptor.algorithm.switch=EncryptInterceptorはアルゴリズム[{0}]を使用しています。AES/GCM/NoPaddingへの切り替えを推奨します。 +encryptInterceptor.algorithm.unsupported=EncryptInterceptor はアルゴリズム [{0}] をサポートしていません encryptInterceptor.decrypt.error.short-message=メッセージの復号に失敗: メッセージの末尾が途切れています encryptInterceptor.decrypt.failed=メッセージの復号に失敗しました。 encryptInterceptor.encrypt.failed=メッセージを暗号化できません。 diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties --- tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties 2026-05-01 18:56:05.000000000 +0000 @@ -20,7 +20,6 @@ domainFilterInterceptor.message.refused=클러스터 [{0}](으)로부터 받은 메시지가 거부되었습니다. encryptInterceptor.algorithm.required=암호화 알고리즘을 완전하게 지정해야 합니다. 예) AES/CBC/PKCS5Padding. -encryptInterceptor.algorithm.unsupported-mode=EncryptInterceptor가 블록 cipher 모드 [{0}]을(를) 지원하지 않습니다. encryptInterceptor.decrypt.error.short-message=메시지를 해독하지 못했습니다: 메시지가 너무 일찍 끝났습니다 (premature end-of-message). encryptInterceptor.decrypt.failed=메시지를 해독하지 못했습니다. encryptInterceptor.encrypt.failed=메시지를 암호화하지 못했습니다. diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties --- tomcat11-11.0.15/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties 2026-05-01 18:56:05.000000000 +0000 @@ -20,7 +20,6 @@ domainFilterInterceptor.message.refused=从集群[{0}]中接收的消息被拒绝 encryptInterceptor.algorithm.required=加密算法是必需的,充分说明,例如AES / CBC / PKCS5Padding -encryptInterceptor.algorithm.unsupported-mode=EncryptInterceptor不支持分组密码模式[{0}] encryptInterceptor.decrypt.error.short-message=解密消息失败: 结尾消息提前结束 encryptInterceptor.decrypt.failed=无法解密信息 encryptInterceptor.encrypt.failed=无法加密信息 diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java --- tomcat11-11.0.15/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java 2026-05-01 18:56:05.000000000 +0000 @@ -81,7 +81,7 @@ public URLConnection openConnection(String url, Map headers, int connectTimeout, int readTimeout) throws IOException { if (log.isDebugEnabled()) { - log.debug(sm.getString("abstractStream.connection", getClass().getSimpleName(), url, headers, + log.debug(sm.getString("abstractStream.connection", getClass().getSimpleName(), url, Integer.toString(connectTimeout), Integer.toString(readTimeout))); } URLConnection connection; diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java --- tomcat11-11.0.15/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java 2026-05-01 18:56:05.000000000 +0000 @@ -46,6 +46,9 @@ public class KubernetesMembershipProvider extends CloudMembershipProvider { + private static final String IPV6_URL = "%s://[%s]:%s/api/%s/namespaces/%s/pods"; + private static final String URL = "%s://%s:%s/api/%s/namespaces/%s/pods"; + private static final Log log = LogFactory.getLog(KubernetesMembershipProvider.class); private Path saTokenPath; @@ -122,7 +125,12 @@ namespace = URLEncoder.encode(namespace, StandardCharsets.UTF_8); labels = labels == null ? null : URLEncoder.encode(labels, StandardCharsets.UTF_8); - url = String.format("%s://%s:%s/api/%s/namespaces/%s/pods", protocol, masterHost, masterPort, ver, namespace); + String urlFormat = URL; + if (masterHost != null && masterHost.indexOf(':') != -1) { + // [] must be used around raw IPv6 + urlFormat = IPV6_URL; + } + url = String.format(urlFormat, protocol, masterHost, masterPort, ver, namespace); if (labels != null && !labels.isEmpty()) { url = url + "?labelSelector=" + labels; } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties --- tomcat11-11.0.15/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties 2026-05-01 18:56:05.000000000 +0000 @@ -17,7 +17,7 @@ # To edit translations see: https://tomcat.apache.org/getinvolved.html#Translations abstractStream.CACertUndefined=CA cert file undefined -abstractStream.connection=[{0}] opening connection: url [{1}], headers [{2}], connectTimeout [{3}], readTimeout [{4}] +abstractStream.connection=[{0}] opening connection: url [{1}], connectTimeout [{2}], readTimeout [{3}] abstractStream.fileNotFound=CA cert file [{0}] not found abstractStream.invalidTimeout=Neither connectTimeout [{0}] nor readTimeout [{1}] can be less than 0 for URLConnection abstractStream.trustManagerError=Could not create trust manager for [{0}] @@ -41,4 +41,4 @@ kubernetesMembershipProvider.serviceAccountTokenMissing=Service account token not found at [{0}] kubernetesMembershipProvider.streamError=Failed to open stream -tokenStream.failedConnection=Failed connection to [{0}] with token [{1}] +tokenStream.failedConnection=Failed connection to [{0}] diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java --- tomcat11-11.0.15/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java 2026-05-01 18:56:05.000000000 +0000 @@ -61,7 +61,7 @@ return super.openStream(url, headers, connectTimeout, readTimeout); } catch (IOException ioe) { // Add debug information - throw new IOException(sm.getString("tokenStream.failedConnection", url, token), ioe); + throw new IOException(sm.getString("tokenStream.failedConnection", url), ioe); } } } \ No newline at end of file diff -Nru tomcat11-11.0.15/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java tomcat11-11.0.22/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java --- tomcat11-11.0.15/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java 2026-05-01 18:56:05.000000000 +0000 @@ -50,6 +50,8 @@ import org.apache.juli.logging.LogFactory; /** + * An abstract replicated map implementation. + * * @param The type of Key * @param The type of Value */ diff -Nru tomcat11-11.0.15/java/org/apache/catalina/util/ServerInfo.java tomcat11-11.0.22/java/org/apache/catalina/util/ServerInfo.java --- tomcat11-11.0.15/java/org/apache/catalina/util/ServerInfo.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/util/ServerInfo.java 2026-05-01 18:56:05.000000000 +0000 @@ -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 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,212 @@ 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) { + // 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; } } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/util/URLEncoder.java tomcat11-11.0.22/java/org/apache/catalina/util/URLEncoder.java --- tomcat11-11.0.15/java/org/apache/catalina/util/URLEncoder.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/util/URLEncoder.java 2026-05-01 18:56:05.000000000 +0000 @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.Charset; +import java.nio.charset.CodingErrorAction; import java.util.BitSet; /** @@ -146,7 +147,15 @@ int maxBytesPerChar = 10; StringBuilder rewrittenPath = new StringBuilder(path.length()); ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar); - OutputStreamWriter writer = new OutputStreamWriter(buf, charset); + /* + * Most calls to this method use UTF-8 where malformed input and unmappable character issues are not expected to + * happen. The only Tomcat code that currently (January 2026) might call this method with something other than + * UTF-8 is the rewrite valve. In that case, the rewrite rules should be consistent with the configured URI + * encoding on the Connector. Given all of this, the IAE is only expected to be thrown as a result of + * configuration errors. + */ + OutputStreamWriter writer = new OutputStreamWriter(buf, charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT)); for (int i = 0; i < path.length(); i++) { int c = path.charAt(i); @@ -160,8 +169,7 @@ writer.write((char) c); writer.flush(); } catch (IOException ioe) { - buf.reset(); - continue; + throw new IllegalArgumentException(ioe); } byte[] ba = buf.toByteArray(); for (byte toEncode : ba) { diff -Nru tomcat11-11.0.15/java/org/apache/catalina/valves/AbstractAccessLogValve.java tomcat11-11.0.22/java/org/apache/catalina/valves/AbstractAccessLogValve.java --- tomcat11-11.0.15/java/org/apache/catalina/valves/AbstractAccessLogValve.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/valves/AbstractAccessLogValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -134,10 +134,25 @@ * The list of our time format types. */ private enum FormatType { + /** + * Common Log Format. + */ CLF, + /** + * Seconds since epoch. + */ SEC, + /** + * Milliseconds since epoch. + */ MSEC, + /** + * Millisecond fraction of timestamp. + */ MSEC_FRAC, + /** + * SimpleDateFormat format. + */ SDF } @@ -145,7 +160,13 @@ * The list of our port types. */ private enum PortType { + /** + * Local port. + */ LOCAL, + /** + * Remote port. + */ REMOTE } @@ -153,16 +174,34 @@ * The list of our ip address types. */ private enum RemoteAddressType { + /** + * Remote address. + */ REMOTE, + /** + * Peer address. + */ PEER } + /** + * The list of identifier types. + */ private enum IdentifierType { + /** + * Connection identifier. + */ CONNECTION, + /** + * Unknown identifier. + */ UNKNOWN } + /** + * Default constructor. + */ public AbstractAccessLogValve() { super(true); } @@ -226,6 +265,9 @@ */ protected static class DateFormatCache { + /** + * Cache for formatted timestamps. + */ protected class Cache { /* CLF log format */ @@ -245,20 +287,41 @@ /* Helper object to be able to call SimpleDateFormat.format(). */ private final Date currentDate = new Date(); + /** + * The cached values. + */ protected final String[] cache; private final SimpleDateFormat formatter; private boolean isCLF = false; private final Cache parent; + /** + * Creates a new cache with the given parent cache. + * + * @param parent The parent cache + */ private Cache(Cache parent) { this(null, parent); } + /** + * Creates a new cache with the given format and parent cache. + * + * @param format The format string + * @param parent The parent cache + */ private Cache(String format, Cache parent) { this(format, null, parent); } + /** + * Creates a new cache with the given format, locale and parent cache. + * + * @param format The format string + * @param loc The locale + * @param parent The parent cache + */ private Cache(String format, Locale loc, Cache parent) { cache = new String[cacheSize]; for (int i = 0; i < cacheSize; i++) { @@ -278,6 +341,13 @@ this.parent = parent; } + /** + * Gets the formatted timestamp for the given time. + * + * @param time The time in milliseconds + * + * @return The formatted timestamp + */ private String getFormatInternal(long time) { long seconds = time / 1000; @@ -352,9 +422,19 @@ private final Locale cacheDefaultLocale; private final DateFormatCache parent; + /** + * The cache for CLF format. + */ protected final Cache cLFCache; private final Map formatCache = new HashMap<>(); + /** + * Creates a new date format cache. + * + * @param size The cache size + * @param loc The default locale + * @param parentFC The parent cache + */ protected DateFormatCache(int size, Locale loc, DateFormatCache parentFC) { cacheSize = size; cacheDefaultLocale = loc; @@ -368,6 +448,14 @@ cLFCache = new Cache(parentCache); } + /** + * Gets the cache for the given format. + * + * @param format The format string + * @param loc The locale + * + * @return The cache + */ private Cache getCache(String format, Locale loc) { Cache cache; if (format == null) { @@ -388,10 +476,26 @@ return cache; } + /** + * Gets the formatted timestamp in CLF format. + * + * @param time The time in milliseconds + * + * @return The formatted timestamp + */ public String getFormat(long time) { return cLFCache.getFormatInternal(time); } + /** + * Gets the formatted timestamp in the given format. + * + * @param format The format string + * @param loc The locale + * @param time The time in milliseconds + * + * @return The formatted timestamp + */ public String getFormat(String format, Locale loc, long time) { return getCache(format, loc).getFormatInternal(time); } @@ -422,6 +526,7 @@ /** * Are we doing conditional logging ? default null. It is the value of conditionIf property. + * If the ServletRequest.getAttribute(conditionIf) yields a non-null value, the logging will be performed. */ protected String conditionIf = null; @@ -474,21 +579,38 @@ // ------------------------------------------------------------- Properties + /** + * Returns the max log message buffer size. + * + * @return the max log message buffer size. + */ public int getMaxLogMessageBufferSize() { return maxLogMessageBufferSize; } - + /** + * Sets the max log message buffer size. + * + * @param maxLogMessageBufferSize The max log message buffer size. + */ public void setMaxLogMessageBufferSize(int maxLogMessageBufferSize) { this.maxLogMessageBufferSize = maxLogMessageBufferSize; } - + /** + * Returns the ipv6 canonical flag. + * + * @return the ipv6 canonical flag. + */ public boolean getIpv6Canonical() { return ipv6Canonical; } - + /** + * Sets the ipv6 canonical flag. + * + * @param ipv6Canonical The ipv6 canonical flag. + */ public void setIpv6Canonical(boolean ipv6Canonical) { this.ipv6Canonical = ipv6Canonical; } @@ -509,6 +631,8 @@ } /** + * Returns the enabled flag. + * * @return the enabled flag. */ public boolean getEnabled() { @@ -516,6 +640,8 @@ } /** + * Sets the enabled flag. + * * @param enabled The enabled to set. */ public void setEnabled(boolean enabled) { @@ -523,6 +649,8 @@ } /** + * Returns the format pattern. + * * @return the format pattern. */ public String getPattern() { @@ -778,13 +906,26 @@ * cache the value in the element since the elements are state-less. */ protected interface CachedElement { + /** + * Cache the value for the specified request. + * + * @param request The request to cache the value for + */ void cache(Request request); } /** - * write thread name - %I + * Write thread name - %I. */ protected static class ThreadNameElement implements AccessLogElement { + /** + * Adds the thread name element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { RequestInfo info = request.getCoyoteRequest().getRequestProcessor(); @@ -797,12 +938,17 @@ } /** - * write local IP address - %A + * Write local IP address - %A. */ protected static class LocalAddrElement implements AccessLogElement { private final String localAddrValue; + /** + * Creates a new local address element. + * + * @param ipv6Canonical Whether to use IPv6 canonical representation + */ public LocalAddrElement(boolean ipv6Canonical) { String init; try { @@ -826,21 +972,29 @@ } /** - * write remote IP address - %a + * Write remote IP address - %a. */ protected class RemoteAddrElement implements AccessLogElement, CachedElement { /** - * Type of address to log + * Type of address to log. */ private static final String remoteAddress = "remote"; private static final String peerAddress = "peer"; private final RemoteAddressType remoteAddressType; + /** + * Creates a new remote address element using remote address type. + */ public RemoteAddrElement() { remoteAddressType = RemoteAddressType.REMOTE; } + /** + * Creates a new remote address element with the specified type. + * + * @param type The address type ("remote" or "peer") + */ public RemoteAddrElement(String type) { switch (type) { case remoteAddress: @@ -893,9 +1047,17 @@ } /** - * write remote host name - %h + * Write remote host name - %h. */ protected class HostElement implements AccessLogElement, CachedElement { + /** + * Adds the host element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { String value = null; @@ -927,9 +1089,17 @@ } /** - * write remote logical username from identd (always returns '-') - %l + * Write remote logical username from identd (always returns '-') - %l. */ protected static class LogicalUserNameElement implements AccessLogElement { + /** + * Adds the logical user name element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { buf.append('-'); @@ -937,9 +1107,17 @@ } /** - * write request protocol - %H + * Write request protocol - %H. */ protected class ProtocolElement implements AccessLogElement { + /** + * Adds the protocol element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { if (requestAttributesEnabled) { @@ -956,18 +1134,21 @@ } /** - * write remote user that was authenticated (if any), else '-' - %u + * Write remote user that was authenticated (if any), else '-' - %u. */ protected static class UserElement implements AccessLogElement { + /** + * Adds the user element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { if (request != null) { - String value = request.getRemoteUser(); - if (value != null) { - escapeAndAppend(value, buf); - } else { - buf.append('-'); - } + escapeAndAppend(request.getRemoteUser(), buf); } else { buf.append('-'); } @@ -975,42 +1156,42 @@ } /** - * write date and time, in configurable format (default CLF) - %t or %{format}t + * Write date and time, in configurable format (default CLF) - %t or %{format}t. */ protected class DateAndTimeElement implements AccessLogElement { /** - * Format prefix specifying request start time + * Format prefix specifying request start time. */ private static final String requestStartPrefix = "begin"; /** - * Format prefix specifying response end time + * Format prefix specifying response end time. */ private static final String responseEndPrefix = "end"; /** - * Separator between optional prefix and rest of format + * Separator between optional prefix and rest of format. */ private static final String prefixSeparator = ":"; /** - * Special format for seconds since epoch + * Special format for seconds since epoch. */ private static final String secFormat = "sec"; /** - * Special format for milliseconds since epoch + * Special format for milliseconds since epoch. */ private static final String msecFormat = "msec"; /** - * Special format for millisecond part of timestamp + * Special format for millisecond part of timestamp. */ private static final String msecFractionFormat = "msec_frac"; /** - * The patterns we use to replace "S" and "SSS" millisecond formatting of SimpleDateFormat by our own handling + * The patterns we use to replace "S" and "SSS" millisecond formatting of SimpleDateFormat by our own handling. */ private static final String msecPattern = "{#}"; private static final String tripleMsecPattern = msecPattern + msecPattern + msecPattern; @@ -1026,6 +1207,9 @@ /* Whether we need to postprocess by adding milliseconds */ private boolean usesMsecs = false; + /** + * Creates a new date and time element using CLF format. + */ protected DateAndTimeElement() { this(null); } @@ -1034,6 +1218,10 @@ * Replace the millisecond formatting character 'S' by some dummy characters in order to make the resulting * formatted time stamps cacheable. We replace the dummy chars later with the actual milliseconds because that's * relatively cheap. + * + * @param format The format string + * + * @return The tidied format string */ private String tidyFormat(String format) { boolean escape = false; @@ -1055,6 +1243,11 @@ return result.toString(); } + /** + * Creates a new date and time element with the specified format. + * + * @param sdf The SimpleDateFormat pattern or special format identifier + */ protected DateAndTimeElement(String sdf) { String format = sdf; boolean needsEscaping = false; @@ -1107,7 +1300,7 @@ Instant requestStartInstant = Instant.from(request.getCoyoteRequest().getStartInstant()); long frac; if (!usesBegin) { - requestStartInstant.plusNanos(time); + requestStartInstant = requestStartInstant.plusNanos(time); } switch (type) { case CLF: @@ -1156,9 +1349,17 @@ } /** - * write first line of the request (method and request URI) - %r + * Write first line of the request (method and request URI) - %r. */ protected static class RequestElement implements AccessLogElement { + /** + * Adds the request element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { if (request != null) { @@ -1169,11 +1370,8 @@ } else { buf.append(request.getMethod()); buf.append(' '); - buf.append(request.getRequestURI()); - if (request.getQueryString() != null) { - buf.append('?'); - buf.append(request.getQueryString()); - } + escapeAndAppend(request.getRequestURI(), buf); + appendQueryString(request.getQueryString(), buf, true, false, false); buf.append(' '); buf.append(request.getProtocol()); } @@ -1184,9 +1382,17 @@ } /** - * write HTTP status code of the response - %s + * Write HTTP status code of the response - %s. */ protected static class HttpStatusCodeElement implements AccessLogElement { + /** + * Adds the HTTP status code element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { if (response != null) { @@ -1205,7 +1411,7 @@ } /** - * write local or remote port for request connection - %p and %{xxx}p + * Write local or remote port for request connection - %p and %{xxx}p. */ protected class PortElement implements AccessLogElement, CachedElement { @@ -1217,10 +1423,18 @@ private final PortType portType; + /** + * Creates a new port element using local port type. + */ public PortElement() { portType = PortType.LOCAL; } + /** + * Creates a new port element with the specified type. + * + * @param type The port type ("local" or "remote") + */ public PortElement(String type) { switch (type) { case remotePort: @@ -1263,18 +1477,28 @@ } /** - * write bytes sent, excluding HTTP headers - %b, %B + * Write bytes sent, excluding HTTP headers - %b, %B. */ protected static class ByteSentElement implements AccessLogElement { private final boolean conversion; /** + * Creates a new ByteSentElement. + * * @param conversion true to write '-' instead of 0 - %b. */ public ByteSentElement(boolean conversion) { this.conversion = conversion; } + /** + * Adds the byte sent element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { // Don't need to flush since trigger for log message is after the @@ -1300,9 +1524,17 @@ } /** - * write request method (GET, POST, etc.) - %m + * Write request method (GET, POST, etc.) - %m. */ protected static class MethodElement implements AccessLogElement { + /** + * Adds the method element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { if (request != null) { @@ -1312,16 +1544,25 @@ } /** - * write time taken to process the request - %D, %T + * Write time taken to process the request - %D, %T. */ protected static class ElapsedTimeElement implements AccessLogElement { + /** + * Style for formatting elapsed time. + */ public enum Style { + /** + * Seconds format. + */ SECONDS { @Override public void append(CharArrayWriter buf, long time) { buf.append(Long.toString(TimeUnit.NANOSECONDS.toSeconds(time))); } }, + /** + * Fractional seconds format. + */ SECONDS_FRACTIONAL { @Override public void append(CharArrayWriter buf, long time) { @@ -1335,18 +1576,27 @@ buf.append(Long.toString(remains % 10)); } }, + /** + * Milliseconds format. + */ MILLISECONDS { @Override public void append(CharArrayWriter buf, long time) { buf.append(Long.toString(TimeUnit.NANOSECONDS.toMillis(time))); } }, + /** + * Microseconds format. + */ MICROSECONDS { @Override public void append(CharArrayWriter buf, long time) { buf.append(Long.toString(TimeUnit.NANOSECONDS.toMicros(time))); } }, + /** + * Nanoseconds format. + */ NANOSECONDS { @Override public void append(CharArrayWriter buf, long time) { @@ -1375,6 +1625,8 @@ } /** + * Creates a new ElapsedTimeElement that will log the time in the specified style. + * * @param micros true, write time in microseconds - %D * @param millis true, write time in milliseconds, if both arguments are false, write * time in seconds - %T @@ -1390,9 +1642,17 @@ } /** - * write time until first byte is written (commit time) in millis - %F + * Write time until first byte is written (commit time) in millis - %F. */ protected static class FirstByteTimeElement implements AccessLogElement { + /** + * Adds the first byte time element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { long commitTime = response.getCoyoteResponse().getCommitTimeNanos(); @@ -1406,26 +1666,54 @@ } /** - * write Query string (prepended with a '?' if it exists) - %q + * Write query string (prepended with a '?' if it exists) - %q. */ protected static class QueryElement implements AccessLogElement { + /** + * Adds the query element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { String query = null; if (request != null) { query = request.getQueryString(); } - if (query != null) { + appendQueryString(query, buf, true, false, true); + } + } + + protected static void appendQueryString(String query, CharArrayWriter buf, + boolean appendDelim, boolean escapeQuoteAsDouble, boolean writeDashOnNull) { + if (query != null) { + if (appendDelim) { buf.append('?'); - buf.append(query); } + // Don't want to write "-" if the query string is empty + if (!query.isEmpty()) { + escapeAndAppend(query, buf, escapeQuoteAsDouble); + } + } else if (writeDashOnNull) { + buf.append('-'); } } /** - * write user session ID - %S + * Write user session ID - %S. */ protected static class SessionIdElement implements AccessLogElement { + /** + * Adds the session ID element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { if (request == null) { @@ -1442,13 +1730,21 @@ } /** - * write requested URL path - %U + * Write requested URL path - %U. */ protected static class RequestURIElement implements AccessLogElement { + /** + * Adds the request URI element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { if (request != null) { - buf.append(request.getRequestURI()); + escapeAndAppend(request.getRequestURI(), buf); } else { buf.append('-'); } @@ -1456,9 +1752,17 @@ } /** - * write local server name - %v + * Write local server name - %v. */ protected class LocalServerNameElement implements AccessLogElement { + /** + * Adds the local server name element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { String value = null; @@ -1483,11 +1787,16 @@ } /** - * write any string + * Write any constant string. */ protected static class StringElement implements AccessLogElement { private final String str; + /** + * Creates a new string element. + * + * @param str The string to write + */ public StringElement(String str) { this.str = str; } @@ -1499,11 +1808,16 @@ } /** - * write incoming headers - %{xxx}i + * Write incoming headers - %{xxx}i. */ protected static class HeaderElement implements AccessLogElement { private final String header; + /** + * Creates a new header element. + * + * @param header The header name + */ public HeaderElement(String header) { this.header = header; } @@ -1524,11 +1838,16 @@ } /** - * write a specific cookie - %{xxx}c + * Write a specific cookie - %{xxx}c. */ protected static class CookieElement implements AccessLogElement { private final String cookieNameToLog; + /** + * Creates a new cookie element. + * + * @param cookieNameToLog The cookie name to log + */ public CookieElement(String cookieNameToLog) { this.cookieNameToLog = cookieNameToLog; } @@ -1562,11 +1881,16 @@ } /** - * write a specific response header - %{xxx}o + * Write a specific response header - %{xxx}o. */ protected static class ResponseHeaderElement implements AccessLogElement { private final String header; + /** + * Creates a new response header element. + * + * @param header The header name + */ public ResponseHeaderElement(String header) { this.header = header; } @@ -1589,11 +1913,16 @@ } /** - * write an attribute in the ServletRequest - %{xxx}r + * Write an attribute in the ServletRequest - %{xxx}r. */ protected static class RequestAttributeElement implements AccessLogElement { private final String attribute; + /** + * Creates a new request attribute element. + * + * @param attribute The attribute name + */ public RequestAttributeElement(String attribute) { this.attribute = attribute; } @@ -1619,11 +1948,16 @@ } /** - * write an attribute in the HttpSession - %{xxx}s + * Write an attribute in the HttpSession - %{xxx}s. */ protected static class SessionAttributeElement implements AccessLogElement { private final String attribute; + /** + * Creates a new session attribute element. + * + * @param attribute The attribute name + */ public SessionAttributeElement(String attribute) { this.attribute = attribute; } @@ -1652,9 +1986,17 @@ } /** - * Write connection status when response is completed - %X + * Write connection status when response is completed - %X. */ protected static class ConnectionStatusElement implements AccessLogElement { + /** + * Adds the connection status element to the buffer. + * + * @param buf The buffer to which the log element should be added + * @param request The request that triggered this access log entry + * @param response The response to the request that triggered this access log entry + * @param time The time taken in nanoseconds to process the request + */ @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { if (response != null && request != null) { @@ -1695,20 +2037,27 @@ /** - * Write identifier element %{xxx}L + * Write identifier element %{xxx}L. */ protected static class IdentifierElement implements AccessLogElement { /** - * Type of identifier to log + * Type of identifier to log. */ private final IdentifierType identifierType; + /** + * Creates a new identifier element with unknown type. + */ public IdentifierElement() { this(""); } - + /** + * Creates a new identifier element with the specified type. + * + * @param type The identifier type ("c" for connection) + */ public IdentifierElement(String type) { switch (type) { case "c": @@ -1785,6 +2134,13 @@ } + /** + * Create an array of cached elements from the given access log elements. + * + * @param elements The access log elements + * + * @return the cached elements array + */ private CachedElement[] createCachedElements(AccessLogElement[] elements) { List list = new ArrayList<>(); for (AccessLogElement element : elements) { @@ -1933,10 +2289,24 @@ * Reviewing the httpd code, characters with the high bit set are escaped. The httpd is assuming a single byte * encoding which may not be true for Tomcat so Tomcat uses the Java \\uXXXX encoding. */ + /** + * Escapes and appends the input string to the destination writer. + * + * @param input The input string + * @param dest The destination writer + */ protected static void escapeAndAppend(String input, CharArrayWriter dest) { escapeAndAppend(input, dest, false); } + + /** + * Escapes and appends the input string to the destination writer. + * + * @param input The input string + * @param dest The destination writer + * @param escapeQuoteAsDouble Whether to escape quotes as double quotes + */ protected static void escapeAndAppend(String input, CharArrayWriter dest, boolean escapeQuoteAsDouble) { if (input == null || input.isEmpty()) { dest.append('-'); diff -Nru tomcat11-11.0.15/java/org/apache/catalina/valves/ExtendedAccessLogValve.java tomcat11-11.0.22/java/org/apache/catalina/valves/ExtendedAccessLogValve.java --- tomcat11-11.0.15/java/org/apache/catalina/valves/ExtendedAccessLogValve.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/valves/ExtendedAccessLogValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -588,12 +588,11 @@ return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { - String query = request.getQueryString(); - if (query != null) { - buf.append(query); - } else { - buf.append('-'); + String query = null; + if (request != null) { + query = request.getQueryString(); } + appendQueryString(query, buf, false, true, true); } }; } @@ -601,11 +600,11 @@ return new AccessLogElement() { @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { - String query = request.getQueryString(); - buf.append(request.getRequestURI()); - if (query != null) { - buf.append('?'); - buf.append(request.getQueryString()); + if (request != null) { + escapeAndAppend(request.getRequestURI(), buf); + appendQueryString(request.getQueryString(), buf, true, true, false); + } else { + buf.append('-'); } } }; diff -Nru tomcat11-11.0.15/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java tomcat11-11.0.22/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java --- tomcat11-11.0.15/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -208,11 +208,11 @@ response.addCookie(sessionCookie); } + String uri = collapseLeadingSlashes(request.getRequestURI()); // Re-write the URI if it contains a ;jsessionid parameter - String uri = request.getRequestURI(); String sessionURIParamName = SessionConfig.getSessionUriParamName(request.getContext()); if (uri.contains(";" + sessionURIParamName + "=")) { - uri = uri.replaceFirst(";" + sessionURIParamName + "=[^&?]*", ""); + uri = uri.replaceFirst(";" + sessionURIParamName + "=[^;/]*", ""); } String queryString = request.getQueryString(); @@ -223,10 +223,32 @@ // NOTE: Do not call response.encodeRedirectURL or the bad // sessionid will be restored - response.setHeader("Location", uri); - response.setStatus(_redirectStatusCode); + response.sendRedirect(uri, _redirectStatusCode); } else { getNext().invoke(request, response); } } + + private static String collapseLeadingSlashes(String s) { + final int len = s.length(); + int i = 0; + + // Find the last consecutive / character + while (i < len && s.charAt(i) == '/') { + i++; + } + + // No leading slashes + if (i == 0) { + return s; + } + + // Nothing but slashes + if (i == len) { + return "/"; + } + + // Multiple; remove all but one + return s.substring(i - 1); + } } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/valves/LocalStrings_fr.properties tomcat11-11.0.22/java/org/apache/catalina/valves/LocalStrings_fr.properties --- tomcat11-11.0.15/java/org/apache/catalina/valves/LocalStrings_fr.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/valves/LocalStrings_fr.properties 2026-05-01 18:56:05.000000000 +0000 @@ -176,6 +176,6 @@ sslValve.invalidProvider=Le fournisseur SSL spécifié pour le connecteur associé avec cette requête de [{0}] est invalide, le certificat n''a pas pu être traité stuckThreadDetectionValve.interrupted=Le fil d'exécution a été interrompu après la fin de la requête, cela sera ignoré -stuckThreadDetectionValve.notifyStuckThreadCompleted=Le Thread [{0}] (id=[{3}]) qui a été préalablement rapporté comme étant bloqué s''est terminé, il a été actif pendant approximativement [{1}] millisecondes, il y a [{2}] thread(s) au total qui sont surveillés par cette valve et qui pourraient être bloqués +stuckThreadDetectionValve.notifyStuckThreadCompleted=Le Thread [{0}] (id=[{3}]) qui a été préalablement rapporté comme étant bloqué s''est terminé, il a été actif pendant approximativement [{1}] millisecondes. {2,choice,0#|0< Il y a [{2}] thread(s) au total qui sont surveillés par cette valve et qui pourraient être bloqués.} stuckThreadDetectionValve.notifyStuckThreadDetected=Le Thread [{0}] (id=[{6}]) a été actif depuis [{1}] millisecondes (depuis [{2}]) pour traiter la même requête pour [{4}] et pourrait être bloqué (le seuil configurable est de [{5}] secondes pour cette StuckThreadDetectionValve), il y a [{3}] thread(s) au total qui sont surveillés par cette valve et qui pourraient être bloqués stuckThreadDetectionValve.notifyStuckThreadInterrupted=Le Thread [{0}] (id=[{5}]) a été interrompu car il a été actif depuis [{1}] millisecondes (depuis [{2}]) pour traiter la même requête pour [{3}] et était probablement bloqué (le seuil configurable est de [{4}] secondes pour cette StuckThreadDetectionValve) diff -Nru tomcat11-11.0.15/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties tomcat11-11.0.22/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties --- tomcat11-11.0.15/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties 2026-05-01 18:56:05.000000000 +0000 @@ -131,6 +131,7 @@ jdbcAccessLogValve.close=无法关闭数据库。 jdbcAccessLogValve.exception=执行插入访问项时发生异常 +persistentValve.acquireInterrupted=对 [{0}] 的请求未能获取每个会话的信号量,因为在等待许可时被中断。 persistentValve.filter.failure=无法编译filter=[{0}] remoteCidrValve.invalid=为[{0}]提供的配置无效。有关详细信息,请参阅以前的消息 @@ -149,6 +150,6 @@ sslValve.certError=无法处理证书字符串[{0}]以创建java.security.cert.X509Certificate对象 sslValve.invalidProvider=与此[{0}]请求关联的连接器上指定的SSL提供程序无效。 无法处理证书数据。 -stuckThreadDetectionValve.notifyStuckThreadCompleted=线程[{0}](id=[{3}])之前报告为卡住,但是已经完成。它活跃了大概[{1}]毫秒。{2,选择,0#|0< 仍有[{2}]个被Valve监控的线程可能卡住} +stuckThreadDetectionValve.notifyStuckThreadCompleted=线程[{0}](id=[{3}])之前报告为卡住,但是已经完成。它活跃了大概[{1}]毫秒。{2,choice,0#|0< 仍有[{2}]个被Valve监控的线程可能卡住} stuckThreadDetectionValve.notifyStuckThreadDetected=线程[{0}](id=[{6}])已处于活动状态[{1}]毫秒(自[{2}]起),以便为[{4}]提供相同的请求,并且可能被卡住(此StuckThreadDetectionValve的配置阈值为[{5}]秒)。总共有[{3}]个线程受此阀监视,可能被卡住。 stuckThreadDetectionValve.notifyStuckThreadInterrupted=线程[{0}](id=[{5}])已被中断,因为它在[{1}]毫秒(自[{2}]起)内处于活动状态,以便为[{3}]提供相同的请求,并且可能被卡住(此StuckThreadDetectionValve的配置中断阈值为[{4}]秒)。 diff -Nru tomcat11-11.0.15/java/org/apache/catalina/valves/rewrite/RewriteValve.java tomcat11-11.0.22/java/org/apache/catalina/valves/rewrite/RewriteValve.java --- tomcat11-11.0.15/java/org/apache/catalina/valves/rewrite/RewriteValve.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/valves/rewrite/RewriteValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -155,7 +155,6 @@ InputStream is = null; // Process configuration file for this valve - // Process configuration file for this valve if (getContainer() instanceof Context) { context = true; String webInfResourcePath = "/WEB-INF/" + resourcePath; diff -Nru tomcat11-11.0.15/java/org/apache/catalina/webresources/AbstractArchiveResource.java tomcat11-11.0.22/java/org/apache/catalina/webresources/AbstractArchiveResource.java --- tomcat11-11.0.15/java/org/apache/catalina/webresources/AbstractArchiveResource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/webresources/AbstractArchiveResource.java 2026-05-01 18:56:05.000000000 +0000 @@ -30,6 +30,9 @@ import org.apache.catalina.util.URLEncoder; +/** + * Abstract resource implementation for archive-based resources. + */ public abstract class AbstractArchiveResource extends AbstractResource { private final AbstractArchiveResourceSet archiveResourceSet; @@ -42,6 +45,11 @@ /* * Deprecated even though this is the "new" constructor as code needs to call the old constructor for now. + * + * @param archiveResourceSet The archive resource set + * @param webAppPath The web application path + * @param baseUrl The base URL + * @param jarEntry The JAR entry */ @Deprecated protected AbstractArchiveResource(AbstractArchiveResourceSet archiveResourceSet, String webAppPath, String baseUrl, @@ -54,6 +62,12 @@ * from the JRE and it has been confirmed that the JRE no longer depends on code base. * * See https://bz.apache.org/bugzilla/show_bug.cgi?id=69426 + * + * @param archiveResourceSet The archive resource set + * @param webAppPath The web application path + * @param baseUrl The base URL + * @param jarEntry The JAR entry + * @param codeBaseUrl The code base URL */ protected AbstractArchiveResource(AbstractArchiveResourceSet archiveResourceSet, String webAppPath, String baseUrl, JarEntry jarEntry, String codeBaseUrl) { @@ -80,18 +94,38 @@ } } + /** + * Returns the archive resource set. + * + * @return the archive resource set + */ protected AbstractArchiveResourceSet getArchiveResourceSet() { return archiveResourceSet; } + /** + * Returns the base. + * + * @return the base + */ protected final String getBase() { return archiveResourceSet.getBase(); } + /** + * Returns the base URL. + * + * @return the base URL + */ protected final String getBaseUrl() { return baseUrl; } + /** + * Returns the resource. + * + * @return the resource + */ protected final JarEntry getResource() { return resource; } @@ -261,6 +295,12 @@ private final AtomicBoolean closed = new AtomicBoolean(false); + /** + * Creates a new JarInputStreamWrapper. + * + * @param jarEntry The JAR entry + * @param is The input stream + */ public JarInputStreamWrapper(JarEntry jarEntry, InputStream is) { this.jarEntry = jarEntry; this.is = is; @@ -324,6 +364,11 @@ return is.markSupported(); } + /** + * Returns the certificates. + * + * @return the certificates + */ public Certificate[] getCertificates() { return jarEntry.getCertificates(); } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java tomcat11-11.0.22/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java --- tomcat11-11.0.15/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java 2026-05-01 18:56:05.000000000 +0000 @@ -34,19 +34,45 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; +/** + * Abstract resource set implementation for archive-based resources. + */ public abstract class AbstractArchiveResourceSet extends AbstractResourceSet { private static final Log log = LogFactory.getLog(AbstractArchiveResourceSet.class); private URL baseUrl; private String baseUrlString; + /** + * The archive JAR file. + */ protected JarFile archive = null; + /** + * The archive entries. + */ protected Map archiveEntries = null; + /** + * The lock for archive operations. + */ protected final Object archiveLock = new Object(); + /** + * The archive use count. + */ protected long archiveUseCount = 0; + /** + * The JAR contents. + */ protected JarContents jarContents; + /** + * Whether to retain the bloom filter for archives. + */ protected boolean retainBloomFilterForArchives = false; + /** + * Sets the base URL. + * + * @param baseUrl The base URL + */ protected final void setBaseUrl(URL baseUrl) { this.baseUrl = baseUrl; if (baseUrl == null) { @@ -61,6 +87,11 @@ return baseUrl; } + /** + * Returns the base URL string. + * + * @return the base URL string + */ protected final String getBaseUrlString() { return baseUrlString; } @@ -279,8 +310,21 @@ } } + /** + * Checks if this is a multi-release JAR. + * + * @return true if this is a multi-release JAR, false otherwise + */ protected abstract boolean isMultiRelease(); + /** + * Creates an archive resource. + * + * @param jarEntry The JAR entry + * @param webAppPath The web application path + * @param manifest The manifest + * @return the archive resource + */ protected abstract WebResource createArchiveResource(JarEntry jarEntry, String webAppPath, Manifest manifest); @Override @@ -332,6 +376,9 @@ } } + /** + * Closes the JAR file. + */ protected void closeJarFile() { synchronized (archiveLock) { archiveUseCount--; diff -Nru tomcat11-11.0.15/java/org/apache/catalina/webresources/AbstractResource.java tomcat11-11.0.22/java/org/apache/catalina/webresources/AbstractResource.java --- tomcat11-11.0.15/java/org/apache/catalina/webresources/AbstractResource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/webresources/AbstractResource.java 2026-05-01 18:56:05.000000000 +0000 @@ -151,6 +151,11 @@ return new TrackedInputStream(root, getName(), is); } + /** + * Returns the input stream for this resource. + * + * @return the input stream for this resource + */ protected abstract InputStream doGetInputStream(); diff -Nru tomcat11-11.0.15/java/org/apache/catalina/webresources/CachedResource.java tomcat11-11.0.22/java/org/apache/catalina/webresources/CachedResource.java --- tomcat11-11.0.15/java/org/apache/catalina/webresources/CachedResource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/webresources/CachedResource.java 2026-05-01 18:56:05.000000000 +0000 @@ -663,5 +663,10 @@ return ((JarURLConnection) resourceURL.openConnection()).getJarFile(); } + @Override + public String getContentType() { + // "content/unknown" is the value used by sun.net.www.URLConnection. It is used here for consistency. + return Objects.requireNonNullElse(getResource().getMimeType(), "content/unknown"); + } } } diff -Nru tomcat11-11.0.15/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties tomcat11-11.0.22/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties --- tomcat11-11.0.15/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties 2026-05-01 18:56:05.000000000 +0000 @@ -60,4 +60,5 @@ standardRoot.invalidPathNormal=资源路径[{0}]已规范化为无效的[{1}] standardRoot.lockedFile=Web应用程序[{0}]无法关闭通过以下堆栈跟踪打开的文件[{1}] standardRoot.noContext=尚未为WebResourceRoot配置上下文 +standardRoot.startInvalidMain=指定的主要资源集合 [{0}] 不是目录或 WAR 文件,或者不可读取(它不存在或缺少对应的访问权限) standardRoot.unsupportedProtocol=此web资源实现不支持URL协议[{0}] diff -Nru tomcat11-11.0.15/java/org/apache/coyote/AbstractProtocol.java tomcat11-11.0.22/java/org/apache/coyote/AbstractProtocol.java --- tomcat11-11.0.15/java/org/apache/coyote/AbstractProtocol.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/AbstractProtocol.java 2026-05-01 18:56:05.000000000 +0000 @@ -59,13 +59,11 @@ */ private static final StringManager sm = StringManager.getManager(AbstractProtocol.class); - /** * Counter used to generate unique JMX names for connectors using automatic port binding. */ private static final AtomicInteger nameCounter = new AtomicInteger(0); - /** * Unique ID for this connector. Only used if the connector is configured to use a random port as the port will * change if stop(), start() is called. @@ -91,6 +89,11 @@ private ScheduledFuture timeoutFuture = null; private ScheduledFuture monitorFuture; + /** + * Creates a new protocol handler. + * + * @param endpoint The endpoint for low-level network I/O + */ public AbstractProtocol(AbstractEndpoint endpoint) { this.endpoint = endpoint; ConnectionHandler cHandler = new ConnectionHandler<>(this); @@ -137,6 +140,11 @@ */ protected ObjectName rgOname = null; + /** + * Gets the MBean name for the Global Request Processor. + * + * @return the MBean name + */ public ObjectName getGlobalRequestProcessorMBeanName() { return rgOname; } @@ -146,11 +154,21 @@ */ protected Adapter adapter; + /** + * Sets the adapter. + * + * @param adapter The adapter + */ @Override public void setAdapter(Adapter adapter) { this.adapter = adapter; } + /** + * Gets the adapter. + * + * @return the adapter + */ @Override public Adapter getAdapter() { return adapter; @@ -165,10 +183,20 @@ */ protected int processorCache = 200; + /** + * Gets the processor cache size. + * + * @return the processor cache size + */ public int getProcessorCache() { return this.processorCache; } + /** + * Sets the maximum number of idle processors to cache. + * + * @param processorCache The processor cache size (-1 for unlimited) + */ public void setProcessorCache(int processorCache) { this.processorCache = processorCache; } @@ -188,6 +216,11 @@ return clientCertProvider; } + /** + * Sets the JSSE provider to use for client certificate conversion. + * + * @param s The provider name + */ public void setClientCertProvider(String s) { this.clientCertProvider = s; } @@ -195,21 +228,40 @@ private int maxHeaderCount = 100; + /** + * Gets the maximum header count. + * + * @return the maximum header count + */ public int getMaxHeaderCount() { return maxHeaderCount; } + /** + * Sets the maximum header count. + * + * @param maxHeaderCount The maximum header count + */ public void setMaxHeaderCount(int maxHeaderCount) { this.maxHeaderCount = maxHeaderCount; } + /** + * Checks if sendfile is supported. + * + * @return true if sendfile is supported + */ @Override public boolean isSendfileSupported() { return endpoint.getUseSendfile(); } - + /** + * Gets the protocol ID. + * + * @return the protocol ID + */ @Override public String getId() { return endpoint.getId(); @@ -218,11 +270,22 @@ // ---------------------- Properties that are passed through to the EndPoint + + /** + * Gets the executor for this protocol handler. + * + * @return the executor + */ @Override public Executor getExecutor() { return endpoint.getExecutor(); } + /** + * Sets the executor for this protocol handler. + * + * @param executor The executor + */ @Override public void setExecutor(Executor executor) { endpoint.setExecutor(executor); @@ -240,71 +303,148 @@ } + /** + * Gets the maximum number of threads. + * + * @return the maximum number of threads + */ public int getMaxThreads() { return endpoint.getMaxThreads(); } + /** + * Sets the maximum number of threads. + * + * @param maxThreads The maximum number of threads + */ public void setMaxThreads(int maxThreads) { endpoint.setMaxThreads(maxThreads); } + /** + * Gets the maximum number of connections. + * + * @return the maximum number of connections + */ public int getMaxConnections() { return endpoint.getMaxConnections(); } + /** + * Sets the maximum number of connections. + * + * @param maxConnections The maximum number of connections + */ public void setMaxConnections(int maxConnections) { endpoint.setMaxConnections(maxConnections); } - + /** + * Gets the minimum number of spare threads. + * + * @return the minimum spare threads + */ public int getMinSpareThreads() { return endpoint.getMinSpareThreads(); } + /** + * Sets the minimum number of spare threads. + * + * @param minSpareThreads The minimum spare threads + */ public void setMinSpareThreads(int minSpareThreads) { endpoint.setMinSpareThreads(minSpareThreads); } - + /** + * Gets the thread priority. + * + * @return the thread priority + */ public int getThreadPriority() { return endpoint.getThreadPriority(); } + /** + * Sets the thread priority. + * + * @param threadPriority The thread priority + */ public void setThreadPriority(int threadPriority) { endpoint.setThreadPriority(threadPriority); } - + /** + * Gets the maximum queue size. + * + * @return the maximum queue size + */ public int getMaxQueueSize() { return endpoint.getMaxQueueSize(); } + /** + * Sets the maximum queue size. + * + * @param maxQueueSize The maximum queue size + */ public void setMaxQueueSize(int maxQueueSize) { endpoint.setMaxQueueSize(maxQueueSize); } + /** + * Gets the accept count. + * + * @return the accept count + */ public int getAcceptCount() { return endpoint.getAcceptCount(); } + /** + * Sets the accept count. + * + * @param acceptCount The accept count + */ public void setAcceptCount(int acceptCount) { endpoint.setAcceptCount(acceptCount); } + /** + * Gets whether TCP no-delay is enabled. + * + * @return true if TCP no-delay is enabled + */ public boolean getTcpNoDelay() { return endpoint.getTcpNoDelay(); } + /** + * Sets whether TCP no-delay is enabled. + * + * @param tcpNoDelay true to enable TCP no-delay + */ public void setTcpNoDelay(boolean tcpNoDelay) { endpoint.setTcpNoDelay(tcpNoDelay); } - + /** + * Gets the connection linger time. + * + * @return the connection linger time + */ public int getConnectionLinger() { return endpoint.getConnectionLinger(); } + + /** + * Sets the connection linger time. + * + * @param connectionLinger The connection linger time + */ public void setConnectionLinger(int connectionLinger) { endpoint.setConnectionLinger(connectionLinger); } @@ -320,66 +460,143 @@ return endpoint.getKeepAliveTimeout(); } + + /** + * Sets the keep-alive timeout. + * + * @param keepAliveTimeout The keep-alive timeout in milliseconds + */ public void setKeepAliveTimeout(int keepAliveTimeout) { endpoint.setKeepAliveTimeout(keepAliveTimeout); } + + /** + * Gets the address. + * + * @return the address + */ public InetAddress getAddress() { return endpoint.getAddress(); } + + /** + * Sets the address. + * + * @param ia The address + */ public void setAddress(InetAddress ia) { endpoint.setAddress(ia); } + /** + * Gets the port. + * + * @return the port + */ public int getPort() { return endpoint.getPort(); } + + /** + * Sets the port. + * + * @param port The port + */ public void setPort(int port) { endpoint.setPort(port); } + /** + * Gets the port offset. + * + * @return the port offset + */ public int getPortOffset() { return endpoint.getPortOffset(); } + + /** + * Sets the port offset. + * + * @param portOffset The port offset + */ public void setPortOffset(int portOffset) { endpoint.setPortOffset(portOffset); } - + /** + * Gets the port with offset applied. + * + * @return the port with offset + */ public int getPortWithOffset() { return endpoint.getPortWithOffset(); } + /** + * Gets the local port. + * + * @return the local port + */ public int getLocalPort() { return endpoint.getLocalPort(); } - /* + + /** + * Gets the connection timeout. * When Tomcat expects data from the client, this is the time Tomcat will wait for that data to arrive before * closing the connection. + * + * @return the connection timeout */ public int getConnectionTimeout() { return endpoint.getConnectionTimeout(); } + + /** + * Sets the connection timeout. + * + * @param timeout The connection timeout in milliseconds + */ public void setConnectionTimeout(int timeout) { endpoint.setConnectionTimeout(timeout); } + + /** + * Gets the connection count. + * + * @return the connection count + */ public long getConnectionCount() { return endpoint.getConnectionCount(); } + + /** + * Sets the acceptor thread priority. + * + * @param threadPriority The thread priority + */ public void setAcceptorThreadPriority(int threadPriority) { endpoint.setAcceptorThreadPriority(threadPriority); } + + /** + * Gets the acceptor thread priority. + * + * @return the acceptor thread priority + */ public int getAcceptorThreadPriority() { return endpoint.getAcceptorThreadPriority(); } @@ -387,6 +604,12 @@ // ---------------------------------------------------------- Public methods + + /** + * Gets the name index for this protocol. + * + * @return the name index + */ public synchronized int getNameIndex() { if (nameIndex == 0) { nameIndex = nameCounter.incrementAndGet(); @@ -397,6 +620,7 @@ /** + * Gets the name of this protocol instance. * The name will be prefix-address-port if address is non-null and prefix-port if the address is null. * * @return A name for this protocol instance that is appropriately quoted for use in an ObjectName. @@ -435,6 +659,11 @@ } + /** + * Adds a processor to the waiting processors set. + * + * @param processor The processor + */ public void addWaitingProcessor(Processor processor) { if (getLog().isTraceEnabled()) { getLog().trace(sm.getString("abstractProtocol.waitingProcessor.add", processor)); @@ -443,6 +672,11 @@ } + /** + * Removes a processor from the waiting processors set. + * + * @param processor The processor + */ public void removeWaitingProcessor(Processor processor) { boolean result = waitingProcessors.remove(processor); if (getLog().isTraceEnabled()) { @@ -455,6 +689,11 @@ /* * Primarily for debugging and testing. Could be exposed via JMX if considered useful. */ + /** + * Gets the count of waiting processors. + * + * @return the waiting processor count + */ public int getWaitingProcessorCount() { return waitingProcessors.size(); } @@ -462,15 +701,29 @@ // ----------------------------------------------- Accessors for sub-classes + /** + * Gets the endpoint. + * + * @return the endpoint + */ protected AbstractEndpoint getEndpoint() { return endpoint; } - + /** + * Gets the handler. + * + * @return the handler + */ public Handler getHandler() { return handler; } + /** + * Sets the handler. + * + * @param handler The handler + */ protected void setHandler(Handler handler) { this.handler = handler; } @@ -532,6 +785,13 @@ protected abstract Processor createProcessor(); + /** + * Create and configure a new Processor instance for upgrade connections. + * + * @param socket The socket for the upgrade connection + * @param upgradeToken The upgrade token containing upgrade information + * @return A fully configured Processor instance that is ready to use + */ protected abstract Processor createUpgradeProcessor(SocketWrapperBase socket, UpgradeToken upgradeToken); @@ -541,14 +801,35 @@ protected ObjectName oname; protected MBeanServer mserver; + /** + * Gets the object name. + * + * @return the object name + */ public ObjectName getObjectName() { return oname; } + /** + * Gets the domain. + * + * @return the domain + */ public String getDomain() { return domain; } + + /** + * Pre-registers this MBean. + * + * @param server The MBean server + * @param name The object name + * + * @return the object name + * + * @throws Exception if registration fails + */ @Override public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception { oname = name; @@ -557,16 +838,29 @@ return name; } + /** + * Post-registers this MBean. + * + * @param registrationDone The registration status + */ @Override public void postRegister(Boolean registrationDone) { // NOOP } + /** + * Pre-deregisters this MBean. + * + * @throws Exception if deregistration fails + */ @Override public void preDeregister() throws Exception { // NOOP } + /** + * Post-deregisters this MBean. + */ @Override public void postDeregister() { // NOOP @@ -605,6 +899,11 @@ * the connector will maintain state and prevent invalid state transitions. */ + /** + * Initializes the protocol handler. + * + * @throws Exception if initialization fails + */ @Override public void init() throws Exception { if (getLog().isInfoEnabled()) { @@ -634,6 +933,11 @@ } + /** + * Starts the protocol handler. + * + * @throws Exception if start fails + */ @Override public void start() throws Exception { if (getLog().isInfoEnabled()) { @@ -676,6 +980,11 @@ } } + /** + * Pauses the protocol handler. + * + * @throws Exception if pause fails + */ @Override public void pause() throws Exception { if (getLog().isInfoEnabled()) { @@ -686,11 +995,21 @@ } + /** + * Checks if the protocol handler is paused. + * + * @return true if paused + */ public boolean isPaused() { return endpoint.isPaused(); } + /** + * Resumes the protocol handler. + * + * @throws Exception if resume fails + */ @Override public void resume() throws Exception { if (getLog().isInfoEnabled()) { @@ -701,6 +1020,11 @@ } + /** + * Stops the protocol handler. + * + * @throws Exception if stop fails + */ @Override public void stop() throws Exception { if (getLog().isInfoEnabled()) { @@ -722,6 +1046,11 @@ } + /** + * Destroys the protocol handler. + * + * @throws Exception if destroy fails + */ @Override public void destroy() throws Exception { if (getLog().isInfoEnabled()) { @@ -753,12 +1082,22 @@ } + /** + * Closes the server socket gracefully. + */ @Override public void closeServerSocketGraceful() { endpoint.closeServerSocketGraceful(); } + /** + * Awaits for connections to close. + * + * @param waitMillis The maximum time to wait in milliseconds + * + * @return the number of connections remaining after the wait + */ @Override public long awaitConnectionsClose(long waitMillis) { getLog().info(sm.getString("abstractProtocol.closeConnectionsAwait", Long.valueOf(waitMillis), getName())); @@ -783,29 +1122,59 @@ private final AtomicLong registerCount = new AtomicLong(0); private final RecycledProcessors recycledProcessors = new RecycledProcessors(this); + /** + * Creates a new connection handler. + * + * @param proto The protocol + */ public ConnectionHandler(AbstractProtocol proto) { this.proto = proto; } + /** + * Gets the protocol. + * + * @return the protocol + */ protected AbstractProtocol getProtocol() { return proto; } + /** + * Gets the logger. + * + * @return the logger + */ protected Log getLog() { return getProtocol().getLog(); } + /** + * Gets the global request processor. + * + * @return the global request processor + */ @Override public Object getGlobal() { return global; } + /** + * Recycles the handler. + */ @Override public void recycle() { recycledProcessors.clear(); } - + /** + * Processes a socket event. + * + * @param wrapper The socket wrapper + * @param status The socket event + * + * @return the socket state + */ @Override public SocketState process(SocketWrapperBase wrapper, SocketEvent status) { if (getLog().isTraceEnabled()) { @@ -1074,6 +1443,12 @@ } + /** + * Performs a long poll on the socket. + * + * @param socket The socket wrapper + * @param processor The processor + */ protected void longPoll(SocketWrapperBase socket, Processor processor) { if (!processor.isAsync()) { // This is currently only used with HTTP @@ -1119,6 +1494,11 @@ } + /** + * Releases the socket wrapper. + * + * @param socketWrapper The socket wrapper + */ @Override public void release(SocketWrapperBase socketWrapper) { Processor processor = (Processor) socketWrapper.takeCurrentProcessor(); @@ -1126,6 +1506,11 @@ } + /** + * Registers a processor. + * + * @param processor The processor + */ protected void register(Processor processor) { if (getProtocol().getDomain() != null) { synchronized (this) { @@ -1148,6 +1533,11 @@ } } + /** + * Unregisters a processor. + * + * @param processor The processor + */ protected void unregister(Processor processor) { if (getProtocol().getDomain() != null) { synchronized (this) { @@ -1172,6 +1562,9 @@ } } + /** + * Pauses all processors. + */ @Override public final void pause() { /* @@ -1195,10 +1588,22 @@ private final transient ConnectionHandler handler; protected final AtomicInteger size = new AtomicInteger(0); + /** + * Creates a new recycled processors pool. + * + * @param handler The connection handler + */ public RecycledProcessors(ConnectionHandler handler) { this.handler = handler; } + /** + * Pushes a processor to the pool. + * + * @param processor The processor + * + * @return true if the processor was pushed + */ @SuppressWarnings("sync-override") // Size may exceed cache size a bit @Override public boolean push(Processor processor) { @@ -1218,6 +1623,11 @@ return result; } + /** + * Pops a processor from the pool. + * + * @return the processor or null if empty + */ @SuppressWarnings("sync-override") // OK if size is too big briefly @Override public Processor pop() { @@ -1228,6 +1638,9 @@ return result; } + /** + * Clears the recycled processors pool. + */ @Override public synchronized void clear() { Processor next = pop(); diff -Nru tomcat11-11.0.15/java/org/apache/coyote/CompressionConfig.java tomcat11-11.0.22/java/org/apache/coyote/CompressionConfig.java --- tomcat11-11.0.15/java/org/apache/coyote/CompressionConfig.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/CompressionConfig.java 2026-05-01 18:56:05.000000000 +0000 @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; +import java.util.Arrays; import java.util.Enumeration; import java.util.HashSet; import java.util.List; @@ -47,6 +48,35 @@ "text/javascript,application/javascript,application/json,application/xml"; private String[] compressibleMimeTypes = null; private int compressionMinSize = 2048; + private Set noCompressionEncodings = + new HashSet<>(Arrays.asList("br", "compress", "dcb", "dcz", "deflate", "gzip", "pack200-gzip", "zstd")); + + + public String getNoCompressionEncodings() { + return String.join(",", noCompressionEncodings); + } + + + /** + * Set the list of content encodings that indicate already-compressed content. + * When content is already encoded with one of these encodings, compression will not be applied + * to prevent double compression. + * + * @param encodings Comma-separated list of encoding names (e.g., "gzip,br.dflate") + */ + public void setNoCompressionEncodings(String encodings) { + Set newEncodings = new HashSet<>(); + if (encodings != null && !encodings.isEmpty()) { + StringTokenizer tokens = new StringTokenizer(encodings, ","); + while (tokens.hasMoreTokens()) { + String token = tokens.nextToken().trim(); + if(!token.isEmpty()) { + newEncodings.add(token); + } + } + } + this.noCompressionEncodings = newEncodings; + } /** @@ -210,9 +240,7 @@ if (tokens.contains("identity")) { // If identity, do not do content modifications useContentEncoding = false; - } else if (tokens.contains("br") || tokens.contains("compress") || tokens.contains("dcb") || - tokens.contains("dcz") || tokens.contains("deflate") || tokens.contains("gzip") || - tokens.contains("pack200-gzip") || tokens.contains("zstd")) { + } else if (noCompressionEncodings.stream().anyMatch(tokens::contains)) { // Content should not be compressed twice return false; } diff -Nru tomcat11-11.0.15/java/org/apache/coyote/LocalStrings.properties tomcat11-11.0.22/java/org/apache/coyote/LocalStrings.properties --- tomcat11-11.0.15/java/org/apache/coyote/LocalStrings.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/LocalStrings.properties 2026-05-01 18:56:05.000000000 +0000 @@ -67,6 +67,7 @@ request.nullReadListener=The listener passed to setReadListener() may not be null request.readListenerSet=The non-blocking read listener has already been set +response.contentlength.invalid=The content-length [{0}] is not a valid Long value and has been ignored. The content length is currently unset. response.encoding.invalid=The encoding [{0}] is not recognised by the JRE response.noTrailers.notSupported=A trailer fields supplier may not be set for this response. Either the underlying protocol does not support trailer fields or the protocol requires that the supplier is set before the response is committed response.notAsync=It is only valid to switch to non-blocking IO within async processing or HTTP upgrade processing diff -Nru tomcat11-11.0.15/java/org/apache/coyote/Request.java tomcat11-11.0.22/java/org/apache/coyote/Request.java --- tomcat11-11.0.15/java/org/apache/coyote/Request.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/Request.java 2026-05-01 18:56:05.000000000 +0000 @@ -70,6 +70,10 @@ */ private static final AtomicLong requestIdGenerator = new AtomicLong(0); + // public static final int NOTE_ADAPTER = 1; // Defined in CoyoteAdapter + public static final int NOTE_BAD_REQUEST = 2; + + // ----------------------------------------------------------- Constructors public Request() { diff -Nru tomcat11-11.0.15/java/org/apache/coyote/Response.java tomcat11-11.0.22/java/org/apache/coyote/Response.java --- tomcat11-11.0.15/java/org/apache/coyote/Response.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/Response.java 2026-05-01 18:56:05.000000000 +0000 @@ -410,8 +410,6 @@ * need to set the header. */ private boolean checkSpecialHeader(String name, String value) { - // XXX Eliminate redundant fields !!! - // ( both header and in special fields ) if (name.equalsIgnoreCase("Content-Type")) { setContentType(value); return true; @@ -424,12 +422,11 @@ } long cL = Long.parseLong(value); setContentLength(cL); - return true; } catch (NumberFormatException ex) { - // Do nothing - the spec doesn't have any "throws" - // and the user might know what they're doing - return false; + setContentLength(-1); + log.warn(sm.getString("response.contentlength.invalid", value), ex); } + return true; } return false; } diff -Nru tomcat11-11.0.15/java/org/apache/coyote/UpgradeProtocol.java tomcat11-11.0.22/java/org/apache/coyote/UpgradeProtocol.java --- tomcat11-11.0.15/java/org/apache/coyote/UpgradeProtocol.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/UpgradeProtocol.java 2026-05-01 18:56:05.000000000 +0000 @@ -23,6 +23,8 @@ public interface UpgradeProtocol { /** + * Returns the name that clients will use to request an upgrade to this protocol. + * * @param isSSLEnabled Is this for a connector that is configured to support TLS. Some protocols (e.g. HTTP/2) only * support HTTP upgrade over non-secure connections. * @@ -32,12 +34,16 @@ String getHttpUpgradeName(boolean isSSLEnabled); /** + * Returns the byte sequence as listed in the IANA registry for this protocol. + * * @return The byte sequence as listed in the IANA registry for this protocol or null if upgrade via * ALPN is not supported. */ byte[] getAlpnIdentifier(); /** + * Returns the name of the protocol as listed in the IANA registry. + * * @return The name of the protocol as listed in the IANA registry if and only if {@link #getAlpnIdentifier()} * returns the UTF-8 encoding of this name. If {@link #getAlpnIdentifier()} returns some other byte * sequence, then this method returns the empty string. If upgrade via ALPN is not supported then @@ -53,6 +59,8 @@ String getAlpnName(); /** + * Returns a processor instance for processing a connection using this protocol. + * * @param socketWrapper The socketWrapper for the connection that requires a processor * @param adapter The Adapter instance that provides access to the standard Engine/Host/Context/Wrapper * processing chain @@ -63,6 +71,8 @@ /** + * Returns an instance of the HTTP upgrade handler for this protocol. + * * @param socketWrapper The socket * @param adapter The Adapter to use to configure the new upgrade handler * @param request A copy (may be incomplete) of the request that triggered the upgrade diff -Nru tomcat11-11.0.15/java/org/apache/coyote/ajp/AbstractAjpProtocol.java tomcat11-11.0.22/java/org/apache/coyote/ajp/AbstractAjpProtocol.java --- tomcat11-11.0.15/java/org/apache/coyote/ajp/AbstractAjpProtocol.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/ajp/AbstractAjpProtocol.java 2026-05-01 18:56:05.000000000 +0000 @@ -43,6 +43,11 @@ protected static final StringManager sm = StringManager.getManager(AbstractAjpProtocol.class); + /** + * Creates a new AJP protocol handler. + * + * @param endpoint The endpoint for low-level network I/O + */ public AbstractAjpProtocol(AbstractEndpoint endpoint) { super(endpoint); setConnectionTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT); @@ -53,6 +58,11 @@ } + /** + * Gets the name of the protocol. + * + * @return the protocol name + */ @Override protected String getProtocolName() { return "Ajp"; @@ -90,6 +100,11 @@ private boolean ajpFlush = true; + /** + * Gets whether AJP flush packets are used. + * + * @return true if flush packets are used + */ public boolean getAjpFlush() { return ajpFlush; } @@ -118,6 +133,11 @@ return tomcatAuthentication; } + /** + * Sets whether authentication should be done in Tomcat. + * + * @param tomcatAuthentication {@code true} if authentication should be performed by Tomcat + */ public void setTomcatAuthentication(boolean tomcatAuthentication) { this.tomcatAuthentication = tomcatAuthentication; } @@ -134,6 +154,11 @@ return tomcatAuthorization; } + /** + * Sets whether authorization should be done by Tomcat. + * + * @param tomcatAuthorization {@code true} if authorization should be performed by Tomcat + */ public void setTomcatAuthorization(boolean tomcatAuthorization) { this.tomcatAuthorization = tomcatAuthorization; } @@ -150,6 +175,11 @@ this.secret = secret; } + /** + * Gets the secret that must be included with every request. + * + * @return the secret + */ protected String getSecret() { return secret; } @@ -157,10 +187,20 @@ private boolean secretRequired = true; + /** + * Sets whether a secret is required with every request. + * + * @param secretRequired {@code true} if a secret is required + */ public void setSecretRequired(boolean secretRequired) { this.secretRequired = secretRequired; } + /** + * Gets whether a secret is required with every request. + * + * @return {@code true} if a secret is required + */ public boolean getSecretRequired() { return secretRequired; } @@ -168,14 +208,29 @@ private Pattern allowedRequestAttributesPattern; + /** + * Sets the pattern for allowed request attributes. + * + * @param allowedRequestAttributesPattern The regex pattern + */ public void setAllowedRequestAttributesPattern(String allowedRequestAttributesPattern) { this.allowedRequestAttributesPattern = Pattern.compile(allowedRequestAttributesPattern); } + /** + * Gets the pattern for allowed request attributes. + * + * @return the pattern string + */ public String getAllowedRequestAttributesPattern() { return allowedRequestAttributesPattern.pattern(); } + /** + * Gets the compiled pattern for allowed request attributes. + * + * @return the pattern + */ protected Pattern getAllowedRequestAttributesPatternInternal() { return allowedRequestAttributesPattern; } @@ -186,15 +241,30 @@ */ private int packetSize = Constants.MAX_PACKET_SIZE; + /** + * Gets the AJP packet size. + * + * @return the packet size + */ public int getPacketSize() { return packetSize; } + /** + * Sets the AJP packet size. + * + * @param packetSize The packet size (must be at least MAX_PACKET_SIZE) + */ public void setPacketSize(int packetSize) { this.packetSize = Math.max(packetSize, Constants.MAX_PACKET_SIZE); } + /** + * Gets the desired buffer size for AJP packets. + * + * @return the desired buffer size + */ @Override public int getDesiredBufferSize() { return getPacketSize() - Constants.SEND_HEAD_LEN; @@ -203,42 +273,79 @@ // --------------------------------------------- SSL is not supported in AJP + /** + * Adds an SSL host configuration. AJP does not support SSL so this logs a warning. + * + * @param sslHostConfig The SSL host configuration + */ @Override public void addSslHostConfig(SSLHostConfig sslHostConfig) { getLog().warn(sm.getString("ajpprotocol.noSSL", sslHostConfig.getHostName())); } - + /** + * Adds an SSL host configuration. AJP does not support SSL so this logs a warning. + * + * @param sslHostConfig The SSL host configuration + * @param replace Whether to replace existing configurations + */ @Override public void addSslHostConfig(SSLHostConfig sslHostConfig, boolean replace) { getLog().warn(sm.getString("ajpprotocol.noSSL", sslHostConfig.getHostName())); } - + /** + * Finds SSL host configurations. AJP does not support SSL so this always returns an empty array. + * + * @return an empty array + */ @Override public SSLHostConfig[] findSslHostConfigs() { return new SSLHostConfig[0]; } - + /** + * Adds an upgrade protocol. AJP does not support upgrade so this logs a warning. + * + * @param upgradeProtocol The upgrade protocol + */ @Override public void addUpgradeProtocol(UpgradeProtocol upgradeProtocol) { getLog().warn(sm.getString("ajpprotocol.noUpgrade", upgradeProtocol.getClass().getName())); } - + /** + * Finds upgrade protocols. AJP does not support upgrade so this always returns an empty array. + * + * @return an empty array + */ @Override public UpgradeProtocol[] findUpgradeProtocols() { return new UpgradeProtocol[0]; } + /** + * Creates a new AJP processor. + * + * @return the processor + */ @Override protected Processor createProcessor() { return new AjpProcessor(this, getAdapter()); } + /** + * Creates an upgrade processor. AJP does not support upgrade so this always throws. + * + * @param socket The socket wrapper + * @param upgradeToken The upgrade token + * + * @return never returns + * + * @throws IllegalStateException always + */ @Override protected Processor createUpgradeProcessor(SocketWrapperBase socket, UpgradeToken upgradeToken) { throw new IllegalStateException( @@ -246,6 +353,11 @@ } + /** + * Starts the AJP protocol handler. Validates that a secret is configured if required. + * + * @throws Exception if start fails + */ @Override public void start() throws Exception { if (getSecretRequired()) { diff -Nru tomcat11-11.0.15/java/org/apache/coyote/ajp/AjpProcessor.java tomcat11-11.0.22/java/org/apache/coyote/ajp/AjpProcessor.java --- tomcat11-11.0.15/java/org/apache/coyote/ajp/AjpProcessor.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/ajp/AjpProcessor.java 2026-05-01 18:56:05.000000000 +0000 @@ -55,6 +55,7 @@ import org.apache.tomcat.util.net.SSLSupport; import org.apache.tomcat.util.net.SocketWrapperBase; import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.util.security.ConstantTime; /** * AJP Processor implementation. @@ -820,7 +821,7 @@ requestHeaderMessage.getBytes(tmpMB); if (secret != null && !secret.isEmpty()) { secretPresentInRequest = true; - if (!tmpMB.equals(secret)) { + if (!ConstantTime.equals(tmpMB.getByteChunk(), secret)) { response.setStatus(403); setErrorState(ErrorState.CLOSE_CLEAN, null); } diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http11/AbstractHttp11Protocol.java tomcat11-11.0.22/java/org/apache/coyote/http11/AbstractHttp11Protocol.java --- tomcat11-11.0.15/java/org/apache/coyote/http11/AbstractHttp11Protocol.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http11/AbstractHttp11Protocol.java 2026-05-01 18:56:05.000000000 +0000 @@ -340,6 +340,15 @@ } + public String getNoCompressionEncodings() { + return compressionConfig.getNoCompressionEncodings(); + } + + public void setNoCompressionEncodings(String encodings) { + compressionConfig.setNoCompressionEncodings(encodings); + } + + public boolean useCompression(Request request, Response response) { return compressionConfig.useCompression(request, response); } diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http11/LocalStrings_fr.properties tomcat11-11.0.22/java/org/apache/coyote/http11/LocalStrings_fr.properties --- tomcat11-11.0.15/java/org/apache/coyote/http11/LocalStrings_fr.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http11/LocalStrings_fr.properties 2026-05-01 18:56:05.000000000 +0000 @@ -39,6 +39,7 @@ http11processor.request.nonNumericContentLength=La requête contenait un en-tête content-length avec une valeur non numérique http11processor.request.prepare=Echec de préparation de la requête http11processor.request.process=Erreur de traitement de la requête +http11processor.request.sni=L'en-tête hôte ne correspond pas à l'hôte SNI http11processor.request.unsupportedEncoding=Erreur lors de la préparation de la requête, l''encodage de transfert [{0}] n''est pas supporté http11processor.request.unsupportedVersion=Erreur lors de la préparation de la requête, la version HTTP [{0}] n''est pas supportée http11processor.response.finish=Erreur en finissant la réponse diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http11/LocalStrings_ja.properties tomcat11-11.0.22/java/org/apache/coyote/http11/LocalStrings_ja.properties --- tomcat11-11.0.15/java/org/apache/coyote/http11/LocalStrings_ja.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http11/LocalStrings_ja.properties 2026-05-01 18:56:05.000000000 +0000 @@ -39,6 +39,7 @@ http11processor.request.nonNumericContentLength=リクエストの content-length ヘッダに数値でない値が含まれています http11processor.request.prepare=リクエスト準備中のエラー http11processor.request.process=リクエスト処理中のエラー +http11processor.request.sni=HostヘッダがSNIホストと一致しません http11processor.request.unsupportedEncoding=リクエストの準備中にエラーが発生しました。サポートされていない Transfer-Encodng\n\ \ [{0}] http11processor.request.unsupportedVersion=リクエストの準備中にエラーが発生しました。サポートされていない HTTP バージョン [{0}] diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java tomcat11-11.0.22/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java --- tomcat11-11.0.15/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java 2026-05-01 18:56:05.000000000 +0000 @@ -30,6 +30,8 @@ import org.apache.coyote.http11.InputFilter; import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.http.parser.ChunkExtension; +import org.apache.tomcat.util.http.parser.ChunkExtension.State; import org.apache.tomcat.util.http.parser.HttpHeaderParser; import org.apache.tomcat.util.http.parser.HttpHeaderParser.HeaderDataSource; import org.apache.tomcat.util.http.parser.HttpHeaderParser.HeaderParseStatus; @@ -106,7 +108,7 @@ private volatile ParseState parseState = ParseState.CHUNK_HEADER; private volatile boolean crFound = false; private volatile int chunkSizeDigitsRead = 0; - private volatile boolean parsingExtension = false; + private volatile State extensionState = null; private final AtomicLong extensionSize = new AtomicLong(0); private final HttpHeaderParser httpHeaderParser; @@ -251,7 +253,7 @@ parseState = ParseState.CHUNK_HEADER; crFound = false; chunkSizeDigitsRead = 0; - parsingExtension = false; + extensionState = null; extensionSize.set(0); httpHeaderParser.recycle(); } @@ -355,19 +357,42 @@ } byte chr = readChunk.get(readChunk.position()); - if (chr == Constants.CR || chr == Constants.LF) { - parsingExtension = false; + + if (extensionState != null) { + try { + extensionState = ChunkExtension.parse(chr, extensionState); + } catch (IOException ioe) { + throwBadRequestException(sm.getString("chunkedInputFilter.invalidHeader")); + } + if (extensionState == State.CR) { + extensionState = null; + if (!parseCRLF()) { + return false; + } + eol = true; + } else { + // Check the size + long extSize = extensionSize.incrementAndGet(); + if (maxExtensionSize > -1 && extSize > maxExtensionSize) { + throwBadRequestException(sm.getString("chunkedInputFilter.maxExtension")); + } + } + } else if (chr == Constants.CR || chr == Constants.LF) { if (!parseCRLF()) { return false; } eol = true; - } else if (chr == Constants.SEMI_COLON && !parsingExtension) { - // First semicolon marks the start of the extension. Further - // semicolons may appear to separate multiple chunk-extensions. - // These need to be processed as part of parsing the extensions. - parsingExtension = true; - extensionSize.incrementAndGet(); - } else if (!parsingExtension) { + } else if (chr == Constants.SEMI_COLON) { + /* + * First semicolon marks the start of the extension. ChunkedExtension parser takes over for the + * remainder of the extension. + */ + extensionState = State.PRE_NAME; + long extSize = extensionSize.incrementAndGet(); + if (maxExtensionSize > -1 && extSize > maxExtensionSize) { + return false; + } + } else { int charValue = HexUtils.getDec(chr); if (charValue != -1 && chunkSizeDigitsRead < 8) { chunkSizeDigitsRead++; @@ -376,17 +401,9 @@ // Isn't valid hex so this is an error condition throwBadRequestException(sm.getString("chunkedInputFilter.invalidHeader")); } - } else { - // Extension 'parsing' - // Note that the chunk-extension is neither parsed nor - // validated. Currently it is simply ignored. - long extSize = extensionSize.incrementAndGet(); - if (maxExtensionSize > -1 && extSize > maxExtensionSize) { - throwBadRequestException(sm.getString("chunkedInputFilter.maxExtension")); - } } - // Parsing the CRLF increments pos + // Parsing the CRLF increments position if (!eol) { readChunk.position(readChunk.position() + 1); } @@ -418,19 +435,47 @@ } byte chr = readChunk.get(readChunk.position()); - if (chr == Constants.CR || chr == Constants.LF) { - parsingExtension = false; + + if (extensionState != null) { + try { + extensionState = ChunkExtension.parse(chr, extensionState); + } catch (IOException ioe) { + /* + * Can't throw the exception here. Need to swallow it. It will be thrown when parseChunkHeader() + * is called. Not very efficient but it is an error condition for something that is hardly ever + * used. + */ + return false; + } + if (extensionState == State.CR) { + extensionState = null; + if (!skipCRLF()) { + return false; + } + eol = true; + } else { + // Check the size + long extSize = extensionSize.incrementAndGet(); + if (maxExtensionSize > -1 && extSize > maxExtensionSize) { + return false; + } + } + } else if (chr == Constants.CR || chr == Constants.LF) { if (!skipCRLF()) { return false; } eol = true; - } else if (chr == Constants.SEMI_COLON && !parsingExtension) { - // First semicolon marks the start of the extension. Further - // semicolons may appear to separate multiple chunk-extensions. - // These need to be processed as part of parsing the extensions. - parsingExtension = true; - extensionSize.incrementAndGet(); - } else if (!parsingExtension) { + } else if (chr == Constants.SEMI_COLON) { + /* + * First semicolon marks the start of the extension. ChunkedExtension parser takes over for the + * remainder of the extension. + */ + extensionState = State.PRE_NAME; + long extSize = extensionSize.incrementAndGet(); + if (maxExtensionSize > -1 && extSize > maxExtensionSize) { + return false; + } + } else { int charValue = HexUtils.getDec(chr); if (charValue != -1 && chunkSizeDigitsRead < 8) { chunkSizeDigitsRead++; @@ -439,17 +484,9 @@ // Isn't valid hex so this is an error condition return false; } - } else { - // Extension 'parsing' - // Note that the chunk-extension is neither parsed nor - // validated. Currently it is simply ignored. - long extSize = extensionSize.incrementAndGet(); - if (maxExtensionSize > -1 && extSize > maxExtensionSize) { - return false; - } } - // Parsing the CRLF increments pos + // Parsing the CRLF increments position if (!eol) { readChunk.position(readChunk.position() + 1); } @@ -504,7 +541,7 @@ /** * Parse CRLF at end of chunk. * - * @return {@code true} if the read is complete or {@code false if incomplete}. In complete reads can only happen + * @return {@code true} if the read is complete or {@code false if incomplete}. Incomplete reads can only happen * with non-blocking I/O. * * @throws IOException An error occurred parsing CRLF diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java tomcat11-11.0.22/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java --- tomcat11-11.0.15/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java 2026-05-01 18:56:05.000000000 +0000 @@ -21,16 +21,14 @@ import java.io.OutputStreamWriter; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.HashSet; -import java.util.Locale; import java.util.Map; -import java.util.Set; import java.util.function.Supplier; import org.apache.coyote.Response; import org.apache.coyote.http11.HttpOutputBuffer; import org.apache.coyote.http11.OutputFilter; import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.http.HeaderUtil; /** * Chunked output filter. @@ -41,26 +39,6 @@ private static final byte[] CRLF_BYTES = { (byte) '\r', (byte) '\n' }; private static final byte[] END_CHUNK_BYTES = { (byte) '0', (byte) '\r', (byte) '\n', (byte) '\r', (byte) '\n' }; - private static final Set disallowedTrailerFieldNames = new HashSet<>(); - - static { - // Always add these in lower case - disallowedTrailerFieldNames.add("age"); - disallowedTrailerFieldNames.add("cache-control"); - disallowedTrailerFieldNames.add("content-length"); - disallowedTrailerFieldNames.add("content-encoding"); - disallowedTrailerFieldNames.add("content-range"); - disallowedTrailerFieldNames.add("content-type"); - disallowedTrailerFieldNames.add("date"); - disallowedTrailerFieldNames.add("expires"); - disallowedTrailerFieldNames.add("location"); - disallowedTrailerFieldNames.add("retry-after"); - disallowedTrailerFieldNames.add("trailer"); - disallowedTrailerFieldNames.add("transfer-encoding"); - disallowedTrailerFieldNames.add("vary"); - disallowedTrailerFieldNames.add("warning"); - } - /** * Next buffer in the pipeline. */ @@ -178,13 +156,13 @@ try (OutputStreamWriter osw = new OutputStreamWriter(baos, StandardCharsets.ISO_8859_1)) { for (Map.Entry trailerField : trailerFields.entrySet()) { // Ignore disallowed headers - if (disallowedTrailerFieldNames.contains(trailerField.getKey().toLowerCase(Locale.ENGLISH))) { + if (HeaderUtil.isHeaderDisallowedInTrailers(trailerField.getKey())) { continue; } - osw.write(trailerField.getKey()); + osw.write(filterForHeaders(trailerField.getKey())); osw.write(':'); osw.write(' '); - osw.write(trailerField.getValue()); + osw.write(filterForHeaders(trailerField.getValue())); osw.write("\r\n"); } } @@ -198,6 +176,33 @@ } + /* + * Filters out CTLs excluding TAB and any code points above 255 (since this is meant to be ISO-8859-1). + * + * This doesn't perform full HTTP validation. For example, it does not limit field names to tokens. + * + * Strictly, correct trailer fields is an application concern. The filtering here is a basic attempt to help + * mis-behaving applications prevent the worst of the potential side-effects of invalid trailer fields. + */ + // package private so it is visible for testing + static String filterForHeaders(String input) { + char[] chars = input.toCharArray(); + boolean updated = false; + for (int i = 0; i < chars.length; i++) { + if (chars[i] < 32 && chars [i] != 9 || chars[i] == 127 || chars[i] > 255) { + chars[i] = ' '; + updated = true; + } + } + + if (updated) { + return new String(chars); + } else { + return input; + } + } + + @Override public void recycle() { response = null; diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http11/filters/GzipOutputFilter.java tomcat11-11.0.22/java/org/apache/coyote/http11/filters/GzipOutputFilter.java --- tomcat11-11.0.15/java/org/apache/coyote/http11/filters/GzipOutputFilter.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http11/filters/GzipOutputFilter.java 2026-05-01 18:56:05.000000000 +0000 @@ -157,11 +157,13 @@ @Override public void flush() throws IOException { - /* NOOP */} + // NOOP + } @Override public void close() throws IOException { - /* NOOP */} + // NOOP + } } diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/HPackHuffman.java tomcat11-11.0.22/java/org/apache/coyote/http2/HPackHuffman.java --- tomcat11-11.0.15/java/org/apache/coyote/http2/HPackHuffman.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/HPackHuffman.java 2026-05-01 18:56:05.000000000 +0000 @@ -19,8 +19,10 @@ import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashSet; +import java.util.Locale; import java.util.Set; +import org.apache.tomcat.util.http.parser.HttpParser; import org.apache.tomcat.util.res.StringManager; public class HPackHuffman { @@ -371,12 +373,35 @@ * @param target The target for the decompressed data * * @throws HpackException If the Huffman encoded value in HPACK headers did not end with EOS padding + * + * @deprecated Will be removed in Tomcat 12. Use {@link #decode(ByteBuffer, int, StringBuilder, boolean)} */ + @Deprecated public static void decode(ByteBuffer data, int length, StringBuilder target) throws HpackException { + decode(data, length, target, false); + } + + /** + * Decodes a huffman encoded string into the target StringBuilder. There must be enough space left in the buffer for + * this method to succeed. + * + * @param data The byte buffer + * @param length The length of data from the buffer to decode + * @param target The target for the decompressed data + * @param isFieldName {@code true} if a field name is being decoded (names have a more restrictive set of allowed + * characters than field values) + * + * @throws HpackException If the Huffman encoded value in HPACK headers did not end with EOS padding + */ + public static void decode(ByteBuffer data, int length, StringBuilder target, boolean isFieldName) + throws HpackException { + assert data.remaining() >= length; int treePos = 0; boolean eosBits = true; int eosBitCount = 0; + boolean firstChar = true; + char c = 'a'; for (int i = 0; i < length; ++i) { byte b = data.get(); int bitPos = 7; @@ -387,11 +412,34 @@ if ((val & LOW_TERMINAL_BIT) == 0) { treePos = val & LOW_MASK; eosBits = false; + // Found a zero, can't be counting EOS bits eosBitCount = 0; } else { - target.append((char) (val & LOW_MASK)); + c = (char) (val & LOW_MASK); + if (isFieldName) { + if (!HttpParser.isToken(c) || Character.isUpperCase(c)) { + throw new IllegalArgumentException(sm + .getString("hpackhuffman.decode.illegalCharacterName", Character.toString(c))); + } + } else { + if (firstChar) { + if (!HttpParser.isFieldVChar(c)) { + throw new IllegalArgumentException(sm.getString( + "hpackhuffman.decode.illegalCharacterValue.start", Character.toString(c))); + } + firstChar = false; + } else { + if (!HttpParser.isFieldContent(c)) { + throw new IllegalArgumentException(sm.getString( + "hpackhuffman.decode.illegalCharacterValue", Character.toString(c))); + } + } + } + target.append(c); treePos = 0; eosBits = true; + // Output a character, reset eosBitCount + eosBitCount = 0; } } else { if (eosBits) { @@ -406,9 +454,31 @@ // as an error throw new HpackException(sm.getString("hpackhuffman.stringLiteralEOS")); } - target.append((char) ((val >> 16) & LOW_MASK)); + c = (char) ((val >> 16) & LOW_MASK); + if (isFieldName) { + if (!HttpParser.isToken(c) || Character.isUpperCase(c)) { + throw new IllegalArgumentException(sm + .getString("hpackhuffman.decode.illegalCharacterName", Character.toString(c))); + } + } else { + if (firstChar) { + if (!HttpParser.isFieldVChar(c)) { + throw new IllegalArgumentException(sm.getString( + "hpackhuffman.decode.illegalCharacterValue.start", Character.toString(c))); + } + firstChar = false; + } else { + if (!HttpParser.isFieldContent(c)) { + throw new IllegalArgumentException(sm.getString( + "hpackhuffman.decode.illegalCharacterValue", Character.toString(c))); + } + } + } + target.append(c); treePos = 0; eosBits = true; + // Output a character, reset eosBitCount + eosBitCount = 0; } } bitPos--; @@ -420,6 +490,10 @@ if (!eosBits) { throw new HpackException(sm.getString("hpackhuffman.huffmanEncodedHpackValueDidNotEndWithEOS")); } + if (!isFieldName && !HttpParser.isFieldVChar(c)) { + throw new IllegalArgumentException( + sm.getString("hpackhuffman.decode.illegalCharacterValue.end", Character.toString(c))); + } } @@ -432,8 +506,29 @@ * @param forceLowercase If the string should be encoded in lower case * * @return true if encoding succeeded + * + * @deprecated Unused. This method will be removed in Tomcat 12 onwards. */ + @Deprecated public static boolean encode(ByteBuffer buffer, String toEncode, boolean forceLowercase) { + if (forceLowercase) { + return encode(buffer, toEncode.toLowerCase(Locale.ENGLISH)); + } else { + return encode(buffer, toEncode); + } + } + + + /** + * Encodes the given string into the buffer. If there is not enough space in the buffer, or the encoded version is + * bigger than the original it will return false and not modify the buffers position. + * + * @param buffer The buffer to encode into + * @param toEncode The string to encode + * + * @return true if encoding succeeded + */ + public static boolean encode(ByteBuffer buffer, String toEncode) { if (buffer.remaining() <= toEncode.length()) { return false; } @@ -448,9 +543,6 @@ throw new IllegalArgumentException( sm.getString("hpack.invalidCharacter", Character.toString(c), Integer.valueOf(c))); } - if (forceLowercase) { - c = Hpack.toLower(c); - } HuffmanCode code = HUFFMAN_CODES[c]; length += code.length; } @@ -464,9 +556,6 @@ byte currentBufferByte = 0; for (int i = 0; i < toEncode.length(); ++i) { char c = toEncode.charAt(i); - if (forceLowercase) { - c = Hpack.toLower(c); - } HuffmanCode code = HUFFMAN_CODES[c]; if (code.length + bytePos <= 8) { // it fits in the current byte diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/Hpack.java tomcat11-11.0.22/java/org/apache/coyote/http2/Hpack.java --- tomcat11-11.0.15/java/org/apache/coyote/http2/Hpack.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/Hpack.java 2026-05-01 18:56:05.000000000 +0000 @@ -217,7 +217,10 @@ } } - + /* + * Unused. Will be removed in Tomcat 12 onwards. + */ + @Deprecated static char toLower(char c) { if (c >= 'A' && c <= 'Z') { return (char) (c + LOWER_DIFF); @@ -225,7 +228,7 @@ return c; } + private Hpack() { } - } diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/HpackDecoder.java tomcat11-11.0.22/java/org/apache/coyote/http2/HpackDecoder.java --- tomcat11-11.0.15/java/org/apache/coyote/http2/HpackDecoder.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/HpackDecoder.java 2026-05-01 18:56:05.000000000 +0000 @@ -20,6 +20,7 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.http.parser.HttpParser; import org.apache.tomcat.util.res.StringManager; /** @@ -114,7 +115,7 @@ buffer.position(originalPos); return; } - String headerValue = readHpackString(buffer); + String headerValue = readHpackString(buffer, false); if (headerValue == null) { buffer.position(originalPos); return; @@ -128,7 +129,7 @@ buffer.position(originalPos); return; } - String headerValue = readHpackString(buffer); + String headerValue = readHpackString(buffer, false); if (headerValue == null) { buffer.position(originalPos); return; @@ -141,7 +142,7 @@ buffer.position(originalPos); return; } - String headerValue = readHpackString(buffer); + String headerValue = readHpackString(buffer, false); if (headerValue == null) { buffer.position(originalPos); return; @@ -202,11 +203,11 @@ } else if (index != 0) { return handleIndexedHeaderName(index); } else { - return readHpackString(buffer); + return readHpackString(buffer, true); } } - private String readHpackString(ByteBuffer buffer) throws HpackException { + private String readHpackString(ByteBuffer buffer, boolean isFieldName) throws HpackException { if (!buffer.hasRemaining()) { return null; } @@ -218,18 +219,34 @@ } boolean huffman = (data & 0b10000000) != 0; if (huffman) { - return readHuffmanString(length, buffer); + return readHuffmanString(length, buffer, isFieldName); } StringBuilder stringBuilder = new StringBuilder(length); for (int i = 0; i < length; ++i) { - stringBuilder.append((char) buffer.get()); + char c = (char) (buffer.get() & 0xFF); + if (isFieldName) { + if (HttpParser.isToken(c) && !Character.isUpperCase(c)) { + stringBuilder.append(c); + } else { + throw new IllegalArgumentException( + sm.getString("hpackdecoder.illegalCharacterName", Character.toString(c))); + } + } else { + if ((i == 0 || i == length - 1) && HttpParser.isFieldVChar(c) || + i > 0 && i < length - 1 && HttpParser.isFieldContent(c)) { + stringBuilder.append(c); + } else { + throw new IllegalArgumentException( + sm.getString("hpackdecoder.illegalCharacterValue", Character.toString(c))); + } + } } return stringBuilder.toString(); } - private String readHuffmanString(int length, ByteBuffer buffer) throws HpackException { + private String readHuffmanString(int length, ByteBuffer buffer, boolean isFieldName) throws HpackException { StringBuilder stringBuilder = new StringBuilder(length); - HPackHuffman.decode(buffer, length, stringBuilder); + HPackHuffman.decode(buffer, length, stringBuilder, isFieldName); return stringBuilder.toString(); } @@ -238,11 +255,7 @@ return Hpack.STATIC_TABLE[index].name; } else { // index is 1 based - if (index > Hpack.STATIC_TABLE_LENGTH + filledTableSlots) { - throw new HpackException(sm.getString("hpackdecoder.headerTableIndexInvalid", Integer.valueOf(index), - Integer.valueOf(Hpack.STATIC_TABLE_LENGTH), Integer.valueOf(filledTableSlots))); - } - int adjustedIndex = getRealIndex(index - Hpack.STATIC_TABLE_LENGTH); + int adjustedIndex = getRealIndex(index); Hpack.HeaderField res = headerTable[adjustedIndex]; if (res == null) { throw new HpackException(sm.getString("hpackdecoder.nullHeader", Integer.valueOf(index))); @@ -262,7 +275,8 @@ if (index <= Hpack.STATIC_TABLE_LENGTH) { addStaticTableEntry(index); } else { - int adjustedIndex = getRealIndex(index - Hpack.STATIC_TABLE_LENGTH); + // index is 1 based + int adjustedIndex = getRealIndex(index); if (log.isTraceEnabled()) { log.trace(sm.getString("hpackdecoder.useDynamic", Integer.valueOf(adjustedIndex))); } @@ -282,15 +296,15 @@ * @return the real index into the array */ int getRealIndex(int index) throws HpackException { - // the index is one based, but our table is zero based, hence -1 - // also because of our ring buffer set up the indexes are reversed + int dynamicIndex = index - Hpack.STATIC_TABLE_LENGTH; + // The index is one based, but our table is zero based + // Also, because of our ring buffer set up, the indexes are reversed // index = 1 is at position firstSlotPosition + filledSlots - int realIndex = (firstSlotPosition + (filledTableSlots - index)) % headerTable.length; - if (realIndex < 0) { + if (dynamicIndex < 1 || dynamicIndex > filledTableSlots) { throw new HpackException(sm.getString("hpackdecoder.headerTableIndexInvalid", Integer.valueOf(index), Integer.valueOf(Hpack.STATIC_TABLE_LENGTH), Integer.valueOf(filledTableSlots))); } - return realIndex; + return (firstSlotPosition + (filledTableSlots - dynamicIndex)) % headerTable.length; } private void addStaticTableEntry(int index) throws HpackException { diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/HpackEncoder.java tomcat11-11.0.22/java/org/apache/coyote/http2/HpackEncoder.java --- tomcat11-11.0.15/java/org/apache/coyote/http2/HpackEncoder.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/HpackEncoder.java 2026-05-01 18:56:05.000000000 +0000 @@ -121,6 +121,22 @@ * @return The state of the encoding process */ State encode(MimeHeaders headers, ByteBuffer target) { + return encode(headers, target, true); + } + + /** + * Encodes the headers into a buffer. + * + * @param headers The headers to encode + * @param target The buffer to which to write the encoded headers + * @param forceLowerCase Normally {@code true} to ensure that header field names are lower case as required for + * HTTP/2 but some tests may deliberately allow upper case characters to test Tomcat's + * handling of such invalid field header names. + * + * @return The state of the encoding process + */ + State encode(MimeHeaders headers, ByteBuffer target, boolean forceLowerCase) { + int it = headersIterator; if (headersIterator == -1) { handleTableSizeChange(target); @@ -133,8 +149,10 @@ } } while (it < currentHeaders.size()) { - // FIXME: Review lowercase policy - String headerName = headers.getName(it).toString().toLowerCase(Locale.US); + String headerName = headers.getName(it).toString(); + if (forceLowerCase) { + headerName = headerName.toLowerCase(Locale.US); + } boolean skip = false; if (firstPass) { if (headerName.charAt(0) != ':') { @@ -208,23 +226,29 @@ return State.COMPLETE; } + /* + * headerName must be lower case by the time this method is called. + * + * The exception to the above rule is test cases which may deliberately use some upper case characters to test how + * Tomcat responds to such invalid input. + */ private void writeHuffmanEncodableName(ByteBuffer target, String headerName) { if (hpackHeaderFunction.shouldUseHuffman(headerName)) { - if (HPackHuffman.encode(target, headerName, true)) { + if (HPackHuffman.encode(target, headerName)) { return; } } target.put((byte) 0); // to use encodeInteger we need to place the first byte in the buffer. Hpack.encodeInteger(target, headerName.length(), 7); for (int j = 0; j < headerName.length(); ++j) { - target.put((byte) Hpack.toLower(headerName.charAt(j))); + target.put((byte) headerName.charAt(j)); } } private void writeHuffmanEncodableValue(ByteBuffer target, String headerName, String val) { if (hpackHeaderFunction.shouldUseHuffman(headerName, val)) { - if (!HPackHuffman.encode(target, val, false)) { + if (!HPackHuffman.encode(target, val)) { writeValueString(target, val); } } else { diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/Http2Parser.java tomcat11-11.0.22/java/org/apache/coyote/http2/Http2Parser.java --- tomcat11-11.0.15/java/org/apache/coyote/http2/Http2Parser.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/Http2Parser.java 2026-05-01 18:56:05.000000000 +0000 @@ -247,6 +247,12 @@ } else { buffer.get(optional); } + /* + * The optional padLength byte and priority bytes (if any) don't count towards the payload size when + * comparing payload size to padLength as required by RFC 9113, section 6.2. + */ + payloadSize -= optionalLen; + if (padding) { padLength = ByteUtil.getOneByte(optional, 0); if (padLength >= payloadSize) { @@ -255,11 +261,10 @@ Http2Error.PROTOCOL_ERROR); } } - - // Ignore RFC 7450 priority data if present - - payloadSize -= optionalLen; + // The padding does not count towards the size of payload that is read below. payloadSize -= padLength; + + // Any RFC 7450 priority data was read into the byte[] optional above. It is ignored. } readHeaderPayload(streamId, payloadSize, buffer); @@ -299,8 +304,11 @@ long errorCode = ByteUtil.getFourBytes(payload, 0); output.reset(streamId, errorCode); + headersCurrentStream = -1; headersEndStream = false; + // Force clearing of header buffer as there may be data left over + afterHeadersCompleteCleanUp(true); } @@ -523,6 +531,8 @@ } catch (HpackException hpe) { throw new ConnectionException(sm.getString("http2Parser.processFrameHeaders.decodingFailed"), Http2Error.COMPRESSION_ERROR, hpe); + } catch (IllegalArgumentException iae) { + throw new StreamException("Invalid headers", Http2Error.PROTOCOL_ERROR, streamId, iae); } // switches to write mode @@ -635,12 +645,6 @@ Http2Error.COMPRESSION_ERROR); } - /* - * Clear the reference to the stream in the HPack decoder now that the headers have been processed so that the - * HPack decoder does not retain a reference to this stream. This aids GC. - */ - hpackDecoder.clearHeaderEmitter(); - synchronized (output) { output.headersEnd(streamId, headersEndStream); @@ -649,10 +653,24 @@ } } + // We know from test above that buffer is empty so no need to force it to be cleared + afterHeadersCompleteCleanUp(false); + } + + + protected void afterHeadersCompleteCleanUp(boolean forceClear) { // Reset size for new request if the buffer was previously expanded if (headerReadBuffer.capacity() > Constants.DEFAULT_HEADER_READ_BUFFER_SIZE) { headerReadBuffer = ByteBuffer.allocate(Constants.DEFAULT_HEADER_READ_BUFFER_SIZE); + } else if (forceClear) { + headerReadBuffer.clear(); } + + /* + * Clear the reference to the stream in the HPack decoder now that the headers have been processed so that the + * HPack decoder does not retain a reference to this stream. This aids GC. + */ + hpackDecoder.clearHeaderEmitter(); } diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/Http2Protocol.java tomcat11-11.0.22/java/org/apache/coyote/http2/Http2Protocol.java --- tomcat11-11.0.15/java/org/apache/coyote/http2/Http2Protocol.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/Http2Protocol.java 2026-05-01 18:56:05.000000000 +0000 @@ -112,6 +112,19 @@ private boolean discardRequestsAndResponses = false; private final SynchronizedStack recycledRequestsAndResponses = new SynchronizedStack<>(); + /* + * Additional time in nanoseconds between sending the first graceful GOAWAY (max stream id) and the final GOAWAY + * (last seen stream id). During this time the server will continue to process new streams on the connection. This + * is to mitigate the race of client-buffered/sent packets for new streams and the final GOAWAY (with last seen + * stream id). By default, Tomcat uses the last computed RTT for this interval, but the RTT might have fluctuated + * due to network or server load conditions, or the client (e.g. nghttp2) might have already buffered frames for + * opening new streams on a connection. + * + * The name "drainTimeout" is taken from Envoy proxy's identical HTTP Connection Manager property and means exactly + * the same. + */ + private long drainTimeout; + @Override public String getHttpUpgradeName(boolean isSSLEnabled) { if (isSSLEnabled) { @@ -409,6 +422,16 @@ } + public long getDrainTimeout() { + return drainTimeout; + } + + + public void setDrainTimeout(long drainTimeout) { + this.drainTimeout = drainTimeout; + } + + Request popRequestAndResponse() { Request requestAndResponse = null; if (!discardRequestsAndResponses) { diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/Http2UpgradeHandler.java tomcat11-11.0.22/java/org/apache/coyote/http2/Http2UpgradeHandler.java --- tomcat11-11.0.15/java/org/apache/coyote/http2/Http2UpgradeHandler.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/Http2UpgradeHandler.java 2026-05-01 18:56:05.000000000 +0000 @@ -139,6 +139,8 @@ private volatile int lastNonFinalDataPayload; private volatile int lastWindowUpdate; + // Time between the "graceful" GOAWAY (max stream id) and the final GOAWAY (last seen stream id) + private long drainTimeout = 0; Http2UpgradeHandler(Http2Protocol protocol, Adapter adapter, Request coyoteRequest, SocketWrapperBase socketWrapper) { @@ -172,6 +174,8 @@ pingManager.initiateDisabled = protocol.getInitiatePingDisabled(); + drainTimeout = protocol.getDrainTimeout(); + // Initial HTTP request becomes stream 1. if (coyoteRequest != null) { if (log.isTraceEnabled()) { @@ -542,7 +546,7 @@ void checkPauseState() throws IOException { if (connectionState.get() == ConnectionState.PAUSING) { - if (pausedNanoTime + pingManager.getRoundTripTimeNano() < System.nanoTime()) { + if (pausedNanoTime + pingManager.getRoundTripTimeNano() + drainTimeout < System.nanoTime()) { connectionState.compareAndSet(ConnectionState.PAUSING, ConnectionState.PAUSED); writeGoAwayFrame(maxProcessedStreamId, Http2Error.NO_ERROR.getCode(), null); } @@ -1165,7 +1169,8 @@ break; } - int share = (int) (s.getConnectionAllocationRequested() * remaining / + // Cast to long during the intermediate calculation to avoid integer overflow in multiplication + int share = (int) ((long) s.getConnectionAllocationRequested() * remaining / requestedAllocationForIncrementalStreams); if (share == 0) { share = 1; diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/LocalStrings.properties tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings.properties --- tomcat11-11.0.15/java/org/apache/coyote/http2/LocalStrings.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings.properties 2026-05-01 18:56:05.000000000 +0000 @@ -46,6 +46,8 @@ hpackdecoder.clearDynamic=Emptying dynamic table hpackdecoder.emitHeader=Emitting header with name [{0}] and value [{1}] hpackdecoder.headerTableIndexInvalid=The header table index [{0}] is not valid as there are [{1}] static entries and [{2}] dynamic entries +hpackdecoder.illegalCharacterName=The illegal [{0}] character was found when decoding an HTTP/2 header field name +hpackdecoder.illegalCharacterValue=The illegal [{0}] character was found when decoding an HTTP/2 header field value hpackdecoder.maxMemorySizeExceeded=The header table size [{0}] exceeds the maximum size [{1}] hpackdecoder.notImplemented=Not yet implemented hpackdecoder.nullHeader=Null header at index [{0}] @@ -54,6 +56,10 @@ hpackdecoder.useStatic=Using header from index [{0}] of static table hpackdecoder.zeroNotValidHeaderTableIndex=Zero is not a valid header table index +hpackhuffman.decode.illegalCharacterName=The illegal [{0}] character was found when decoding an HTTP/2 header field name +hpackhuffman.decode.illegalCharacterValue=The illegal [{0}] character was found when decoding an HTTP/2 header field value +hpackhuffman.decode.illegalCharacterValue.end=The illegal [{0}] character was found when decoding the final character in an HTTP/2 header field value +hpackhuffman.decode.illegalCharacterValue.start=The illegal [{0}] character was found when decoding the first character in an HTTP/2 header field value hpackhuffman.huffmanEncodedHpackValueDidNotEndWithEOS=Huffman encoded value in HPACK headers did not end with EOS padding hpackhuffman.stringLiteralEOS=Huffman encoded value in HPACK headers contained the EOS symbol hpackhuffman.stringLiteralTooMuchPadding=More than 7 bits of EOS padding were provided at the end of an Huffman encoded string literal @@ -92,19 +98,20 @@ stream.clientResetRequest=Client reset the stream before the request was fully read stream.closed=Connection [{0}], Stream [{1}], Unable to write to stream once it has been closed -stream.header.case=Connection [{0}], Stream [{1}], HTTP header name [{2}] must be in lower case stream.header.connection=Connection [{0}], Stream [{1}], HTTP header [{2}] is not permitted in an HTTP/2 request stream.header.contentLength=Connection [{0}], Stream [{1}], The content length header value [{2}] does not agree with the size of the data received [{3}] stream.header.debug=Connection [{0}], Stream [{1}], HTTP header [{2}], Value [{3}] stream.header.duplicate=Connection [{0}], Stream [{1}], received multiple [{2}] headers stream.header.empty=Connection [{0}], Stream [{1}], Invalid empty header name +stream.header.inconsistentScheme=Connection [{0}], Stream [{1}], The scheme [{2}] is not consistent with the TLS enabled setting of [{3}] stream.header.invalid=Connection [{0}], Stream [{1}], The header [{2}] contained invalid value [{3}] +stream.header.invalidConnect=Connection [{0}], Stream [{1}], The CONNECT request was invalid as neither :scheme nor :path should be present stream.header.noPath=Connection [{0}], Stream [{1}], The [:path] pseudo header was empty stream.header.required=Connection [{0}], Stream [{1}], One or more required headers was missing stream.header.te=Connection [{0}], Stream [{1}], HTTP header [te] is not permitted to have the value [{2}] in an HTTP/2 request stream.header.unexpectedPseudoHeader=Connection [{0}], Stream [{1}], Pseudo header [{2}] received after a regular header stream.header.unknownPseudoHeader=Connection [{0}], Stream [{1}], Unknown pseudo header [{2}] received -stream.host.inconsistent=Connection [{0}], Stream [{1}], The header host header [{2}] is inconsistent with previously provided values for host [{3}] and/or port [{4}] +stream.host.inconsistent=Connection [{0}], Stream [{1}], The host header [{2}] is inconsistent with previously provided values for host [{3}] and/or port [{4}] stream.host.sni=Connection [{0}], Stream [{1}], The host header [{2}] does not match the SNI host [{3}] stream.inputBuffer.copy=Copying [{0}] bytes from inBuffer to outBuffer stream.inputBuffer.dispatch=Data added to inBuffer when read interest is registered. Triggering a read dispatch diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/LocalStrings_fr.properties tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_fr.properties --- tomcat11-11.0.15/java/org/apache/coyote/http2/LocalStrings_fr.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_fr.properties 2026-05-01 18:56:05.000000000 +0000 @@ -92,7 +92,6 @@ stream.clientResetRequest=Le client a réinitialisé la stream avant qu'elle ait été complètement lue stream.closed=Connection [{0}], Flux [{1}], Impossible d''écrire sur un flux après sa fermeture -stream.header.case=Connection [{0}], Flux [{1}], Le nom d''en-tête HTTP [{2}] doit être en miniscules stream.header.connection=Connection [{0}], Flux [{1}], L''en-tête HTTP [{2}] n''est pas autorisé dans une requête HTTP/2 stream.header.contentLength=Connection [{0}], Flux [{1}], La valeur de l''en-tête content-length [{2}] ne correspond pas à la taille des données reçue [{3}] stream.header.debug=Connection [{0}], Flux [{1}], en-tête HTTP [{2}], valeur [{3}] @@ -105,6 +104,7 @@ stream.header.unexpectedPseudoHeader=Connection [{0}], Flux [{1}], Le pseudo en-tête [{2}] a été reçu après un en-tête normal stream.header.unknownPseudoHeader=Connection [{0}], Flux [{1}], Un pseudo en-tête inconnu [{2}] a été reçu stream.host.inconsistent=Connection [{0}], Stream [{1}], L''en tête hôte [{2}] est inconsistant avec les valeurs fournies précédemment pour l''hôte [{3}] et/ou le port [{4}] +stream.host.sni=Connection [{0}], Stream [{1}], l''en-tête hôte [{2}] ne correspond pas à l''hôte SNI [{3}] stream.inputBuffer.copy=Copide de [{0}] octets depuis inBuffer vers outBuffer stream.inputBuffer.dispatch=Des données on été ajoutées dans inBuffer alors que la lecture est surveillée, envoi d'un évènement de lecture stream.inputBuffer.empty=Le tampon d'entrée du flux est vide, attente de données diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/LocalStrings_ja.properties tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_ja.properties --- tomcat11-11.0.15/java/org/apache/coyote/http2/LocalStrings_ja.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_ja.properties 2026-05-01 18:56:05.000000000 +0000 @@ -92,7 +92,6 @@ stream.clientResetRequest=リクエストが完全に読み取られる前にクライアントがストリームをリセットしました stream.closed=コネクション [{0}]、ストリーム [{1}]、切断したストリームには書き込みできません -stream.header.case=コネクション [{0}]、ストリーム [{1}]、HTTP ヘッダー名 [{2}] は小文字でなければなりません。 stream.header.connection=コネクション [{0}]、ストリーム [{1}]、HTTP/2 のリクエストには HTTP ヘッダー [{2}] を指定することはできません。 stream.header.contentLength=コネクション [{0}]、ストリーム [{1}]、content length ヘッダーの値 [{2}] と受信したデータ長 [{3}] は一致しません。 stream.header.debug=コネクション [{0}]、ストリーム [{1}]、HTTP ヘッダー [{2}]、値は [{3}] @@ -105,6 +104,7 @@ stream.header.unexpectedPseudoHeader=コネクション [{0}]、ストリーム [{1}]、通常のヘッダーの後に疑似ヘッダー [{2}] を受信しました。 stream.header.unknownPseudoHeader=コネクション [{0}]、ストリーム [{1}]、未知の疑似ヘッダー [{2}] を受信しました。 stream.host.inconsistent=Connection [{0}]、Stream [{1}]で、ホストヘッダー [{2}] は、以前提供されたホスト [{3}] および/またはポート [{4}] の値と矛盾しています +stream.host.sni=コネクション [{0}]、ストリーム [{1}]、Hostヘッダ [{2}] が SNI ホスト [{3}] と一致しません stream.inputBuffer.copy=入力バッファーから出力バッファーへコピーしたのは [{0}] バイトです。 stream.inputBuffer.dispatch=read interest が登録されると、inBufferにデータが追加されます。読み取りディスパッチをトリガします。 stream.inputBuffer.empty=ストリーム入力バッファが空です。 より多くのデータを待っています。 diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/LocalStrings_ko.properties tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_ko.properties --- tomcat11-11.0.15/java/org/apache/coyote/http2/LocalStrings_ko.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_ko.properties 2026-05-01 18:56:05.000000000 +0000 @@ -86,7 +86,6 @@ pingManager.roundTripTime=연결 [{0}]: 라운드 트립 시간이 [{1}] 나노초(ns)로 측정되었습니다. stream.closed=연결 [{0}], 스트림 [{1}], 한번 닫힌 스트림에 쓰기를 할 수 없습니다. -stream.header.case=연결 [{0}], 스트림 [{1}], HTTP 헤더 이름 [{2}]은(는) 반드시 소문자여야 합니다. stream.header.connection=연결 [{0}], 스트림 [{1}], HTTP 헤더 [{2}]은 HTTP/2 요청에서 허용되지 않습니다. stream.header.contentLength=연결 [{0}], 스트림 [{1}], 해당 Content-Length 헤더 값 [{2}]은(는) 수신된 데이터의 크기 [{3}]와(과) 일치하지 않습니다. stream.header.debug=연결 [{0}], 스트림 [{1}], HTTP 헤더: [{2}], 값: [{3}] diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties --- tomcat11-11.0.15/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties 2026-05-01 18:56:05.000000000 +0000 @@ -87,7 +87,6 @@ pingManager.roundTripTime=连接[{0}]往返时间测量为[{1}]ns stream.closed=连接[{0}],流[{1}],一旦关闭就无法写入流 -stream.header.case=连接[{0}],流[{1}],HTTP标头名称[{2}]必须小写 stream.header.connection=HTTP/2请求中不允许连接[{0}]、流[{1}]、HTTP头[{2}] stream.header.contentLength=连接[{0}],流[{1}],内容长度头值[{2}]与接收的数据大小[{3}]不一致 stream.header.debug=连接[{0}],流[{1}],HTTP标头[{2}],值[{3}] diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/Stream.java tomcat11-11.0.22/java/org/apache/coyote/http2/Stream.java --- tomcat11-11.0.15/java/org/apache/coyote/http2/Stream.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/Stream.java 2026-05-01 18:56:05.000000000 +0000 @@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashSet; -import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -46,6 +45,7 @@ import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.http.HeaderUtil; import org.apache.tomcat.util.http.Method; import org.apache.tomcat.util.http.MimeHeaders; import org.apache.tomcat.util.http.parser.Host; @@ -321,19 +321,23 @@ log.trace(sm.getString("stream.header.debug", getConnectionId(), getIdAsString(), name, value)); } - // Header names must be lowercase - if (!name.toLowerCase(Locale.US).equals(name)) { - throw new HpackException(sm.getString("stream.header.case", getConnectionId(), getIdAsString(), name)); - } + // Field header names being all lower case is enforced in HpackDecoder. if (HTTP_CONNECTION_SPECIFIC_HEADERS.contains(name)) { - throw new HpackException( - sm.getString("stream.header.connection", getConnectionId(), getIdAsString(), name)); + headerException = new StreamException( + sm.getString("stream.header.connection", getConnectionId(), getIdAsString(), name), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); + // No need for further processing. The stream will be reset. + return; } if ("te".equals(name)) { if (!"trailers".equals(value)) { - throw new HpackException(sm.getString("stream.header.te", getConnectionId(), getIdAsString(), value)); + headerException = + new StreamException(sm.getString("stream.header.te", getConnectionId(), getIdAsString(), value), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); + // No need for further processing. The stream will be reset. + return; } } @@ -344,7 +348,11 @@ } if (name.isEmpty()) { - throw new HpackException(sm.getString("stream.header.empty", getConnectionId(), getIdAsString())); + headerException = + new StreamException(sm.getString("stream.header.empty", getConnectionId(), getIdAsString()), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); + // No need for further processing. The stream will be reset. + return; } boolean pseudoHeader = name.charAt(0) == ':'; @@ -369,51 +377,64 @@ configureVoidOutputFilter(); } } else { - throw new HpackException( - sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":method")); + headerException = new StreamException( + sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":method"), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); } break; } case ":scheme": { if (coyoteRequest.scheme().isNull()) { coyoteRequest.scheme().setString(value); + // Check scheme is consistent with TLS usage + if ("https".equals(value) != handler.getProtocol().getHttp11Protocol().isSSLEnabled()) { + headerException = new StreamException( + sm.getString("stream.header.inconsistentScheme", getConnectionId(), getIdAsString(), + value, Boolean.toString(handler.getProtocol().getHttp11Protocol().isSSLEnabled())), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); + } } else { - throw new HpackException( - sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":scheme")); + headerException = new StreamException( + sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":scheme"), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); } break; } case ":path": { if (!coyoteRequest.requestURI().isNull()) { - throw new HpackException( - sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":path")); - } - if (value.isEmpty()) { - throw new HpackException(sm.getString("stream.header.noPath", getConnectionId(), getIdAsString())); - } - int queryStart = value.indexOf('?'); - String uri; - if (queryStart == -1) { - uri = value; + headerException = new StreamException( + sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":path"), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); + } else if (value.isEmpty()) { + headerException = new StreamException( + sm.getString("stream.header.noPath", getConnectionId(), getIdAsString()), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); } else { - uri = value.substring(0, queryStart); - String query = value.substring(queryStart + 1); - coyoteRequest.queryString().setString(query); - } - // Bug 61120. Set the URI as bytes rather than String so: - // - any path parameters are correctly processed - // - the normalization security checks are performed that prevent - // directory traversal attacks - byte[] uriBytes = uri.getBytes(StandardCharsets.ISO_8859_1); - coyoteRequest.requestURI().setBytes(uriBytes, 0, uriBytes.length); + int queryStart = value.indexOf('?'); + String uri; + if (queryStart == -1) { + uri = value; + } else { + uri = value.substring(0, queryStart); + String query = value.substring(queryStart + 1); + coyoteRequest.queryString().setString(query); + } + // Bug 61120. Set the URI as bytes rather than String so: + // - any path parameters are correctly processed + // - the normalization security checks are performed that prevent + // directory traversal attacks + byte[] uriBytes = uri.getBytes(StandardCharsets.ISO_8859_1); + coyoteRequest.requestURI().setBytes(uriBytes, 0, uriBytes.length); + } break; } case ":authority": { if (coyoteRequest.serverName().isNull()) { - parseAuthority(value, false); + parseAuthority(value); } else { - throw new HpackException( - sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":authority")); + headerException = new StreamException( + sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":authority"), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); } break; } @@ -432,15 +453,16 @@ if (coyoteRequest.serverName().isNull()) { // No :authority header. This is first host header. Use it. hostHeaderSeen = true; - parseAuthority(value, true); + parseAuthority(value); } else if (!hostHeaderSeen) { // First host header - must be consistent with :authority hostHeaderSeen = true; compareAuthority(value); } else { // Multiple hosts headers - illegal - throw new HpackException( - sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), "host")); + headerException = new StreamException( + sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), "host"), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); } break; } @@ -471,9 +493,7 @@ headerException = new StreamException( sm.getString("stream.header.unknownPseudoHeader", getConnectionId(), getIdAsString(), name), Http2Error.PROTOCOL_ERROR, getIdAsInt()); - } - - if (headerState == HEADER_STATE_TRAILER) { + } else if (headerState == HEADER_STATE_TRAILER) { // HTTP/2 headers are already always lower case coyoteRequest.getMimeTrailerFields().addValue(name).setString(value); } else { @@ -490,44 +510,47 @@ streamOutputBuffer.closed = true; } - private void parseAuthority(String value, boolean host) throws HpackException { + private void parseAuthority(String value) { int i; try { i = Host.parse(value); + if (i > -1) { + coyoteRequest.serverName().setString(value.substring(0, i)); + coyoteRequest.setServerPort(Integer.parseInt(value.substring(i + 1))); + } else { + coyoteRequest.serverName().setString(value); + } } catch (IllegalArgumentException iae) { - // Host value invalid - throw new HpackException(sm.getString("stream.header.invalid", getConnectionId(), getIdAsString(), - host ? "host" : ":authority", value)); - } - if (i > -1) { - coyoteRequest.serverName().setString(value.substring(0, i)); - coyoteRequest.setServerPort(Integer.parseInt(value.substring(i + 1))); - } else { - coyoteRequest.serverName().setString(value); + // Bad :authority / host header -> 400 response + coyoteRequest.setNote(Request.NOTE_BAD_REQUEST, Boolean.TRUE); } // Match host name with SNI if required - if (!handler.getProtocol().getHttp11Protocol().checkSni(handler.getSniHostName(), coyoteRequest.serverName().getString())) { - throw new HpackException(sm.getString("stream.host.sni", getConnectionId(), getIdAsString(), value, - handler.getSniHostName())); + if (!handler.getProtocol().getHttp11Protocol().checkSni(handler.getSniHostName(), + coyoteRequest.serverName().getString())) { + headerException = new StreamException(sm.getString("stream.host.sni", getConnectionId(), getIdAsString(), + value, handler.getSniHostName()), Http2Error.PROTOCOL_ERROR, getIdAsInt()); } } - private void compareAuthority(String value) throws HpackException { + private void compareAuthority(String value) { int i; try { i = Host.parse(value); + if (i == -1 && + (!value.equals(coyoteRequest.serverName().getString()) || coyoteRequest.getServerPort() != -1) || + i > -1 && ((!value.substring(0, i).equals(coyoteRequest.serverName().getString()) || + Integer.parseInt(value.substring(i + 1)) != coyoteRequest.getServerPort()))) { + // Host value inconsistent + headerException = new StreamException( + sm.getString("stream.host.inconsistent", getConnectionId(), getIdAsString(), value, + coyoteRequest.serverName().getString(), + Integer.toString(coyoteRequest.getServerPort())), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); + } } catch (IllegalArgumentException iae) { - // Host value invalid - throw new HpackException( - sm.getString("stream.header.invalid", getConnectionId(), getIdAsString(), "host", value)); - } - if (i == -1 && (!value.equals(coyoteRequest.serverName().getString()) || coyoteRequest.getServerPort() != -1) || - i > -1 && ((!value.substring(0, i).equals(coyoteRequest.serverName().getString()) || - Integer.parseInt(value.substring(i + 1)) != coyoteRequest.getServerPort()))) { - // Host value inconsistent - throw new HpackException(sm.getString("stream.host.inconsistent", getConnectionId(), getIdAsString(), value, - coyoteRequest.serverName().getString(), Integer.toString(coyoteRequest.getServerPort()))); + // Bad :authority / host header -> 400 response + coyoteRequest.setNote(Request.NOTE_BAD_REQUEST, Boolean.TRUE); } } @@ -551,12 +574,32 @@ } - final boolean receivedEndOfHeaders() throws ConnectionException { - if (coyoteRequest.getMethod() == null || coyoteRequest.scheme().isNull() || - !Method.CONNECT.equals(coyoteRequest.getMethod()) && coyoteRequest.requestURI().isNull()) { - throw new ConnectionException(sm.getString("stream.header.required", getConnectionId(), getIdAsString()), - Http2Error.PROTOCOL_ERROR); + final boolean receivedEndOfHeaders() throws StreamException { + boolean missingHeader = false; + + if (coyoteRequest.getMethod() == null) { + missingHeader = true; + } else if (Method.CONNECT.equals(coyoteRequest.getMethod())) { + // CONNECT only + if (!coyoteRequest.scheme().isNull() || !coyoteRequest.requestURI().isNull()) { + throw new StreamException(sm.getString("stream.header.invalidConnect", getConnectionId(), + getIdAsString()), Http2Error.PROTOCOL_ERROR, getIdAsInt()); + } + if (coyoteRequest.serverName().isNull()) { + missingHeader = true; + } + } else { + // All other methods + if (coyoteRequest.scheme().isNull() || coyoteRequest.requestURI().isNull()) { + missingHeader = true; + } + } + + if (missingHeader) { + throw new StreamException(sm.getString("stream.header.required", getConnectionId(), getIdAsString()), + Http2Error.PROTOCOL_ERROR, getIdAsInt()); } + // Cookie headers need to be concatenated into a single header // See RFC 7540 8.1.2.5 // Can only do this once the headers are fully received @@ -579,8 +622,8 @@ final void writeTrailers() throws IOException { - Supplier> supplier = coyoteResponse.getTrailerFields(); - if (supplier == null) { + Supplier> trailerFieldsSupplier = coyoteResponse.getTrailerFields(); + if (trailerFieldsSupplier == null) { // No supplier was set, end of stream will already have been sent return; } @@ -591,17 +634,22 @@ */ MimeHeaders mimeHeaders = new MimeHeaders(); - Map headerMap = supplier.get(); - if (headerMap == null) { - headerMap = Collections.emptyMap(); + Map trailerFields = trailerFieldsSupplier.get(); + if (trailerFields == null) { + trailerFields = Collections.emptyMap(); } // Copy the contents of the Map to the MimeHeaders // TODO: Is there benefit in refactoring this? Is MimeHeaders too // heavyweight? Can we reduce the copy/conversions? - for (Map.Entry headerEntry : headerMap.entrySet()) { - MessageBytes mb = mimeHeaders.addValue(headerEntry.getKey()); - mb.setString(headerEntry.getValue()); + for (Map.Entry trailerField : trailerFields.entrySet()) { + // Ignore disallowed headers + if (HeaderUtil.isHeaderDisallowedInTrailers(trailerField.getKey())) { + continue; + } + + MessageBytes mb = mimeHeaders.addValue(trailerField.getKey()); + mb.setString(trailerField.getValue()); } handler.writeHeaders(this, mimeHeaders, true, Constants.DEFAULT_HEADERS_FRAME_SIZE); diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/StreamException.java tomcat11-11.0.22/java/org/apache/coyote/http2/StreamException.java --- tomcat11-11.0.15/java/org/apache/coyote/http2/StreamException.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/StreamException.java 2026-05-01 18:56:05.000000000 +0000 @@ -34,6 +34,12 @@ } + StreamException(String msg, Http2Error error, int streamId, Throwable cause) { + super(msg, error, cause); + this.streamId = streamId; + } + + int getStreamId() { return streamId; } diff -Nru tomcat11-11.0.15/java/org/apache/coyote/http2/StreamProcessor.java tomcat11-11.0.22/java/org/apache/coyote/http2/StreamProcessor.java --- tomcat11-11.0.15/java/org/apache/coyote/http2/StreamProcessor.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/coyote/http2/StreamProcessor.java 2026-05-01 18:56:05.000000000 +0000 @@ -502,19 +502,28 @@ * The checks performed below are based on the checks in Http11InputBuffer. */ private boolean validateRequest() { - HttpParser httpParser = handler.getProtocol().getHttp11Protocol().getHttpParser(); + // Check for issues during header processing. Include: + // - invalid (incorrectly formatted) :authority header + // - invalid (incorrectly formatted) host header + if (request.getNote(Request.NOTE_BAD_REQUEST) != null) { + // Notes not reset when request is recycled + request.setNote(Request.NOTE_BAD_REQUEST, null); + return false; + } // Method name must be a token if (!HttpParser.isToken(request.getMethod())) { return false; } - // Scheme must adhere to RFC 3986 + // Scheme must adhere to RFC 3986 - null scheme possible with CONNECT String scheme = request.scheme().toString(); - if (!HttpParser.isScheme(scheme)) { + if (scheme != null && !HttpParser.isScheme(scheme)) { return false; } + HttpParser httpParser = handler.getProtocol().getHttp11Protocol().getHttpParser(); + // Invalid character in request target // (other checks such as valid %nn happen later) ByteChunk bc = request.requestURI().getByteChunk(); diff -Nru tomcat11-11.0.15/java/org/apache/el/ExpressionFactoryImpl.java tomcat11-11.0.22/java/org/apache/el/ExpressionFactoryImpl.java --- tomcat11-11.0.15/java/org/apache/el/ExpressionFactoryImpl.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/el/ExpressionFactoryImpl.java 2026-05-01 18:56:05.000000000 +0000 @@ -29,7 +29,7 @@ import org.apache.el.util.MessageFactory; /** - * @see jakarta.el.ExpressionFactory + * Implementation of {@link jakarta.el.ExpressionFactory}. */ @aQute.bnd.annotation.spi.ServiceProvider(value = ExpressionFactory.class) public class ExpressionFactoryImpl extends ExpressionFactory { diff -Nru tomcat11-11.0.15/java/org/apache/el/parser/AstValue.java tomcat11-11.0.22/java/org/apache/el/parser/AstValue.java --- tomcat11-11.0.15/java/org/apache/el/parser/AstValue.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/el/parser/AstValue.java 2026-05-01 18:56:05.000000000 +0000 @@ -87,33 +87,30 @@ // Method call at end of expression ctx.setPropertyResolved(false); property = this.children[i].getValue(ctx); - i += 2; - if (property == null) { throw new PropertyNotFoundException( MessageFactory.get("error.unreachable.property", this.children[i].getImage())); } + i += 2; } else if (i + 1 < propCount) { // Object with property not at end of expression property = this.children[i].getValue(ctx); ctx.setPropertyResolved(false); base = resolver.getValue(ctx, base, property); i++; - } else { // Object with property at end of expression ctx.setPropertyResolved(false); property = this.children[i].getValue(ctx); - i++; - if (property == null) { throw new PropertyNotFoundException( MessageFactory.get("error.unreachable.property", this.children[i].getImage())); } + i++; } if (base == null) { throw new PropertyNotFoundException( - MessageFactory.get("error.unreachable.property", this.children[i].getImage())); + MessageFactory.get("error.unreachable.property", this.children[propCount - 1].getImage())); } } diff -Nru tomcat11-11.0.15/java/org/apache/jasper/compiler/Compiler.java tomcat11-11.0.22/java/org/apache/jasper/compiler/Compiler.java --- tomcat11-11.0.15/java/org/apache/jasper/compiler/Compiler.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/jasper/compiler/Compiler.java 2026-05-01 18:56:05.000000000 +0000 @@ -37,6 +37,7 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.Jar; +import org.apache.tomcat.util.descriptor.tld.TldResourcePath; import org.apache.tomcat.util.scan.JarFactory; /** @@ -474,7 +475,35 @@ String key = include.getKey(); URL includeUrl; long includeLastModified; - if (key.startsWith("jar:jar:")) { + if (key.startsWith("uri:")) { + // Key is a stable taglib URI used for TLDs in JARs outside + // the web application (avoids baking absolute paths into the + // generated code). Two forms exist: + // "uri:" – the JAR file itself + // "uri:!/" – a TLD entry within the JAR + int bangSlash = key.indexOf("!/"); + String tagUri = bangSlash < 0 + ? key.substring(4) + : key.substring(4, bangSlash); + TldCache tldCache = ctxt.getOptions().getTldCache(); + TldResourcePath tldPath = tldCache.getTldResourcePath(tagUri); + if (tldPath == null) { + return true; + } + try (Jar jar = tldPath.openJar()) { + if (jar == null) { + return true; + } + if (bangSlash < 0) { + // JAR-level key: check the JAR file's last-modified + includeLastModified = jar.getLastModified(); + } else { + // TLD-entry key: check the entry's last-modified within the JAR + String entryName = key.substring(bangSlash + 2); + includeLastModified = jar.getLastModified(entryName); + } + } + } else if (key.startsWith("jar:jar:")) { // Assume we constructed this correctly int entryStart = key.lastIndexOf("!/"); String entry = key.substring(entryStart + 2); diff -Nru tomcat11-11.0.15/java/org/apache/jasper/compiler/Generator.java tomcat11-11.0.22/java/org/apache/jasper/compiler/Generator.java --- tomcat11-11.0.15/java/org/apache/jasper/compiler/Generator.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/jasper/compiler/Generator.java 2026-05-01 18:56:05.000000000 +0000 @@ -31,6 +31,7 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -552,7 +553,7 @@ if (!dependants.isEmpty()) { out.printil("static {"); out.pushIndent(); - out.printin("_jspx_dependants = new java.util.HashMap("); + out.printin("_jspx_dependants = new java.util.LinkedHashMap("); out.print("" + dependants.size()); out.println(");"); for (Entry entry : dependants.entrySet()) { @@ -569,8 +570,8 @@ // Static data for getImports() List imports = pageInfo.getImports(); - Set packages = new HashSet<>(); - Set classes = new HashSet<>(); + Set packages = new LinkedHashSet<>(); + Set classes = new LinkedHashSet<>(); for (String importName : imports) { String trimmed = importName.trim(); if (trimmed.endsWith(".*")) { @@ -2091,6 +2092,10 @@ writeNewInstance(tagHandlerVar, tagHandlerClass); } + // Wrap use of tag in try/finally to ensure clean-up takes place + out.printil("try {"); + out.pushIndent(); + // includes setting the context generateSetters(n, tagHandlerVar, handlerInfo, false); @@ -2270,13 +2275,15 @@ out.pushIndent(); out.printin(tagHandlerVar); out.println(".doFinally();"); - } - - if (n.implementsTryCatchFinally()) { out.popIndent(); out.printil("}"); } + // Ensure clean-up takes place + out.popIndent(); + out.printil("} finally {"); + out.pushIndent(); + if (usePooling(n)) { // Print tag reuse out.printin(n.getTagHandlerPoolName()); @@ -2289,6 +2296,8 @@ out.print(tagHandlerVar); out.println(", _jsp_getInstanceManager());"); } + out.popIndent(); + out.printil("}"); // Declare and synchronize AT_END scripting variables (must do this // outside the try/catch/finally block) diff -Nru tomcat11-11.0.15/java/org/apache/jasper/compiler/JDTCompiler.java tomcat11-11.0.22/java/org/apache/jasper/compiler/JDTCompiler.java --- tomcat11-11.0.15/java/org/apache/jasper/compiler/JDTCompiler.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/jasper/compiler/JDTCompiler.java 2026-05-01 18:56:05.000000000 +0000 @@ -334,10 +334,17 @@ } else if (opt.equals("24")) { settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_24); } else if (opt.equals("25")) { + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_25); + } else if (opt.equals("26")) { // Constant not available in latest ECJ version shipped with // Tomcat. May be supported in a snapshot build. // This is checked against the actual version below. - settings.put(CompilerOptions.OPTION_Source, "25"); + settings.put(CompilerOptions.OPTION_Source, "26"); + } else if (opt.equals("27")) { + // Constant not available in latest ECJ version shipped with + // Tomcat. May be supported in a snapshot build. + // This is checked against the actual version below. + settings.put(CompilerOptions.OPTION_Source, "27"); } else { log.warn(Localizer.getMessage("jsp.warning.unknown.sourceVM", opt)); settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_17); @@ -421,11 +428,20 @@ settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_24); settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_24); } else if (opt.equals("25")) { + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_25); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_25); + } else if (opt.equals("26")) { + // Constant not available in latest ECJ version shipped with + // Tomcat. May be supported in a snapshot build. + // This is checked against the actual version below. + settings.put(CompilerOptions.OPTION_TargetPlatform, "26"); + settings.put(CompilerOptions.OPTION_Compliance, "26"); + } else if (opt.equals("27")) { // Constant not available in latest ECJ version shipped with // Tomcat. May be supported in a snapshot build. // This is checked against the actual version below. - settings.put(CompilerOptions.OPTION_TargetPlatform, "25"); - settings.put(CompilerOptions.OPTION_Compliance, "25"); + settings.put(CompilerOptions.OPTION_TargetPlatform, "27"); + settings.put(CompilerOptions.OPTION_Compliance, "27"); } else { log.warn(Localizer.getMessage("jsp.warning.unknown.targetVM", opt)); settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_17); diff -Nru tomcat11-11.0.15/java/org/apache/jasper/compiler/PageInfo.java tomcat11-11.0.22/java/org/apache/jasper/compiler/PageInfo.java --- tomcat11-11.0.15/java/org/apache/jasper/compiler/PageInfo.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/jasper/compiler/PageInfo.java 2026-05-01 18:56:05.000000000 +0000 @@ -21,6 +21,7 @@ import java.util.Deque; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -111,7 +112,7 @@ this.jspPrefixMapper = new HashMap<>(); this.xmlPrefixMapper = new HashMap<>(); this.nonCustomTagPrefixMap = new HashMap<>(); - this.dependants = new HashMap<>(); + this.dependants = new LinkedHashMap<>(); this.includePrelude = new ArrayList<>(); this.includeCoda = new ArrayList<>(); this.pluginDcls = new ArrayList<>(); diff -Nru tomcat11-11.0.15/java/org/apache/jasper/compiler/TagFileProcessor.java tomcat11-11.0.22/java/org/apache/jasper/compiler/TagFileProcessor.java --- tomcat11-11.0.15/java/org/apache/jasper/compiler/TagFileProcessor.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/jasper/compiler/TagFileProcessor.java 2026-05-01 18:56:05.000000000 +0000 @@ -574,18 +574,31 @@ String tagFilePath = tagFileInfo.getPath(); if (tagFilePath.startsWith("/META-INF/")) { // For tags in JARs, add the TLD and the tag as a dependency - TldResourcePath tldResourcePath = compiler.getCompilationContext() - .getTldResourcePath(tagFileInfo.getTagInfo().getTagLibrary().getURI()); - + String tagLibraryUri = tagFileInfo.getTagInfo().getTagLibrary().getURI(); + TldResourcePath tldResourcePath = + compiler.getCompilationContext().getTldResourcePath(tagLibraryUri); + String tldWebAppPath = tldResourcePath.getWebappPath(); try (Jar jar = tldResourcePath.openJar()) { if (jar != null) { + /* + * If the JAR is not in the web application path, use the stable Tag Library URI as the + * dependency key to keep the generated code deterministic across build environments. + */ + String tldKey; + String tagKey; + if (tldWebAppPath == null) { + tldKey = "uri:" + tagLibraryUri + "!/" + tldResourcePath.getEntryName(); + tagKey = "uri:" + tagLibraryUri + "!/" + tagFilePath.substring(1); + } else { + tldKey = jar.getURL(tldResourcePath.getEntryName()); + tagKey = jar.getURL(tagFilePath.substring(1)); + } // Add TLD - pageInfo.addDependant(jar.getURL(tldResourcePath.getEntryName()), + pageInfo.addDependant(tldKey, Long.valueOf(jar.getLastModified(tldResourcePath.getEntryName()))); // Add Tag - pageInfo.addDependant(jar.getURL(tagFilePath.substring(1)), - Long.valueOf(jar.getLastModified(tagFilePath.substring(1)))); + pageInfo.addDependant(tagKey, Long.valueOf(jar.getLastModified(tagFilePath.substring(1)))); } else { pageInfo.addDependant(tagFilePath, compiler.getCompilationContext().getLastModified(tagFilePath)); diff -Nru tomcat11-11.0.15/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java tomcat11-11.0.22/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java --- tomcat11-11.0.15/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java 2026-05-01 18:56:05.000000000 +0000 @@ -23,7 +23,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.net.URLConnection; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -133,30 +132,24 @@ } if (jar != null) { if (path == null) { - // JAR not in the web application so add it directly - URL jarUrl = jar.getJarFileURL(); - long lastMod; - URLConnection urlConn = null; - try { - urlConn = jarUrl.openConnection(); - lastMod = urlConn.getLastModified(); - } catch (IOException ioe) { - throw new JasperException(ioe); - } finally { - if (urlConn != null) { - try { - urlConn.getInputStream().close(); - } catch (IOException ignore) { - // Ignore - } - } - } - pageInfo.addDependant(jarUrl.toExternalForm(), Long.valueOf(lastMod)); + // JAR not in the web application so add it directly. Use the + // stable taglib URI as the dependency key instead of the + // absolute JAR URL to keep the generated servlet code + // deterministic across build environments. + long lastMod = jar.getLastModified(); + pageInfo.addDependant("uri:" + uriIn, Long.valueOf(lastMod)); } - // Add TLD within the JAR to the dependency list + // Add TLD within the JAR to the dependency list. For external + // JARs (path == null) use a stable "uri:...!/entryName" key + // instead of the absolute jar.getURL(entryName) to keep the + // generated servlet code deterministic across build environments. String entryName = tldResourcePath.getEntryName(); try { - pageInfo.addDependant(jar.getURL(entryName), Long.valueOf(jar.getLastModified(entryName))); + String tldKey = path != null + ? jar.getURL(entryName) + : "uri:" + uriIn + "!/" + entryName; + pageInfo.addDependant(tldKey, + Long.valueOf(jar.getLastModified(entryName))); } catch (IOException ioe) { throw new JasperException(ioe); } diff -Nru tomcat11-11.0.15/java/org/apache/jasper/compiler/TextOptimizer.java tomcat11-11.0.22/java/org/apache/jasper/compiler/TextOptimizer.java --- tomcat11-11.0.15/java/org/apache/jasper/compiler/TextOptimizer.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/jasper/compiler/TextOptimizer.java 2026-05-01 18:56:05.000000000 +0000 @@ -21,6 +21,7 @@ import org.apache.jasper.TrimSpacesOption; /** + * Optimizes text in JSP pages. */ public class TextOptimizer { diff -Nru tomcat11-11.0.15/java/org/apache/jasper/runtime/JspWriterImpl.java tomcat11-11.0.22/java/org/apache/jasper/runtime/JspWriterImpl.java --- tomcat11-11.0.15/java/org/apache/jasper/runtime/JspWriterImpl.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/jasper/runtime/JspWriterImpl.java 2026-05-01 18:56:05.000000000 +0000 @@ -52,7 +52,7 @@ * @param autoFlush true to automatically flush on buffer full, false to throw an overflow * exception in that case * - * @exception IllegalArgumentException If sz is <= 0 + * @exception IllegalArgumentException If sz is < 0 */ public JspWriterImpl(ServletResponse response, int sz, boolean autoFlush) { super(sz, autoFlush); diff -Nru tomcat11-11.0.15/java/org/apache/naming/LocalStrings_zh_CN.properties tomcat11-11.0.22/java/org/apache/naming/LocalStrings_zh_CN.properties --- tomcat11-11.0.15/java/org/apache/naming/LocalStrings_zh_CN.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/naming/LocalStrings_zh_CN.properties 2026-05-01 18:56:05.000000000 +0000 @@ -21,6 +21,8 @@ contextBindings.unknownContext=未知.上下文名:[{0}] namingContext.alreadyBound=名称[{0}]已在此上下文中绑定 +namingContext.contextExpected=名称 [{0}] 未被绑定到一个域 +namingContext.failResolvingReference=解析名称为 [{0}] 的引用时发生意外的异常。 namingContext.invalidName=名称无效 namingContext.nameNotBound=名称[{0}]未在此上下文中绑定。找不到[{1}]。 namingContext.noAbsoluteName=无法为此命名空间生成绝对名称 diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/Jar.java tomcat11-11.0.22/java/org/apache/tomcat/Jar.java --- tomcat11-11.0.15/java/org/apache/tomcat/Jar.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/Jar.java 2026-05-01 18:56:05.000000000 +0000 @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.net.URLConnection; import java.util.jar.Manifest; /** @@ -48,6 +49,31 @@ InputStream getInputStream(String name) throws IOException; /** + * Obtain the last modified time for the JAR. + * + * @return The time (in the same format as {@link System#currentTimeMillis()}) that the resource was last modified. + * Returns -1 if the entry does not exist + * + * @throws IOException if an I/O error occurs while processing the JAR file + */ + default long getLastModified() throws IOException { + URL jarUrl = getJarFileURL(); + URLConnection urlConn = null; + try { + urlConn = jarUrl.openConnection(); + return urlConn.getLastModified(); + } finally { + if (urlConn != null) { + try { + urlConn.getInputStream().close(); + } catch (IOException ignore) { + // Ignore + } + } + } + } + + /** * Obtain the last modified time for the given resource in the JAR. * * @param name Entry to obtain the modification time for diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/AbandonedTrace.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/AbandonedTrace.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/AbandonedTrace.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/AbandonedTrace.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -131,12 +131,12 @@ * @return List of objects. */ protected List getTrace() { - final int size = traceList.size(); - if (size == 0) { - return Collections.emptyList(); - } - final ArrayList result = new ArrayList<>(size); synchronized (this.traceList) { + final int size = traceList.size(); + if (size == 0) { + return Collections.emptyList(); + } + final ArrayList result = new ArrayList<>(size); final Iterator> iter = traceList.iterator(); while (iter.hasNext()) { final AbandonedTrace trace = iter.next().get(); @@ -147,8 +147,8 @@ result.add(trace); } } + return result; } - return result; } /** diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSource.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSource.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSource.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -69,8 +69,8 @@ private static final Log log = LogFactory.getLog(BasicDataSource.class); static { - // Attempt to prevent deadlocks - see DBCP - 272 - DriverManager.getDrivers(); + // Attempt to prevent deadlocks - see DBCP-272 + DriverManager.getDrivers(); // NOPMD } /** @@ -126,7 +126,7 @@ * The property that controls if the pooled connections cache some state rather than query the database for current * state to improve performance. */ - private boolean cacheState = true; + private volatile boolean cacheState = true; /** * The instance of the JDBC Driver to use. @@ -192,7 +192,7 @@ */ private boolean poolPreparedStatements; - private boolean clearStatementPoolOnReturn; + private volatile boolean clearStatementPoolOnReturn; /** *

    @@ -303,19 +303,19 @@ /** * Controls access to the underlying connection. */ - private boolean accessToUnderlyingConnectionAllowed; + private volatile boolean accessToUnderlyingConnectionAllowed; private Duration maxConnDuration = Duration.ofMillis(-1); - private boolean logExpiredConnections = true; + private volatile boolean logExpiredConnections = true; private String jmxName; - private boolean registerConnectionMBean = true; + private volatile boolean registerConnectionMBean = true; - private boolean autoCommitOnReturn = true; + private volatile boolean autoCommitOnReturn = true; - private boolean rollbackOnReturn = true; + private volatile boolean rollbackOnReturn = true; private volatile Set disconnectionSqlCodes; @@ -326,7 +326,7 @@ */ private volatile Set disconnectionIgnoreSqlCodes; - private boolean fastFailValidation; + private volatile boolean fastFailValidation; /** * The object pool that internally manages our connections. @@ -362,6 +362,13 @@ private ObjectNameWrapper registeredJmxObjectName; /** + * Constructs a new instance. + */ + public BasicDataSource() { + // empty + } + + /** * Adds a custom connection property to the set that will be passed to our JDBC driver. This MUST * be called before the first connection is retrieved (along with all the other configuration property setters). * Calls to this method after the connection pool has been initialized have no effect. @@ -431,7 +438,7 @@ * with the specified {@link ClassLoader}. *

  • If {code driverClassName} is specified and the previous attempt fails, the class is loaded using the * context class loader of the current thread.
  • - *
  • If a driver still isn't loaded one is loaded via the {@link DriverManager} using the specified {code connectionString}. + *
  • If a driver still isn't loaded one is loaded via the {@link DriverManager} using the specified {code connectionString}.
  • * *

    * This method exists so subclasses can replace the implementation class. @@ -1982,8 +1989,8 @@ *

    * * @param disconnectionSqlCodes SQL State codes considered to signal fatal conditions - * @since 2.1 * @throws IllegalArgumentException if any SQL state codes overlap with those in {@link #disconnectionIgnoreSqlCodes}. + * @since 2.1 */ public void setDisconnectionSqlCodes(final Collection disconnectionSqlCodes) { Utils.checkSqlCodes(disconnectionSqlCodes, this.disconnectionIgnoreSqlCodes); @@ -2071,8 +2078,10 @@ } /** + * Sets whether connections created by this factory will fast fail validation. + * + * @param fastFailValidation true means connections created by this factory will fast fail validation. * @see #getFastFailValidation() - * @param fastFailValidation true means connections created by this factory will fast fail validation * @since 2.1 */ public void setFastFailValidation(final boolean fastFailValidation) { @@ -2116,6 +2125,8 @@ } /** + * Sets whether to log abandoned resources. + * * @param logAbandoned new logAbandoned property value */ public void setLogAbandoned(final boolean logAbandoned) { @@ -2353,8 +2364,9 @@ } /** - * @param removeAbandonedOnBorrow true means abandoned connections may be removed when connections are borrowed from - * the pool. + * Sets abandoned connections may be removed when connections are borrowed from the pool. + * + * @param removeAbandonedOnBorrow true means abandoned connections may be removed when connections are borrowed from the pool. * @see #getRemoveAbandonedOnBorrow() */ public void setRemoveAbandonedOnBorrow(final boolean removeAbandonedOnBorrow) { @@ -2362,6 +2374,8 @@ } /** + * Sets whether abandoned connections may be removed on pool maintenance. + * * @param removeAbandonedOnMaintenance true means abandoned connections may be removed on pool maintenance. * @see #getRemoveAbandonedOnMaintenance() */ diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -49,12 +49,12 @@ import org.apache.tomcat.dbcp.pool2.impl.GenericObjectPoolConfig; /** - * JNDI object factory that creates an instance of {@code BasicDataSource} that has been configured based on the - * {@code RefAddr} values of the specified {@code Reference}, which must match the names and data types of the - * {@code BasicDataSource} bean properties with the following exceptions: + * JNDI object factory that creates an instance of {@link BasicDataSource} that has been configured based on the + * {@link RefAddr} values of the specified {@code Reference}, which must match the names and data types of the + * {@link BasicDataSource} bean properties with the following exceptions: *
      *
    • {@code connectionInitSqls} must be passed to this factory as a single String using semicolon to delimit the - * statements whereas {@code BasicDataSource} requires a collection of Strings.
    • + * statements whereas {@link BasicDataSource} requires a collection of Strings. *
    * * @since 2.0 @@ -131,7 +131,6 @@ */ private static final String PROP_DISCONNECTION_IGNORE_SQL_CODES = "disconnectionIgnoreSqlCodes"; - /* * Block with obsolete properties from DBCP 1.x. Warn users that these are ignored and they should use the 2.x * properties. @@ -175,11 +174,11 @@ "Property " + NUPROP_MAX_ACTIVE + " is not used in DBCP2, use " + PROP_MAX_TOTAL + " instead. " + PROP_MAX_TOTAL + " default value is " + GenericObjectPoolConfig.DEFAULT_MAX_TOTAL + "."); NUPROP_WARNTEXT.put(NUPROP_REMOVE_ABANDONED, - "Property " + NUPROP_REMOVE_ABANDONED + " is not used in DBCP2," + " use one or both of " + "Property " + NUPROP_REMOVE_ABANDONED + " is not used in DBCP2, use one or both of " + PROP_REMOVE_ABANDONED_ON_BORROW + " or " + PROP_REMOVE_ABANDONED_ON_MAINTENANCE + " instead. " + "Both have default value set to false."); NUPROP_WARNTEXT.put(NUPROP_MAXWAIT, - "Property " + NUPROP_MAXWAIT + " is not used in DBCP2" + " , use " + PROP_MAX_WAIT_MILLIS + " instead. " + "Property " + NUPROP_MAXWAIT + " is not used in DBCP2 , use " + PROP_MAX_WAIT_MILLIS + " instead. " + PROP_MAX_WAIT_MILLIS + " default value is " + BaseObjectPoolConfig.DEFAULT_MAX_WAIT + "."); } @@ -240,31 +239,31 @@ value = value.toUpperCase(Locale.ROOT); int level = PoolableConnectionFactory.UNKNOWN_TRANSACTION_ISOLATION; switch (value) { - case "NONE": - level = Connection.TRANSACTION_NONE; - break; - case "READ_COMMITTED": - level = Connection.TRANSACTION_READ_COMMITTED; - break; - case "READ_UNCOMMITTED": - level = Connection.TRANSACTION_READ_UNCOMMITTED; - break; - case "REPEATABLE_READ": - level = Connection.TRANSACTION_REPEATABLE_READ; - break; - case "SERIALIZABLE": - level = Connection.TRANSACTION_SERIALIZABLE; - break; - default: - try { - level = Integer.parseInt(value); - } catch (final NumberFormatException e) { - System.err.println("Could not parse defaultTransactionIsolation: " + value); - System.err.println("WARNING: defaultTransactionIsolation not set"); - System.err.println("using default value of database driver"); - level = PoolableConnectionFactory.UNKNOWN_TRANSACTION_ISOLATION; - } - break; + case "NONE": + level = Connection.TRANSACTION_NONE; + break; + case "READ_COMMITTED": + level = Connection.TRANSACTION_READ_COMMITTED; + break; + case "READ_UNCOMMITTED": + level = Connection.TRANSACTION_READ_UNCOMMITTED; + break; + case "REPEATABLE_READ": + level = Connection.TRANSACTION_REPEATABLE_READ; + break; + case "SERIALIZABLE": + level = Connection.TRANSACTION_SERIALIZABLE; + break; + default: + try { + level = Integer.parseInt(value); + } catch (final NumberFormatException e) { + System.err.println("Could not parse defaultTransactionIsolation: " + value); + System.err.println("WARNING: defaultTransactionIsolation not set"); + System.err.println("using default value of database driver"); + level = PoolableConnectionFactory.UNKNOWN_TRANSACTION_ISOLATION; + } + break; } dataSource.setDefaultTransactionIsolation(level); }); @@ -306,9 +305,10 @@ final String value = properties.getProperty(PROP_CONNECTION_PROPERTIES); if (value != null) { - for (final Object key : getProperties(value).keySet()) { - final String propertyName = Objects.toString(key, null); - dataSource.addConnectionProperty(propertyName, getProperties(value).getProperty(propertyName)); + final Properties connectionProperties = getProperties(value); + for (final Object key : connectionProperties.keySet()) { + final String propertyName = Objects.toString(key); + dataSource.addConnectionProperty(propertyName, connectionProperties.getProperty(propertyName)); } } @@ -376,7 +376,14 @@ } /** - * Creates and return a new {@code BasicDataSource} instance. If no instance can be created, return + * Constructs a new instance. + */ + public BasicDataSourceFactory() { + // empty + } + + /** + * Creates and return a new {@link BasicDataSource} instance. If no instance can be created, return * {@code null} instead. * * @param obj diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceMXBean.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceMXBean.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceMXBean.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/BasicDataSourceMXBean.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -20,15 +20,15 @@ import java.sql.SQLException; /** - * Abstract factory interface for creating {@link java.sql.Connection}s. + * Abstract factory interface for creating {@link Connection}s. * * @since 2.0 */ public interface ConnectionFactory { /** - * Create a new {@link java.sql.Connection} in an implementation specific fashion. + * Create a new {@link Connection} in an implementation specific fashion. * - * @return a new {@link java.sql.Connection} + * @return a new {@link Connection} * @throws SQLException * if a database error occurs creating the connection */ diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactoryFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactoryFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactoryFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ConnectionFactoryFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/Constants.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/Constants.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/Constants.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/Constants.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -61,4 +61,14 @@ * @since 2.9.0 */ public static final String KEY_USER = "user"; + + /** + * Deprecated, only contains static methods. + * + * @deprecated Will be private in the next major version. + */ + @Deprecated + public Constants() { + // empty + } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DataSourceConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DataSourceConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DataSourceConnectionFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DataSourceConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -86,6 +86,8 @@ } /** + * Gets the data source.. + * * @return The data source. * @since 2.6.0 */ @@ -94,7 +96,9 @@ } /** - * @return The user name. + * Gets the user name, may be null. + * + * @return The user name, may be null. * @since 2.6.0 */ public String getUserName() { @@ -102,7 +106,9 @@ } /** - * @return The user password. + * Gets the user password, may be null. + * + * @return The user password, may be null. * @since 2.6.0 */ public char[] getUserPassword() { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DataSourceMXBean.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DataSourceMXBean.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DataSourceMXBean.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DataSourceMXBean.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingCallableStatement.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingCallableStatement.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingCallableStatement.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingCallableStatement.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingConnection.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingConnection.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingConnection.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingConnection.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -69,8 +69,7 @@ private volatile C connection; private volatile boolean closed; - - private boolean cacheState = true; + private volatile boolean cacheState = true; private Boolean cachedAutoCommit; private Boolean cachedReadOnly; private String cachedCatalog; @@ -167,6 +166,11 @@ } } + /** + * Closes the underlying connection for {@link #close()}. + * + * @throws SQLException SQLException if a database access error occurs. + */ protected final void closeInternal() throws SQLException { try { passivate(); @@ -284,7 +288,7 @@ @Override public Statement createStatement(final int resultSetType, final int resultSetConcurrency, - final int resultSetHoldability) throws SQLException { + final int resultSetHoldability) throws SQLException { checkOpen(); try { return init(new DelegatingStatement(this, @@ -426,14 +430,14 @@ } /** - * If my underlying {@link Connection} is not a {@code DelegatingConnection}, returns it, otherwise recursively + * If my underlying {@link Connection} is not a {@link DelegatingConnection}, returns it, otherwise recursively * invokes this method on my delegate. *

    - * Hence this method will return the first delegate that is not a {@code DelegatingConnection}, or {@code null} when - * no non-{@code DelegatingConnection} delegate can be found by traversing this chain. + * Hence this method will return the first delegate that is not a {@link DelegatingConnection}, or {@code null} when + * no non-{@link DelegatingConnection} delegate can be found by traversing this chain. *

    *

    - * This method is useful when you may have nested {@code DelegatingConnection}s, and you want to make sure to obtain + * This method is useful when you may have nested {@link DelegatingConnection}s, and you want to make sure to obtain * a "genuine" {@link Connection}. *

    * @@ -541,11 +545,11 @@ } /** - * Handles the given {@code SQLException}. + * Handles the given {@link SQLException}. * * @param The throwable type. * @param e The SQLException - * @return the given {@code SQLException} + * @return the given {@link SQLException} * @since 2.7.0 */ protected T handleExceptionNoThrow(final T e) { @@ -643,10 +647,7 @@ @Override public boolean isWrapperFor(final Class iface) throws SQLException { - if (iface.isAssignableFrom(getClass())) { - return true; - } - if (iface.isAssignableFrom(connection.getClass())) { + if (iface.isAssignableFrom(getClass()) || iface.isAssignableFrom(connection.getClass())) { return true; } return connection.isWrapperFor(iface); @@ -710,7 +711,7 @@ @Override public CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency, - final int resultSetHoldability) throws SQLException { + final int resultSetHoldability) throws SQLException { checkOpen(); try { return init(new DelegatingCallableStatement(this, @@ -758,7 +759,7 @@ @Override public PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency, - final int resultSetHoldability) throws SQLException { + final int resultSetHoldability) throws SQLException { checkOpen(); try { return init(new DelegatingPreparedStatement(this, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingDatabaseMetaData.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingDatabaseMetaData.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingDatabaseMetaData.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingDatabaseMetaData.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -264,13 +264,13 @@ } /** - * If my underlying {@link ResultSet} is not a {@code DelegatingResultSet}, returns it, otherwise recursively invokes this method on my delegate. + * If my underlying {@link ResultSet} is not a {@link DelegatingResultSet}, returns it, otherwise recursively invokes this method on my delegate. *

    - * Hence this method will return the first delegate that is not a {@code DelegatingResultSet}, or {@code null} when no non-{@code DelegatingResultSet} + * Hence this method will return the first delegate that is not a {@link DelegatingResultSet}, or {@code null} when no non-{@link DelegatingResultSet} * delegate can be found by traversing this chain. *

    *

    - * This method is useful when you may have nested {@code DelegatingResultSet}s, and you want to make sure to obtain a "genuine" {@link ResultSet}. + * This method is useful when you may have nested {@link DelegatingResultSet}s, and you want to make sure to obtain a "genuine" {@link ResultSet}. *

    * * @return the innermost database meta data. @@ -580,10 +580,7 @@ @Override public boolean isWrapperFor(final Class iface) throws SQLException { - if (iface.isAssignableFrom(getClass())) { - return true; - } - if (iface.isAssignableFrom(databaseMetaData.getClass())) { + if (iface.isAssignableFrom(getClass()) || iface.isAssignableFrom(databaseMetaData.getClass())) { return true; } return databaseMetaData.isWrapperFor(iface); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingPreparedStatement.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingPreparedStatement.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingPreparedStatement.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingPreparedStatement.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -167,10 +167,14 @@ } } + /** + * Prepares internal states before calling {@link #passivate()}. + * + * @throws SQLException Thrown closing a traced resource or calling {@link #passivate()}. + */ protected void prepareToReturn() throws SQLException { setClosedInternal(true); removeThisTrace(getConnectionInternal()); - // The JDBC spec requires that a statement close any open // ResultSet's when it is closed. // FIXME The PreparedStatement we're wrapping should handle this for us. @@ -184,7 +188,6 @@ throw new SQLExceptionList(thrownList); } } - super.passivate(); } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingResultSet.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingResultSet.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingResultSet.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingResultSet.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -595,14 +595,14 @@ } /** - * If my underlying {@link ResultSet} is not a {@code DelegatingResultSet}, returns it, otherwise recursively + * If my underlying {@link ResultSet} is not a {@link DelegatingResultSet}, returns it, otherwise recursively * invokes this method on my delegate. *

    - * Hence this method will return the first delegate that is not a {@code DelegatingResultSet}, or {@code null} when - * no non-{@code DelegatingResultSet} delegate can be found by traversing this chain. + * Hence this method will return the first delegate that is not a {@link DelegatingResultSet}, or {@code null} when + * no non-{@link DelegatingResultSet} delegate can be found by traversing this chain. *

    *

    - * This method is useful when you may have nested {@code DelegatingResultSet}s, and you want to make sure to obtain + * This method is useful when you may have nested {@link DelegatingResultSet}s, and you want to make sure to obtain * a "genuine" {@link ResultSet}. *

    * @@ -1052,6 +1052,12 @@ } } + /** + * Handles a SQL exception by delegating to a DelegatingStatement or DelegatingConnection. + * + * @param e The exception to handle. + * @throws SQLException Throws the given exception if not handled. + */ protected void handleException(final SQLException e) throws SQLException { if (statement instanceof DelegatingStatement) { ((DelegatingStatement) statement).handleException(e); @@ -1123,10 +1129,7 @@ @Override public boolean isWrapperFor(final Class iface) throws SQLException { - if (iface.isAssignableFrom(getClass())) { - return true; - } - if (iface.isAssignableFrom(resultSet.getClass())) { + if (iface.isAssignableFrom(getClass()) || iface.isAssignableFrom(resultSet.getClass())) { return true; } return resultSet.isWrapperFor(iface); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -44,7 +44,7 @@ /** The connection that created me. **/ private DelegatingConnection connection; - private boolean closed; + private volatile boolean closed; /** * Create a wrapper for the Statement which traces this Statement to the Connection which created it and the code @@ -62,6 +62,8 @@ } /** + * Activates this instance by delegating to the underlying statement. + * * @throws SQLException * thrown by the delegating statement. * @since 2.4.0 made public, was protected in 2.3.0. @@ -92,6 +94,11 @@ } } + /** + * Checks whether this instance is closed and throws an exception if it is. + * + * @throws SQLException Thrown if this instance is closed. + */ protected void checkOpen() throws SQLException { if (isClosed()) { throw new SQLException(this.getClass().getName() + " with address: \"" + toString() + "\" is closed."); @@ -390,6 +397,11 @@ return getConnectionInternal(); // return the delegating connection that created this } + /** + * Gets the internal connection. + * + * @return the internal connection. + */ protected DelegatingConnection getConnectionInternal() { return connection; } @@ -438,14 +450,14 @@ } /** - * If my underlying {@link Statement} is not a {@code DelegatingStatement}, returns it, otherwise recursively + * If my underlying {@link Statement} is not a {@link DelegatingStatement}, returns it, otherwise recursively * invokes this method on my delegate. *

    - * Hence this method will return the first delegate that is not a {@code DelegatingStatement} or {@code null} when - * no non-{@code DelegatingStatement} delegate can be found by traversing this chain. + * Hence this method will return the first delegate that is not a {@link DelegatingStatement} or {@code null} when + * no non-{@link DelegatingStatement} delegate can be found by traversing this chain. *

    *

    - * This method is useful when you may have nested {@code DelegatingStatement}s, and you want to make sure to obtain + * This method is useful when you may have nested {@link DelegatingStatement}s, and you want to make sure to obtain * a "genuine" {@link Statement}. *

    * @@ -612,6 +624,12 @@ } } + /** + * Delegates the exception to the internal connection if set, otherwise rethrows it. + * + * @param e The exception to handle. + * @throws SQLException The given exception if not handled. + */ protected void handleException(final SQLException e) throws SQLException { if (connection == null) { throw e; @@ -627,6 +645,11 @@ return closed; } + /** + * Tests whether this instance is closed. + * + * @return whether this instance is closed. + */ protected boolean isClosedInternal() { return closed; } @@ -655,16 +678,15 @@ @Override public boolean isWrapperFor(final Class iface) throws SQLException { - if (iface.isAssignableFrom(getClass())) { - return true; - } - if (iface.isAssignableFrom(statement.getClass())) { + if (iface.isAssignableFrom(getClass()) || iface.isAssignableFrom(statement.getClass())) { return true; } return statement.isWrapperFor(iface); } /** + * Passivates this instance by delegating to the underlying statement. + * * @throws SQLException * thrown by the delegating statement. * @since 2.4.0 made public, was protected in 2.3.0. @@ -675,6 +697,11 @@ } } + /** + * Sets the closed internal state. + * + * @param closed whether the instance is now closed. + */ protected void setClosedInternal(final boolean closed) { this.closed = closed; } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DriverConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DriverConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DriverConnectionFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DriverConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -85,6 +85,6 @@ @Override public String toString() { return this.getClass().getName() + " [" + driver + ";" + connectionString + ";" - + Utils.cloneWithoutCredentials(properties) + "]"; + + Utils.cloneWithoutCredentials(properties) + "]"; } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DriverFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DriverFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DriverFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DriverFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -60,7 +60,7 @@ } else { // Usage of DriverManager is not possible, as it does not // respect the ContextClassLoader - // N.B. This cast may cause ClassCastException which is + // This cast may cause ClassCastException which is // handled below driverToUse = (Driver) driverFromCCL.getConstructor().newInstance(); if (!driverToUse.acceptsURL(url)) { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DriverManagerConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DriverManagerConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/DriverManagerConnectionFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/DriverManagerConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -29,12 +29,12 @@ public class DriverManagerConnectionFactory implements ConnectionFactory { static { - // Related to DBCP-212 + // Related to DBCP-272 // Driver manager does not sync loading of drivers that use the service // provider interface. This will cause issues is multi-threaded // environments. This hack makes sure the drivers are loaded before // DBCP tries to use them. - DriverManager.getDrivers(); + DriverManager.getDrivers(); // NOPMD } private final String connectionUri; @@ -123,6 +123,8 @@ } /** + * Gets the connection URI. + * * @return The connection URI. * @since 2.6.0 */ @@ -131,7 +133,9 @@ } /** - * @return The Properties. + * Gets the Properties, may be null. + * + * @return The Properties, may be null. * @since 2.6.0 */ public Properties getProperties() { @@ -139,7 +143,9 @@ } /** - * @return The user name. + * Gets the user name, may be null. + * + * @return The user name, may be null. * @since 2.6.0 */ public String getUserName() { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/Jdbc41Bridge.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/Jdbc41Bridge.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/Jdbc41Bridge.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/Jdbc41Bridge.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -485,4 +485,14 @@ } } + /** + * Deprecated, this class only contains static methods. + * + * @deprecated Constructor will be private in the next major release. + */ + @Deprecated + public Jdbc41Bridge() { + // empty + } + } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/LifetimeExceededException.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/LifetimeExceededException.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/LifetimeExceededException.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/LifetimeExceededException.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/ListException.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ListException.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/ListException.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ListException.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -27,6 +27,9 @@ private static final long serialVersionUID = 1L; + /** + * A list of causes. + */ private final List exceptionList; /** diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/ObjectNameWrapper.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ObjectNameWrapper.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/ObjectNameWrapper.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/ObjectNameWrapper.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PStmtKey.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PStmtKey.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PStmtKey.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PStmtKey.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -17,6 +17,7 @@ package org.apache.tomcat.dbcp.dbcp2; import java.sql.Connection; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Arrays; @@ -48,17 +49,17 @@ private static final StatementBuilder StatementColumnNames = (c, k) -> c.prepareStatement(k.sql, k.columnNames); private static final StatementBuilder StatementConcurrency = (c, k) -> c.prepareStatement(k.sql, k.resultSetType.intValue(), k.resultSetConcurrency.intValue()); private static final StatementBuilder StatementHoldability = (c, k) -> c.prepareStatement(k.sql, k.resultSetType.intValue(), k.resultSetConcurrency.intValue(), - k.resultSetHoldability.intValue()); + k.resultSetHoldability.intValue()); private static final StatementBuilder StatementSQL = (c, k) -> c.prepareStatement(k.sql); private static StatementBuilder match(final StatementType statementType, final StatementBuilder prep, final StatementBuilder call) { switch (Objects.requireNonNull(statementType, "statementType")) { - case PREPARED_STATEMENT: - return prep; - case CALLABLE_STATEMENT: - return call; - default: - throw new IllegalArgumentException(statementType.toString()); + case PREPARED_STATEMENT: + return prep; + case CALLABLE_STATEMENT: + return call; + default: + throw new IllegalArgumentException(statementType.toString()); } } @@ -68,20 +69,20 @@ private final String sql; /** - * Result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or - * {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * Result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or + * {@link ResultSet#TYPE_SCROLL_SENSITIVE}. */ private final Integer resultSetType; /** - * Result set concurrency. A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * Result set concurrency. A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. */ private final Integer resultSetConcurrency; /** - * Result set holdability. One of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} - * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * Result set holdability. One of the following {@link ResultSet} constants: {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} + * or {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. */ private final Integer resultSetHoldability; @@ -96,8 +97,8 @@ private final String schema; /** - * A flag indicating whether auto-generated keys should be returned; one of {@code Statement.RETURN_GENERATED_KEYS} or - * {@code Statement.NO_GENERATED_KEYS}. + * A flag indicating whether auto-generated keys should be returned; one of {@link Statement#RETURN_GENERATED_KEYS} or + * {@link Statement#NO_GENERATED_KEYS}. */ private final Integer autoGeneratedKeys; @@ -136,10 +137,10 @@ * Constructs a key to uniquely identify a prepared statement. * * @param sql The SQL statement. - * @param resultSetType A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. - * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * @param resultSetType A result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. * @deprecated Use {@link #PStmtKey(String, String, String, int, int)}. */ @Deprecated @@ -165,7 +166,7 @@ * @param sql The SQL statement. * @param catalog The catalog. * @param autoGeneratedKeys A flag indicating whether auto-generated keys should be returned; one of - * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * {@link Statement#RETURN_GENERATED_KEYS} or {@link Statement#NO_GENERATED_KEYS}. * @deprecated Use {@link #PStmtKey(String, String, String, int)}. */ @Deprecated @@ -178,10 +179,10 @@ * * @param sql The SQL statement. * @param catalog The catalog. - * @param resultSetType A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. - * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * @param resultSetType A result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. * @deprecated Use {@link #PStmtKey(String, String, String, int, int)}. */ @Deprecated @@ -194,12 +195,12 @@ * * @param sql The SQL statement. * @param catalog The catalog. - * @param resultSetType a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. - * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE} - * @param resultSetHoldability One of the following {@code ResultSet} constants: - * {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @param resultSetType a result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE} + * @param resultSetHoldability One of the following {@link ResultSet} constants: + * {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} or {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. * @deprecated Use {@link #PStmtKey(String, String, String, int, int, int)}. */ @Deprecated @@ -212,12 +213,12 @@ * * @param sql The SQL statement. * @param catalog The catalog. - * @param resultSetType a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE} - * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. - * @param resultSetHoldability One of the following {@code ResultSet} constants: - * {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @param resultSetType a result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE} + * @param resultSetConcurrency A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. + * @param resultSetHoldability One of the following {@link ResultSet} constants: + * {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} or {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. * @param statementType The SQL statement type, prepared or callable. * @deprecated Use {@link #PStmtKey(String, String, String, int, int, int, PoolingConnection.StatementType)} */ @@ -225,7 +226,7 @@ public PStmtKey(final String sql, final String catalog, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability, final StatementType statementType) { this(sql, catalog, null, Integer.valueOf(resultSetType), Integer.valueOf(resultSetConcurrency), Integer.valueOf(resultSetHoldability), null, null, null, statementType, - k -> match(statementType, StatementHoldability, CallHoldability)); + k -> match(statementType, StatementHoldability, CallHoldability)); } /** @@ -233,17 +234,17 @@ * * @param sql The SQL statement. * @param catalog The catalog. - * @param resultSetType A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. - * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * @param resultSetType A result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. * @param statementType The SQL statement type, prepared or callable. * @deprecated Use {@link #PStmtKey(String, String, String, int, int, PoolingConnection.StatementType)}. */ @Deprecated public PStmtKey(final String sql, final String catalog, final int resultSetType, final int resultSetConcurrency, final StatementType statementType) { this(sql, catalog, null, Integer.valueOf(resultSetType), Integer.valueOf(resultSetConcurrency), null, null, null, null, statementType, - k -> match(statementType, StatementConcurrency, CallConcurrency)); + k -> match(statementType, StatementConcurrency, CallConcurrency)); } /** @@ -280,13 +281,13 @@ * @param catalog The catalog. * @param statementType The SQL statement type, prepared or callable. * @param autoGeneratedKeys A flag indicating whether auto-generated keys should be returned; one of - * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * {@link Statement#RETURN_GENERATED_KEYS} or {@link Statement#NO_GENERATED_KEYS}. * @deprecated Use {@link #PStmtKey(String, String, String, PoolingConnection.StatementType, Integer)} */ @Deprecated public PStmtKey(final String sql, final String catalog, final StatementType statementType, final Integer autoGeneratedKeys) { this(sql, catalog, null, null, null, null, autoGeneratedKeys, null, null, statementType, - k -> match(statementType, StatementAutoGeneratedKeys, CallSQL)); + k -> match(statementType, StatementAutoGeneratedKeys, CallSQL)); } /** @@ -308,7 +309,7 @@ * @param catalog The catalog. * @param schema The schema * @param autoGeneratedKeys A flag indicating whether auto-generated keys should be returned; one of - * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * {@link Statement#RETURN_GENERATED_KEYS} or {@link Statement#NO_GENERATED_KEYS}. * @since 2.5.0 */ public PStmtKey(final String sql, final String catalog, final String schema, final int autoGeneratedKeys) { @@ -321,10 +322,10 @@ * @param sql The SQL statement. * @param catalog The catalog. * @param schema The schema - * @param resultSetType A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. - * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * @param resultSetType A result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. */ public PStmtKey(final String sql, final String catalog, final String schema, final int resultSetType, final int resultSetConcurrency) { this(sql, catalog, schema, resultSetType, resultSetConcurrency, StatementType.PREPARED_STATEMENT); @@ -336,12 +337,12 @@ * @param sql The SQL statement. * @param catalog The catalog. * @param schema The schema - * @param resultSetType a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. - * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE} - * @param resultSetHoldability One of the following {@code ResultSet} constants: - * {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @param resultSetType a result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE} + * @param resultSetHoldability One of the following {@link ResultSet} constants: + * {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} or {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. * @since 2.5.0 */ public PStmtKey(final String sql, final String catalog, final String schema, final int resultSetType, final int resultSetConcurrency, @@ -355,19 +356,19 @@ * @param sql The SQL statement. * @param catalog The catalog. * @param schema The schema. - * @param resultSetType a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE} - * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. - * @param resultSetHoldability One of the following {@code ResultSet} constants: - * {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * @param resultSetType a result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE} + * @param resultSetConcurrency A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. + * @param resultSetHoldability One of the following {@link ResultSet} constants: + * {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} or {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. * @param statementType The SQL statement type, prepared or callable. * @since 2.5.0 */ public PStmtKey(final String sql, final String catalog, final String schema, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability, final StatementType statementType) { this(sql, catalog, schema, Integer.valueOf(resultSetType), Integer.valueOf(resultSetConcurrency), Integer.valueOf(resultSetHoldability), null, null, null, statementType, - k -> match(statementType, StatementHoldability, CallHoldability)); + k -> match(statementType, StatementHoldability, CallHoldability)); } /** @@ -376,17 +377,17 @@ * @param sql The SQL statement. * @param catalog The catalog. * @param schema The schema. - * @param resultSetType A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. - * @param resultSetConcurrency A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * @param resultSetType A result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. + * @param resultSetConcurrency A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. * @param statementType The SQL statement type, prepared or callable. * @since 2.5.0 */ public PStmtKey(final String sql, final String catalog, final String schema, final int resultSetType, final int resultSetConcurrency, final StatementType statementType) { this(sql, catalog, schema, Integer.valueOf(resultSetType), Integer.valueOf(resultSetConcurrency), null, null, null, null, statementType, - k -> match(statementType, StatementConcurrency, CallConcurrency)); + k -> match(statementType, StatementConcurrency, CallConcurrency)); } /** @@ -403,8 +404,8 @@ } private PStmtKey(final String sql, final String catalog, final String schema, final Integer resultSetType, final Integer resultSetConcurrency, - final Integer resultSetHoldability, final Integer autoGeneratedKeys, final int[] columnIndexes, final String[] columnNames, - final StatementType statementType, final Function statementBuilder) { + final Integer resultSetHoldability, final Integer autoGeneratedKeys, final int[] columnIndexes, final String[] columnNames, + final StatementType statementType, final Function statementBuilder) { this.sql = Objects.requireNonNull(sql, "sql").trim(); this.catalog = catalog; this.schema = schema; @@ -420,8 +421,8 @@ // Root constructor. private PStmtKey(final String sql, final String catalog, final String schema, final Integer resultSetType, final Integer resultSetConcurrency, - final Integer resultSetHoldability, final Integer autoGeneratedKeys, final int[] columnIndexes, final String[] columnNames, - final StatementType statementType, final StatementBuilder statementBuilder) { + final Integer resultSetHoldability, final Integer autoGeneratedKeys, final int[] columnIndexes, final String[] columnNames, + final StatementType statementType, final StatementBuilder statementBuilder) { this.sql = sql; this.catalog = catalog; this.schema = schema; @@ -456,12 +457,12 @@ * @param schema The schema. * @param statementType The SQL statement type, prepared or callable. * @param autoGeneratedKeys A flag indicating whether auto-generated keys should be returned; one of - * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * {@link Statement#RETURN_GENERATED_KEYS} or {@link Statement#NO_GENERATED_KEYS}. * @since 2.5.0 */ public PStmtKey(final String sql, final String catalog, final String schema, final StatementType statementType, final Integer autoGeneratedKeys) { this(sql, catalog, schema, null, null, null, autoGeneratedKeys, null, null, statementType, - k -> match(statementType, StatementAutoGeneratedKeys, CallSQL)); + k -> match(statementType, StatementAutoGeneratedKeys, CallSQL)); } /** @@ -516,35 +517,16 @@ if (this == obj) { return true; } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { + if (obj == null || getClass() != obj.getClass()) { return false; } final PStmtKey other = (PStmtKey) obj; - if (!Objects.equals(autoGeneratedKeys, other.autoGeneratedKeys)) { - return false; - } - if (!Objects.equals(catalog, other.catalog)) { - return false; - } - if (!Arrays.equals(columnIndexes, other.columnIndexes)) { - return false; - } - if (!Arrays.equals(columnNames, other.columnNames)) { - return false; - } - if (!Objects.equals(resultSetConcurrency, other.resultSetConcurrency)) { - return false; - } - if (!Objects.equals(resultSetHoldability, other.resultSetHoldability)) { - return false; - } - if (!Objects.equals(resultSetType, other.resultSetType)) { + if (!Objects.equals(autoGeneratedKeys, other.autoGeneratedKeys) || !Objects.equals(catalog, other.catalog) + || !Arrays.equals(columnIndexes, other.columnIndexes) || !Arrays.equals(columnNames, other.columnNames)) { return false; } - if (!Objects.equals(schema, other.schema)) { + if (!Objects.equals(resultSetConcurrency, other.resultSetConcurrency) || !Objects.equals(resultSetHoldability, other.resultSetHoldability) + || !Objects.equals(resultSetType, other.resultSetType) || !Objects.equals(schema, other.schema)) { return false; } if (!Objects.equals(sql, other.sql)) { @@ -554,8 +536,8 @@ } /** - * Gets a flag indicating whether auto-generated keys should be returned; one of {@code Statement.RETURN_GENERATED_KEYS} - * or {@code Statement.NO_GENERATED_KEYS}. + * Gets a flag indicating whether auto-generated keys should be returned; one of {@link Statement#RETURN_GENERATED_KEYS} + * or {@link Statement#NO_GENERATED_KEYS}. * * @return a flag indicating whether auto-generated keys should be returned. */ @@ -591,8 +573,8 @@ } /** - * Gets the result set concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * Gets the result set concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. * * @return The result set concurrency type. */ @@ -601,8 +583,8 @@ } /** - * Gets the result set holdability, one of the following {@code ResultSet} constants: - * {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * Gets the result set holdability, one of the following {@link ResultSet} constants: + * {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} or {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. * * @return The result set holdability. */ @@ -611,8 +593,8 @@ } /** - * Gets the result set type, one of {@code ResultSet.TYPE_FORWARD_ONLY}, {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or - * {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * Gets the result set type, one of {@link ResultSet#TYPE_FORWARD_ONLY}, {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or + * {@link ResultSet#TYPE_SCROLL_SENSITIVE}. * * @return the result set type. */ @@ -650,7 +632,7 @@ @Override public int hashCode() { return Objects.hash(autoGeneratedKeys, catalog, Integer.valueOf(Arrays.hashCode(columnIndexes)), Integer.valueOf(Arrays.hashCode(columnNames)), - resultSetConcurrency, resultSetHoldability, resultSetType, schema, sql, statementType); + resultSetConcurrency, resultSetHoldability, resultSetType, schema, sql, statementType); } @Override diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolableCallableStatement.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableCallableStatement.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolableCallableStatement.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableCallableStatement.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnection.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnection.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnection.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnection.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -24,6 +24,7 @@ import java.time.Duration; import java.util.Collection; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -59,8 +60,9 @@ private final ObjectNameWrapper jmxObjectName; - // Use a prepared statement for validation, retaining the last used SQL to - // check if the validation query has changed. + /** + * Use a prepared statement for validation, retaining the last used SQL to check if the validation query has changed. + */ private PreparedStatement validationPreparedStatement; private String lastValidationSql; @@ -68,7 +70,7 @@ * Indicate that unrecoverable SQLException was thrown when using this connection. Such a connection should be * considered broken and not pass validation in the future. */ - private boolean fatalSqlExceptionThrown; + private final AtomicBoolean fatalSqlExceptionThrown = new AtomicBoolean(); /** * SQL State codes considered to signal fatal conditions. Overrides the defaults in @@ -83,13 +85,14 @@ */ private final Collection disconnectionIgnoreSqlCodes; - /** Whether or not to fast fail validation after fatal connection errors */ private final boolean fastFailValidation; private final Lock lock = new ReentrantLock(); /** + * Constructs a new instance. + * * @param conn * my underlying connection * @param pool @@ -103,6 +106,8 @@ } /** + * Constructs a new instance. + * * @param conn * my underlying connection * @param pool @@ -237,6 +242,8 @@ } /** + * Gets the disconnection SQL codes. + * * @return The disconnection SQL codes. * @since 2.6.0 */ @@ -254,7 +261,7 @@ @Override protected void handleException(final SQLException e) throws SQLException { - fatalSqlExceptionThrown |= isFatalException(e); + fatalSqlExceptionThrown.compareAndSet(false, isFatalException(e)); super.handleException(e); } @@ -264,6 +271,7 @@ * This method should not be used by a client to determine whether or not a connection should be return to the * connection pool (by calling {@link #close()}). Clients should always attempt to return a connection to the pool * once it is no longer required. + *

    */ @Override public boolean isClosed() throws SQLException { @@ -303,13 +311,15 @@ return false; } fatalException = disconnectionSqlCodes == null - ? sqlState.startsWith(Utils.DISCONNECTION_SQL_CODE_PREFIX) || Utils.isDisconnectionSqlCode(sqlState) - : disconnectionSqlCodes.contains(sqlState); + ? sqlState.startsWith(Utils.DISCONNECTION_SQL_CODE_PREFIX) || Utils.isDisconnectionSqlCode(sqlState) + : disconnectionSqlCodes.contains(sqlState); } return fatalException; } /** + * Tests whether to fail-fast. + * * @return Whether to fail-fast. * @since 2.6.0 */ @@ -384,11 +394,11 @@ * Validates the connection, using the following algorithm: *
      *
    1. If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously - * thrown a fatal disconnection exception, a {@code SQLException} is thrown.
    2. + * thrown a fatal disconnection exception, a {@link SQLException} is thrown. *
    3. If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it - * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.
    4. - *
    5. If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at - * least one row, this method returns successfully. If not, {@code SQLException} is thrown.
    6. + * returns {@code false}, {@link SQLException} is thrown; otherwise, this method returns successfully. + *
    7. If {@code sql} is not null, it is executed as a query and if the resulting {@link ResultSet} contains at + * least one row, this method returns successfully. If not, {@link SQLException} is thrown.
    8. *
    * * @param sql @@ -400,7 +410,7 @@ * @since 2.10.0 */ public void validate(final String sql, Duration timeoutDuration) throws SQLException { - if (fastFailValidation && fatalSqlExceptionThrown) { + if (fastFailValidation && fatalSqlExceptionThrown.get()) { throw new SQLException(Utils.getMessage("poolableConnection.validate.fastFail")); } @@ -438,11 +448,11 @@ * Validates the connection, using the following algorithm: *
      *
    1. If {@code fastFailValidation} (constructor argument) is {@code true} and this connection has previously - * thrown a fatal disconnection exception, a {@code SQLException} is thrown.
    2. + * thrown a fatal disconnection exception, a {@link SQLException} is thrown. *
    3. If {@code sql} is null, the driver's #{@link Connection#isValid(int) isValid(timeout)} is called. If it - * returns {@code false}, {@code SQLException} is thrown; otherwise, this method returns successfully.
    4. - *
    5. If {@code sql} is not null, it is executed as a query and if the resulting {@code ResultSet} contains at - * least one row, this method returns successfully. If not, {@code SQLException} is thrown.
    6. + * returns {@code false}, {@link SQLException} is thrown; otherwise, this method returns successfully. + *
    7. If {@code sql} is not null, it is executed as a query and if the resulting {@link ResultSet} contains at + * least one row, this method returns successfully. If not, {@link SQLException} is thrown.
    8. *
    * * @param sql diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -66,7 +66,7 @@ private Collection disconnectionIgnoreSqlCodes; - private boolean fastFailValidation = true; + private volatile boolean fastFailValidation = true; private volatile ObjectPool pool; @@ -74,23 +74,23 @@ private Boolean defaultAutoCommit; - private boolean autoCommitOnReturn = true; + private volatile boolean autoCommitOnReturn = true; - private boolean rollbackOnReturn = true; + private volatile boolean rollbackOnReturn = true; - private int defaultTransactionIsolation = UNKNOWN_TRANSACTION_ISOLATION; + private volatile int defaultTransactionIsolation = UNKNOWN_TRANSACTION_ISOLATION; private String defaultCatalog; private String defaultSchema; - private boolean cacheState; + private volatile boolean cacheState; - private boolean poolStatements; + private volatile boolean poolStatements; - private boolean clearStatementPoolOnReturn; + private volatile boolean clearStatementPoolOnReturn; - private int maxOpenPreparedStatements = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL_PER_KEY; + private volatile int maxOpenPreparedStatements = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL_PER_KEY; private Duration maxConnDuration = Duration.ofMillis(-1); @@ -99,7 +99,7 @@ private Duration defaultQueryTimeoutDuration; /** - * Creates a new {@code PoolableConnectionFactory}. + * Creates a new {@link PoolableConnectionFactory}. * * @param connFactory * the {@link ConnectionFactory} from which to obtain base {@link Connection}s @@ -113,29 +113,26 @@ @Override public void activateObject(final PooledObject p) throws SQLException { - validateLifetime(p); - - final PoolableConnection pConnection = p.getObject(); - pConnection.activate(); - - if (defaultAutoCommit != null && pConnection.getAutoCommit() != defaultAutoCommit.booleanValue()) { - pConnection.setAutoCommit(defaultAutoCommit.booleanValue()); + final PoolableConnection poolableConnection = p.getObject(); + poolableConnection.activate(); + if (defaultAutoCommit != null && poolableConnection.getAutoCommit() != defaultAutoCommit.booleanValue()) { + poolableConnection.setAutoCommit(defaultAutoCommit.booleanValue()); } if (defaultTransactionIsolation != UNKNOWN_TRANSACTION_ISOLATION - && pConnection.getTransactionIsolation() != defaultTransactionIsolation) { - pConnection.setTransactionIsolation(defaultTransactionIsolation); + && poolableConnection.getTransactionIsolation() != defaultTransactionIsolation) { + poolableConnection.setTransactionIsolation(defaultTransactionIsolation); } - if (defaultReadOnly != null && pConnection.isReadOnly() != defaultReadOnly.booleanValue()) { - pConnection.setReadOnly(defaultReadOnly.booleanValue()); + if (defaultReadOnly != null && poolableConnection.isReadOnly() != defaultReadOnly.booleanValue()) { + poolableConnection.setReadOnly(defaultReadOnly.booleanValue()); } - if (defaultCatalog != null && !defaultCatalog.equals(pConnection.getCatalog())) { - pConnection.setCatalog(defaultCatalog); + if (defaultCatalog != null && !defaultCatalog.equals(poolableConnection.getCatalog())) { + poolableConnection.setCatalog(defaultCatalog); } - if (defaultSchema != null && !defaultSchema.equals(Jdbc41Bridge.getSchema(pConnection))) { - Jdbc41Bridge.setSchema(pConnection, defaultSchema); + if (defaultSchema != null && !defaultSchema.equals(Jdbc41Bridge.getSchema(poolableConnection))) { + Jdbc41Bridge.setSchema(poolableConnection, defaultSchema); } - pConnection.setDefaultQueryTimeout(defaultQueryTimeoutDuration); + poolableConnection.setDefaultQueryTimeout(defaultQueryTimeoutDuration); } @Override @@ -699,8 +696,8 @@ * @param disconnectionSqlCodes * The disconnection SQL codes. * @see #getDisconnectionSqlCodes() - * @since 2.1 * @throws IllegalArgumentException if any SQL state codes overlap with those in {@link #disconnectionIgnoreSqlCodes}. + * @since 2.1 */ public void setDisconnectionSqlCodes(final Collection disconnectionSqlCodes) { Utils.checkSqlCodes(disconnectionSqlCodes, this.disconnectionIgnoreSqlCodes); @@ -718,9 +715,10 @@ } /** + * Sets whether connections created by this factory will fast fail validation. + * + * @param fastFailValidation true means connections created by this factory will fast fail validation * @see #isFastFailValidation() - * @param fastFailValidation - * true means connections created by this factory will fast fail validation * @since 2.1 */ public void setFastFailValidation(final boolean fastFailValidation) { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionMXBean.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionMXBean.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionMXBean.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolableConnectionMXBean.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolablePreparedStatement.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolablePreparedStatement.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolablePreparedStatement.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolablePreparedStatement.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolingConnection.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolingConnection.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolingConnection.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolingConnection.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -74,9 +74,9 @@ } /** Pool of {@link PreparedStatement}s. and {@link CallableStatement}s */ - private KeyedObjectPool pStmtPool; + private KeyedObjectPool stmtPool; - private boolean clearStatementPoolOnReturn; + private volatile boolean clearStatementPoolOnReturn; /** * Constructs a new instance. @@ -109,9 +109,9 @@ @Override public synchronized void close() throws SQLException { try { - if (null != pStmtPool) { - final KeyedObjectPool oldPool = pStmtPool; - pStmtPool = null; + if (null != stmtPool) { + final KeyedObjectPool oldPool = stmtPool; + stmtPool = null; try { oldPool.close(); } catch (final RuntimeException e) { @@ -140,9 +140,9 @@ * @since 2.8.0 */ public void connectionReturnedToPool() throws SQLException { - if (pStmtPool != null && clearStatementPoolOnReturn) { + if (stmtPool != null && clearStatementPoolOnReturn) { try { - pStmtPool.clear(); + stmtPool.clear(); } catch (final Exception e) { throw new SQLException("Error clearing statement pool", e); } @@ -168,7 +168,7 @@ * the SQL string used to define the statement * @param autoGeneratedKeys * A flag indicating whether auto-generated keys should be returned; one of - * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * {@link Statement#RETURN_GENERATED_KEYS} or {@link Statement#NO_GENERATED_KEYS}. * * @return the PStmtKey created for the given arguments. */ @@ -341,7 +341,7 @@ * @since 2.8.0 */ public KeyedObjectPool getStatementPool() { - return pStmtPool; + return stmtPool; } /** @@ -361,11 +361,11 @@ if (key.getStmtType() == StatementType.PREPARED_STATEMENT) { final PreparedStatement statement = (PreparedStatement) key.createStatement(getDelegate()); @SuppressWarnings({"rawtypes", "unchecked" }) // Unable to find way to avoid this - final PoolablePreparedStatement pps = new PoolablePreparedStatement(statement, key, pStmtPool, this); + final PoolablePreparedStatement pps = new PoolablePreparedStatement(statement, key, stmtPool, this); return new DefaultPooledObject<>(pps); } final CallableStatement statement = (CallableStatement) key.createStatement(getDelegate()); - final PoolableCallableStatement pcs = new PoolableCallableStatement(statement, key, pStmtPool, this); + final PoolableCallableStatement pcs = new PoolableCallableStatement(statement, key, stmtPool, this); return new DefaultPooledObject<>(pcs); } @@ -474,11 +474,11 @@ * Wraps an underlying exception. */ private PreparedStatement prepareStatement(final PStmtKey key) throws SQLException { - if (null == pStmtPool) { + if (null == stmtPool) { throw new SQLException("Statement pool is null - closed or invalid PoolingConnection."); } try { - return pStmtPool.borrowObject(key); + return stmtPool.borrowObject(key); } catch (final NoSuchElementException e) { throw new SQLException("MaxOpenPreparedStatements limit reached", e); } catch (final RuntimeException e) { @@ -509,7 +509,7 @@ * the SQL string used to define the PreparedStatement * @param autoGeneratedKeys * A flag indicating whether auto-generated keys should be returned; one of - * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * {@link Statement#RETURN_GENERATED_KEYS} or {@link Statement#NO_GENERATED_KEYS}. * @return a {@link PoolablePreparedStatement} * @throws SQLException * Wraps an underlying exception. @@ -610,19 +610,19 @@ * the prepared statement pool. */ public void setStatementPool(final KeyedObjectPool pool) { - pStmtPool = pool; + stmtPool = pool; } @Override public synchronized String toString() { - if (pStmtPool instanceof GenericKeyedObjectPool) { + if (stmtPool instanceof GenericKeyedObjectPool) { // DBCP-596 PoolingConnection.toString() causes StackOverflowError - final GenericKeyedObjectPool gkop = (GenericKeyedObjectPool) pStmtPool; + final GenericKeyedObjectPool gkop = (GenericKeyedObjectPool) stmtPool; if (gkop.getFactory() == this) { - return "PoolingConnection: " + pStmtPool.getClass() + "@" + System.identityHashCode(pStmtPool); + return "PoolingConnection: " + stmtPool.getClass() + "@" + System.identityHashCode(stmtPool); } } - return "PoolingConnection: " + Objects.toString(pStmtPool); + return "PoolingConnection: " + Objects.toString(stmtPool); } /** diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolingDataSource.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolingDataSource.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolingDataSource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolingDataSource.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -129,7 +129,7 @@ } /** - * Returns a {@link java.sql.Connection} from my pool, according to the contract specified by + * Returns a {@link Connection} from my pool, according to the contract specified by * {@link ObjectPool#borrowObject}. */ @Override @@ -164,7 +164,6 @@ throw new UnsupportedOperationException(); } - /** * Throws {@link UnsupportedOperationException}. * diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolingDriver.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolingDriver.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/PoolingDriver.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/PoolingDriver.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/SQLExceptionList.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/SQLExceptionList.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/SQLExceptionList.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/SQLExceptionList.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -31,6 +31,10 @@ public class SQLExceptionList extends SQLException { private static final long serialVersionUID = 1L; + + /** + * The list of causes. + */ private final List causeList; /** diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/SwallowedExceptionLogger.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/SwallowedExceptionLogger.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/SwallowedExceptionLogger.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/SwallowedExceptionLogger.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/Utils.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/Utils.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/Utils.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/Utils.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -39,10 +39,10 @@ public final class Utils { private static final ResourceBundle messages = ResourceBundle - .getBundle(Utils.class.getPackage().getName() + ".LocalStrings"); + .getBundle(Utils.class.getPackage().getName() + ".LocalStrings"); /** - * Any SQL_STATE starting with this value is considered a fatal disconnect. + * Any SQL State starting with this value is considered a fatal disconnect. */ public static final String DISCONNECTION_SQL_CODE_PREFIX = "08"; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/ConnectionImpl.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/ConnectionImpl.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/ConnectionImpl.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/ConnectionImpl.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -19,6 +19,7 @@ import java.sql.CallableStatement; import java.sql.Connection; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import org.apache.tomcat.dbcp.dbcp2.DelegatingCallableStatement; @@ -26,9 +27,9 @@ import org.apache.tomcat.dbcp.dbcp2.DelegatingPreparedStatement; /** - * This class is the {@code Connection} that will be returned from - * {@code PooledConnectionImpl.getConnection()}. Most methods are wrappers around the JDBC 1.x - * {@code Connection}. A few exceptions include preparedStatement and close. In accordance with the JDBC + * This class is the {@link Connection} that will be returned from + * {@link PooledConnectionImpl#getConnection()}. Most methods are wrappers around the JDBC 1.x + * {@link Connection}. A few exceptions include preparedStatement and close. In accordance with the JDBC * specification this Connection cannot be used after closed() is called. Any further usage will result in an * SQLException. *

    @@ -131,13 +132,13 @@ } /** - * If pooling of {@code CallableStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may - * be returned, otherwise delegate to the wrapped JDBC 1.x {@link java.sql.Connection}. + * If pooling of {@link CallableStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may + * be returned, otherwise delegate to the wrapped JDBC 1.x {@link Connection}. * * @param sql * an SQL statement that may contain one or more '?' parameter placeholders. Typically, this statement is * specified using JDBC call escape syntax. - * @return a default {@code CallableStatement} object containing the pre-compiled SQL statement. + * @return a default {@link CallableStatement} object containing the pre-compiled SQL statement. * @throws SQLException * Thrown if a database access error occurs or this method is called on a closed connection. * @since 2.4.0 @@ -154,23 +155,23 @@ } /** - * If pooling of {@code CallableStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may - * be returned, otherwise delegate to the wrapped JDBC 1.x {@link java.sql.Connection}. + * If pooling of {@link CallableStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may + * be returned, otherwise delegate to the wrapped JDBC 1.x {@link Connection}. * * @param sql - * a {@code String} object that is the SQL statement to be sent to the database; may contain on or + * a {@link String} object that is the SQL statement to be sent to the database; may contain on or * more '?' parameters. * @param resultSetType - * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * a result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. * @param resultSetConcurrency - * a concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. - * @return a {@code CallableStatement} object containing the pre-compiled SQL statement that will produce - * {@code ResultSet} objects with the given type and concurrency. + * a concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. + * @return a {@link CallableStatement} object containing the pre-compiled SQL statement that will produce + * {@link ResultSet} objects with the given type and concurrency. * @throws SQLException * Thrown if a database access error occurs, this method is called on a closed connection or the given - * parameters are not {@code ResultSet} constants indicating type and concurrency. + * parameters are not {@link ResultSet} constants indicating type and concurrency. * @since 2.4.0 */ @Override @@ -187,26 +188,26 @@ } /** - * If pooling of {@code CallableStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may - * be returned, otherwise delegate to the wrapped JDBC 1.x {@link java.sql.Connection}. + * If pooling of {@link CallableStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may + * be returned, otherwise delegate to the wrapped JDBC 1.x {@link Connection}. * * @param sql - * a {@code String} object that is the SQL statement to be sent to the database; may contain on or + * a {@link String} object that is the SQL statement to be sent to the database; may contain on or * more '?' parameters. * @param resultSetType - * one of the following {@code ResultSet} constants: {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * one of the following {@link ResultSet} constants: {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. * @param resultSetConcurrency - * one of the following {@code ResultSet} constants: {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * one of the following {@link ResultSet} constants: {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. * @param resultSetHoldability - * one of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} - * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. - * @return a new {@code CallableStatement} object, containing the pre-compiled SQL statement, that will - * generate {@code ResultSet} objects with the given type, concurrency, and holdability. + * one of the following {@link ResultSet} constants: {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} + * or {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. + * @return a new {@link CallableStatement} object, containing the pre-compiled SQL statement, that will + * generate {@link ResultSet} objects with the given type, concurrency, and holdability. * @throws SQLException * Thrown if a database access error occurs, this method is called on a closed connection or the given - * parameters are not {@code ResultSet} constants indicating type, concurrency, and holdability. + * parameters are not {@link ResultSet} constants indicating type, concurrency, and holdability. * @since 2.4.0 */ @Override @@ -223,8 +224,8 @@ } /** - * If pooling of {@code PreparedStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may - * be returned, otherwise delegate to the wrapped JDBC 1.x {@link java.sql.Connection}. + * If pooling of {@link PreparedStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may + * be returned, otherwise delegate to the wrapped JDBC 1.x {@link Connection}. * * @param sql * SQL statement to be prepared @@ -259,8 +260,8 @@ // /** - * If pooling of {@code PreparedStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may - * be returned, otherwise delegate to the wrapped JDBC 1.x {@link java.sql.Connection}. + * If pooling of {@link PreparedStatement}s is turned on in the {@link DriverAdapterCPDS}, a pooled object may + * be returned, otherwise delegate to the wrapped JDBC 1.x {@link Connection}. * * @throws SQLException * if this connection is closed or an error occurs in the wrapped connection. diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/DriverAdapterCPDS.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/DriverAdapterCPDS.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/DriverAdapterCPDS.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/DriverAdapterCPDS.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -18,7 +18,9 @@ import java.io.PrintWriter; import java.io.Serializable; +import java.sql.Connection; import java.sql.DriverManager; +import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.time.Duration; @@ -35,6 +37,7 @@ import javax.naming.StringRefAddr; import javax.naming.spi.ObjectFactory; import javax.sql.ConnectionPoolDataSource; +import javax.sql.DataSource; import javax.sql.PooledConnection; import org.apache.tomcat.dbcp.dbcp2.Constants; @@ -49,22 +52,22 @@ /** *

    * An adapter for JDBC drivers that do not include an implementation of {@link javax.sql.ConnectionPoolDataSource}, but - * still include a {@link java.sql.DriverManager} implementation. {@code ConnectionPoolDataSource}s are not used - * within general applications. They are used by {@code DataSource} implementations that pool - * {@code Connection}s, such as {@link org.apache.tomcat.dbcp.dbcp2.datasources.SharedPoolDataSource}. A J2EE container - * will normally provide some method of initializing the {@code ConnectionPoolDataSource} whose attributes are + * still include a {@link java.sql.DriverManager} implementation. {@link ConnectionPoolDataSource}s are not used + * within general applications. They are used by {@link DataSource} implementations that pool + * {@link Connection}s, such as {@link org.apache.tomcat.dbcp.dbcp2.datasources.SharedPoolDataSource}. A J2EE container + * will normally provide some method of initializing the {@link ConnectionPoolDataSource} whose attributes are * presented as bean getters/setters and then deploying it via JNDI. It is then available as a source of physical - * connections to the database, when the pooling {@code DataSource} needs to create a new physical connection. + * connections to the database, when the pooling {link DataSource} needs to create a new physical connection. *

    *

    * Although normally used within a JNDI environment, the DriverAdapterCPDS can be instantiated and initialized as any - * bean and then attached directly to a pooling {@code DataSource}. {@code Jdbc2PoolDataSource} can use the - * {@code ConnectionPoolDataSource} with or without the use of JNDI. + * bean and then attached directly to a pooling {link DataSource}. {@code Jdbc2PoolDataSource} can use the + * {link ConnectionPoolDataSource} with or without the use of JNDI. *

    *

    - * The DriverAdapterCPDS also provides {@code PreparedStatement} pooling which is not generally available in jdbc2 - * {@code ConnectionPoolDataSource} implementation, but is addressed within the JDBC 3 specification. The - * {@code PreparedStatement} pool in DriverAdapterCPDS has been in the DBCP package for some time, but it has not + * The DriverAdapterCPDS also provides {@link PreparedStatement} pooling which is not generally available in jdbc2 + * {@link ConnectionPoolDataSource} implementation, but is addressed within the JDBC 3 specification. The + * {@link PreparedStatement} pool in DriverAdapterCPDS has been in the DBCP package for some time, but it has not * undergone extensive testing in the configuration used here. It should be considered experimental and can be toggled * with the poolPreparedStatements attribute. *

    @@ -110,7 +113,7 @@ static { // Attempt to prevent deadlocks - see DBCP-272 - DriverManager.getDrivers(); + DriverManager.getDrivers(); // NOPMD } /** Description */ @@ -129,19 +132,28 @@ private String driver; /** Login TimeOut in seconds */ - private int loginTimeout; + private volatile int loginTimeout; /** Log stream. NOT USED */ private transient PrintWriter logWriter; - // PreparedStatement pool properties - private boolean poolPreparedStatements; - private int maxIdle = 10; + /** PreparedStatement pool property defaults to false. */ + private volatile boolean poolPreparedStatements; + + /** PreparedStatement pool property defaults to 10. */ + private volatile int maxIdle = 10; + + /** PreparedStatement pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_DURATION_BETWEEN_EVICTION_RUNS}. */ private Duration durationBetweenEvictionRuns = BaseObjectPoolConfig.DEFAULT_DURATION_BETWEEN_EVICTION_RUNS; - private int numTestsPerEvictionRun = -1; + + /** PreparedStatement pool property defaults to -1. */ + private volatile int numTestsPerEvictionRun = -1; + + /** PreparedStatement pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_MIN_EVICTABLE_IDLE_DURATION}. */ private Duration minEvictableIdleDuration = BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_DURATION; - private int maxPreparedStatements = -1; + /** Maximum number of prepared statements, defaults to -1, meaning no limit. */ + private volatile int maxPreparedStatements = -1; /** Whether or not getConnection has been called */ private volatile boolean getConnectionCalled; @@ -248,7 +260,7 @@ /** * Gets the maximum number of prepared statements. * - * @return maxPrepartedStatements value + * @return maxPrepartedStatements, defaults to -1, meaning no limit. */ public int getMaxPreparedStatements() { return maxPreparedStatements; @@ -537,7 +549,7 @@ } /** - * Whether to toggle the pooling of {@code PreparedStatement}s + * Tests whether to toggle the pooling of {@link PreparedStatement}s * * @return value of poolPreparedStatements. */ @@ -656,7 +668,7 @@ /** * Sets the maximum number of prepared statements. * - * @param maxPreparedStatements the new maximum number of prepared statements + * @param maxPreparedStatements the new maximum number of prepared statements, <= 0 means no limit. */ public void setMaxPreparedStatements(final int maxPreparedStatements) { this.maxPreparedStatements = maxPreparedStatements; @@ -736,7 +748,7 @@ } /** - * Whether to toggle the pooling of {@code PreparedStatement}s + * Sets whether to toggle the pooling of {@link PreparedStatement}s * * @param poolPreparedStatements true to pool statements. * @throws IllegalStateException if {@link #getPooledConnection()} has been called diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PStmtKeyCPDS.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PStmtKeyCPDS.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PStmtKeyCPDS.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PStmtKeyCPDS.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -17,6 +17,8 @@ package org.apache.tomcat.dbcp.dbcp2.cpdsadapter; import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; import org.apache.tomcat.dbcp.dbcp2.PStmtKey; @@ -46,7 +48,7 @@ * The SQL statement. * @param autoGeneratedKeys * A flag indicating whether auto-generated keys should be returned; one of - * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * {@link Statement#RETURN_GENERATED_KEYS} or {@link Statement#NO_GENERATED_KEYS}. */ public PStmtKeyCPDS(final String sql, final int autoGeneratedKeys) { super(sql, null, autoGeneratedKeys); @@ -58,11 +60,11 @@ * @param sql * The SQL statement. * @param resultSetType - * A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * A result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. * @param resultSetConcurrency - * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. */ public PStmtKeyCPDS(final String sql, final int resultSetType, final int resultSetConcurrency) { super(sql, resultSetType, resultSetConcurrency); @@ -74,14 +76,14 @@ * @param sql * The SQL statement. * @param resultSetType - * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * a result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. * @param resultSetConcurrency - * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE} + * A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE} * @param resultSetHoldability - * One of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} - * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * One of the following {@link ResultSet} constants: {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} + * or {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. */ public PStmtKeyCPDS(final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PooledConnectionImpl.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PooledConnectionImpl.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PooledConnectionImpl.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/PooledConnectionImpl.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -19,6 +19,7 @@ import java.sql.CallableStatement; import java.sql.Connection; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; @@ -81,10 +82,10 @@ /** * Flag set to true, once {@link #close()} is called. */ - private boolean closed; + private volatile boolean closed; /** My pool of {@link PreparedStatement}s. */ - private KeyedObjectPool pStmtPool; + private KeyedObjectPool stmtPool; /** * Controls access to the underlying connection. @@ -146,8 +147,8 @@ } /** - * Closes the physical connection and marks this {@code PooledConnection} so that it may not be used to - * generate any more logical {@code Connection}s. + * Closes the physical connection and marks this {@link PooledConnection} so that it may not be used to + * generate any more logical {@link Connection}s. * * @throws SQLException * Thrown when an error occurs or the connection is already closed. @@ -157,11 +158,11 @@ assertOpen(); closed = true; try { - if (pStmtPool != null) { + if (stmtPool != null) { try { - pStmtPool.close(); + stmtPool.close(); } finally { - pStmtPool = null; + stmtPool = null; } } } catch (final RuntimeException e) { @@ -195,7 +196,7 @@ * The SQL statement. * @param autoGeneratedKeys * A flag indicating whether auto-generated keys should be returned; one of - * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * {@link Statement#RETURN_GENERATED_KEYS} or {@link Statement#NO_GENERATED_KEYS}. * @return a key to uniquely identify a prepared statement. */ protected PStmtKey createKey(final String sql, final int autoGeneratedKeys) { @@ -208,11 +209,11 @@ * @param sql * The SQL statement. * @param resultSetType - * A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * A result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. * @param resultSetConcurrency - * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. * @return a key to uniquely identify a prepared statement. */ protected PStmtKey createKey(final String sql, final int resultSetType, final int resultSetConcurrency) { @@ -225,14 +226,14 @@ * @param sql * The SQL statement. * @param resultSetType - * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * a result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. * @param resultSetConcurrency - * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE} + * A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE} * @param resultSetHoldability - * One of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} - * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * One of the following {@link ResultSet} constants: {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} + * or {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. * @return a key to uniquely identify a prepared statement. */ protected PStmtKey createKey(final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) { @@ -245,21 +246,21 @@ * @param sql * The SQL statement. * @param resultSetType - * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE} + * a result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE} * @param resultSetConcurrency - * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. * @param resultSetHoldability - * One of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} - * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. + * One of the following {@link ResultSet} constants: {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} + * or {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. * @param statementType * The SQL statement type, prepared or callable. * @return a key to uniquely identify a prepared statement. * @since 2.4.0 */ protected PStmtKey createKey(final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability, - final StatementType statementType) { + final StatementType statementType) { return new PStmtKey(sql, getCatalogOrNull(), getSchemaOrNull(), resultSetType, resultSetConcurrency, resultSetHoldability, statementType); } @@ -269,11 +270,11 @@ * @param sql * The SQL statement. * @param resultSetType - * A result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * A result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. * @param resultSetConcurrency - * A concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * A concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. * @param statementType * The SQL statement type, prepared or callable. * @return a key to uniquely identify a prepared statement. @@ -424,13 +425,13 @@ if (key.getStmtType() == StatementType.PREPARED_STATEMENT) { final PreparedStatement statement = (PreparedStatement) key.createStatement(connection); @SuppressWarnings({"rawtypes", "unchecked" }) // Unable to find way to avoid this - final PoolablePreparedStatement pps = new PoolablePreparedStatement(statement, key, pStmtPool, + final PoolablePreparedStatement pps = new PoolablePreparedStatement(statement, key, stmtPool, delegatingConnection); return new DefaultPooledObject<>(pps); } final CallableStatement statement = (CallableStatement) key.createStatement(connection); @SuppressWarnings("unchecked") - final PoolableCallableStatement pcs = new PoolableCallableStatement(statement, key, pStmtPool, + final PoolableCallableStatement pcs = new PoolableCallableStatement(statement, key, stmtPool, (DelegatingConnection) delegatingConnection); return new DefaultPooledObject<>(pcs); } @@ -466,17 +467,17 @@ * @param sql * an SQL statement that may contain one or more '?' parameter placeholders. Typically, this statement is * specified using JDBC call escape syntax. - * @return a default {@code CallableStatement} object containing the pre-compiled SQL statement. + * @return a default {@link CallableStatement} object containing the pre-compiled SQL statement. * @throws SQLException * Thrown if a database access error occurs or this method is called on a closed connection. * @since 2.4.0 */ CallableStatement prepareCall(final String sql) throws SQLException { - if (pStmtPool == null) { + if (stmtPool == null) { return getRawConnection().prepareCall(sql); } try { - return (CallableStatement) pStmtPool.borrowObject(createKey(sql, StatementType.CALLABLE_STATEMENT)); + return (CallableStatement) stmtPool.borrowObject(createKey(sql, StatementType.CALLABLE_STATEMENT)); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { @@ -488,28 +489,28 @@ * Creates or obtains a {@link CallableStatement} from my pool. * * @param sql - * a {@code String} object that is the SQL statement to be sent to the database; may contain on or + * a {@link String} object that is the SQL statement to be sent to the database; may contain on or * more '?' parameters. * @param resultSetType - * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * a result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. * @param resultSetConcurrency - * a concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. - * @return a {@code CallableStatement} object containing the pre-compiled SQL statement that will produce - * {@code ResultSet} objects with the given type and concurrency. + * a concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. + * @return a {@link CallableStatement} object containing the pre-compiled SQL statement that will produce + * {@link ResultSet} objects with the given type and concurrency. * @throws SQLException * Thrown if a database access error occurs, this method is called on a closed connection or the given - * parameters are not {@code ResultSet} constants indicating type and concurrency. + * parameters are not {@link ResultSet} constants indicating type and concurrency. * @since 2.4.0 */ CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency) throws SQLException { - if (pStmtPool == null) { + if (stmtPool == null) { return getRawConnection().prepareCall(sql, resultSetType, resultSetConcurrency); } try { - return (CallableStatement) pStmtPool.borrowObject( + return (CallableStatement) stmtPool.borrowObject( createKey(sql, resultSetType, resultSetConcurrency, StatementType.CALLABLE_STATEMENT)); } catch (final RuntimeException e) { throw e; @@ -522,31 +523,31 @@ * Creates or obtains a {@link CallableStatement} from my pool. * * @param sql - * a {@code String} object that is the SQL statement to be sent to the database; may contain on or + * a {@link String} object that is the SQL statement to be sent to the database; may contain on or * more '?' parameters. * @param resultSetType - * one of the following {@code ResultSet} constants: {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * one of the following {@link ResultSet} constants: {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. * @param resultSetConcurrency - * one of the following {@code ResultSet} constants: {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * one of the following {@link ResultSet} constants: {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. * @param resultSetHoldability - * one of the following {@code ResultSet} constants: {@code ResultSet.HOLD_CURSORS_OVER_COMMIT} - * or {@code ResultSet.CLOSE_CURSORS_AT_COMMIT}. - * @return a new {@code CallableStatement} object, containing the pre-compiled SQL statement, that will - * generate {@code ResultSet} objects with the given type, concurrency, and holdability. + * one of the following {@link ResultSet} constants: {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} + * or {@link ResultSet#CLOSE_CURSORS_AT_COMMIT}. + * @return a new {@link CallableStatement} object, containing the pre-compiled SQL statement, that will + * generate {@link ResultSet} objects with the given type, concurrency, and holdability. * @throws SQLException * Thrown if a database access error occurs, this method is called on a closed connection or the given - * parameters are not {@code ResultSet} constants indicating type, concurrency, and holdability. + * parameters are not {@link ResultSet} constants indicating type, concurrency, and holdability. * @since 2.4.0 */ CallableStatement prepareCall(final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) throws SQLException { - if (pStmtPool == null) { + if (stmtPool == null) { return getRawConnection().prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); } try { - return (CallableStatement) pStmtPool.borrowObject(createKey(sql, resultSetType, resultSetConcurrency, + return (CallableStatement) stmtPool.borrowObject(createKey(sql, resultSetType, resultSetConcurrency, resultSetHoldability, StatementType.CALLABLE_STATEMENT)); } catch (final RuntimeException e) { throw e; @@ -564,11 +565,11 @@ * the borrow failed. */ PreparedStatement prepareStatement(final String sql) throws SQLException { - if (pStmtPool == null) { + if (stmtPool == null) { return getRawConnection().prepareStatement(sql); } try { - return pStmtPool.borrowObject(createKey(sql)); + return stmtPool.borrowObject(createKey(sql)); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { @@ -583,18 +584,18 @@ * an SQL statement that may contain one or more '?' IN parameter placeholders. * @param autoGeneratedKeys * a flag indicating whether auto-generated keys should be returned; one of - * {@code Statement.RETURN_GENERATED_KEYS} or {@code Statement.NO_GENERATED_KEYS}. + * {@link Statement#RETURN_GENERATED_KEYS} or {@link Statement#NO_GENERATED_KEYS}. * @return a {@link PoolablePreparedStatement} * @throws SQLException Thrown if a database access error occurs, this method is called on a closed connection, or * the borrow failed. * @see Connection#prepareStatement(String, int) */ PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys) throws SQLException { - if (pStmtPool == null) { + if (stmtPool == null) { return getRawConnection().prepareStatement(sql, autoGeneratedKeys); } try { - return pStmtPool.borrowObject(createKey(sql, autoGeneratedKeys)); + return stmtPool.borrowObject(createKey(sql, autoGeneratedKeys)); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { @@ -606,14 +607,14 @@ * Creates or obtains a {@link PreparedStatement} from my pool. * * @param sql - * a {@code String} object that is the SQL statement to be sent to the database; may contain one or + * a {@link String} object that is the SQL statement to be sent to the database; may contain one or * more '?' IN parameters. * @param resultSetType - * a result set type; one of {@code ResultSet.TYPE_FORWARD_ONLY}, - * {@code ResultSet.TYPE_SCROLL_INSENSITIVE}, or {@code ResultSet.TYPE_SCROLL_SENSITIVE}. + * a result set type; one of {@link ResultSet#TYPE_FORWARD_ONLY}, + * {@link ResultSet#TYPE_SCROLL_INSENSITIVE}, or {@link ResultSet#TYPE_SCROLL_SENSITIVE}. * @param resultSetConcurrency - * a concurrency type; one of {@code ResultSet.CONCUR_READ_ONLY} or - * {@code ResultSet.CONCUR_UPDATABLE}. + * a concurrency type; one of {@link ResultSet#CONCUR_READ_ONLY} or + * {@link ResultSet#CONCUR_UPDATABLE}. * * @return a {@link PoolablePreparedStatement}. * @throws SQLException Thrown if a database access error occurs, this method is called on a closed connection, or @@ -622,11 +623,11 @@ */ PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency) throws SQLException { - if (pStmtPool == null) { + if (stmtPool == null) { return getRawConnection().prepareStatement(sql, resultSetType, resultSetConcurrency); } try { - return pStmtPool.borrowObject(createKey(sql, resultSetType, resultSetConcurrency)); + return stmtPool.borrowObject(createKey(sql, resultSetType, resultSetConcurrency)); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { @@ -636,11 +637,11 @@ PreparedStatement prepareStatement(final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) throws SQLException { - if (pStmtPool == null) { + if (stmtPool == null) { return getRawConnection().prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); } try { - return pStmtPool.borrowObject(createKey(sql, resultSetType, resultSetConcurrency, resultSetHoldability)); + return stmtPool.borrowObject(createKey(sql, resultSetType, resultSetConcurrency, resultSetHoldability)); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { @@ -649,11 +650,11 @@ } PreparedStatement prepareStatement(final String sql, final int[] columnIndexes) throws SQLException { - if (pStmtPool == null) { + if (stmtPool == null) { return getRawConnection().prepareStatement(sql, columnIndexes); } try { - return pStmtPool.borrowObject(createKey(sql, columnIndexes)); + return stmtPool.borrowObject(createKey(sql, columnIndexes)); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { @@ -662,11 +663,11 @@ } PreparedStatement prepareStatement(final String sql, final String[] columnNames) throws SQLException { - if (pStmtPool == null) { + if (stmtPool == null) { return getRawConnection().prepareStatement(sql, columnNames); } try { - return pStmtPool.borrowObject(createKey(sql, columnNames)); + return stmtPool.borrowObject(createKey(sql, columnNames)); } catch (final RuntimeException e) { throw e; } catch (final Exception e) { @@ -699,7 +700,7 @@ } public void setStatementPool(final KeyedObjectPool statementPool) { - pStmtPool = statementPool; + stmtPool = statementPool; } /** @@ -720,8 +721,8 @@ builder.append(statementEventListeners); builder.append(", closed="); builder.append(closed); - builder.append(", pStmtPool="); - builder.append(pStmtPool); + builder.append(", stmtPool="); + builder.append(stmtPool); builder.append(", accessToUnderlyingConnectionAllowed="); builder.append(accessToUnderlyingConnectionAllowed); builder.append("]"); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/package-info.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/package-info.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/package-info.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/cpdsadapter/package-info.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/AbstractConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/AbstractConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/AbstractConnectionFactory.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/AbstractConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,147 @@ +/* + * 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 + * + * https://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.dbcp.dbcp2.datasources; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.sql.ConnectionPoolDataSource; +import javax.sql.PooledConnection; + +import org.apache.tomcat.dbcp.dbcp2.Utils; +import org.apache.tomcat.dbcp.pool2.PooledObject; + +/** + * Abstracts services for connection factories in this package. + */ +class AbstractConnectionFactory { + + protected final ConnectionPoolDataSource cpds; + protected Duration maxConnDuration = Duration.ofMillis(-1); + protected final boolean rollbackAfterValidation; + + /** + * Map of PooledConnectionAndInfo instances + */ + protected final Map pcMap = new ConcurrentHashMap<>(); + + /** + * Map of PooledConnections for which close events are ignored. Connections are muted when they are being validated. + */ + protected final Set validatingSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); + protected final String validationQuery; + protected final Duration validationQueryTimeoutDuration; + + AbstractConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, + final Duration validationQueryTimeoutDuration, final boolean rollbackAfterValidation) { + this.cpds = cpds; + this.validationQuery = validationQuery; + this.validationQueryTimeoutDuration = validationQueryTimeoutDuration; + this.rollbackAfterValidation = rollbackAfterValidation; + } + + /** + * Sets the maximum lifetime of a connection after which the connection will always fail activation, + * passivation and validation. + * + * @param duration + * A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds. + * @since 2.10.0 + */ + void setMaxConn(final Duration duration) { + this.maxConnDuration = duration; + } + + /** + * Converts a duration to seconds where a duration less than one second becomes 1 second. + * + * @param duration the duration to convert. + * @return a duration to seconds where a duration less than one second becomes 1 second. + * @throws ArithmeticException if the query validation timeout does not fit as seconds in an int. + */ + private int toSeconds(final Duration duration) { + if (duration.isNegative() || duration.isZero()) { + return 0; + } + final long seconds = validationQueryTimeoutDuration.getSeconds(); + return seconds != 0 ? Math.toIntExact(seconds) : 1; + } + + protected void validateLifetime(final PooledObject pooledObject) throws SQLException { + Utils.validateLifetime(pooledObject, maxConnDuration); + } + + public boolean validateObject(final PooledObject pooledObject) { + try { + validateLifetime(pooledObject); + } catch (final Exception e) { + return false; + } + boolean valid = false; + final PooledConnection pooledConn = pooledObject.getObject().getPooledConnection(); + Connection conn = null; + // logical Connection from the PooledConnection must be closed + // before another one can be requested and closing it will + // generate an event. Keep track so we know not to return + // the PooledConnection + validatingSet.add(pooledConn); + try { + final int timeoutSeconds = toSeconds(validationQueryTimeoutDuration); + if (validationQuery == null) { + try { + conn = pooledConn.getConnection(); + valid = conn.isValid(timeoutSeconds); + } catch (final SQLException e) { + valid = false; + } + } else { + Statement stmt = null; + ResultSet rset = null; + try { + conn = pooledConn.getConnection(); + stmt = conn.createStatement(); + if (timeoutSeconds > 0) { + stmt.setQueryTimeout(timeoutSeconds); + } + rset = stmt.executeQuery(validationQuery); + valid = rset.next(); + if (rollbackAfterValidation) { + conn.rollback(); + } + } catch (final Exception e) { + valid = false; + } finally { + Utils.closeQuietly((AutoCloseable) rset); + Utils.closeQuietly((AutoCloseable) stmt); + } + } + } finally { + Utils.closeQuietly((AutoCloseable) conn); + validatingSet.remove(pooledConn); + } + return valid; + } + +} diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/CPDSConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/CPDSConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/CPDSConnectionFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/CPDSConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -17,21 +17,14 @@ package org.apache.tomcat.dbcp.dbcp2.datasources; import java.sql.Connection; -import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; import java.time.Duration; -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import javax.sql.ConnectionEvent; import javax.sql.ConnectionEventListener; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; -import org.apache.tomcat.dbcp.dbcp2.Utils; import org.apache.tomcat.dbcp.pool2.ObjectPool; import org.apache.tomcat.dbcp.pool2.PooledObject; import org.apache.tomcat.dbcp.pool2.PooledObjectFactory; @@ -42,31 +35,16 @@ * * @since 2.0 */ -final class CPDSConnectionFactory +final class CPDSConnectionFactory extends AbstractConnectionFactory implements PooledObjectFactory, ConnectionEventListener, PooledConnectionManager { private static final String NO_KEY_MESSAGE = "close() was called on a Connection, but I have no record of the underlying PooledConnection."; - private final ConnectionPoolDataSource cpds; - private final String validationQuery; - private final Duration validationQueryTimeoutDuration; - private final boolean rollbackAfterValidation; private ObjectPool pool; private UserPassKey userPassKey; - private Duration maxConnDuration = Duration.ofMillis(-1); /** - * Map of PooledConnections for which close events are ignored. Connections are muted when they are being validated. - */ - private final Set validatingSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); - - /** - * Map of PooledConnectionAndInfo instances - */ - private final Map pcMap = new ConcurrentHashMap<>(); - - /** - * Creates a new {@code PoolableConnectionFactory}. + * Creates a new {@link PoolableConnectionFactory}. * * @param cpds * the ConnectionPoolDataSource from which to obtain PooledConnection's @@ -86,97 +64,14 @@ */ CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, final Duration validationQueryTimeoutDuration, final boolean rollbackAfterValidation, final String userName, - final char[] userPassword) { - this.cpds = cpds; - this.validationQuery = validationQuery; - this.validationQueryTimeoutDuration = validationQueryTimeoutDuration; + final char[] userPassword) { + super(cpds, validationQuery, validationQueryTimeoutDuration, rollbackAfterValidation); this.userPassKey = new UserPassKey(userName, userPassword); - this.rollbackAfterValidation = rollbackAfterValidation; - } - - /** - * Creates a new {@code PoolableConnectionFactory}. - * - * @param cpds - * the ConnectionPoolDataSource from which to obtain PooledConnection's - * @param validationQuery - * a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one - * row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate - * connections. - * @param validationQueryTimeoutDuration - * Timeout in seconds before validation fails - * @param rollbackAfterValidation - * whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s. - * @param userName - * The user name to use to create connections - * @param userPassword - * The password to use to create connections - * @since 2.10.0 - */ - CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, final Duration validationQueryTimeoutDuration, - final boolean rollbackAfterValidation, final String userName, final String userPassword) { - this(cpds, validationQuery, validationQueryTimeoutDuration, rollbackAfterValidation, userName, Utils.toCharArray(userPassword)); - } - - /** - * Creates a new {@code PoolableConnectionFactory}. - * - * @param cpds - * the ConnectionPoolDataSource from which to obtain PooledConnection's - * @param validationQuery - * a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one - * row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate - * connections. - * @param validationQueryTimeoutSeconds - * Timeout in seconds before validation fails - * @param rollbackAfterValidation - * whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s. - * @param userName - * The user name to use to create connections - * @param userPassword - * The password to use to create connections - * @since 2.4.0 - * @deprecated Use {@link #CPDSConnectionFactory(ConnectionPoolDataSource, String, Duration, boolean, String, char[])}. - */ - @Deprecated - CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, - final int validationQueryTimeoutSeconds, final boolean rollbackAfterValidation, final String userName, - final char[] userPassword) { - this.cpds = cpds; - this.validationQuery = validationQuery; - this.validationQueryTimeoutDuration = Duration.ofSeconds(validationQueryTimeoutSeconds); - this.userPassKey = new UserPassKey(userName, userPassword); - this.rollbackAfterValidation = rollbackAfterValidation; - } - - /** - * Creates a new {@code PoolableConnectionFactory}. - * - * @param cpds - * the ConnectionPoolDataSource from which to obtain PooledConnection's - * @param validationQuery - * a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one - * row. May be {@code null} in which case {@link Connection#isValid(int)} will be used to validate - * connections. - * @param validationQueryTimeoutSeconds - * Timeout in seconds before validation fails - * @param rollbackAfterValidation - * whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s. - * @param userName - * The user name to use to create connections - * @param userPassword - * The password to use to create connections - * @deprecated Use {@link #CPDSConnectionFactory(ConnectionPoolDataSource, String, Duration, boolean, String, String)}. - */ - @Deprecated - CPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, final int validationQueryTimeoutSeconds, - final boolean rollbackAfterValidation, final String userName, final String userPassword) { - this(cpds, validationQuery, validationQueryTimeoutSeconds, rollbackAfterValidation, userName, Utils.toCharArray(userPassword)); } @Override - public void activateObject(final PooledObject p) throws SQLException { - validateLifetime(p); + public void activateObject(final PooledObject pooledObject) throws SQLException { + validateLifetime(pooledObject); } /** @@ -216,7 +111,7 @@ try { pool.returnObject(pci); } catch (final Exception e) { - System.err.println("CLOSING DOWN CONNECTION AS IT COULD " + "NOT BE RETURNED TO THE POOL"); + System.err.println("CLOSING DOWN CONNECTION AS IT COULD NOT BE RETURNED TO THE POOL"); pc.removeConnectionEventListener(this); try { doDestroyObject(pci); @@ -296,8 +191,9 @@ throw new IllegalStateException(NO_KEY_MESSAGE); } try { - pool.invalidateObject(pci); // Destroy instance and update pool counters pool.close(); // Clear any other instances in this pool and kill others as they come back + // Calling close before invalidate ensures that invalidate will not trigger a create attempt + pool.invalidateObject(pci); // Destroy instance and update pool counters } catch (final Exception ex) { throw new SQLException("Error invalidating connection", ex); } @@ -328,50 +224,12 @@ } /** - * Sets the maximum Duration of a connection after which the connection will always fail activation, - * passivation and validation. - * - * @param maxConnDuration - * A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds. - * @since 2.10.0 - */ - public void setMaxConn(final Duration maxConnDuration) { - this.maxConnDuration = maxConnDuration; - } - - /** - * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation, - * passivation and validation. - * - * @param maxConnDuration - * A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds. - * @since 2.9.0 - * @deprecated Use {@link #setMaxConn(Duration)}. - */ - @Deprecated - public void setMaxConnLifetime(final Duration maxConnDuration) { - this.maxConnDuration = maxConnDuration; - } - - /** - * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation, - * passivation and validation. - * - * @param maxConnLifetimeMillis - * A value of zero or less indicates an infinite lifetime. The default value is -1. - * @deprecated Use {@link #setMaxConn(Duration)}. - */ - @Deprecated - public void setMaxConnLifetimeMillis(final long maxConnLifetimeMillis) { - setMaxConnLifetime(Duration.ofMillis(maxConnLifetimeMillis)); - } - - /** * Sets the database password used when creating new connections. * * @param userPassword * new password */ + @Override public synchronized void setPassword(final char[] userPassword) { this.userPassKey = new UserPassKey(userPassKey.getUserName(), userPassword); } @@ -420,61 +278,4 @@ builder.append("]"); return builder.toString(); } - - private void validateLifetime(final PooledObject p) throws SQLException { - Utils.validateLifetime(p, maxConnDuration); - } - - @Override - public boolean validateObject(final PooledObject p) { - try { - validateLifetime(p); - } catch (final Exception e) { - return false; - } - boolean valid = false; - final PooledConnection pconn = p.getObject().getPooledConnection(); - Connection conn = null; - validatingSet.add(pconn); - if (null == validationQuery) { - Duration timeoutDuration = validationQueryTimeoutDuration; - if (timeoutDuration.isNegative()) { - timeoutDuration = Duration.ZERO; - } - try { - conn = pconn.getConnection(); - valid = conn.isValid((int) timeoutDuration.getSeconds()); - } catch (final SQLException e) { - valid = false; - } finally { - Utils.closeQuietly((AutoCloseable) conn); - validatingSet.remove(pconn); - } - } else { - Statement stmt = null; - ResultSet rset = null; - // logical Connection from the PooledConnection must be closed - // before another one can be requested and closing it will - // generate an event. Keep track so we know not to return - // the PooledConnection - validatingSet.add(pconn); - try { - conn = pconn.getConnection(); - stmt = conn.createStatement(); - rset = stmt.executeQuery(validationQuery); - valid = rset.next(); - if (rollbackAfterValidation) { - conn.rollback(); - } - } catch (final Exception e) { - valid = false; - } finally { - Utils.closeQuietly((AutoCloseable) rset); - Utils.closeQuietly((AutoCloseable) stmt); - Utils.closeQuietly((AutoCloseable) conn); - validatingSet.remove(pconn); - } - } - return valid; - } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/CharArray.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/CharArray.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/CharArray.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/CharArray.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -23,19 +23,18 @@ import org.apache.tomcat.dbcp.dbcp2.Utils; /** - * A {@code char} array wrapper that does not reveal its contents inadvertently through toString(). In contrast to, for - * example, AtomicReference which toString()'s its contents. - * + * A {@code char} array wrapper that does not reveal its contents inadvertently through toString(). In contrast to, for example, AtomicReference which + * toString()'s its contents. + *

    * May contain null. + *

    * * @since 2.9.0 */ final class CharArray implements Serializable { - private static final long serialVersionUID = 1L; - static final CharArray NULL = new CharArray((char[]) null); - + private static final long serialVersionUID = 1L; private final char[] chars; CharArray(final char[] chars) { @@ -55,6 +54,18 @@ return Utils.toString(chars); } + /** + * Clears the content of the char array. + * + * @return {@code this} instance. + */ + CharArray clear() { + if (chars != null) { + Arrays.fill(chars, '\0'); + } + return this; + } + @Override public boolean equals(final Object obj) { if (this == obj) { @@ -80,5 +91,4 @@ public int hashCode() { return Arrays.hashCode(chars); } - } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSource.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSource.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSource.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -41,30 +41,30 @@ /** *

    - * The base class for {@code SharedPoolDataSource} and {@code PerUserPoolDataSource}. Many of the + * The base class for {@link SharedPoolDataSource} and {@link PerUserPoolDataSource}. Many of the * configuration properties are shared and defined here. This class is declared public in order to allow particular * usage with commons-beanutils; do not make direct use of it outside of commons-dbcp2. *

    * *

    - * A J2EE container will normally provide some method of initializing the {@code DataSource} whose attributes are + * A J2EE container will normally provide some method of initializing the {@link DataSource} whose attributes are * presented as bean getters/setters and then deploying it via JNDI. It is then available to an application as a source * of pooled logical connections to the database. The pool needs a source of physical connections. This source is in the - * form of a {@code ConnectionPoolDataSource} that can be specified via the {@link #setDataSourceName(String)} used + * form of a {@link ConnectionPoolDataSource} that can be specified via the {@link #setDataSourceName(String)} used * to lookup the source via JNDI. *

    * *

    * Although normally used within a JNDI environment, A DataSource can be instantiated and initialized as any bean. In - * this case the {@code ConnectionPoolDataSource} will likely be instantiated in a similar manner. This class + * this case the {@link ConnectionPoolDataSource} will likely be instantiated in a similar manner. This class * allows the physical source of connections to be attached directly to this pool using the * {@link #setConnectionPoolDataSource(ConnectionPoolDataSource)} method. *

    * *

    * The dbcp package contains an adapter, {@link org.apache.tomcat.dbcp.dbcp2.cpdsadapter.DriverAdapterCPDS}, that can be - * used to allow the use of {@code DataSource}'s based on this class with JDBC driver implementations that do not - * supply a {@code ConnectionPoolDataSource}, but still provide a {@link java.sql.Driver} implementation. + * used to allow the use of {@link DataSource}'s based on this class with JDBC driver implementations that do not + * supply a {@link ConnectionPoolDataSource}, but still provide a {@link java.sql.Driver} implementation. *

    * *

    @@ -111,32 +111,70 @@ /** Instance key */ private String instanceKey; - // Pool properties - private boolean defaultBlockWhenExhausted = BaseObjectPoolConfig.DEFAULT_BLOCK_WHEN_EXHAUSTED; + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_BLOCK_WHEN_EXHAUSTED}. */ + private volatile boolean defaultBlockWhenExhausted = BaseObjectPoolConfig.DEFAULT_BLOCK_WHEN_EXHAUSTED; + + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_EVICTION_POLICY_CLASS_NAME}. */ private String defaultEvictionPolicyClassName = BaseObjectPoolConfig.DEFAULT_EVICTION_POLICY_CLASS_NAME; - private boolean defaultLifo = BaseObjectPoolConfig.DEFAULT_LIFO; - private int defaultMaxIdle = GenericKeyedObjectPoolConfig.DEFAULT_MAX_IDLE_PER_KEY; - private int defaultMaxTotal = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL; + + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_LIFO}. */ + private volatile boolean defaultLifo = BaseObjectPoolConfig.DEFAULT_LIFO; + + /** Pool property defaults to {@link GenericKeyedObjectPoolConfig#DEFAULT_MAX_IDLE_PER_KEY}. */ + private volatile int defaultMaxIdle = GenericKeyedObjectPoolConfig.DEFAULT_MAX_IDLE_PER_KEY; + + /** Pool property defaults to {@link GenericKeyedObjectPoolConfig#DEFAULT_MAX_TOTAL}. */ + private volatile int defaultMaxTotal = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL; + + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_MAX_WAIT}. */ private Duration defaultMaxWaitDuration = BaseObjectPoolConfig.DEFAULT_MAX_WAIT; + + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_MIN_EVICTABLE_IDLE_DURATION}. */ private Duration defaultMinEvictableIdleDuration = BaseObjectPoolConfig.DEFAULT_MIN_EVICTABLE_IDLE_DURATION; - private int defaultMinIdle = GenericKeyedObjectPoolConfig.DEFAULT_MIN_IDLE_PER_KEY; - private int defaultNumTestsPerEvictionRun = BaseObjectPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN; + + /** Pool property defaults to {@link GenericKeyedObjectPoolConfig#DEFAULT_MIN_IDLE_PER_KEY}. */ + private volatile int defaultMinIdle = GenericKeyedObjectPoolConfig.DEFAULT_MIN_IDLE_PER_KEY; + + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_NUM_TESTS_PER_EVICTION_RUN}. */ + private volatile int defaultNumTestsPerEvictionRun = BaseObjectPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN; + + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_SOFT_MIN_EVICTABLE_IDLE_DURATION}. */ private Duration defaultSoftMinEvictableIdleDuration = BaseObjectPoolConfig.DEFAULT_SOFT_MIN_EVICTABLE_IDLE_DURATION; - private boolean defaultTestOnCreate = BaseObjectPoolConfig.DEFAULT_TEST_ON_CREATE; - private boolean defaultTestOnBorrow = BaseObjectPoolConfig.DEFAULT_TEST_ON_BORROW; - private boolean defaultTestOnReturn = BaseObjectPoolConfig.DEFAULT_TEST_ON_RETURN; - private boolean defaultTestWhileIdle = BaseObjectPoolConfig.DEFAULT_TEST_WHILE_IDLE; + + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_TEST_ON_CREATE}. */ + private volatile boolean defaultTestOnCreate = BaseObjectPoolConfig.DEFAULT_TEST_ON_CREATE; + + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_TEST_ON_BORROW}. */ + private volatile boolean defaultTestOnBorrow = BaseObjectPoolConfig.DEFAULT_TEST_ON_BORROW; + + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_TEST_ON_RETURN}. */ + private volatile boolean defaultTestOnReturn = BaseObjectPoolConfig.DEFAULT_TEST_ON_RETURN; + + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_TEST_WHILE_IDLE}. */ + private volatile boolean defaultTestWhileIdle = BaseObjectPoolConfig.DEFAULT_TEST_WHILE_IDLE; + + /** Pool property defaults to {@link BaseObjectPoolConfig#DEFAULT_DURATION_BETWEEN_EVICTION_RUNS}. */ private Duration defaultDurationBetweenEvictionRuns = BaseObjectPoolConfig.DEFAULT_DURATION_BETWEEN_EVICTION_RUNS; - // Connection factory properties + /** Connection factory property defaults to null. */ private String validationQuery; + + /** Connection factory property defaults to -1 seconds. */ private Duration validationQueryTimeoutDuration = Duration.ofSeconds(-1); - private boolean rollbackAfterValidation; + + /** Connection factory property defaults to false. */ + private volatile boolean rollbackAfterValidation; + + /** Connection factory property defaults to -1 milliseconds. */ private Duration maxConnDuration = Duration.ofMillis(-1); - // Connection properties + /** Connection property defaults to false. */ private Boolean defaultAutoCommit; - private int defaultTransactionIsolation = UNKNOWN_TRANSACTIONISOLATION; + + /** Connection property defaults to {@link #UNKNOWN_TRANSACTIONISOLATION}. */ + private volatile int defaultTransactionIsolation = UNKNOWN_TRANSACTIONISOLATION; + + /** Connection property defaults to false. */ private Boolean defaultReadOnly; /** @@ -198,7 +236,7 @@ public Connection getConnection(final String userName, final String userPassword) throws SQLException { if (instanceKey == null) { throw new SQLException("Must set the ConnectionPoolDataSource " - + "through setDataSourceName or setConnectionPoolDataSource" + " before calling getConnection."); + + "through setDataSourceName or setConnectionPoolDataSource before calling getConnection."); } getConnectionCalled = true; PooledConnectionAndInfo info = null; @@ -220,7 +258,7 @@ // Password has not changed, so refuse client, but return connection to the pool closeDueToException(info); throw new SQLException( - "Given password did not match password used" + " to create the PooledConnection.", ex); + "Given password did not match password used to create the PooledConnection.", ex); } catch (final javax.naming.NamingException ne) { throw new SQLException("NamingException encountered connecting to database", ne); } @@ -233,7 +271,7 @@ // Destroy and remove from pool manager.invalidate(info.getPooledConnection()); // Reset the password on the factory if using CPDSConnectionFactory - manager.setPassword(upkey.getPassword()); + manager.setPassword(upkey.getPasswordCharArray()); info = null; for (int i = 0; i < 10; i++) { // Bound the number of retries - only needed if bad instances return try { @@ -269,6 +307,12 @@ } } + /** + * Gets the pooled connection manager for the given key. + * + * @param upkey the key. + * @return the pooled connection manager for the given key. + */ protected abstract PooledConnectionManager getConnectionManager(UserPassKey upkey); /** @@ -416,11 +460,11 @@ } /** - * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool - * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * Gets the default value for {@link + * GenericObjectPool#getSoftMinEvictableIdleDuration()} for each per user pool. * - * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool - * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * @return The default value for {@link + * GenericObjectPool#getSoftMinEvictableIdleDuration()} for each per user pool. * @since 2.10.0 */ public Duration getDefaultSoftMinEvictableIdleDuration() { @@ -428,10 +472,10 @@ } /** - * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * Gets the default value for {@link * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. * - * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * @return The default value for {@link * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. * @deprecated Use {@link #getDefaultSoftMinEvictableIdleDuration()}. */ @@ -441,10 +485,10 @@ } /** - * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * Gets the default value for {@link * GenericObjectPool#getTestOnBorrow()} for each per user pool. * - * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * @return The default value for {@link * GenericObjectPool#getTestOnBorrow()} for each per user pool. */ public boolean getDefaultTestOnBorrow() { @@ -452,10 +496,10 @@ } /** - * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * Gets the default value for {@link * GenericObjectPool#getTestOnCreate()} for each per user pool. * - * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * @return The default value for {@link * GenericObjectPool#getTestOnCreate()} for each per user pool. */ public boolean getDefaultTestOnCreate() { @@ -463,10 +507,10 @@ } /** - * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * Gets the default value for {@link * GenericObjectPool#getTestOnReturn()} for each per user pool. * - * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * @return The default value for {@link * GenericObjectPool#getTestOnReturn()} for each per user pool. */ public boolean getDefaultTestOnReturn() { @@ -474,10 +518,10 @@ } /** - * Gets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * Gets the default value for {@link * GenericObjectPool#getTestWhileIdle()} for each per user pool. * - * @return The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * @return The default value for {@link * GenericObjectPool#getTestWhileIdle()} for each per user pool. */ public boolean getDefaultTestWhileIdle() { @@ -666,7 +710,7 @@ /** * Gets the value of defaultAutoCommit, which defines the state of connections handed out from this pool. The value * can be changed on the Connection using Connection.setAutoCommit(boolean). The default is {@code null} which - * will use the default value for the drive. + * will use the default value for the driver. * * @return value of defaultAutoCommit. */ @@ -677,7 +721,7 @@ /** * Gets the value of defaultReadOnly, which defines the state of connections handed out from this pool. The value * can be changed on the Connection using Connection.setReadOnly(boolean). The default is {@code null} which - * will use the default value for the drive. + * will use the default value for the driver. * * @return value of defaultReadOnly. */ @@ -686,7 +730,7 @@ } /** - * Whether a rollback will be issued after executing the SQL query that will be used to validate connections from + * Tests whether a rollback will be issued after executing the SQL query that will be used to validate connections from * this pool before returning them to the caller. * * @return true if a rollback will be issued after executing the validation query @@ -733,7 +777,7 @@ + "set using setConnectionPoolDataSource."); } if (this.dataSourceName != null) { - throw new IllegalStateException("The DataSourceName has already been set. " + "It cannot be altered."); + throw new IllegalStateException("The DataSourceName has already been set. It cannot be altered."); } this.dataSourceName = dataSourceName; instanceKey = InstanceKeyDataSourceFactory.registerNewInstance(this); @@ -742,7 +786,7 @@ /** * Sets the value of defaultAutoCommit, which defines the state of connections handed out from this pool. The value * can be changed on the Connection using Connection.setAutoCommit(boolean). The default is {@code null} which - * will use the default value for the drive. + * will use the default value for the driver. * * @param defaultAutoCommit * Value to assign to defaultAutoCommit. @@ -902,7 +946,7 @@ /** * Sets the value of defaultReadOnly, which defines the state of connections handed out from this pool. The value * can be changed on the Connection using Connection.setReadOnly(boolean). The default is {@code null} which - * will use the default value for the drive. + * will use the default value for the driver. * * @param defaultReadOnly * Value to assign to defaultReadOnly. @@ -913,12 +957,12 @@ } /** - * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool - * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * Sets the default value for {@link + * GenericObjectPool#getSoftMinEvictableIdleDuration()} for each per user pool. * * @param defaultSoftMinEvictableIdleDuration - * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool - * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. + * The default value for {@link + * GenericObjectPool#getSoftMinEvictableIdleDuration()} for each per user pool. * @since 2.10.0 */ public void setDefaultSoftMinEvictableIdle(final Duration defaultSoftMinEvictableIdleDuration) { @@ -927,11 +971,11 @@ } /** - * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * Sets the default value for {@link * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. * * @param softMinEvictableIdleTimeMillis - * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * The default value for {@link * GenericObjectPool#getSoftMinEvictableIdleTimeMillis()} for each per user pool. * @deprecated Use {@link #setDefaultSoftMinEvictableIdle(Duration)}. */ @@ -942,11 +986,11 @@ } /** - * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * Sets the default value for {@link * GenericObjectPool#getTestOnBorrow()} for each per user pool. * * @param testOnBorrow - * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * The default value for {@link * GenericObjectPool#getTestOnBorrow()} for each per user pool. */ public void setDefaultTestOnBorrow(final boolean testOnBorrow) { @@ -955,11 +999,11 @@ } /** - * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * Sets the default value for {@link * GenericObjectPool#getTestOnCreate()} for each per user pool. * * @param testOnCreate - * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * The default value for {@link * GenericObjectPool#getTestOnCreate()} for each per user pool. */ public void setDefaultTestOnCreate(final boolean testOnCreate) { @@ -968,11 +1012,11 @@ } /** - * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * Sets the default value for {@link * GenericObjectPool#getTestOnReturn()} for each per user pool. * * @param testOnReturn - * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * The default value for {@link * GenericObjectPool#getTestOnReturn()} for each per user pool. */ public void setDefaultTestOnReturn(final boolean testOnReturn) { @@ -981,11 +1025,11 @@ } /** - * Sets the default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * Sets the default value for {@link * GenericObjectPool#getTestWhileIdle()} for each per user pool. * * @param testWhileIdle - * The default value for {@link org.apache.tomcat.dbcp.pool2.impl.GenericObjectPool + * The default value for {@link * GenericObjectPool#getTestWhileIdle()} for each per user pool. */ public void setDefaultTestWhileIdle(final boolean testWhileIdle) { @@ -1017,14 +1061,14 @@ public void setDefaultTransactionIsolation(final int defaultTransactionIsolation) { assertInitializationAllowed(); switch (defaultTransactionIsolation) { - case Connection.TRANSACTION_NONE: - case Connection.TRANSACTION_READ_COMMITTED: - case Connection.TRANSACTION_READ_UNCOMMITTED: - case Connection.TRANSACTION_REPEATABLE_READ: - case Connection.TRANSACTION_SERIALIZABLE: - break; - default: - throw new IllegalArgumentException(BAD_TRANSACTION_ISOLATION); + case Connection.TRANSACTION_NONE: + case Connection.TRANSACTION_READ_COMMITTED: + case Connection.TRANSACTION_READ_UNCOMMITTED: + case Connection.TRANSACTION_REPEATABLE_READ: + case Connection.TRANSACTION_SERIALIZABLE: + break; + default: + throw new IllegalArgumentException(BAD_TRANSACTION_ISOLATION); } this.defaultTransactionIsolation = defaultTransactionIsolation; } @@ -1143,7 +1187,7 @@ } /** - * Whether a rollback will be issued after executing the SQL query that will be used to validate connections from + * Sets whether a rollback will be issued after executing the SQL query that will be used to validate connections from * this pool before returning them to the caller. Default behavior is NOT to issue a rollback. The setting will only * have an effect if a validation query is set * @@ -1155,6 +1199,13 @@ this.rollbackAfterValidation = rollbackAfterValidation; } + /** + * Sets up the defaults for a given connection. + * + * @param connection The target connection. + * @param userName The user name for the connection. + * @throws SQLException if a database access error occurs or this method is called on a closed connection + */ protected abstract void setupDefaults(Connection connection, String userName) throws SQLException; /** @@ -1193,9 +1244,18 @@ this.validationQueryTimeoutDuration = Duration.ofSeconds(validationQueryTimeoutSeconds); } + /** + * Tests and returns whether a JNDI context can be created to lookup a ConnectionPoolDataSource to then access a PooledConnection connection. + * + * @param userName An optional user name, may be null. + * @param userPassword An optional user user password, may be null. + * @return A ConnectionPoolDataSource from a JNDI context. + * @throws javax.naming.NamingException if a naming exception is encountered. + * @throws SQLException if a ConnectionPoolDataSource or PooledConnection is not available. + */ protected ConnectionPoolDataSource testCPDS(final String userName, final String userPassword) throws javax.naming.NamingException, SQLException { - // The source of physical db connections + // The source of physical database connections ConnectionPoolDataSource cpds = this.dataSource; if (cpds == null) { Context ctx = null; @@ -1206,12 +1266,11 @@ } final Object ds = ctx.lookup(dataSourceName); if (!(ds instanceof ConnectionPoolDataSource)) { - throw new SQLException("Illegal configuration: " + "DataSource " + dataSourceName + " (" - + ds.getClass().getName() + ")" + " doesn't implement javax.sql.ConnectionPoolDataSource"); + throw new SQLException("Illegal configuration: DataSource " + dataSourceName + " (" + + ds.getClass().getName() + ") doesn't implement javax.sql.ConnectionPoolDataSource"); } cpds = (ConnectionPoolDataSource) ds; } - // try to get a connection with the supplied userName/password PooledConnection conn = null; try { @@ -1247,6 +1306,11 @@ return builder.toString(); } + /** + * Appends this instance's fields to a string builder. + * + * @param builder the target string builder. + */ protected void toStringFields(final StringBuilder builder) { builder.append("getConnectionCalled="); builder.append(getConnectionCalled); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSourceFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSourceFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSourceFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/InstanceKeyDataSourceFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -37,7 +37,7 @@ import org.apache.tomcat.dbcp.dbcp2.Utils; /** - * A JNDI ObjectFactory which creates {@code SharedPoolDataSource}s or {@code PerUserPoolDataSource}s + * A JNDI ObjectFactory which creates {@link SharedPoolDataSource}s or {@link PerUserPoolDataSource}s * * @since 2.0 */ @@ -116,7 +116,7 @@ } } - private Boolean booleanValueOf(RefAddr refAddr) { + private Boolean booleanValueOf(final RefAddr refAddr) { return Boolean.valueOf(toString(refAddr)); } @@ -340,11 +340,11 @@ } } - private Duration toDurationFromMillis(RefAddr refAddr) { + private Duration toDurationFromMillis(final RefAddr refAddr) { return Duration.ofMillis(parseLong(refAddr)); } - private Duration toDurationFromSeconds(RefAddr refAddr) { + private Duration toDurationFromSeconds(final RefAddr refAddr) { return Duration.ofSeconds(parseInt(refAddr)); } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/KeyedCPDSConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/KeyedCPDSConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/KeyedCPDSConnectionFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/KeyedCPDSConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -17,21 +17,14 @@ package org.apache.tomcat.dbcp.dbcp2.datasources; import java.sql.Connection; -import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; import java.time.Duration; -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import javax.sql.ConnectionEvent; import javax.sql.ConnectionEventListener; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; -import org.apache.tomcat.dbcp.dbcp2.Utils; import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; import org.apache.tomcat.dbcp.pool2.KeyedPooledObjectFactory; import org.apache.tomcat.dbcp.pool2.PooledObject; @@ -42,31 +35,14 @@ * * @since 2.0 */ -final class KeyedCPDSConnectionFactory implements KeyedPooledObjectFactory, - ConnectionEventListener, PooledConnectionManager { +final class KeyedCPDSConnectionFactory extends AbstractConnectionFactory + implements KeyedPooledObjectFactory, ConnectionEventListener, PooledConnectionManager { - private static final String NO_KEY_MESSAGE = "close() was called on a Connection, but " - + "I have no record of the underlying PooledConnection."; - - private final ConnectionPoolDataSource cpds; - private final String validationQuery; - private final Duration validationQueryTimeoutDuration; - private final boolean rollbackAfterValidation; + private static final String NO_KEY_MESSAGE = "close() was called on a Connection, but I have no record of the underlying PooledConnection."; private KeyedObjectPool pool; - private Duration maxConnLifetime = Duration.ofMillis(-1); - - /** - * Map of PooledConnections for which close events are ignored. Connections are muted when they are being validated. - */ - private final Set validatingSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); - - /** - * Map of PooledConnectionAndInfo instances - */ - private final Map pcMap = new ConcurrentHashMap<>(); /** - * Creates a new {@code KeyedPoolableConnectionFactory}. + * Creates a new {@code KeyedCPDSConnectionFactory}. * * @param cpds * the ConnectionPoolDataSource from which to obtain PooledConnections @@ -74,44 +50,20 @@ * a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one * row. May be {@code null} in which case3 {@link Connection#isValid(int)} will be used to validate * connections. - * @param validationQueryTimeoutSeconds + * @param validationQueryTimeoutDuration * The Duration to allow for the validation query to complete * @param rollbackAfterValidation * whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s. * @since 2.10.0 */ KeyedCPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, - final Duration validationQueryTimeoutSeconds, final boolean rollbackAfterValidation) { - this.cpds = cpds; - this.validationQuery = validationQuery; - this.validationQueryTimeoutDuration = validationQueryTimeoutSeconds; - this.rollbackAfterValidation = rollbackAfterValidation; - } - - /** - * Creates a new {@code KeyedPoolableConnectionFactory}. - * - * @param cpds - * the ConnectionPoolDataSource from which to obtain PooledConnections - * @param validationQuery - * a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one - * row. May be {@code null} in which case3 {@link Connection#isValid(int)} will be used to validate - * connections. - * @param validationQueryTimeoutSeconds - * The time, in seconds, to allow for the validation query to complete - * @param rollbackAfterValidation - * whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s. - * @deprecated Use {@link #KeyedCPDSConnectionFactory(ConnectionPoolDataSource, String, Duration, boolean)}. - */ - @Deprecated - KeyedCPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery, - final int validationQueryTimeoutSeconds, final boolean rollbackAfterValidation) { - this(cpds, validationQuery, Duration.ofSeconds(validationQueryTimeoutSeconds), rollbackAfterValidation); + final Duration validationQueryTimeoutDuration, final boolean rollbackAfterValidation) { + super(cpds, validationQuery, validationQueryTimeoutDuration, rollbackAfterValidation); } @Override - public void activateObject(final UserPassKey key, final PooledObject p) throws SQLException { - validateLifetime(p); + public void activateObject(final UserPassKey ignored, final PooledObject pooledObject) throws SQLException { + validateLifetime(pooledObject); } /** @@ -146,7 +98,7 @@ try { pool.returnObject(pci.getUserPassKey(), pci); } catch (final Exception e) { - System.err.println("CLOSING DOWN CONNECTION AS IT COULD " + "NOT BE RETURNED TO THE POOL"); + System.err.println("CLOSING DOWN CONNECTION AS IT COULD NOT BE RETURNED TO THE POOL"); pc.removeConnectionEventListener(this); try { pool.invalidateObject(pci.getUserPassKey(), pci); @@ -185,8 +137,8 @@ * Closes the PooledConnection and stops listening for events from it. */ @Override - public void destroyObject(final UserPassKey key, final PooledObject p) throws SQLException { - final PooledConnection pooledConnection = p.getObject().getPooledConnection(); + public void destroyObject(final UserPassKey ignored, final PooledObject pooledObject) throws SQLException { + final PooledConnection pooledConnection = pooledObject.getObject().getPooledConnection(); pooledConnection.removeConnectionEventListener(this); pcMap.remove(pooledConnection); pooledConnection.close(); @@ -241,62 +193,28 @@ } else { pooledConnection = cpds.getPooledConnection(userName, password); } - if (pooledConnection == null) { throw new IllegalStateException("Connection pool data source returned null from getPooledConnection"); } - // should we add this object as a listener or the pool. // consider the validateObject method in decision pooledConnection.addConnectionEventListener(this); final PooledConnectionAndInfo pci = new PooledConnectionAndInfo(pooledConnection, userPassKey); pcMap.put(pooledConnection, pci); - return new DefaultPooledObject<>(pci); } @Override - public void passivateObject(final UserPassKey key, final PooledObject p) throws SQLException { - validateLifetime(p); - } - - /** - * Sets the maximum lifetime of a connection after which the connection will always fail activation, - * passivation and validation. - * - * @param maxConnLifetimeMillis - * A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds. - * @since 2.10.0 - */ - public void setMaxConn(final Duration maxConnLifetimeMillis) { - this.maxConnLifetime = maxConnLifetimeMillis; + public void passivateObject(final UserPassKey ignored, final PooledObject pooledObject) throws SQLException { + validateLifetime(pooledObject); } /** - * Sets the maximum lifetime of a connection after which the connection will always fail activation, - * passivation and validation. - * - * @param maxConnLifetimeMillis - * A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds. - * @since 2.9.0 - * @deprecated Use {@link #setMaxConn(Duration)}. + * Does nothing. This factory does not cache user credentials. */ - @Deprecated - public void setMaxConnLifetime(final Duration maxConnLifetimeMillis) { - this.maxConnLifetime = maxConnLifetimeMillis; - } - - /** - * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation, - * passivation and validation. - * - * @param maxConnLifetimeMillis - * A value of zero or less indicates an infinite lifetime. The default value is -1. - * @deprecated Use {@link #setMaxConnLifetime(Duration)}. - */ - @Deprecated - public void setMaxConnLifetimeMillis(final long maxConnLifetimeMillis) { - setMaxConn(Duration.ofMillis(maxConnLifetimeMillis)); + @Override + public void setPassword(final char[] password) { + // Does nothing. This factory does not cache user credentials. } /** @@ -311,69 +229,21 @@ this.pool = pool; } - private void validateLifetime(final PooledObject pooledObject) throws SQLException { - Utils.validateLifetime(pooledObject, maxConnLifetime); - } - /** * Validates a pooled connection. + *

    + * A query validation timeout greater than 0 and less than 1 second is converted to 1 second. + *

    * - * @param key + * @param ignored * ignored * @param pooledObject * wrapped {@code PooledConnectionAndInfo} containing the connection to validate * @return true if validation succeeds + * @throws ArithmeticException if the query validation timeout does not fit as seconds in an int. */ @Override - public boolean validateObject(final UserPassKey key, final PooledObject pooledObject) { - try { - validateLifetime(pooledObject); - } catch (final Exception e) { - return false; - } - boolean valid = false; - final PooledConnection pooledConn = pooledObject.getObject().getPooledConnection(); - Connection conn = null; - validatingSet.add(pooledConn); - if (null == validationQuery) { - Duration timeoutDuration = validationQueryTimeoutDuration; - if (timeoutDuration.isNegative()) { - timeoutDuration = Duration.ZERO; - } - try { - conn = pooledConn.getConnection(); - valid = conn.isValid((int) timeoutDuration.getSeconds()); - } catch (final SQLException e) { - valid = false; - } finally { - Utils.closeQuietly((AutoCloseable) conn); - validatingSet.remove(pooledConn); - } - } else { - Statement stmt = null; - ResultSet rset = null; - // logical Connection from the PooledConnection must be closed - // before another one can be requested and closing it will - // generate an event. Keep track so we know not to return - // the PooledConnection - validatingSet.add(pooledConn); - try { - conn = pooledConn.getConnection(); - stmt = conn.createStatement(); - rset = stmt.executeQuery(validationQuery); - valid = rset.next(); - if (rollbackAfterValidation) { - conn.rollback(); - } - } catch (final Exception e) { - valid = false; - } finally { - Utils.closeQuietly((AutoCloseable) rset); - Utils.closeQuietly((AutoCloseable) stmt); - Utils.closeQuietly((AutoCloseable) conn); - validatingSet.remove(pooledConn); - } - } - return valid; + public boolean validateObject(final UserPassKey ignored, final PooledObject pooledObject) { + return super.validateObject(pooledObject); } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSource.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSource.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSource.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -30,6 +30,7 @@ import javax.naming.Reference; import javax.naming.StringRefAddr; import javax.sql.ConnectionPoolDataSource; +import javax.sql.DataSource; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; @@ -41,7 +42,7 @@ /** *

    - * A pooling {@code DataSource} appropriate for deployment within J2EE environment. There are many configuration + * A pooling {@link DataSource} appropriate for deployment within J2EE environment. There are many configuration * options, most of which are defined in the parent class. This datasource uses individual pools per user, and some * properties can be set specifically for a given user, if the deployment environment can support initialization of * mapped properties. So for example, a pool of admin or write-access Connections can be guaranteed a certain number of @@ -641,7 +642,7 @@ } /** - * Returns a {@code PerUserPoolDataSource} {@link Reference}. + * Returns a {@link PerUserPoolDataSource} {@link Reference}. */ @Override public Reference getReference() throws NamingException { @@ -684,7 +685,7 @@ // the factory with the pool, so we do not have to do so // explicitly) final CPDSConnectionFactory factory = new CPDSConnectionFactory(cpds, getValidationQuery(), getValidationQueryTimeoutDuration(), - isRollbackAfterValidation(), userName, password); + isRollbackAfterValidation(), userName, Utils.toCharArray(password)); factory.setMaxConn(getMaxConnDuration()); // Create an object pool to contain our PooledConnections final GenericObjectPool pool = new GenericObjectPool<>(factory); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -24,7 +24,7 @@ import javax.naming.Reference; /** - * A JNDI ObjectFactory which creates {@code SharedPoolDataSource}s + * A JNDI ObjectFactory which creates {@link SharedPoolDataSource}s * * @since 2.0 */ diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/PoolKey.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PoolKey.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/PoolKey.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PoolKey.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -40,10 +40,7 @@ if (this == obj) { return true; } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { + if (obj == null || getClass() != obj.getClass()) { return false; } final PoolKey other = (PoolKey) obj; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionAndInfo.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionAndInfo.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionAndInfo.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionAndInfo.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionManager.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionManager.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionManager.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/PooledConnectionManager.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.tomcat.dbcp.dbcp2.datasources; import java.sql.SQLException; @@ -31,9 +32,9 @@ * Closes the connection pool associated with the given user. * * @param userName - * user name + * user name. * @throws SQLException - * if an error occurs closing idle connections in the pool + * if an error occurs closing idle connections in the pool. */ void closePool(String userName) throws SQLException; @@ -41,27 +42,27 @@ * Closes the PooledConnection and remove it from the connection pool to which it belongs, adjusting pool counters. * * @param pc - * PooledConnection to be invalidated + * PooledConnection to be invalidated. * @throws SQLException - * if an SQL error occurs closing the connection + * if an SQL error occurs closing the connection. */ void invalidate(PooledConnection pc) throws SQLException; -// /** -// * Sets the database password used when creating connections. -// * -// * @param password password used when authenticating to the database -// * @since 2.10.0 -// */ -// default void setPassword(char[] password) { -// setPassword(String.copyValueOf(password)); -// } + /** + * Sets the database password used when creating connections. + * + * @param password password used when authenticating to the database. + * @since 2.14.0 + */ + default void setPassword(final char[] password) { + setPassword(String.copyValueOf(password)); + } /** * Sets the database password used when creating connections. * * @param password - * password used when authenticating to the database + * password used when authenticating to the database. */ void setPassword(String password); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSource.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSource.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSource.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -25,6 +25,7 @@ import javax.naming.Reference; import javax.naming.StringRefAddr; import javax.sql.ConnectionPoolDataSource; +import javax.sql.DataSource; import org.apache.tomcat.dbcp.pool2.KeyedObjectPool; import org.apache.tomcat.dbcp.pool2.impl.GenericKeyedObjectPool; @@ -32,7 +33,7 @@ /** *

    - * A pooling {@code DataSource} appropriate for deployment within J2EE environment. There are many configuration + * A pooling {@link DataSource} appropriate for deployment within J2EE environment. There are many configuration * options, most of which are defined in the parent class. All users (based on user name) share a single maximum number * of Connections in this data source. *

    @@ -54,7 +55,7 @@ /** * Max total defaults to {@link GenericKeyedObjectPoolConfig#DEFAULT_MAX_TOTAL}. */ - private int maxTotal = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL; + private volatile int maxTotal = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL; /** * Maps user credentials to pooled connection with credentials. diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSourceFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSourceFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSourceFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/SharedPoolDataSourceFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -20,7 +20,7 @@ import javax.naming.Reference; /** - * A JNDI ObjectFactory which creates {@code SharedPoolDataSource}s + * A JNDI ObjectFactory which creates {@link SharedPoolDataSource}s * * @since 2.0 */ diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/UserPassKey.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/UserPassKey.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/UserPassKey.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/UserPassKey.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -26,7 +26,7 @@ * Holds a user name and password pair. Serves as a poolable object key for the {@link KeyedObjectPool} backing a * {@link SharedPoolDataSource}. Two instances with the same user name are considered equal. This ensures that there * will be only one keyed pool for each user in the pool. The password is used (along with the user name) by the - * {@code KeyedCPDSConnectionFactory} when creating new connections. + * {@link KeyedCPDSConnectionFactory} when creating new connections. *

    * *

    @@ -60,6 +60,17 @@ } /** + * Clears the content of the name and password. + * + * @return {@code this} instance. + */ + UserPassKey clear() { + name.clear(); + password.clear(); + return this; + } + + /** * Only takes the user name into account. */ @Override @@ -67,10 +78,7 @@ if (this == obj) { return true; } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { + if (obj == null || getClass() != obj.getClass()) { return false; } final UserPassKey other = (UserPassKey) obj; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/package-info.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/package-info.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/datasources/package-info.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/datasources/package-info.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -68,7 +68,7 @@ private transient TransactionSynchronizationRegistry transactionSynchronizationRegistry; @Override - protected ConnectionFactory createConnectionFactory() throws SQLException { + protected synchronized ConnectionFactory createConnectionFactory() throws SQLException { if (transactionManager == null) { throw new SQLException("Transaction manager must be set before a connection can be created"); } @@ -108,7 +108,7 @@ } @Override - protected DataSource createDataSourceInstance() throws SQLException { + protected synchronized DataSource createDataSourceInstance() throws SQLException { final PoolingDataSource pds = new ManagedDataSource<>(getConnectionPool(), transactionRegistry); pds.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed()); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/DataSourceXAConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/DataSourceXAConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/DataSourceXAConnectionFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/DataSourceXAConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -158,15 +158,35 @@ @Override public Connection createConnection() throws SQLException { // create a new XAConnection - final XAConnection xaConnection; - if (userName == null) { - xaConnection = xaDataSource.getXAConnection(); - } else { - xaConnection = xaDataSource.getXAConnection(userName, Utils.toString(userPassword)); + XAConnection xaConnection = null; + Connection connection = null; + final XAResource xaResource; + try { + if (userName == null) { + xaConnection = xaDataSource.getXAConnection(); + } else { + xaConnection = xaDataSource.getXAConnection(userName, Utils.toString(userPassword)); + } + // get the real connection and XAResource from the connection + connection = xaConnection.getConnection(); + xaResource = xaConnection.getXAResource(); + } catch (final SQLException sqle) { + if (connection != null) { + try { + connection.close(); + } catch (final SQLException ignored) { + // Ignore + } + } + if (xaConnection != null) { + try { + xaConnection.close(); + } catch (final SQLException ignored) { + // Ignore + } + } + throw sqle; } - // get the real connection and XAResource from the connection - final Connection connection = xaConnection.getConnection(); - final XAResource xaResource = xaConnection.getXAResource(); // register the XA resource for the connection transactionRegistry.registerConnection(connection, xaResource); // The Connection we're returning is a handle on the XAConnection. diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/LocalXAConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/LocalXAConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/LocalXAConnectionFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/LocalXAConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -103,7 +103,7 @@ connection.commit(); } } catch (final SQLException e) { - throw (XAException) new XAException().initCause(e); + throw newXAException("Commit failed.", e); } finally { try { connection.setAutoCommit(originalAutoCommit); @@ -180,6 +180,10 @@ return this == xaResource; } + private XAException newXAException(final String message, final SQLException cause) { + return (XAException) new XAException(message).initCause(cause); + } + /** * This method does nothing since the LocalXAConnection does not support two-phase-commit. This method will * return XAResource.XA_RDONLY if the connection isReadOnly(). This assumes that the physical connection is @@ -242,7 +246,7 @@ try { connection.rollback(); } catch (final SQLException e) { - throw (XAException) new XAException().initCause(e); + throw newXAException("Rollback failed.", e); } finally { try { connection.setAutoCommit(originalAutoCommit); @@ -282,12 +286,10 @@ public synchronized void start(final Xid xid, final int flag) throws XAException { if (flag == TMNOFLAGS) { // first time in this transaction - // make sure we aren't already in another tx if (this.currentXid != null) { throw new XAException("Already enlisted in another transaction with xid " + xid); } - // save off the current auto commit flag, so it can be restored after the transaction completes try { originalAutoCommit = connection.getAutoCommit(); @@ -295,20 +297,16 @@ // no big deal, just assume it was off originalAutoCommit = true; } - // update the auto commit flag try { connection.setAutoCommit(false); } catch (final SQLException e) { - throw (XAException) new XAException("Count not turn off auto commit for a XA transaction") - .initCause(e); + throw newXAException("Count not turn off auto commit for a XA transaction", e); } - this.currentXid = xid; } else if (flag == TMRESUME) { if (!xid.equals(this.currentXid)) { - throw new XAException("Attempting to resume in different transaction: expected " + this.currentXid - + ", but was " + xid); + throw new XAException("Attempting to resume in different transaction: expected " + this.currentXid + ", but was " + xid); } } else { throw new XAException("Unknown start flag " + flag); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedConnection.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedConnection.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedConnection.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedConnection.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -51,6 +51,14 @@ * @since 2.0 */ protected class CompletionListener implements TransactionContextListener { + + /** + * Constructs a new instance. + */ + public CompletionListener() { + // empty + } + @Override public void afterCompletion(final TransactionContext completedContext, final boolean committed) { if (completedContext == transactionContext) { @@ -207,27 +215,23 @@ lock.lock(); try { transactionContext.completeTransaction(); + // If we were using a shared connection, clear the reference now that + // the transaction has completed + if (isSharedConnection) { + setDelegate(null); + isSharedConnection = false; + } } finally { lock.unlock(); } - - // If we were using a shared connection, clear the reference now that - // the transaction has completed - if (isSharedConnection) { - setDelegate(null); - isSharedConnection = false; - } - // autoCommit may have been changed directly on the underlying connection clearCachedState(); - // If this connection was closed during the transaction and there is // still a delegate present close it final Connection delegate = getDelegateInternal(); if (isClosedInternal() && delegate != null) { try { setDelegate(null); - if (!delegate.isClosed()) { delegate.close(); } @@ -253,15 +257,12 @@ // our listener is called. In that rare case, trigger the transaction complete call now transactionComplete(); } - // the existing transaction context ended (or we didn't have one), get the active transaction context transactionContext = transactionRegistry.getActiveTransactionContext(); - // if there is an active transaction context, and it already has a shared connection, use it if (transactionContext != null && transactionContext.getSharedConnection() != null) { // A connection for the connection factory has already been enrolled // in the transaction, replace our delegate with the enrolled connection - // return current connection to the pool final C connection = getDelegateInternal(); setDelegate(null); @@ -277,17 +278,14 @@ } } } - // add a listener to the transaction context transactionContext.addTransactionContextListener(new CompletionListener()); - // Set our delegate to the shared connection. Note that this will // always be of type C since it has been shared by another // connection from the same pool. @SuppressWarnings("unchecked") final C shared = (C) transactionContext.getSharedConnection(); setDelegate(shared); - // remember that we are using a shared connection, so it can be cleared after the // transaction completes isSharedConnection = true; @@ -303,12 +301,10 @@ throw new SQLException("Unable to acquire a new connection from the pool", e); } } - // if we have a transaction, out delegate becomes the shared delegate if (transactionContext != null) { // add a listener to the transaction context transactionContext.addTransactionContextListener(new CompletionListener()); - // register our connection as the shared connection try { transactionContext.setSharedConnection(connection); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedDataSource.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedDataSource.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedDataSource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/ManagedDataSource.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnection.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnection.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnection.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnection.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnectionFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/PoolableManagedConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/SynchronizationAdapter.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/SynchronizationAdapter.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/SynchronizationAdapter.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/SynchronizationAdapter.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContext.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContext.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContext.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContext.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContextListener.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContextListener.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContextListener.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionContextListener.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionRegistry.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionRegistry.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionRegistry.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/TransactionRegistry.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/XAConnectionFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/XAConnectionFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/XAConnectionFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/XAConnectionFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/package-info.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/package-info.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/managed/package-info.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/managed/package-info.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -29,7 +29,6 @@ * transaction is committed or rolled back the enlisted connections are * committed or rolled back. *

    - * *

    * This package supports full XADataSources and non-XA data sources using * local transaction semantics. non-XA data sources commit and rollback as diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/package-info.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/package-info.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/dbcp2/package-info.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/dbcp2/package-info.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/BaseObject.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/BaseObject.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/BaseObject.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/BaseObject.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -23,6 +23,13 @@ */ public abstract class BaseObject { + /** + * Constructs a new instance. + */ + public BaseObject() { + // empty + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/BaseObjectPool.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/BaseObjectPool.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/BaseObjectPool.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/BaseObjectPool.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -33,6 +33,13 @@ private volatile boolean closed; /** + * Constructs a new instance. + */ + public BaseObjectPool() { + // empty + } + + /** * Not supported in this base implementation. Subclasses should override * this behavior. * diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/DestroyMode.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/DestroyMode.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/DestroyMode.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/DestroyMode.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/KeyedObjectPool.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/KeyedObjectPool.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/KeyedObjectPool.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/KeyedObjectPool.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/KeyedPooledObjectFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/KeyedPooledObjectFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/KeyedPooledObjectFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/KeyedPooledObjectFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -62,7 +62,7 @@ * While clients of a {@link KeyedObjectPool} borrow and return instances of * the underlying value type V, the factory methods act on instances of * {@link PooledObject PooledObject<V>}. These are the object wrappers that - * pools use to track and maintain state informations about the objects that + * pools use to track and maintain state information about the objects that * they manage. *

    * diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/ObjectPool.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/ObjectPool.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/ObjectPool.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/ObjectPool.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/PoolUtils.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PoolUtils.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/PoolUtils.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PoolUtils.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; @@ -46,6 +47,9 @@ * frequently. */ private static final class ErodingFactor { + + private static final float MAX_INTERVAL = 15f; + /** Determines frequency of "erosion" events */ private final float factor; @@ -53,7 +57,9 @@ private transient volatile long nextShrinkMillis; /** High water mark - largest numIdle encountered */ - private transient volatile int idleHighWaterMark; + private transient volatile int idleHighWaterMark = 1; + + private final ReentrantLock lock = new ReentrantLock(); /** * Creates a new ErodingFactor with the given erosion factor. @@ -61,10 +67,9 @@ * @param factor * erosion factor */ - ErodingFactor(final float factor) { + private ErodingFactor(final float factor) { this.factor = factor; - nextShrinkMillis = System.currentTimeMillis() + (long) (900000 * factor); // now + 15 min * factor - idleHighWaterMark = 1; + nextShrinkMillis = System.currentTimeMillis() + (long) (900_000 * factor); // now + 15 min * factor } /** @@ -72,7 +77,7 @@ * * @return next shrink time */ - public long getNextShrink() { + private long getNextShrink() { return nextShrinkMillis; } @@ -81,7 +86,7 @@ */ @Override public String toString() { - return "ErodingFactor{" + "factor=" + factor + + return "ErodingFactor{factor=" + factor + ", idleHighWaterMark=" + idleHighWaterMark + '}'; } @@ -95,11 +100,14 @@ */ public void update(final long nowMillis, final int numIdle) { final int idle = Math.max(0, numIdle); - idleHighWaterMark = Math.max(idle, idleHighWaterMark); - final float maxInterval = 15f; - final float minutes = maxInterval + - (1f - maxInterval) / idleHighWaterMark * idle; - nextShrinkMillis = nowMillis + (long) (minutes * 60000f * factor); + lock.lock(); + try { + idleHighWaterMark = Math.max(idle, idleHighWaterMark); + final float minutes = MAX_INTERVAL + (1f - MAX_INTERVAL) / idleHighWaterMark * idle; + nextShrinkMillis = nowMillis + (long) (minutes * 60000f * factor); + } finally { + lock.unlock(); + } } } /** @@ -129,7 +137,7 @@ * events * @see #erodingFactor */ - protected ErodingKeyedObjectPool(final KeyedObjectPool keyedPool, + private ErodingKeyedObjectPool(final KeyedObjectPool keyedPool, final ErodingFactor erodingFactor) { if (keyedPool == null) { throw new IllegalArgumentException( @@ -150,7 +158,7 @@ * events * @see #erodingFactor */ - ErodingKeyedObjectPool(final KeyedObjectPool keyedPool, + private ErodingKeyedObjectPool(final KeyedObjectPool keyedPool, final float factor) { this(keyedPool, new ErodingFactor(factor)); } @@ -315,7 +323,7 @@ */ @Override public String toString() { - return "ErodingKeyedObjectPool{" + "factor=" + + return "ErodingKeyedObjectPool{factor=" + erodingFactor + ", keyedPool=" + keyedPool + '}'; } } @@ -346,7 +354,7 @@ * events * @see #factor */ - ErodingObjectPool(final ObjectPool pool, final float factor) { + private ErodingObjectPool(final ObjectPool pool, final float factor) { this.pool = pool; this.factor = new ErodingFactor(factor); } @@ -355,7 +363,7 @@ * {@inheritDoc} */ @Override - public void addObject() throws Exception{ + public void addObject() throws Exception { pool.addObject(); } @@ -457,7 +465,7 @@ */ @Override public String toString() { - return "ErodingObjectPool{" + "factor=" + factor + ", pool=" + + return "ErodingObjectPool{factor=" + factor + ", pool=" + pool + '}'; } } @@ -486,7 +494,7 @@ * @param factor * erosion factor */ - ErodingPerKeyKeyedObjectPool(final KeyedObjectPool keyedPool, final float factor) { + private ErodingPerKeyKeyedObjectPool(final KeyedObjectPool keyedPool, final float factor) { super(keyedPool, null); this.factor = factor; } @@ -506,7 +514,7 @@ */ @Override public String toString() { - return "ErodingPerKeyKeyedObjectPool{" + "factor=" + factor + + return "ErodingPerKeyKeyedObjectPool{factor=" + factor + ", keyedPool=" + getKeyedPool() + '}'; } } @@ -1726,7 +1734,7 @@ * @return a synchronized view of the specified KeyedPooledObjectFactory. */ public static KeyedPooledObjectFactory synchronizedKeyedPooledFactory( - final KeyedPooledObjectFactory keyedFactory) { + final KeyedPooledObjectFactory keyedFactory) { return new SynchronizedKeyedPooledObjectFactory<>(keyedFactory); } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/PooledObject.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PooledObject.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/PooledObject.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PooledObject.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -31,17 +31,41 @@ * @param the type of object in the pool. * @since 2.0 */ +@SuppressWarnings("deprecation") public interface PooledObject extends Comparable> { /** - * Tests whether the given PooledObject is null or contains a null. + * Gets the wrapped object or null. * - * @param pooledObject the PooledObject to test. - * @return whether the given PooledObject is null or contains a null. + * @param the type of object in the pool. + * @param pooledObject the PooledObject to unwrap, may be null. + * @return the wrapped object or null. + * @since 2.13.0 + */ + static T getObject(final PooledObject pooledObject) { + return pooledObject != null ? pooledObject.getObject() : null; + } + + /** + * Tests whether the given PooledObject is null or wraps a null. + * + * @param pooledObject the PooledObject to test, may be null. + * @return whether the given PooledObject is null or wraps a null. * @since 2.12.0 */ static boolean isNull(final PooledObject pooledObject) { - return pooledObject == null || pooledObject.getObject() == null; + return getObject(pooledObject) == null; + } + + /** + * Tests whether the given PooledObject isn't null and doesn't wraps a null. + * + * @param pooledObject the PooledObject to test, may be null. + * @return whether the given PooledObject isn't null and doesn't wraps a null. + * @since 2.13.0 + */ + static boolean nonNull(final PooledObject pooledObject) { + return getObject(pooledObject) != null; } /** @@ -250,6 +274,7 @@ * @return the last time this object was used * @since 2.11.0 */ + @SuppressWarnings("javadoc") default Instant getLastUsedInstant() { return Instant.ofEpochMilli(getLastUsedTime()); } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/PooledObjectFactory.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PooledObjectFactory.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/PooledObjectFactory.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PooledObjectFactory.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -70,85 +70,85 @@ */ public interface PooledObjectFactory { - /** - * Reinitializes an instance to be returned by the pool. - * - * @param p a {@code PooledObject} wrapping the instance to be activated - * @throws Exception if there is a problem activating {@code obj}, - * this exception may be swallowed by the pool. - * - * @see #destroyObject - */ - void activateObject(PooledObject p) throws Exception; - - /** - * Destroys an instance no longer needed by the pool, using the default (NORMAL) - * DestroyMode. - *

    - * It is important for implementations of this method to be aware that there - * is no guarantee about what state {@code obj} will be in and the - * implementation should be prepared to handle unexpected errors. - *

    - *

    - * Also, an implementation must take in to consideration that instances lost - * to the garbage collector may never be destroyed. - *

    - * - * @param p a {@code PooledObject} wrapping the instance to be destroyed - * @throws Exception should be avoided as it may be swallowed by - * the pool implementation. - * - * @see #validateObject - * @see ObjectPool#invalidateObject - */ - void destroyObject(PooledObject p) throws Exception; - - /** - * Destroys an instance no longer needed by the pool, using the provided - * DestroyMode. - * - * @param p a {@code PooledObject} wrapping the instance to be destroyed - * @param destroyMode DestroyMode providing context to the factory - * @throws Exception should be avoided as it may be swallowed by - * the pool implementation. - * - * @see #validateObject - * @see ObjectPool#invalidateObject - * @see #destroyObject(PooledObject) - * @see DestroyMode - * @since 2.9.0 - */ - default void destroyObject(final PooledObject p, final DestroyMode destroyMode) throws Exception { - destroyObject(p); - } - - /** - * Creates an instance that can be served by the pool and wrap it in a - * {@link PooledObject} to be managed by the pool. - * - * @return a {@code PooledObject} wrapping an instance that can be served by the pool, not null. - * @throws Exception if there is a problem creating a new instance, - * this will be propagated to the code requesting an object. - */ - PooledObject makeObject() throws Exception; - - /** - * Uninitializes an instance to be returned to the idle object pool. - * - * @param p a {@code PooledObject} wrapping the instance to be passivated - * @throws Exception if there is a problem passivating {@code obj}, - * this exception may be swallowed by the pool. - * - * @see #destroyObject - */ - void passivateObject(PooledObject p) throws Exception; - - /** - * Ensures that the instance is safe to be returned by the pool. - * - * @param p a {@code PooledObject} wrapping the instance to be validated - * @return {@code false} if {@code obj} is not valid and should - * be dropped from the pool, {@code true} otherwise. - */ - boolean validateObject(PooledObject p); + /** + * Reinitializes an instance to be returned by the pool. + * + * @param p a {@code PooledObject} wrapping the instance to be activated + * @throws Exception if there is a problem activating {@code obj}, + * this exception may be swallowed by the pool. + * + * @see #destroyObject + */ + void activateObject(PooledObject p) throws Exception; + + /** + * Destroys an instance no longer needed by the pool, using the default (NORMAL) + * DestroyMode. + *

    + * It is important for implementations of this method to be aware that there + * is no guarantee about what state {@code obj} will be in and the + * implementation should be prepared to handle unexpected errors. + *

    + *

    + * Also, an implementation must take in to consideration that instances lost + * to the garbage collector may never be destroyed. + *

    + * + * @param p a {@code PooledObject} wrapping the instance to be destroyed + * @throws Exception should be avoided as it may be swallowed by + * the pool implementation. + * + * @see #validateObject + * @see ObjectPool#invalidateObject + */ + void destroyObject(PooledObject p) throws Exception; + + /** + * Destroys an instance no longer needed by the pool, using the provided + * DestroyMode. + * + * @param p a {@code PooledObject} wrapping the instance to be destroyed + * @param destroyMode DestroyMode providing context to the factory + * @throws Exception should be avoided as it may be swallowed by + * the pool implementation. + * + * @see #validateObject + * @see ObjectPool#invalidateObject + * @see #destroyObject(PooledObject) + * @see DestroyMode + * @since 2.9.0 + */ + default void destroyObject(final PooledObject p, final DestroyMode destroyMode) throws Exception { + destroyObject(p); + } + + /** + * Creates an instance that can be served by the pool and wrap it in a + * {@link PooledObject} to be managed by the pool. + * + * @return a {@code PooledObject} wrapping an instance that can be served by the pool, not null. + * @throws Exception if there is a problem creating a new instance, + * this will be propagated to the code requesting an object. + */ + PooledObject makeObject() throws Exception; + + /** + * Uninitializes an instance to be returned to the idle object pool. + * + * @param p a {@code PooledObject} wrapping the instance to be passivated + * @throws Exception if there is a problem passivating {@code obj}, + * this exception may be swallowed by the pool. + * + * @see #destroyObject + */ + void passivateObject(PooledObject p) throws Exception; + + /** + * Ensures that the instance is safe to be returned by the pool. + * + * @param p a {@code PooledObject} wrapping the instance to be validated + * @return {@code false} if {@code obj} is not valid and should + * be dropped from the pool, {@code true} otherwise. + */ + boolean validateObject(PooledObject p); } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/PooledObjectState.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PooledObjectState.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/PooledObjectState.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/PooledObjectState.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/SwallowedExceptionListener.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/SwallowedExceptionListener.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/SwallowedExceptionListener.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/SwallowedExceptionListener.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/TrackedUse.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/TrackedUse.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/TrackedUse.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/TrackedUse.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/UsageTracking.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/UsageTracking.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/UsageTracking.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/UsageTracking.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/AbandonedConfig.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/AbandonedConfig.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/AbandonedConfig.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/AbandonedConfig.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/BaseGenericObjectPool.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/BaseGenericObjectPool.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/BaseGenericObjectPool.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/BaseGenericObjectPool.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -205,41 +205,53 @@ * @param type of objects in the pool */ static class IdentityWrapper { + + /** + * Creates a new instance for the object inside a {@link PooledObject}. + * + * @param The type of the object in the {@link PooledObject}. + * @param p The {@link PooledObject}. + * @return a new instance. + */ + static IdentityWrapper unwrap(final PooledObject p) { + return new IdentityWrapper<>(p.getObject()); + } + /** Wrapped object */ - private final T instance; + private final T object; /** * Constructs a wrapper for an instance. * - * @param instance object to wrap + * @param object object to wrap */ - IdentityWrapper(final T instance) { - this.instance = instance; + IdentityWrapper(final T object) { + this.object = object; } @Override @SuppressWarnings("rawtypes") public boolean equals(final Object other) { - return other instanceof IdentityWrapper && ((IdentityWrapper) other).instance == instance; + return other instanceof IdentityWrapper && ((IdentityWrapper) other).object == object; } /** * @return the wrapped object */ - public T getObject() { - return instance; + T getObject() { + return object; } @Override public int hashCode() { - return System.identityHashCode(instance); + return System.identityHashCode(object); } @Override public String toString() { final StringBuilder builder = new StringBuilder(); builder.append("IdentityWrapper [instance="); - builder.append(instance); + builder.append(object); builder.append("]"); return builder.toString(); } @@ -395,6 +407,7 @@ private volatile SwallowedExceptionListener swallowedExceptionListener; private volatile boolean messageStatistics; + private volatile boolean collectDetailedStatistics = BaseObjectPoolConfig.DEFAULT_COLLECT_DETAILED_STATISTICS; /** Additional configuration properties for abandoned object tracking. */ protected volatile AbandonedConfig abandonedConfig; @@ -483,6 +496,16 @@ } /** + * Returns the duration since the given start time. + * + * @param startInstant the start time + * @return the duration since the given start time + */ + final Duration durationSince(final Instant startInstant) { + return Duration.between(startInstant, Instant.now()); + } + + /** * Tries to ensure that the configured minimum number of idle instances are * available in the pool. * @throws Exception if an error occurs creating idle instances @@ -525,6 +548,20 @@ } /** + * Gets whether detailed timing statistics collection is enabled. + * When {@code false}, the pool will not collect detailed timing statistics for + * mean active time, mean idle time, and mean borrow wait time, + * improving performance under high load. + * + * @return {@code true} if detailed statistics collection is enabled, + * {@code false} if disabled for improved performance. + * @since 2.13.0 + */ + public boolean getCollectDetailedStatistics() { + return collectDetailedStatistics; + } + + /** * Gets the total number of objects created for this pool over the lifetime of * the pool. * @return the created object count @@ -977,7 +1014,6 @@ * removal is configured for this pool; Integer.MAX_VALUE otherwise. * * @see AbandonedConfig#getRemoveAbandonedTimeoutDuration() - * @see AbandonedConfig#getRemoveAbandonedTimeoutDuration() * @deprecated Use {@link #getRemoveAbandonedTimeoutDuration()}. * @since 2.11.0 */ @@ -1226,8 +1262,9 @@ } /** - * Tests whether this pool instance been closed. - * @return {@code true} when this pool has been closed. + * Tests whether this pool instance is closed. + * + * @return {@code true} when this pool is closed. */ public final boolean isClosed() { return closed; @@ -1258,7 +1295,7 @@ } while (!registered) { try { - ObjectName objName; + final ObjectName objName; // Skip the numeric suffix for the first pool in case there is // only one so the names are cleaner. if (i == 1) { @@ -1349,6 +1386,21 @@ } /** + * Sets whether detailed timing statistics collection is enabled. + * When {@code false}, the pool will not collect detailed timing statistics, + * improving performance under high load at the cost of reduced monitoring capabilities. + *

    + * This setting affects data collection for mean active time, mean idle time, and mean borrow wait time. + *

    + * + * @param collectDetailedStatistics whether to collect detailed statistics. + * @since 2.13.0 + */ + public void setCollectDetailedStatistics(final boolean collectDetailedStatistics) { + this.collectDetailedStatistics = collectDetailedStatistics; + } + + /** * Sets the receiver with the given configuration. * * @param config Initialization source. @@ -1374,6 +1426,7 @@ setEvictionPolicy(policy); } setEvictorShutdownTimeout(config.getEvictorShutdownTimeoutDuration()); + setCollectDetailedStatistics(config.getCollectDetailedStatistics()); } /** @@ -1945,6 +1998,7 @@ startEvictor(Duration.ofMillis(-1L)); } + /** * Swallows an exception and notifies the configured listener for swallowed * exceptions queue. @@ -2045,17 +2099,19 @@ */ final void updateStatsBorrow(final PooledObject p, final Duration waitDuration) { borrowedCount.incrementAndGet(); - idleTimes.add(p.getIdleDuration()); - waitTimes.add(waitDuration); - - // lock-free optimistic-locking maximum - Duration currentMaxDuration; - do { - currentMaxDuration = maxBorrowWaitDuration.get(); - if (currentMaxDuration.compareTo(waitDuration) >= 0) { - break; - } - } while (!maxBorrowWaitDuration.compareAndSet(currentMaxDuration, waitDuration)); + // Only collect detailed statistics if enabled + if (collectDetailedStatistics) { + idleTimes.add(p.getIdleDuration()); + waitTimes.add(waitDuration); + // lock-free optimistic-locking maximum + Duration currentMaxDuration; + do { + currentMaxDuration = maxBorrowWaitDuration.get(); + if (currentMaxDuration.compareTo(waitDuration) >= 0) { + break; + } + } while (!maxBorrowWaitDuration.compareAndSet(currentMaxDuration, waitDuration)); + } } /** @@ -2066,7 +2122,25 @@ */ final void updateStatsReturn(final Duration activeTime) { returnedCount.incrementAndGet(); - activeTimes.add(activeTime); + // Only collect detailed statistics if enabled + if (collectDetailedStatistics) { + activeTimes.add(activeTime); + } + } + + /** + * Waits for notification on the given object for the specified duration. + * Duration.ZERO causes the thread to wait indefinitely. + * + * @param obj the object to wait on + * @param duration the duration to wait + * @throws InterruptedException if interrupted while waiting + * @throws IllegalArgumentException if the duration is negative + */ + final void wait(final Object obj, final Duration duration) throws InterruptedException { + if (!duration.isNegative()) { + obj.wait(duration.toMillis(), duration.getNano() % 1_000_000); + } } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -30,6 +30,7 @@ * @param Type of element pooled. * @since 2.0 */ +@SuppressWarnings("deprecation") public abstract class BaseObjectPoolConfig extends BaseObject implements Cloneable { /** @@ -254,6 +255,19 @@ */ public static final String DEFAULT_EVICTION_POLICY_CLASS_NAME = DefaultEvictionPolicy.class.getName(); + /** + * The default value for the {@code collectDetailedStatistics} configuration + * attribute. When {@code true}, the pool will collect detailed timing statistics + * for monitoring purposes. When {@code false}, detailed statistics collection + * is disabled, improving performance under high load. + *

    + * This setting affects data collection for mean active time, mean idle time, and mean borrow wait time. + *

    + * + * @since 2.13.0 + */ + public static final boolean DEFAULT_COLLECT_DETAILED_STATISTICS = true; + private boolean lifo = DEFAULT_LIFO; private boolean fairness = DEFAULT_FAIRNESS; @@ -291,6 +305,15 @@ private String jmxNameBase = DEFAULT_JMX_NAME_BASE; + private boolean collectDetailedStatistics = DEFAULT_COLLECT_DETAILED_STATISTICS; + + /** + * Constructs a new instance. + */ + public BaseObjectPoolConfig() { + // empty + } + /** * Gets the value for the {@code blockWhenExhausted} configuration attribute for pools created with this configuration instance. * @@ -303,6 +326,23 @@ } /** + * Gets the value for the {@code collectDetailedStatistics} configuration attribute + * for pools created with this configuration instance. + *

    + * This setting affects data collection for mean active time, mean idle time, and mean borrow wait time. + *

    + * + * @return {@code true} if detailed statistics collection is enabled, + * {@code false} if disabled for improved performance. + * @see GenericObjectPool#getCollectDetailedStatistics() + * @see GenericKeyedObjectPool#getCollectDetailedStatistics() + * @since 2.13.0 + */ + public boolean getCollectDetailedStatistics() { + return collectDetailedStatistics; + } + + /** * Gets the value for the {@code timeBetweenEvictionRuns} configuration attribute for pools created with this configuration instance. * * @return The current setting of {@code timeBetweenEvictionRuns} for this configuration instance @@ -624,6 +664,25 @@ } /** + * Sets the value for the {@code collectDetailedStatistics} configuration attribute + * for pools created with this configuration instance. When {@code false}, the pool + * will not collect detailed timing statistics, improving performance under high load + * at the cost of reduced monitoring capabilities. + *

    + * This setting affects data collection for mean active time, mean idle time, and mean borrow wait time. + *

    + * + * @param collectDetailedStatistics The new setting of {@code collectDetailedStatistics} + * for this configuration instance. + * @see GenericObjectPool#getCollectDetailedStatistics() + * @see GenericKeyedObjectPool#getCollectDetailedStatistics() + * @since 2.13.0 + */ + public void setCollectDetailedStatistics(final boolean collectDetailedStatistics) { + this.collectDetailedStatistics = collectDetailedStatistics; + } + + /** * Sets the value for the {@code evictionPolicyClass} configuration attribute for pools created with this configuration instance. * * @param evictionPolicy The new setting of {@code evictionPolicyClass} for this configuration instance @@ -953,5 +1012,7 @@ builder.append(jmxNamePrefix); builder.append(", jmxNameBase="); builder.append(jmxNameBase); + builder.append(", collectDetailedStatistics="); + builder.append(collectDetailedStatistics); } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/CallStack.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/CallStack.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/CallStack.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/CallStack.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/DefaultEvictionPolicy.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultEvictionPolicy.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/DefaultEvictionPolicy.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultEvictionPolicy.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -31,7 +31,7 @@ * {@link GenericKeyedObjectPoolConfig#getMinIdlePerKey()} idle objects in * the pool and the object has been idle for longer than * {@link GenericObjectPool#getSoftMinEvictableIdleDuration()} / - * {@link GenericKeyedObjectPool#getSoftMinEvictableIdleDuration()} + * {@link GenericKeyedObjectPool#getSoftMinEvictableIdleDuration()} * *

    * This class is immutable and thread-safe. @@ -42,6 +42,13 @@ */ public class DefaultEvictionPolicy implements EvictionPolicy { + /** + * Constructs a new instance. + */ + public DefaultEvictionPolicy() { + // empty + } + @Override public boolean evict(final EvictionConfig config, final PooledObject underTest, final int idleCount) { // @formatter:off diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObject.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObject.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObject.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObject.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfo.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfo.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfo.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfo.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfoMBean.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfoMBean.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfoMBean.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/DefaultPooledObjectInfoMBean.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/EvictionConfig.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/EvictionConfig.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/EvictionConfig.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/EvictionConfig.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/EvictionPolicy.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/EvictionPolicy.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/EvictionPolicy.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/EvictionPolicy.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -23,7 +23,7 @@ * DefaultEvictionPolicy} for a pool, users must provide an implementation of * this interface that provides the required eviction policy. * - * @param the type of objects in the pool + * @param the type of objects in the pool. * @since 2.0 */ public interface EvictionPolicy { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/EvictionTimer.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/EvictionTimer.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/EvictionTimer.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/EvictionTimer.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -19,6 +19,7 @@ import java.lang.ref.WeakReference; import java.time.Duration; import java.util.HashMap; +import java.util.Iterator; import java.util.Map.Entry; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -30,7 +31,7 @@ *

    * This class is currently implemented using {@link ScheduledThreadPoolExecutor}. This implementation may change in any * future release. This class keeps track of how many pools are using it. If no pools are using the timer, it is - * cancelled. This prevents a thread being left running which, in application server environments, can lead to memory + * canceled. This prevents a thread being left running which, in application server environments, can lead to memory * leads and/or prevent applications from shutting down or reloading cleanly. *

    *

    @@ -67,11 +68,17 @@ @Override public void run() { synchronized (EvictionTimer.class) { - for (final Entry.Evictor>, WeakRunner.Evictor>> entry : TASK_MAP - .entrySet()) { + /* + * Need to use iterator over TASK_MAP so entries can be removed when iterating without triggering a + * ConcurrentModificationException. + */ + final Iterator.Evictor>, WeakRunner.Evictor>>> iterator = + TASK_MAP.entrySet().iterator(); + while (iterator.hasNext()) { + final Entry.Evictor>, WeakRunner.Evictor>> entry = iterator.next(); if (entry.getKey().get() == null) { executor.remove(entry.getValue()); - TASK_MAP.remove(entry.getKey()); + iterator.remove(); } } if (TASK_MAP.isEmpty() && executor != null) { @@ -98,7 +105,7 @@ * @param ref the reference to track. */ private WeakRunner(final WeakReference ref) { - this.ref = ref; + this.ref = ref; } @Override @@ -107,8 +114,10 @@ if (task != null) { task.run(); } else { - executor.remove(this); - TASK_MAP.remove(ref); + synchronized (EvictionTimer.class) { + executor.remove(this); + TASK_MAP.remove(ref); + } } } } @@ -118,13 +127,13 @@ /** Keys are weak references to tasks, values are runners managed by executor. */ private static final HashMap< - WeakReference.Evictor>, - WeakRunner.Evictor>> TASK_MAP = new HashMap<>(); // @GuardedBy("EvictionTimer.class") + WeakReference.Evictor>, + WeakRunner.Evictor>> TASK_MAP = new HashMap<>(); // @GuardedBy("EvictionTimer.class") /** * Removes the specified eviction task from the timer. * - * @param evictor Task to be cancelled. + * @param evictor Task to be canceled. * @param timeout If the associated executor is no longer required, how * long should this thread wait for the executor to * terminate? @@ -189,8 +198,8 @@ * server environments. * * @param task Task to be scheduled. - * @param delay Delay in milliseconds before task is executed. - * @param period Time in milliseconds between executions. + * @param delay Duration before task is executed. + * @param period Duration between executions. */ static synchronized void schedule( final BaseGenericObjectPool.Evictor task, final Duration delay, final Duration period) { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPool.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPool.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPool.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPool.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -28,8 +28,8 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.TreeMap; +import java.util.concurrent.BlockingDeque; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -128,16 +128,16 @@ * @param fairness true means client threads waiting to borrow / return instances * will be served as if waiting in a FIFO queue. */ - ObjectDeque(final boolean fairness) { + private ObjectDeque(final boolean fairness) { idleObjects = new LinkedBlockingDeque<>(fairness); } /** * Gets all the objects for the current key. * - * @return All the objects + * @return All the objects, */ - public Map, PooledObject> getAllObjects() { + Map, PooledObject> getAllObjects() { return allObjects; } @@ -145,27 +145,27 @@ * Gets the number of instances created - number destroyed. * Should always be less than or equal to maxTotalPerKey. * - * @return The net instance addition count for this deque + * @return The net instance addition count for this deque. */ - public AtomicInteger getCreateCount() { + AtomicInteger getCreateCount() { return createCount; } /** * Gets the idle objects for the current key. * - * @return The idle objects + * @return The idle objects. */ - public LinkedBlockingDeque> getIdleObjects() { + LinkedBlockingDeque> getIdleObjects() { return idleObjects; } /** * Gets the number of threads with an interest registered in this key. * - * @return The number of threads with a registered interest in this key + * @return The number of threads with a registered interest in this key. */ - public AtomicLong getNumInterested() { + AtomicLong getNumInterested() { return numInterested; } @@ -201,6 +201,12 @@ private volatile int maxTotalPerKey = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL_PER_KEY; + private volatile boolean reuseCapacityOnReturn = + GenericKeyedObjectPoolConfig.DEFAULT_REUSE_CAPACITY_ON_RETURN; + + private volatile boolean reuseCapacityOnMaintenance = + GenericKeyedObjectPoolConfig.DEFAULT_REUSE_CAPACITY_ON_MAINTENANCE; + private final KeyedPooledObjectFactory factory; private final boolean fairness; @@ -209,8 +215,13 @@ * My hash of sub-pools (ObjectQueue). The list of keys must be kept * in step with {@link #poolKeyList} using {@link #keyLock} to ensure any * changes to the list of current keys is made in a thread-safe manner. + * + * Correct operation of the pool requires that a Map implementation is used that + * supports concurrent read and write (e.g. ensureMinIdle() iterates over the key set + * while other threads may be adding or removing keys) therefore explicitly define + * this field as ConcurrentHashMap rather than Map. */ - private final Map> poolMap = + private final ConcurrentHashMap> poolMap = new ConcurrentHashMap<>(); // @GuardedBy("keyLock") for write access (and some read access) /* @@ -298,9 +309,9 @@ * @throws Exception If the associated factory fails to passivate the object */ private void addIdleObject(final K key, final PooledObject p) throws Exception { - if (!PooledObject.isNull(p)) { + if (PooledObject.nonNull(p)) { factory.passivateObject(key, p); - final LinkedBlockingDeque> idleObjects = poolMap.get(key).getIdleObjects(); + final BlockingDeque> idleObjects = poolMap.get(key).getIdleObjects(); if (getLifo()) { idleObjects.addFirst(p); } else { @@ -330,9 +341,18 @@ @Override public void addObject(final K key) throws Exception { assertOpen(); - register(key); + final ObjectDeque objectDeque = register(key); try { - addIdleObject(key, create(key)); + // Attempt create and add only if there is capacity to add + // > to the overall instance count + // > to the pool under the key + final int maxtTotalPerKey = getMaxTotalPerKey(); + final int maxTotal = getMaxTotal(); + if ((maxTotal < 0 || getNumActive() + getNumIdle() < maxTotal) + && (maxtTotalPerKey < 0 || objectDeque.allObjects.size() < maxtTotalPerKey)) { + // Attempt to create and add a new instance under key + addIdleObject(key, create(key, getMaxWaitDuration())); + } } finally { deregister(key); } @@ -400,8 +420,8 @@ *

    * * @param key pool key - * @param borrowMaxWaitMillis The time to wait in milliseconds for an object - * to become available + * @param maxWaitDuration The time to wait for an object to become + * available * * @return object instance from the keyed pool * @throws NoSuchElementException if a keyed object instance cannot be @@ -409,10 +429,12 @@ * * @throws Exception if a keyed object instance cannot be returned due to an * error + * @since 2.12.2 */ - public T borrowObject(final K key, final long borrowMaxWaitMillis) throws Exception { + public T borrowObject(final K key, final Duration maxWaitDuration) throws Exception { assertOpen(); - + final Instant startInstant = Instant.now(); + Duration remainingWaitDuration = maxWaitDuration; final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getRemoveAbandonedOnBorrow() && getNumIdle() < 2 && getNumActive() > getMaxTotal() - 3) { @@ -426,27 +448,28 @@ final boolean blockWhenExhausted = getBlockWhenExhausted(); boolean create; - final Instant waitTime = Instant.now(); final ObjectDeque objectDeque = register(key); try { while (p == null) { + remainingWaitDuration = maxWaitDuration.minus(durationSince(startInstant)); create = false; p = objectDeque.getIdleObjects().pollFirst(); if (p == null) { - p = create(key); - if (!PooledObject.isNull(p)) { + p = create(key, remainingWaitDuration); + if (PooledObject.nonNull(p)) { create = true; } + remainingWaitDuration = maxWaitDuration.minus(durationSince(startInstant)); } if (blockWhenExhausted) { if (PooledObject.isNull(p)) { - p = borrowMaxWaitMillis < 0 ? objectDeque.getIdleObjects().takeFirst(): - objectDeque.getIdleObjects().pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS); + p = maxWaitDuration.isNegative() ? objectDeque.getIdleObjects().takeFirst() + : objectDeque.getIdleObjects().pollFirst(remainingWaitDuration); } if (PooledObject.isNull(p)) { throw new NoSuchElementException(appendStats( - "Timeout waiting for idle object, borrowMaxWaitMillis=" + borrowMaxWaitMillis)); + "Timeout waiting for idle object, borrowMaxWaitMillis=" + maxWaitDuration.toMillis())); } } else if (PooledObject.isNull(p)) { throw new NoSuchElementException(appendStats("Pool exhausted")); @@ -455,7 +478,7 @@ p = null; } - if (!PooledObject.isNull(p)) { + if (PooledObject.nonNull(p)) { try { factory.activateObject(key, p); } catch (final Exception e) { @@ -466,12 +489,13 @@ } p = null; if (create) { - final NoSuchElementException nsee = new NoSuchElementException(appendStats("Unable to activate object")); + final NoSuchElementException nsee = new NoSuchElementException( + appendStats("Unable to activate object")); nsee.initCause(e); throw nsee; } } - if (!PooledObject.isNull(p) && getTestOnBorrow()) { + if (PooledObject.nonNull(p) && getTestOnBorrow()) { boolean validate = false; Throwable validationThrowable = null; try { @@ -502,11 +526,78 @@ deregister(key); } - updateStatsBorrow(p, Duration.between(waitTime, Instant.now())); + updateStatsBorrow(p, Duration.between(startInstant, Instant.now())); return p.getObject(); } + + /** + * Borrows an object from the sub-pool associated with the given key using + * the specified waiting time which only applies if + * {@link #getBlockWhenExhausted()} is true. + *

    + * If there is one or more idle instances available in the sub-pool + * associated with the given key, then an idle instance will be selected + * based on the value of {@link #getLifo()}, activated and returned. If + * activation fails, or {@link #getTestOnBorrow() testOnBorrow} is set to + * {@code true} and validation fails, the instance is destroyed and the + * next available instance is examined. This continues until either a valid + * instance is returned or there are no more idle instances available. + *

    + *

    + * If there are no idle instances available in the sub-pool associated with + * the given key, behavior depends on the {@link #getMaxTotalPerKey() + * maxTotalPerKey}, {@link #getMaxTotal() maxTotal}, and (if applicable) + * {@link #getBlockWhenExhausted()} and the value passed in to the + * {@code borrowMaxWaitMillis} parameter. If the number of instances checked + * out from the sub-pool under the given key is less than + * {@code maxTotalPerKey} and the total number of instances in + * circulation (under all keys) is less than {@code maxTotal}, a new + * instance is created, activated and (if applicable) validated and returned + * to the caller. If validation fails, a {@code NoSuchElementException} + * will be thrown. If the factory returns null when creating an instance, + * a {@code NullPointerException} is thrown. + *

    + *

    + * If the associated sub-pool is exhausted (no available idle instances and + * no capacity to create new ones), this method will either block + * ({@link #getBlockWhenExhausted()} is true) or throw a + * {@code NoSuchElementException} + * ({@link #getBlockWhenExhausted()} is false). + * The length of time that this method will block when + * {@link #getBlockWhenExhausted()} is true is determined by the value + * passed in to the {@code borrowMaxWait} parameter. + *

    + *

    + * When {@code maxTotal} is set to a positive value and this method is + * invoked when at the limit with no idle instances available under the requested + * key, an attempt is made to create room by clearing the oldest 15% of the + * elements from the keyed sub-pools. + *

    + *

    + * When the pool is exhausted, multiple calling threads may be + * simultaneously blocked waiting for instances to become available. A + * "fairness" algorithm has been implemented to ensure that threads receive + * available instances in request arrival order. + *

    + * + * @param key pool key + * @param maxWaitMillis The time to wait in milliseconds for an object to become + * available + * + * @return object instance from the keyed pool + * @throws NoSuchElementException if a keyed object instance cannot be + * returned because the pool is exhausted. + * + * @throws Exception if a keyed object instance cannot be returned due to an + * error + */ + + public T borrowObject(final K key, final long maxWaitMillis) throws Exception { + return borrowObject(key, Duration.ofMillis(maxWaitMillis)); + } + /** * Calculate the number of objects that need to be created to attempt to * maintain the minimum number of idle objects while not exceeded the limits @@ -607,7 +698,7 @@ final ObjectDeque objectDeque = register(key); int freedCapacity = 0; try { - final LinkedBlockingDeque> idleObjects = objectDeque.getIdleObjects(); + final BlockingDeque> idleObjects = objectDeque.getIdleObjects(); PooledObject p = idleObjects.poll(); while (p != null) { try { @@ -698,7 +789,7 @@ jmxUnregister(); // Release any threads that were waiting for an object - poolMap.values().forEach(e -> e.getIdleObjects().interuptTakeWaiters()); + poolMap.values().forEach(e -> e.getIdleObjects().interruptTakeWaiters()); // This clear cleans up the keys now any waiting threads have been // interrupted clear(); @@ -708,11 +799,16 @@ /** * Creates a new pooled object or null. * - * @param key Key associated with new pooled object. + * @param key Key associated with new pooled object. + * @param maxWaitDuration The time to wait in this method. If negative or ZERO, + * this method may wait indefinitely. * @return The new, wrapped pooled object. May return null. * @throws Exception If the objection creation fails. */ - private PooledObject create(final K key) throws Exception { + private PooledObject create(final K key, final Duration maxWaitDuration) throws Exception { + final Instant startInstant = Instant.now(); + Duration remainingWaitDuration = maxWaitDuration.isNegative() ? Duration.ZERO : maxWaitDuration; + int maxTotalPerKeySave = getMaxTotalPerKey(); // Per key if (maxTotalPerKeySave < 0) { maxTotalPerKeySave = Integer.MAX_VALUE; @@ -744,6 +840,7 @@ // call the factory Boolean create = null; while (create == null) { + remainingWaitDuration = maxWaitDuration.isNegative() ? Duration.ZERO : maxWaitDuration.minus(durationSince(startInstant)); synchronized (objectDeque.makeObjectCountLock) { final long newCreateCount = objectDeque.getCreateCount().incrementAndGet(); // Check against the per key limit @@ -757,12 +854,13 @@ // create a new object. Return and wait for an object to // be returned. create = Boolean.FALSE; - } else { + } else if (!remainingWaitDuration.isNegative()) { // There are makeObject() calls in progress that might // bring the pool to capacity. Those calls might also // fail so wait until they complete and then re-test if // the pool is at capacity or not. - objectDeque.makeObjectCountLock.wait(); + objectDeque.makeObjectCountLock.wait(remainingWaitDuration.toMillis(), + remainingWaitDuration.getNano() % 1_000_000); } } else { // The pool is not at capacity. Create a new object. @@ -808,7 +906,7 @@ } createdCount.incrementAndGet(); - objectDeque.getAllObjects().put(new IdentityWrapper<>(p.getObject()), p); + objectDeque.getAllObjects().put(IdentityWrapper.unwrap(p), p); return p; } @@ -857,13 +955,13 @@ /** * Destroy the wrapped, pooled object. * - * @param key The key associated with the object to destroy. + * @param key The key associated with the object to destroy * @param toDestroy The wrapped object to be destroyed * @param always Should the object be destroyed even if it is not currently * in the set of idle objects for the given key - * @param destroyMode DestroyMode context provided to the factory + * @param destroyMode {@link DestroyMode} context provided to the factory * @return {@code true} if the object was destroyed, otherwise {@code false} - * @throws Exception If the object destruction failed + * @throws Exception If the factory throws an exception during destruction */ private boolean destroy(final K key, final PooledObject toDestroy, final boolean always, final DestroyMode destroyMode) throws Exception { @@ -881,7 +979,7 @@ } } if (isIdle || always) { - objectDeque.getAllObjects().remove(new IdentityWrapper<>(toDestroy.getObject())); + objectDeque.getAllObjects().remove(IdentityWrapper.unwrap(toDestroy)); toDestroy.invalidate(); try { @@ -1086,8 +1184,6 @@ } } underTest.endEvictionTest(idleObjects); - // TODO - May need to add code here once additional - // states are used } } } @@ -1096,6 +1192,9 @@ if (ac != null && ac.getRemoveAbandonedOnMaintenance()) { removeAbandoned(ac); } + if (reuseCapacityOnMaintenance) { + reuseCapacity(); + } } /** @@ -1258,6 +1357,32 @@ return result; } + /** + * Gets whether to call {@link #reuseCapacity()} during pool maintenance (eviction). + * When true, the pool will attempt to reuse freed capacity at the end of each + * eviction run. + * + * @return {@code true} if capacity reuse is enabled during maintenance, {@code false} otherwise + * @see #setReuseCapacityOnMaintenance(boolean) + * @since 2.13.0 + */ + public boolean getReuseCapacityOnMaintenance() { + return reuseCapacityOnMaintenance; + } + + /** + * Gets whether to call {@link #reuseCapacity()} when returning objects to the pool. + * When true, the pool will check if there are threads waiting to borrow objects + * and attempt to reuse the capacity freed by the return operation. + * + * @return {@code true} if capacity reuse is enabled on return, {@code false} otherwise + * @see #setReuseCapacityOnReturn(boolean) + * @since 2.13.0 + */ + public boolean getReuseCapacityOnReturn() { + return reuseCapacityOnReturn; + } + @SuppressWarnings("boxing") // Commons Pool uses auto-boxing @Override String getStatsString() { @@ -1333,7 +1458,7 @@ * to be borrowed) and active (currently borrowed). *

    * Note: This is named listAllObjects so it is presented as an operation via - * JMX. That means it won't be invoked unless the explicitly requested + * JMX. That means it won't be invoked unless explicitly requested * whereas all attributes will be automatically requested when viewing the * attributes for an object in a tool like JConsole. *

    @@ -1505,7 +1630,7 @@ } final int maxIdle = getMaxIdlePerKey(); - final LinkedBlockingDeque> idleObjects = objectDeque.getIdleObjects(); + final BlockingDeque> idleObjects = objectDeque.getIdleObjects(); if (isClosed() || maxIdle > -1 && maxIdle <= idleObjects.size()) { try { @@ -1527,7 +1652,7 @@ } } } finally { - if (hasBorrowWaiters()) { + if (reuseCapacityOnReturn && hasBorrowWaiters()) { reuseCapacity(); } updateStatsReturn(activeTime); @@ -1550,7 +1675,7 @@ private void reuseCapacity() { final int maxTotalPerKeySave = getMaxTotalPerKey(); int maxQueueLength = 0; - LinkedBlockingDeque> mostLoadedPool = null; + BlockingDeque> mostLoadedPool = null; K mostLoadedKey = null; // Find the most loaded pool that could take a new instance @@ -1571,7 +1696,7 @@ try { // If there is no capacity to add, create will return null // and addIdleObject will no-op. - addIdleObject(mostLoadedKey, create(mostLoadedKey)); + addIdleObject(mostLoadedKey, create(mostLoadedKey, Duration.ZERO)); } catch (final Exception e) { swallowException(e); } finally { @@ -1585,7 +1710,7 @@ *

    * Always activates {@link #reuseCapacity()} at least once. * - * @param newCapacity number of new instances to attempt to create. + * @param newCapacity number of times to call {@link #reuseCapacity()} */ private void reuseCapacity(final int newCapacity) { final int bound = newCapacity < 1 ? 1 : newCapacity; @@ -1606,6 +1731,8 @@ setMaxTotalPerKey(conf.getMaxTotalPerKey()); setMaxTotal(conf.getMaxTotal()); setMinIdlePerKey(conf.getMinIdlePerKey()); + setReuseCapacityOnReturn(conf.getReuseCapacityOnReturn()); + setReuseCapacityOnMaintenance(conf.getReuseCapacityOnMaintenance()); } /** @@ -1662,6 +1789,34 @@ this.minIdlePerKey = minIdlePerKey; } + /** + * Sets whether to call {@link #reuseCapacity()} during pool maintenance (eviction). + * When enabled, the pool will attempt to reuse capacity at the end of each + * eviction run. + * + * @param reuseCapacityOnMaintenance {@code true} to enable capacity reuse during + * maintenance, {@code false} to disable + * @see #getReuseCapacityOnMaintenance() + * @since 2.13.0 + */ + public void setReuseCapacityOnMaintenance(final boolean reuseCapacityOnMaintenance) { + this.reuseCapacityOnMaintenance = reuseCapacityOnMaintenance; + } + + /** + * Sets whether to call {@link #reuseCapacity()} when returning objects to the pool. + * When enabled, the pool will check if there are threads waiting to borrow objects + * and attempt to reuse capacity (across pools) on return. + * + * @param reuseCapacityOnReturn {@code true} to enable capacity reuse on return, + * {@code false} to disable + * @see #getReuseCapacityOnReturn() + * @since 2.13.0 + */ + public void setReuseCapacityOnReturn(final boolean reuseCapacityOnReturn) { + this.reuseCapacityOnReturn = reuseCapacityOnReturn; + } + @Override protected void toStringAppendFields(final StringBuilder builder) { super.toStringAppendFields(builder); @@ -1689,6 +1844,10 @@ builder.append(evictionKey); builder.append(", abandonedConfig="); builder.append(abandonedConfig); + builder.append(", reuseCapacityOnReturn="); + builder.append(reuseCapacityOnReturn); + builder.append(", reuseCapacityOnMaintenance="); + builder.append(reuseCapacityOnMaintenance); } /** diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolConfig.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolConfig.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolConfig.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolConfig.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -58,6 +58,22 @@ */ public static final int DEFAULT_MAX_IDLE_PER_KEY = 8; + /** + * The default value for the {@code reuseCapacityOnReturn} configuration attribute: {@value}. + * + * @see GenericKeyedObjectPool#getReuseCapacityOnReturn() + * @since 2.13.0 + */ + public static final boolean DEFAULT_REUSE_CAPACITY_ON_RETURN = true; + + /** + * The default value for the {@code reuseCapacityOnMaintenance} configuration attribute: {@value}. + * + * @see GenericKeyedObjectPool#getReuseCapacityOnMaintenance() + * @since 2.13.0 + */ + public static final boolean DEFAULT_REUSE_CAPACITY_ON_MAINTENANCE = false; + private int minIdlePerKey = DEFAULT_MIN_IDLE_PER_KEY; private int maxIdlePerKey = DEFAULT_MAX_IDLE_PER_KEY; @@ -66,6 +82,10 @@ private int maxTotal = DEFAULT_MAX_TOTAL; + private boolean reuseCapacityOnReturn = DEFAULT_REUSE_CAPACITY_ON_RETURN; + + private boolean reuseCapacityOnMaintenance = DEFAULT_REUSE_CAPACITY_ON_MAINTENANCE; + /** * Constructs a new configuration with default settings. */ @@ -135,6 +155,34 @@ } /** + * Gets the value for the {@code reuseCapacityOnMaintenance} configuration attribute + * for pools created with this configuration instance. + * + * @return The current setting of {@code reuseCapacityOnMaintenance} for this + * configuration instance + * + * @see GenericKeyedObjectPool#getReuseCapacityOnMaintenance() + * @since 2.13.0 + */ + public boolean getReuseCapacityOnMaintenance() { + return reuseCapacityOnMaintenance; + } + + /** + * Gets the value for the {@code reuseCapacityOnReturn} configuration attribute + * for pools created with this configuration instance. + * + * @return The current setting of {@code reuseCapacityOnReturn} for this + * configuration instance + * + * @see GenericKeyedObjectPool#getReuseCapacityOnReturn() + * @since 2.13.0 + */ + public boolean getReuseCapacityOnReturn() { + return reuseCapacityOnReturn; + } + + /** * Sets the value for the {@code maxIdlePerKey} configuration attribute for * pools created with this configuration instance. * @@ -186,6 +234,34 @@ this.minIdlePerKey = minIdlePerKey; } + /** + * Sets the value for the {@code reuseCapacityOnMaintenance} configuration attribute for + * pools created with this configuration instance. + * + * @param reuseCapacityOnMaintenance The new setting of {@code reuseCapacityOnMaintenance} + * for this configuration instance + * + * @see GenericKeyedObjectPool#setReuseCapacityOnMaintenance(boolean) + * @since 2.13.0 + */ + public void setReuseCapacityOnMaintenance(final boolean reuseCapacityOnMaintenance) { + this.reuseCapacityOnMaintenance = reuseCapacityOnMaintenance; + } + + /** + * Sets the value for the {@code reuseCapacityOnReturn} configuration attribute for + * pools created with this configuration instance. + * + * @param reuseCapacityOnReturn The new setting of {@code reuseCapacityOnReturn} + * for this configuration instance + * + * @see GenericKeyedObjectPool#setReuseCapacityOnReturn(boolean) + * @since 2.13.0 + */ + public void setReuseCapacityOnReturn(final boolean reuseCapacityOnReturn) { + this.reuseCapacityOnReturn = reuseCapacityOnReturn; + } + @Override protected void toStringAppendFields(final StringBuilder builder) { super.toStringAppendFields(builder); @@ -197,5 +273,9 @@ builder.append(maxTotalPerKey); builder.append(", maxTotal="); builder.append(maxTotal); + builder.append(", reuseCapacityOnReturn="); + builder.append(reuseCapacityOnReturn); + builder.append(", reuseCapacityOnMaintenance="); + builder.append(reuseCapacityOnMaintenance); } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolMXBean.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolMXBean.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolMXBean.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericKeyedObjectPoolMXBean.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPool.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPool.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPool.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPool.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -82,13 +82,7 @@ // JMX specific attributes private static final String ONAME_BASE = - "org.apache.commons.pool2:type=GenericObjectPool,name="; - - private static void wait(final Object obj, final Duration duration) throws InterruptedException { - if (!duration.isNegative()) { - obj.wait(duration.toMillis(), duration.getNano() % 1_000_000); - } - } + "org.apache.commons.pool2:type=GenericObjectPool,name="; private volatile String factoryType; @@ -102,17 +96,17 @@ * All of the objects currently associated with this pool in any state. It * excludes objects that have been destroyed. The size of * {@link #allObjects} will always be less than or equal to {@link - * #_maxActive}. Map keys are pooled objects, values are the PooledObject + * #getMaxTotal()}. Map keys are pooled objects, values are the PooledObject * wrappers used internally by the pool. */ private final ConcurrentHashMap, PooledObject> allObjects = new ConcurrentHashMap<>(); /* * The combined count of the currently created objects and those in the - * process of being created. Under load, it may exceed {@link #_maxActive} + * process of being created. Under load, it may exceed {@link #getMaxTotal()} * if multiple threads try and create a new object at the same time but * {@link #create()} will ensure that there are never more than - * {@link #_maxActive} objects created at any one time. + * {@link #getMaxTotal()} objects created at any one time. */ private final AtomicLong createCount = new AtomicLong(); @@ -188,7 +182,7 @@ * @throws Exception If the factory fails to passivate the object */ private void addIdleObject(final PooledObject p) throws Exception { - if (!PooledObject.isNull(p)) { + if (PooledObject.nonNull(p)) { factory.passivateObject(p); if (getLifo()) { idleObjects.addFirst(p); @@ -199,10 +193,11 @@ } /** - * Creates an object, and place it into the pool. addObject() is useful for + * Creates an object, and places it into the pool. addObject() is useful for * "pre-loading" a pool with idle objects. *

    - * If there is no capacity available to add to the pool, this is a no-op + * If there is no capacity available to add to the pool, or there are already + * {@link #getMaxIdle()} idle instances in the pool, this is a no-op * (no exception, no impact to the pool). *

    *

    @@ -217,11 +212,16 @@ if (factory == null) { throw new IllegalStateException("Cannot add objects without a factory."); } - addIdleObject(create(getMaxWaitDuration())); + final int localMaxIdle = getMaxIdle(); + final int localMaxTotal = getMaxTotal(); + if ((localMaxIdle < 0 || getNumIdle() < localMaxIdle) && + (localMaxTotal < 0 || createCount.get() < localMaxTotal)) { + addIdleObject(create(getMaxWaitDuration())); + } } /** - * Equivalent to {@link #borrowObject(long) + * Equivalent to {@link #borrowObject(Duration) * borrowObject}({@link #getMaxWaitDuration()}). * * {@inheritDoc} @@ -246,8 +246,8 @@ *

    * If there are no idle instances available in the pool, behavior depends on * the {@link #getMaxTotal() maxTotal}, (if applicable) - * {@link #getBlockWhenExhausted()} and the value passed in to the - * {@code borrowMaxWaitMillis} parameter. If the number of instances + * {@link #getBlockWhenExhausted()} and the value passed in the + * {@code maxWaitDuration} parameter. If the number of instances * checked out from the pool is less than {@code maxTotal,} a new * instance is created, activated and (if applicable) validated and returned * to the caller. If validation fails, a {@code NoSuchElementException} @@ -261,8 +261,9 @@ * {@code NoSuchElementException} (if * {@link #getBlockWhenExhausted()} is false). The length of time that this * method will block when {@link #getBlockWhenExhausted()} is true is - * determined by the value passed in to the {@code borrowMaxWaitMillis} - * parameter. + * determined by the value passed in to the {@code maxWaitDuration} + * parameter. Passing a negative duration will cause this method to block + * indefinitely until an object becomes available. *

    *

    * When the pool is exhausted, multiple calling threads may be @@ -271,17 +272,17 @@ * available instances in request arrival order. *

    * - * @param borrowMaxWaitDuration The time to wait for an object to become available, not null. + * @param maxWaitDuration The time to wait for an object to become available, not null. * @return object instance from the pool * @throws NoSuchElementException if an instance cannot be returned * @throws Exception if an object instance cannot be returned due to an error * @since 2.10.0 */ - public T borrowObject(final Duration borrowMaxWaitDuration) throws Exception { + public T borrowObject(final Duration maxWaitDuration) throws Exception { assertOpen(); final Instant startInstant = Instant.now(); - final boolean negativeDuration = borrowMaxWaitDuration.isNegative(); - Duration remainingWaitDuration = borrowMaxWaitDuration; + final boolean negativeDuration = maxWaitDuration.isNegative(); + Duration remainingWaitDuration = maxWaitDuration; final AbandonedConfig ac = this.abandonedConfig; if (ac != null && ac.getRemoveAbandonedOnBorrow() && getNumIdle() < 2 && getNumActive() > getMaxTotal() - 3) { removeAbandoned(ac); @@ -292,22 +293,22 @@ final boolean blockWhenExhausted = getBlockWhenExhausted(); boolean create; while (p == null) { - remainingWaitDuration = remainingWaitDuration.minus(durationSince(startInstant)); + remainingWaitDuration = maxWaitDuration.minus(durationSince(startInstant)); create = false; p = idleObjects.pollFirst(); if (p == null) { p = create(remainingWaitDuration); - if (!PooledObject.isNull(p)) { + if (PooledObject.nonNull(p)) { create = true; } } if (blockWhenExhausted) { if (PooledObject.isNull(p)) { + remainingWaitDuration = maxWaitDuration.minus(durationSince(startInstant)); p = negativeDuration ? idleObjects.takeFirst() : idleObjects.pollFirst(remainingWaitDuration); } if (PooledObject.isNull(p)) { - throw new NoSuchElementException(appendStats( - "Timeout waiting for idle object, borrowMaxWaitDuration=" + remainingWaitDuration)); + throw new NoSuchElementException(appendStats("Timeout waiting for idle object, maxWaitDuration=" + remainingWaitDuration)); } } else if (PooledObject.isNull(p)) { throw new NoSuchElementException(appendStats("Pool exhausted")); @@ -326,8 +327,7 @@ } p = null; if (create) { - final NoSuchElementException nsee = new NoSuchElementException( - appendStats("Unable to activate object")); + final NoSuchElementException nsee = new NoSuchElementException(appendStats("Unable to activate object")); nsee.initCause(e); throw nsee; } @@ -350,8 +350,7 @@ } p = null; if (create) { - final NoSuchElementException nsee = new NoSuchElementException( - appendStats("Unable to validate object")); + final NoSuchElementException nsee = new NoSuchElementException(appendStats("Unable to validate object")); nsee.initCause(validationThrowable); throw nsee; } @@ -363,10 +362,6 @@ return p.getObject(); } - private Duration durationSince(final Instant startInstant) { - return Duration.between(startInstant, Instant.now()); - } - /** * Borrows an object from the pool using the specific waiting time which only * applies if {@link #getBlockWhenExhausted()} is true. @@ -383,7 +378,7 @@ * If there are no idle instances available in the pool, behavior depends on * the {@link #getMaxTotal() maxTotal}, (if applicable) * {@link #getBlockWhenExhausted()} and the value passed in to the - * {@code borrowMaxWaitMillis} parameter. If the number of instances + * {@code maxWaitMillis} parameter. If the number of instances * checked out from the pool is less than {@code maxTotal,} a new * instance is created, activated and (if applicable) validated and returned * to the caller. If validation fails, a {@code NoSuchElementException} @@ -397,8 +392,9 @@ * {@code NoSuchElementException} (if * {@link #getBlockWhenExhausted()} is false). The length of time that this * method will block when {@link #getBlockWhenExhausted()} is true is - * determined by the value passed in to the {@code borrowMaxWaitMillis} - * parameter. + * determined by the value passed in to the {@code maxWaitMillis} + * parameter. Passing a negative duration will cause this method to block + * indefinitely until an object becomes available. *

    *

    * When the pool is exhausted, multiple calling threads may be @@ -407,15 +403,17 @@ * available instances in request arrival order. *

    * - * @param borrowMaxWaitMillis The time to wait in milliseconds for an object + * @param maxWaitMillis The time to wait in milliseconds for an object * to become available * @return object instance from the pool * @throws NoSuchElementException if an instance cannot be returned * @throws Exception if an object instance cannot be returned due to an * error + * @deprecated Use {@link #borrowObject(Duration)}. */ - public T borrowObject(final long borrowMaxWaitMillis) throws Exception { - return borrowObject(Duration.ofMillis(borrowMaxWaitMillis)); + @Deprecated + public T borrowObject(final long maxWaitMillis) throws Exception { + return borrowObject(Duration.ofMillis(maxWaitMillis)); } /** @@ -481,7 +479,7 @@ jmxUnregister(); // Release any threads that were waiting for an object - idleObjects.interuptTakeWaiters(); + idleObjects.interruptTakeWaiters(); } } @@ -495,13 +493,13 @@ * If the factory makeObject returns null, this method throws a NullPointerException. *

    * - * @param maxWaitDuration The time to wait for an object to become available. + * @param maxWaitDurationRequest The time to wait for capacity to create. * @return The new wrapped pooled object or null. - * @throws Exception if the object factory's {@code makeObject} fails + * @throws Exception if the object factory's {@code makeObject} fails. */ - private PooledObject create(final Duration maxWaitDuration) throws Exception { + private PooledObject create(final Duration maxWaitDurationRequest) throws Exception { final Instant startInstant = Instant.now(); - Duration remainingWaitDuration = maxWaitDuration.isNegative() ? Duration.ZERO : maxWaitDuration; + final Duration maxWaitDuration = maxWaitDurationRequest.isNegative() ? Duration.ZERO : maxWaitDurationRequest; int localMaxTotal = getMaxTotal(); // This simplifies the code later in this method if (localMaxTotal < 0) { @@ -516,7 +514,7 @@ Boolean create = null; while (create == null) { // remainingWaitDuration handles spurious wakeup from wait(). - remainingWaitDuration = remainingWaitDuration.minus(durationSince(startInstant)); + final Duration remainingWaitDuration = maxWaitDuration.minus(durationSince(startInstant)); synchronized (makeObjectCountLock) { final long newCreateCount = createCount.incrementAndGet(); if (newCreateCount > localMaxTotal) { @@ -580,7 +578,7 @@ } createdCount.incrementAndGet(); - allObjects.put(new IdentityWrapper<>(p.getObject()), p); + allObjects.put(IdentityWrapper.unwrap(p), p); return p; } @@ -595,7 +593,7 @@ private void destroy(final PooledObject toDestroy, final DestroyMode destroyMode) throws Exception { toDestroy.invalidate(); idleObjects.remove(toDestroy); - allObjects.remove(new IdentityWrapper<>(toDestroy.getObject())); + allObjects.remove(IdentityWrapper.unwrap(toDestroy)); try { factory.destroyObject(toDestroy, destroyMode); } finally { @@ -604,6 +602,7 @@ } } + /** * Tries to ensure that {@code idleCount} idle instances exist in the pool. *

    @@ -760,8 +759,6 @@ } } underTest.endEvictionTest(idleObjects); - // TODO - May need to add code here once additional - // states are used } } } @@ -917,7 +914,8 @@ * {@inheritDoc} *

    * Activation of this method decrements the active count and attempts to destroy the instance, using the provided - * {@link DestroyMode}. + * {@link DestroyMode}. To ensure liveness of the pool, {@link #addObject()} is called to replace the invalidated + * instance. *

    * * @throws Exception if an exception occurs destroying the object @@ -938,7 +936,9 @@ destroy(p, destroyMode); } } - ensureIdle(1, false); + if (!isClosed()) { + addObject(); + } } /** @@ -957,11 +957,12 @@ public Set listAllObjects() { return allObjects.values().stream().map(DefaultPooledObjectInfo::new).collect(Collectors.toSet()); } + /** * Tries to ensure that {@link #getMinIdle()} idle instances are available * in the pool. * - * @throws Exception If the associated factory throws an exception + * @throws Exception If the associated factory throws an exception. * @since 2.4 */ public void preparePool() throws Exception { @@ -973,7 +974,10 @@ /** * Recovers abandoned objects which have been checked out but - * not used since longer than the removeAbandonedTimeout. + * not used since longer than the removeAbandonedTimeout. For each object + * deemed abandoned, {@link #invalidateObject(Object)} is called. This + * results in the object being destroyed and then {@link #addObject()} is + * called to try to replace it. * * @param abandonedConfig The configuration to use to identify abandoned objects */ @@ -1021,75 +1025,76 @@ } return; // Object was abandoned and removed } + synchronized (p) { + markReturningState(p); - markReturningState(p); - - final Duration activeTime = p.getActiveDuration(); + final Duration activeTime = p.getActiveDuration(); - if (getTestOnReturn() && !factory.validateObject(p)) { - try { - destroy(p, DestroyMode.NORMAL); - } catch (final Exception e) { - swallowException(e); - } - try { - ensureIdle(1, false); - } catch (final Exception e) { - swallowException(e); + if (getTestOnReturn() && !factory.validateObject(p)) { + try { + destroy(p, DestroyMode.NORMAL); + } catch (final Exception e) { + swallowException(e); + } + try { + ensureIdle(1, false); + } catch (final Exception e) { + swallowException(e); + } + updateStatsReturn(activeTime); + return; } - updateStatsReturn(activeTime); - return; - } - try { - factory.passivateObject(p); - } catch (final Exception e1) { - swallowException(e1); try { - destroy(p, DestroyMode.NORMAL); - } catch (final Exception e) { - swallowException(e); - } - try { - ensureIdle(1, false); - } catch (final Exception e) { - swallowException(e); + factory.passivateObject(p); + } catch (final Exception e1) { + swallowException(e1); + try { + destroy(p, DestroyMode.NORMAL); + } catch (final Exception e) { + swallowException(e); + } + try { + ensureIdle(1, false); + } catch (final Exception e) { + swallowException(e); + } + updateStatsReturn(activeTime); + return; } - updateStatsReturn(activeTime); - return; - } - if (!p.deallocate()) { - throw new IllegalStateException( - "Object has already been returned to this pool or is invalid"); - } - - final int maxIdleSave = getMaxIdle(); - if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) { - try { - destroy(p, DestroyMode.NORMAL); - } catch (final Exception e) { - swallowException(e); - } - try { - ensureIdle(1, false); - } catch (final Exception e) { - swallowException(e); + if (!p.deallocate()) { + throw new IllegalStateException( + "Object has already been returned to this pool or is invalid"); } - } else { - if (getLifo()) { - idleObjects.addFirst(p); + + final int maxIdleSave = getMaxIdle(); + if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) { + try { + destroy(p, DestroyMode.NORMAL); + } catch (final Exception e) { + swallowException(e); + } + try { + ensureIdle(1, false); + } catch (final Exception e) { + swallowException(e); + } } else { - idleObjects.addLast(p); - } - if (isClosed()) { - // Pool closed while object was being added to idle objects. - // Make sure the returned object is destroyed rather than left - // in the idle object pool (which would effectively be a leak) - clear(); + if (getLifo()) { + idleObjects.addFirst(p); + } else { + idleObjects.addLast(p); + } + if (isClosed()) { + // Pool closed while object was being added to idle objects. + // Make sure the returned object is destroyed rather than left + // in the idle object pool (which would effectively be a leak) + clear(); + } } + updateStatsReturn(activeTime); } - updateStatsReturn(activeTime); } /** diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolConfig.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolConfig.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolConfig.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolConfig.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -57,6 +57,13 @@ private int minIdle = DEFAULT_MIN_IDLE; + /** + * Constructs a new instance. + */ + public GenericObjectPoolConfig() { + // empty + } + @SuppressWarnings("unchecked") @Override public GenericObjectPoolConfig clone() { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolMXBean.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolMXBean.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolMXBean.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/GenericObjectPoolMXBean.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/InterruptibleReentrantLock.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/InterruptibleReentrantLock.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/InterruptibleReentrantLock.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/InterruptibleReentrantLock.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -37,18 +37,18 @@ * Constructs a new InterruptibleReentrantLock with the given fairness policy. * * @param fairness true means threads should acquire contended locks as if - * waiting in a FIFO queue + * waiting in a FIFO queue. */ InterruptibleReentrantLock(final boolean fairness) { super(fairness); } /** - * Interrupts the threads that are waiting on a specific condition + * Interrupts the threads that are waiting on a specific condition. * * @param condition the condition on which the threads are waiting. */ - public void interruptWaiters(final Condition condition) { + void interruptWaiters(final Condition condition) { getWaitingThreads(condition).forEach(Thread::interrupt); } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/LinkedBlockingDeque.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/LinkedBlockingDeque.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/LinkedBlockingDeque.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/LinkedBlockingDeque.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -22,10 +22,10 @@ import java.time.Duration; import java.util.AbstractQueue; import java.util.Collection; -import java.util.Deque; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.concurrent.BlockingDeque; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; @@ -66,7 +66,7 @@ * @since 2.0 */ final class LinkedBlockingDeque extends AbstractQueue - implements Deque, Serializable { + implements BlockingDeque, Serializable { /* * Implemented as a simple doubly-linked list protected by a @@ -101,7 +101,7 @@ /** * The next node to return in next() */ - Node next; + Node next; /** * nextItem holds on to item fields because once we claim that @@ -223,18 +223,26 @@ /** Descending iterator */ private final class DescendingItr extends AbstractItr { @Override - Node firstNode() { return last; } + Node firstNode() { + return last; + } @Override - Node nextNode(final Node n) { return n.prev; } + Node nextNode(final Node n) { + return n.prev; + } } /** Forward iterator */ private final class Itr extends AbstractItr { @Override - Node firstNode() { return first; } + Node firstNode() { + return first; + } @Override - Node nextNode(final Node n) { return n.next; } + Node nextNode(final Node n) { + return n.next; } + } /** * Doubly-linked list node class. @@ -471,6 +479,7 @@ * @throws NullPointerException if c is null * @throws IllegalArgumentException if c is this instance */ + @Override public int drainTo(final Collection c) { return drainTo(c, Integer.MAX_VALUE); } @@ -490,6 +499,7 @@ * @throws NullPointerException if c is null * @throws IllegalArgumentException if c is this instance */ + @Override public int drainTo(final Collection collection, final int maxElements) { Objects.requireNonNull(collection, "collection"); if (collection == this) { @@ -547,10 +557,10 @@ * * @return number of threads waiting on this deque's notEmpty condition. */ - public int getTakeQueueLength() { + int getTakeQueueLength() { lock.lock(); try { - return lock.getWaitQueueLength(notEmpty); + return lock.getWaitQueueLength(notEmpty); } finally { lock.unlock(); } @@ -562,7 +572,7 @@ * * @return true if there is at least one thread waiting on this deque's notEmpty condition. */ - public boolean hasTakeWaiters() { + boolean hasTakeWaiters() { lock.lock(); try { return lock.hasWaiters(notEmpty); @@ -575,7 +585,7 @@ * Interrupts the threads currently waiting to take an object from the pool. See disclaimer on accuracy in * {@link java.util.concurrent.locks.ReentrantLock#getWaitingThreads(Condition)}. */ - public void interuptTakeWaiters() { + void interruptTakeWaiters() { lock.lock(); try { lock.interruptWaiters(notEmpty); @@ -685,6 +695,7 @@ * @throws InterruptedException if the thread is interrupted whilst waiting * for space */ + @Override public boolean offer(final E e, final long timeout, final TimeUnit unit) throws InterruptedException { return offerLast(e, timeout, unit); } @@ -711,7 +722,7 @@ * @throws InterruptedException if the thread is interrupted whilst waiting * for space */ - public boolean offerFirst(final E e, final Duration timeout) throws InterruptedException { + boolean offerFirst(final E e, final Duration timeout) throws InterruptedException { Objects.requireNonNull(e, "e"); long nanos = timeout.toNanos(); lock.lockInterruptibly(); @@ -740,6 +751,7 @@ * @throws InterruptedException if the thread is interrupted whilst waiting * for space */ + @Override public boolean offerFirst(final E e, final long timeout, final TimeUnit unit) throws InterruptedException { return offerFirst(e, PoolImplUtils.toDuration(timeout, unit)); } @@ -795,6 +807,7 @@ * @throws InterruptedException if the thread is interrupted whist waiting * for space */ + @Override public boolean offerLast(final E e, final long timeout, final TimeUnit unit) throws InterruptedException { return offerLast(e, PoolImplUtils.toDuration(timeout, unit)); } @@ -856,6 +869,7 @@ * @return the unlinked element * @throws InterruptedException if the current thread is interrupted */ + @Override public E poll(final long timeout, final TimeUnit unit) throws InterruptedException { return pollFirst(timeout, unit); } @@ -904,6 +918,7 @@ * @return the unlinked element * @throws InterruptedException if the current thread is interrupted */ + @Override public E pollFirst(final long timeout, final TimeUnit unit) throws InterruptedException { return pollFirst(PoolImplUtils.toDuration(timeout, unit)); } @@ -926,7 +941,7 @@ * @return the unlinked element * @throws InterruptedException if the current thread is interrupted */ - public E pollLast(final Duration timeout) + E pollLast(final Duration timeout) throws InterruptedException { long nanos = timeout.toNanos(); lock.lockInterruptibly(); @@ -953,6 +968,7 @@ * @return the unlinked element * @throws InterruptedException if the current thread is interrupted */ + @Override public E pollLast(final long timeout, final TimeUnit unit) throws InterruptedException { return pollLast(PoolImplUtils.toDuration(timeout, unit)); @@ -981,6 +997,7 @@ * @throws InterruptedException if the thread is interrupted whilst waiting * for space */ + @Override public void put(final E e) throws InterruptedException { putLast(e); } @@ -994,6 +1011,7 @@ * @throws InterruptedException if the thread is interrupted whilst waiting * for space */ + @Override public void putFirst(final E e) throws InterruptedException { Objects.requireNonNull(e, "e"); lock.lock(); @@ -1015,6 +1033,7 @@ * @throws InterruptedException if the thread is interrupted whilst waiting * for space */ + @Override public void putLast(final E e) throws InterruptedException { Objects.requireNonNull(e, "e"); lock.lock(); @@ -1030,12 +1049,11 @@ // Stack methods /** - * Reconstitutes this deque from a stream (that is, - * deserialize it). + * Reconstitutes this deque from a stream (that is, deserialize it). + * * @param s the stream */ - private void readObject(final ObjectInputStream s) - throws IOException, ClassNotFoundException { + private void readObject(final ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); count = 0; first = null; @@ -1043,7 +1061,7 @@ // Read in all elements and place in queue for (;;) { @SuppressWarnings("unchecked") - final E item = (E)s.readObject(); + final E item = (E) s.readObject(); if (item == null) { break; } @@ -1066,6 +1084,7 @@ * * @return The number of additional elements the queue is able to accept */ + @Override public int remainingCapacity() { lock.lock(); try { @@ -1238,6 +1257,7 @@ * @return the unlinked element * @throws InterruptedException if the current thread is interrupted */ + @Override public E take() throws InterruptedException { return takeFirst(); } @@ -1249,6 +1269,7 @@ * @return the unlinked element * @throws InterruptedException if the current thread is interrupted */ + @Override public E takeFirst() throws InterruptedException { lock.lock(); try { @@ -1269,6 +1290,7 @@ * @return the unlinked element * @throws InterruptedException if the current thread is interrupted */ + @Override public E takeLast() throws InterruptedException { lock.lock(); try { @@ -1319,12 +1341,11 @@ lock.lock(); try { if (a.length < count) { - a = (T[])java.lang.reflect.Array.newInstance - (a.getClass().getComponentType(), count); + a = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), count); } int k = 0; for (Node p = first; p != null; p = p.next) { - a[k++] = (T)p.item; + a[k++] = (T) p.item; } if (a.length > k) { a[k] = null; @@ -1362,9 +1383,8 @@ p.next = n; n.prev = p; x.item = null; - // Don't mess with x's links. They may still be in use by - // an iterator. - --count; + // Don't mess with x's links. They may still be in use by an iterator. + --count; notFull.signal(); } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/NoOpCallStack.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/NoOpCallStack.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/NoOpCallStack.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/NoOpCallStack.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -27,7 +27,7 @@ public class NoOpCallStack implements CallStack { /** - * Singleton instance. + * The singleton instance. */ public static final CallStack INSTANCE = new NoOpCallStack(); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/PoolImplUtils.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/PoolImplUtils.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/PoolImplUtils.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/PoolImplUtils.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -197,22 +197,22 @@ static ChronoUnit toChronoUnit(final TimeUnit timeUnit) { // TODO when using Java >= 9: Use TimeUnit.toChronoUnit(). switch (Objects.requireNonNull(timeUnit)) { - case NANOSECONDS: - return ChronoUnit.NANOS; - case MICROSECONDS: - return ChronoUnit.MICROS; - case MILLISECONDS: - return ChronoUnit.MILLIS; - case SECONDS: - return ChronoUnit.SECONDS; - case MINUTES: - return ChronoUnit.MINUTES; - case HOURS: - return ChronoUnit.HOURS; - case DAYS: - return ChronoUnit.DAYS; - default: - throw new IllegalArgumentException(timeUnit.toString()); + case NANOSECONDS: + return ChronoUnit.NANOS; + case MICROSECONDS: + return ChronoUnit.MICROS; + case MILLISECONDS: + return ChronoUnit.MILLIS; + case SECONDS: + return ChronoUnit.SECONDS; + case MINUTES: + return ChronoUnit.MINUTES; + case HOURS: + return ChronoUnit.HOURS; + case DAYS: + return ChronoUnit.DAYS; + default: + throw new IllegalArgumentException(timeUnit.toString()); } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/PooledSoftReference.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/PooledSoftReference.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/PooledSoftReference.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/PooledSoftReference.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/SoftReferenceObjectPool.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/SoftReferenceObjectPool.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/SoftReferenceObjectPool.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/SoftReferenceObjectPool.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -23,6 +23,7 @@ import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.concurrent.BlockingDeque; import org.apache.tomcat.dbcp.pool2.BaseObjectPool; import org.apache.tomcat.dbcp.pool2.ObjectPool; @@ -62,12 +63,11 @@ private long createCount; // @GuardedBy("this") /** Idle references - waiting to be borrowed */ - private final LinkedBlockingDeque> idleReferences = - new LinkedBlockingDeque<>(); + private final BlockingDeque> idleReferences = new LinkedBlockingDeque<>(); /** All references - checked out or waiting to be borrowed. */ private final ArrayList> allReferences = - new ArrayList<>(); + new ArrayList<>(); /** * Constructs a {@code SoftReferenceObjectPool} with the specified factory. @@ -262,7 +262,7 @@ } /** - * Destroys a {@code PooledSoftReference} and removes it from the idle and all + * Destroys a {@link PooledSoftReference} and removes it from the idle and all * references pools. * * @param toDestroy PooledSoftReference to destroy @@ -329,7 +329,7 @@ final PooledSoftReference ref = findReference(obj); if (ref == null) { throw new IllegalStateException( - "Object to invalidate is not currently part of this pool"); + "Object to invalidate is not currently part of this pool"); } if (factory != null) { destroy(ref); @@ -392,7 +392,7 @@ final PooledSoftReference ref = findReference(obj); if (ref == null) { throw new IllegalStateException( - "Returned object not currently part of this pool"); + "Returned object not currently part of this pool"); } if (factory != null) { if (!factory.validateObject(ref)) { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/ThrowableCallStack.java tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/ThrowableCallStack.java --- tomcat11-11.0.15/java/org/apache/tomcat/dbcp/pool2/impl/ThrowableCallStack.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/dbcp/pool2/impl/ThrowableCallStack.java 2026-05-01 18:56:05.000000000 +0000 @@ -6,7 +6,7 @@ * (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 + * https://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, @@ -42,7 +42,7 @@ /** * Constructs a new instance with its message set to the now instant. */ - Snapshot() { + private Snapshot() { this(Instant.now()); } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/jni/SSL.java tomcat11-11.0.22/java/org/apache/tomcat/jni/SSL.java --- tomcat11-11.0.15/java/org/apache/tomcat/jni/SSL.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/jni/SSL.java 2026-05-01 18:56:05.000000000 +0000 @@ -621,21 +621,33 @@ public static native String[] getCiphers(long ssl); /** - * Returns the cipher suites available for negotiation in SSL handshake.
    + * Set the TLSv1.2 and below ciphers available for negotiation the in TLS handshake. + *

    * This complex directive uses a colon-separated cipher-spec string consisting of OpenSSL cipher specifications to - * configure the Cipher Suite the client is permitted to negotiate in the SSL handshake phase. Notice that this - * directive can be used both in per-server and per-directory context. In per-server context it applies to the - * standard SSL handshake when a connection is established. In per-directory context it forces an SSL renegotiation - * with the reconfigured Cipher Suite after the HTTP request was read but before the HTTP response is sent. + * configure the ciphers the client is permitted to negotiate in the TLS handshake phase. * - * @param ssl the SSL instance (SSL *) - * @param ciphers an SSL cipher specification + * @param ssl The SSL instance (SSL *) + * @param cipherList An OpenSSL cipher specification. * * @return true if the operation was successful * * @throws Exception An error occurred */ - public static native boolean setCipherSuites(long ssl, String ciphers) throws Exception; + public static native boolean setCipherSuites(long ssl, String cipherList) throws Exception; + + /** + * Set the TLSv1.3 cipher suites available for negotiation the in TLS handshake. + *

    + * This uses a colon-separated list of TLSv1.3 cipher suite names in preference order. + * + * @param ssl The SSL instance (SSL *) + * @param cipherSuites An OpenSSL cipher suite list. + * + * @return true if the operation was successful + * + * @throws Exception An error occurred + */ + public static native boolean setCipherSuitesEx(long ssl, String cipherSuites) throws Exception; /** * Returns the ID of the session as byte array representation. diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/jni/SSLConf.java tomcat11-11.0.22/java/org/apache/tomcat/jni/SSLConf.java --- tomcat11-11.0.15/java/org/apache/tomcat/jni/SSLConf.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/jni/SSLConf.java 2026-05-01 18:56:05.000000000 +0000 @@ -53,7 +53,11 @@ public static native void free(long cctx); /** - * Check a command with an SSL_CONF context. + * Optionally used to check a command with an SSL_CONF context. + *

    + * This call is also used to pass Tomcat specific settings to Tomcat Native. It must be called for for each Tomcat + * specific setting (e.g. {@link org.apache.tomcat.util.net.openssl.OpenSSLConfCmd#NO_OCSP_CHECK}) before {@link + * #assign(long, long)} is called. * * @param cctx SSL_CONF context to use. * @param name command name. @@ -71,6 +75,10 @@ /** * Assign an SSL context to an SSL_CONF context. All following calls to {@link #apply(long, String, String)} will be * applied to this SSL context. + *

    + * For Tomcat specific settings this call applies previous settings set via calls to {@link + * #check(long, String, String)}. Further calls to {@link #check(long, String, String)} after a call to this method + * will have no effect. * * @param cctx SSL_CONF context to use. * @param ctx SSL context to assign to the given SSL_CONF context. @@ -82,6 +90,8 @@ /** * Apply a command to an SSL_CONF context. + *

    + * This call has no effect for Tomcat specific settings. * * @param cctx SSL_CONF context to use. * @param name command name. @@ -97,6 +107,8 @@ /** * Finish commands for an SSL_CONF context. + *

    + * This call has no effect for Tomcat specific settings. * * @param cctx SSL_CONF context to use. * diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/jni/SSLContext.java tomcat11-11.0.22/java/org/apache/tomcat/jni/SSLContext.java --- tomcat11-11.0.15/java/org/apache/tomcat/jni/SSLContext.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/jni/SSLContext.java 2026-05-01 18:56:05.000000000 +0000 @@ -98,21 +98,33 @@ public static native String[] getCiphers(long ctx); /** - * Cipher Suite available for negotiation in SSL handshake.
    + * Set the TLSv1.2 and below ciphers available for negotiation the in TLS handshake. + *

    * This complex directive uses a colon-separated cipher-spec string consisting of OpenSSL cipher specifications to - * configure the Cipher Suite the client is permitted to negotiate in the SSL handshake phase. Notice that this - * directive can be used both in per-server and per-directory context. In per-server context it applies to the - * standard SSL handshake when a connection is established. In per-directory context it forces an SSL renegotiation - * with the reconfigured Cipher Suite after the HTTP request was read but before the HTTP response is sent. + * configure the ciphers the client is permitted to negotiate in the TLS handshake phase. * - * @param ctx Server or Client context to use. - * @param ciphers An OpenSSL cipher specification. + * @param ctx Server or Client context to use. + * @param cipherList An OpenSSL cipher specification. * * @return true if the operation was successful * * @throws Exception An error occurred */ - public static native boolean setCipherSuite(long ctx, String ciphers) throws Exception; + public static native boolean setCipherSuite(long ctx, String cipherList) throws Exception; + + /** + * Set the TLSv1.3 cipher suites available for negotiation the in TLS handshake. + *

    + * This uses a colon-separated list of TLSv1.3 cipher suite names in preference order. + * + * @param ctx Server or Client context to use. + * @param cipherSuites An OpenSSL cipher suite list. + * + * @return true if the operation was successful + * + * @throws Exception An error occurred + */ + public static native boolean setCipherSuitesEx(long ctx, String cipherSuites) throws Exception; /** * Set File of concatenated PEM-encoded CA CRLs or directory of PEM-encoded CA Certificates for Client Auth
    diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/Const.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/Const.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/Const.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/Const.java 2026-05-01 18:56:05.000000000 +0000 @@ -24,199 +24,202 @@ /** * Java class file format Magic number: {@value}. * - * @see The ClassFile Structure - * in The Java Virtual Machine Specification + * @see The ClassFile + * Structure in The Java Virtual Machine Specification */ public static final int JVM_CLASSFILE_MAGIC = 0xCAFEBABE; /** * One of the access flags for fields, methods, or classes: {@value}. * - * @see Flag definitions for - * Classes in the Java Virtual Machine Specification (Java SE 9 Edition). - * @see Flag definitions for Fields - * in the Java Virtual Machine Specification (Java SE 9 Edition). - * @see Flag definitions for Methods - * in the Java Virtual Machine Specification (Java SE 9 Edition). + * @see Flag definitions + * for Classes in the Java Virtual Machine Specification (Java SE 9 Edition). + * @see Flag definitions for + * Fields in the Java Virtual Machine Specification (Java SE 9 Edition). + * @see Flag definitions for + * Methods in the Java Virtual Machine Specification (Java SE 9 Edition). * @see Flag - * definitions for Inner Classes in the Java Virtual Machine Specification (Java SE 9 Edition). + * definitions for Inner Classes in the Java Virtual Machine Specification (Java SE 9 Edition). */ - public static final short ACC_FINAL = 0x0010; + public static final short ACC_FINAL = 0x0010; /** * One of the access flags for classes: {@value}. * * @see #ACC_FINAL */ - public static final short ACC_INTERFACE = 0x0200; + public static final short ACC_INTERFACE = 0x0200; /** * One of the access flags for methods or classes: {@value}. * * @see #ACC_FINAL */ - public static final short ACC_ABSTRACT = 0x0400; + public static final short ACC_ABSTRACT = 0x0400; /** * One of the access flags for classes: {@value}. * * @see #ACC_FINAL */ - public static final short ACC_ANNOTATION = 0x2000; + public static final short ACC_ANNOTATION = 0x2000; /** * Marks a constant pool entry as type UTF-8: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_Utf8 = 1; /* - * The description of the constant pool is at: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4 - * References below are to the individual sections + * The description of the constant pool is at: + * https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4 References below are to the individual + * sections */ /** * Marks a constant pool entry as type Integer: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_Integer = 3; /** * Marks a constant pool entry as type Float: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_Float = 4; /** * Marks a constant pool entry as type Long: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_Long = 5; /** * Marks a constant pool entry as type Double: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_Double = 6; /** * Marks a constant pool entry as a Class: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_Class = 7; /** * Marks a constant pool entry as a Field Reference: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_Fieldref = 9; /** * Marks a constant pool entry as type String: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_String = 8; /** * Marks a constant pool entry as a Method Reference: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_Methodref = 10; /** * Marks a constant pool entry as an Interface Method Reference: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_InterfaceMethodref = 11; /** * Marks a constant pool entry as a name and type: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_NameAndType = 12; /** * Marks a constant pool entry as a Method Handle: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_MethodHandle = 15; /** * Marks a constant pool entry as a Method Type: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_MethodType = 16; /** * Marks a constant pool entry as dynamically computed: {@value}. * - * @see Change request for JEP - * 309 + * @see Change request for + * JEP 309 */ public static final byte CONSTANT_Dynamic = 17; /** * Marks a constant pool entry as an Invoke Dynamic: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_InvokeDynamic = 18; /** * Marks a constant pool entry as a Module Reference: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_Module = 19; /** * Marks a constant pool entry as a Package Reference: {@value}. * - * @see The Constant Pool in The - * Java Virtual Machine Specification + * @see The Constant Pool in + * The Java Virtual Machine Specification */ public static final byte CONSTANT_Package = 20; /** * The names of the types of entries in a constant pool. Use getConstantName instead */ - private static final String[] CONSTANT_NAMES = {"", "CONSTANT_Utf8", "", "CONSTANT_Integer", "CONSTANT_Float", "CONSTANT_Long", "CONSTANT_Double", - "CONSTANT_Class", "CONSTANT_String", "CONSTANT_Fieldref", "CONSTANT_Methodref", "CONSTANT_InterfaceMethodref", "CONSTANT_NameAndType", "", "", - "CONSTANT_MethodHandle", "CONSTANT_MethodType", "CONSTANT_Dynamic", "CONSTANT_InvokeDynamic", "CONSTANT_Module", "CONSTANT_Package"}; + private static final String[] CONSTANT_NAMES = { "", "CONSTANT_Utf8", "", "CONSTANT_Integer", "CONSTANT_Float", + "CONSTANT_Long", "CONSTANT_Double", "CONSTANT_Class", "CONSTANT_String", "CONSTANT_Fieldref", + "CONSTANT_Methodref", "CONSTANT_InterfaceMethodref", "CONSTANT_NameAndType", "", "", + "CONSTANT_MethodHandle", "CONSTANT_MethodType", "CONSTANT_Dynamic", "CONSTANT_InvokeDynamic", + "CONSTANT_Module", "CONSTANT_Package" }; /** * The maximum number of dimensions in an array: {@value}. One of the limitations of the Java Virtual Machine. * - * @see Field Descriptors in - * The Java Virtual Machine Specification + * @see Field Descriptors + * in The Java Virtual Machine Specification */ public static final int MAX_ARRAY_DIMENSIONS = 255; @@ -224,6 +227,7 @@ * Minor version number of class files for Java 22: {@value}. * * @see #MAJOR_22 + * * @since 6.10.0 */ public static final short MINOR_22 = 0; @@ -232,6 +236,7 @@ * Minor version number of class files for Java 23: {@value}. * * @see #MAJOR_23 + * * @since 6.10.0 */ public static final short MINOR_23 = 0; @@ -240,6 +245,7 @@ * Minor version number of class files for Java 24: {@value}. * * @see #MAJOR_24 + * * @since 6.10.0 */ public static final short MINOR_24 = 0; @@ -248,6 +254,7 @@ * Major version number of class files for Java 22: {@value}. * * @see #MINOR_22 + * * @since 6.10.0 */ public static final short MAJOR_22 = 66; @@ -256,6 +263,7 @@ * Major version number of class files for Java 23: {@value}. * * @see #MINOR_23 + * * @since 6.10.0 */ public static final short MAJOR_23 = 67; @@ -264,6 +272,7 @@ * Major version number of class files for Java 24: {@value}. * * @see #MINOR_24 + * * @since 6.10.0 */ public static final short MAJOR_24 = 68; @@ -272,6 +281,7 @@ * Minor version number of class files for Java 25: {@value}. * * @see #MAJOR_25 + * * @since 6.11.0 */ public static final short MINOR_25 = 0; @@ -280,14 +290,34 @@ * Major version number of class files for Java 25: {@value}. * * @see #MINOR_25 + * * @since 6.11.0 */ public static final short MAJOR_25 = 69; /** + * Minor version number of class files for Java 26: {@value}. + * + * @see #MAJOR_26 + * + * @since 6.12.0 + */ + public static final short MINOR_26 = 0; + + /** + * Major version number of class files for Java 26: {@value}. + * + * @see #MINOR_26 + * + * @since 6.12.0 + */ + public static final short MAJOR_26 = 70; + + /** * Get the CONSTANT_NAMES entry at the given index. * * @param index index into {@code CONSTANT_NAMES}. + * * @return the CONSTANT_NAMES entry at the given index */ public static String getConstantName(final int index) { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java 2026-05-01 18:56:05.000000000 +0000 @@ -28,6 +28,11 @@ this.annotationEntry = annotationEntry; } + /** + * Gets the annotation entry. + * + * @return the annotation entry. + */ public AnnotationEntry getAnnotationEntry() { return annotationEntry; } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java 2026-05-01 18:56:05.000000000 +0000 @@ -55,14 +55,18 @@ } /** - * @return the annotation type name + * Gets the annotation type name. + * + * @return the annotation type name. */ public String getAnnotationType() { return constantPool.getConstantUtf8(typeIndex).getBytes(); } /** - * @return the element value pairs in this annotation entry + * Gets the element value pairs in this annotation entry. + * + * @return the element value pairs in this annotation entry. */ public List getElementValuePairs() { return elementValuePairs; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java 2026-05-01 18:56:05.000000000 +0000 @@ -60,31 +60,31 @@ /** - * Parses the given Java class file and return an object that represents the contained data, i.e., constants, methods, + * Parses the given Java class file and return an object that represents the contained data, that is, constants, methods, * fields and commands. A ClassFormatException is raised, if the file is not a valid .class file. (This does * not include verification of the byte code as it is performed by the Java interpreter). * - * @return Class object representing the parsed class file + * @return Class object representing the parsed class file. * @throws IOException if an I/O error occurs. - * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file. */ public JavaClass parse() throws IOException, ClassFormatException { - //****************** Read headers ******************************** + // -- Read headers -- // Check magic tag of class file readID(); // Get compiler version readVersion(); - //***************** Read constant pool and related ************** + // -- Read constant pool and related **************/ // Read constant pool entries readConstantPool(); // Get class information readClassInfo(); - // Get interface information, i.e., implemented interfaces + // Get interface information, that is, implemented interfaces readInterfaces(); - //***************** Read class fields and methods *************** - // Read class fields, i.e., the variables of the class + // -- Read class fields and methods -- + // Read class fields, that is, the variables of the class readFields(); - // Read class methods, i.e., the functions in the class + // Read class methods, that is, the functions in the class readMethods(); // Read class attributes readAttributes(false); @@ -99,7 +99,7 @@ * Reads information about the attributes of the class. * @param fieldOrMethod false if processing a class * @throws IOException if an I/O error occurs. - * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file. */ private void readAttributes(boolean fieldOrMethod) throws IOException, ClassFormatException { final int attributesCount = dataInputStream.readUnsignedShort(); @@ -141,7 +141,7 @@ * Reads information about the class and its super class. * * @throws IOException if an I/O error occurs. - * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file. */ private void readClassInfo() throws IOException, ClassFormatException { accessFlags = dataInputStream.readUnsignedShort(); @@ -254,7 +254,7 @@ * Reads major and minor version of compiler which created the file. * * @throws IOException if an I/O error occurs. - * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file + * @throws ClassFormatException if a class is malformed or cannot be interpreted as a class file. */ private void readVersion() throws IOException, ClassFormatException { // file.readUnsignedShort(); // Unused minor diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/Constant.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/Constant.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/Constant.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/Constant.java 2026-05-01 18:56:05.000000000 +0000 @@ -30,8 +30,8 @@ /** * Reads one constant from the given input, the type depends on a tag byte. * - * @param dataInput Input stream - * @return Constant object + * @param dataInput Input stream. + * @return Constant object. * @throws IOException if an I/O error occurs reading from the given {@code dataInput}. * @throws ClassFormatException if the next byte is not recognized */ @@ -89,7 +89,9 @@ } /** - * @return Tag of constant, i.e., its type. No setTag() method to avoid confusion. + * Gets the tag of constant. + * + * @return Tag of constant, that is, its type. No setTag() method to avoid confusion. */ public final byte getTag() { return tag; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java 2026-05-01 18:56:05.000000000 +0000 @@ -34,7 +34,7 @@ /** * Constructs an instance from file data. * - * @param dataInput Input stream + * @param dataInput Input stream. * @throws IOException if an I/O error occurs reading from the given {@code dataInput}. */ ConstantClass(final DataInput dataInput) throws IOException { @@ -44,6 +44,8 @@ /** + * Gets the name index in constant pool of class name. + * * @return Name index in constant pool of class name. */ public int getNameIndex() { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java 2026-05-01 18:56:05.000000000 +0000 @@ -34,7 +34,7 @@ /** * Initialize instance from file data. * - * @param file Input stream + * @param file Input stream. * @throws IOException if an I/O error occurs. */ ConstantDouble(final DataInput file) throws IOException { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java 2026-05-01 18:56:05.000000000 +0000 @@ -34,7 +34,7 @@ /** * Initialize instance from file data. * - * @param file Input stream + * @param file Input stream. * @throws IOException if an I/O error occurs. */ ConstantFloat(final DataInput file) throws IOException { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java 2026-05-01 18:56:05.000000000 +0000 @@ -34,7 +34,7 @@ /** * Initialize instance from file data. * - * @param file Input stream + * @param file Input stream. * @throws IOException if an I/O error occurs. */ ConstantInteger(final DataInput file) throws IOException { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java 2026-05-01 18:56:05.000000000 +0000 @@ -34,7 +34,7 @@ /** * Initialize instance from file data. * - * @param file Input stream + * @param file Input stream. * @throws IOException if an I/O error occurs. */ ConstantLong(final DataInput file) throws IOException { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java 2026-05-01 18:56:05.000000000 +0000 @@ -22,7 +22,7 @@ import org.apache.tomcat.util.bcel.Const; /** - * This class represents the constant pool, i.e., a table of constants, of a parsed classfile. It may contain null references, due to the JVM specification that + * This class represents the constant pool, that is, a table of constants, of a parsed classfile. It may contain null references, due to the JVM specification that * skips an entry after an 8-byte constant (double, long) entry. Those interested in generating constant pools programmatically should see * ConstantPoolGen. * diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java 2026-05-01 18:56:05.000000000 +0000 @@ -20,7 +20,7 @@ import java.util.List; /** - * Represents a Java class, i.e., the data structures, constant pool, fields, methods and commands contained in a Java + * Represents a Java class, that is, the data structures, constant pool, fields, methods and commands contained in a Java * .class file. See JVM specification for details. The intent of * this class is to represent a parsed or otherwise existing class file. Those interested in programmatically generating * classes should see the ClassGen class. diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java 2026-05-01 18:56:05.000000000 +0000 @@ -27,7 +27,7 @@ } /** - * @return Value entry index in the cpool + * @return Value entry index in the constant pool. */ public int getIndex() { return index; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/package-info.java tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/package-info.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/bcel/package-info.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/bcel/package-info.java 2026-05-01 18:56:05.000000000 +0000 @@ -16,7 +16,7 @@ */ /** - * Basic classes for the Apache Byte Code Engineering Library (BCEL) and constants defined by the - * JVM specification. + * Basic classes for the Apache Byte Code Engineering Library (BCEL) and + * constants defined by the JVM specification. */ package org.apache.tomcat.util.bcel; \ No newline at end of file diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/buf/Asn1Parser.java tomcat11-11.0.22/java/org/apache/tomcat/util/buf/Asn1Parser.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/buf/Asn1Parser.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/buf/Asn1Parser.java 2026-05-01 18:56:05.000000000 +0000 @@ -26,8 +26,6 @@ /** * This is a very basic ASN.1 parser that provides the limited functionality required by Tomcat. It is a long way from a * complete parser. - *

    - * TODO: Consider extending/re-writing this parser and refactoring the SpnegoTokenFixer to use it. */ public class Asn1Parser { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/http/HeaderUtil.java tomcat11-11.0.22/java/org/apache/tomcat/util/http/HeaderUtil.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/http/HeaderUtil.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/http/HeaderUtil.java 2026-05-01 18:56:05.000000000 +0000 @@ -16,8 +16,38 @@ */ package org.apache.tomcat.util.http; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + public class HeaderUtil { + private static final Set disallowedTrailerFieldNames = new HashSet<>(); + + static { + // Always add these in lower case + disallowedTrailerFieldNames.add("age"); + disallowedTrailerFieldNames.add("cache-control"); + disallowedTrailerFieldNames.add("content-length"); + disallowedTrailerFieldNames.add("content-encoding"); + disallowedTrailerFieldNames.add("content-range"); + disallowedTrailerFieldNames.add("content-type"); + disallowedTrailerFieldNames.add("date"); + disallowedTrailerFieldNames.add("expires"); + disallowedTrailerFieldNames.add("location"); + disallowedTrailerFieldNames.add("retry-after"); + disallowedTrailerFieldNames.add("trailer"); + disallowedTrailerFieldNames.add("transfer-encoding"); + disallowedTrailerFieldNames.add("vary"); + disallowedTrailerFieldNames.add("warning"); + } + + + public static boolean isHeaderDisallowedInTrailers(String headerName) { + return disallowedTrailerFieldNames.contains(headerName.toLowerCase(Locale.ENGLISH)); + } + + /** * Converts an HTTP header line in byte form to a printable String. Bytes corresponding to visible ASCII characters * will be converted to those characters. All other bytes (0x00 to 0x1F, 0x7F to 0xFF) will be represented in 0xNN diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/http/RequestUtil.java tomcat11-11.0.22/java/org/apache/tomcat/util/http/RequestUtil.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/http/RequestUtil.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/http/RequestUtil.java 2026-05-01 18:56:05.000000000 +0000 @@ -32,7 +32,7 @@ /** * Normalize a relative URI path. This method normalizes "/./", "/../", "//" and "\". If the input path is an * attempt to 'escape the root' (e.g. /../input.txt) then {@code null} is returned to prevent attempts to 'escape - * the root'. WARNING - No other URI validation checks are performed. + * the root'. URI paths containing null bytes will be rejected. * * @param path Relative path to be normalized * @@ -46,7 +46,7 @@ /** * Normalize a relative URI path. This method normalizes "/./", "/../" and "//". This method optionally normalizes * "\". If the input path is an attempt to 'escape the root' (e.g. /../input.txt) then {@code null} is returned to - * prevent attempts to 'escape the root'. WARNING - No other URI validation checks are performed. + * prevent attempts to 'escape the root'. URI paths containing null bytes will be rejected. * * @param path Relative path to be normalized * @param replaceBackSlash Should '\\' be normalized to '/' @@ -55,10 +55,16 @@ */ public static String normalize(String path, boolean replaceBackSlash) { + // Keep behaviour aligned with CoyoteAdapter.normalize() if (path == null) { return null; } + // Reject paths containing null bytes + if (path.indexOf(0) > -1) { + return null; + } + // Create a place for the normalized path String normalized = path; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/ChunkExtension.java tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/ChunkExtension.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/ChunkExtension.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/ChunkExtension.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,130 @@ +/* + * 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.util.http.parser; + +import java.io.IOException; + +import org.apache.tomcat.util.res.StringManager; + +/* + * Unlike other HTTP parsers, this is a stateless (state is held by the calling code), streaming parser as chunk headers + * are read as part of the request body and it is not always possible to buffer then entire chunk header in memory. + */ +public class ChunkExtension { + + private static final StringManager sm = StringManager.getManager(ChunkExtension.class); + + public static State parse(byte b, State state) throws IOException { + + char c = (char) (0xFF & b); + + switch (state) { + case PRE_NAME: + if (HttpParser.isWhiteSpace(c)) { + return State.PRE_NAME; + } else if (HttpParser.isToken(c)) { + return State.NAME; + } + break; + case NAME: + if (HttpParser.isWhiteSpace(c)) { + return State.POST_NAME; + } else if (HttpParser.isToken(c)) { + return State.NAME; + } else if (c == ';') { + return State.PRE_NAME; + } else if (c == '=') { + return State.EQUALS; + } else if (c == '\r') { + return State.CR; + } + break; + case POST_NAME: + if (HttpParser.isWhiteSpace(c)) { + return State.POST_NAME; + } else if (c == ';') { + return State.PRE_NAME; + } else if (c == '=') { + return State.EQUALS; + } else if (c == '\r') { + return State.CR; + } + break; + case EQUALS: + if (HttpParser.isWhiteSpace(c)) { + return State.EQUALS; + } else if (HttpParser.isToken(c)) { + return State.VALUE; + } else if (c == '"') { + return State.QUOTED_VALUE; + } + break; + case VALUE: + if (HttpParser.isToken(c)) { + return State.VALUE; + } else if (HttpParser.isWhiteSpace(c)) { + return State.POST_VALUE; + } else if (c == ';') { + return State.PRE_NAME; + } else if (c == '\r') { + return State.CR; + } + break; + case QUOTED_VALUE: + if (c == '"') { + return State.POST_VALUE; + } else if (c == '\\' || c == 127) { + throw new IOException(sm.getString("chunkExtension.invalid")); + } else if (c == '\t') { + return State.QUOTED_VALUE; + } else if (c > 31) { + return State.QUOTED_VALUE; + } + break; + case POST_VALUE: + if (HttpParser.isWhiteSpace(c)) { + return State.POST_VALUE; + } else if (c == ';') { + return State.PRE_NAME; + } else if (c == '\r') { + return State.CR; + } + break; + case CR: + break; + } + + throw new IOException(sm.getString("chunkExtension.invalid")); + } + + + private ChunkExtension() { + // Tomcat doesn't use this data. It only parses it to ensure that it is correctly formatted. + } + + + public enum State { + PRE_NAME, + NAME, + POST_NAME, + EQUALS, + VALUE, + QUOTED_VALUE, + POST_VALUE, + CR + } +} diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/EntityTag.java tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/EntityTag.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/EntityTag.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/EntityTag.java 2026-05-01 18:56:05.000000000 +0000 @@ -51,7 +51,7 @@ while (true) { boolean strong = false; - HttpParser.skipLws(input); + HttpParser.skipWhitespace(input); switch (HttpParser.skipConstant(input, "W/")) { case EOF: @@ -79,7 +79,7 @@ } } - HttpParser.skipLws(input); + HttpParser.skipWhitespace(input); switch (HttpParser.skipConstant(input, ",")) { case EOF: diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/HttpParser.java tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/HttpParser.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/HttpParser.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/HttpParser.java 2026-05-01 18:56:05.000000000 +0000 @@ -33,7 +33,7 @@ private static final StringManager sm = StringManager.getManager(HttpParser.class); - private static final int ARRAY_SIZE = 128; + private static final int ARRAY_SIZE = 256; private static final boolean[] IS_CONTROL = new boolean[ARRAY_SIZE]; private static final boolean[] IS_SEPARATOR = new boolean[ARRAY_SIZE]; @@ -47,6 +47,8 @@ private static final boolean[] IS_SUBDELIM = new boolean[ARRAY_SIZE]; private static final boolean[] IS_USERINFO = new boolean[ARRAY_SIZE]; private static final boolean[] IS_RELAXABLE = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_FIELD_VCHAR = new boolean[ARRAY_SIZE]; + private static final boolean[] IS_FIELD_CONTENT = new boolean[ARRAY_SIZE]; private static final HttpParser DEFAULT; @@ -66,7 +68,7 @@ } // Token: Anything 0-127 that is not a control and not a separator - if (!IS_CONTROL[i] && !IS_SEPARATOR[i]) { + if (!IS_CONTROL[i] && !IS_SEPARATOR[i] && i < 128) { IS_TOKEN[i] = true; } @@ -75,7 +77,6 @@ IS_HEX[i] = true; } - // Not valid for HTTP protocol // "HTTP/" DIGIT "." DIGIT if (i == 'H' || i == 'T' || i == 'P' || i == '/' || i == '.' || (i >= '0' && i <= '9')) { IS_HTTP_PROTOCOL[i] = true; @@ -114,6 +115,16 @@ i == '{' || i == '|' || i == '}') { IS_RELAXABLE[i] = true; } + + // field-vchar is VCHAR / obs-text + if (i > 32 && i < 127 || i > 127) { + IS_FIELD_VCHAR[i] = true; + } + + // field-content = field-vchar [ 1*( SP / HTAB / field-vchar ) field-vchar ] + if (IS_FIELD_VCHAR[i] || i == '\t' || i == ' ') { + IS_FIELD_CONTENT[i] = true; + } } DEFAULT = new HttpParser(null, null); @@ -378,6 +389,10 @@ } + public static boolean isWhiteSpace(int c) { + return c == 9 || c == 32; + } + public static boolean isAbsolutePath(int c) { return DEFAULT.isAbsolutePathRelaxed(c); } @@ -389,8 +404,7 @@ public static boolean isControl(int c) { - // Fast for valid control characters, slower for some incorrect - // ones + // Fast for valid control characters, slower for some incorrect ones try { return IS_CONTROL[c]; } catch (ArrayIndexOutOfBoundsException ex) { @@ -399,15 +413,36 @@ } - // Skip any LWS and position to read the next character. The next character - // is returned as being able to 'peek()' it allows a small optimisation in - // some cases. - static int skipLws(Reader input) throws IOException { + public static boolean isFieldVChar(int c) { + // Fast for valid field-vchar characters, slower for some incorrect ones + try { + return IS_FIELD_VCHAR[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + public static boolean isFieldContent(int c) { + // Fast for valid field-content characters, slower for some incorrect ones + try { + return IS_FIELD_CONTENT[c]; + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + } + + + /* + * Skip any whitespace and position to read the next character. The next character is returned as being able to + * 'peek()' it allows a small optimisation in some cases. + */ + static int skipWhitespace(Reader input) throws IOException { input.mark(1); int c = input.read(); - while (c == 32 || c == 9 || c == 10 || c == 13) { + while (c == 32 || c == 9) { input.mark(1); c = input.read(); } @@ -419,7 +454,7 @@ static SkipResult skipConstant(Reader input, String constant) throws IOException { int len = constant.length(); - skipLws(input); + skipWhitespace(input); input.mark(len); int c = input.read(); @@ -445,7 +480,7 @@ static String readToken(Reader input) throws IOException { StringBuilder result = new StringBuilder(); - skipLws(input); + skipWhitespace(input); input.mark(1); int c = input.read(); @@ -472,7 +507,7 @@ static String readDigits(Reader input) throws IOException { StringBuilder result = new StringBuilder(); - skipLws(input); + skipWhitespace(input); input.mark(1); int c = input.read(); @@ -491,6 +526,19 @@ /** * @return the number if digits were found, -1 if no data was found or if data other than digits was found */ + static long readInteger(Reader input) throws IOException { + String digits = readDigits(input); + + if (digits.isEmpty()) { + return -1; + } + + return Integer.parseInt(digits); + } + + /** + * @return the number if digits were found, -1 if no data was found or if data other than digits was found + */ static long readLong(Reader input) throws IOException { String digits = readDigits(input); @@ -507,7 +555,7 @@ */ static String readQuotedString(Reader input, boolean returnQuoted) throws IOException { - skipLws(input); + skipWhitespace(input); int c = input.read(); if (c != '"') { @@ -544,7 +592,7 @@ static String readTokenOrQuotedString(Reader input, boolean returnQuoted) throws IOException { // Peek at next character to enable correct method to be called - int c = skipLws(input); + int c = skipWhitespace(input); if (c == '"') { return readQuotedString(input, returnQuoted); @@ -567,7 +615,7 @@ StringBuilder result = new StringBuilder(); boolean quoted = false; - skipLws(input); + skipWhitespace(input); input.mark(1); int c = input.read(); @@ -620,7 +668,7 @@ StringBuilder result = new StringBuilder(); boolean quoted = false; - skipLws(input); + skipWhitespace(input); input.mark(1); int c = input.read(); @@ -664,7 +712,7 @@ } static double readWeight(Reader input, char delimiter) throws IOException { - skipLws(input); + skipWhitespace(input); int c = input.read(); if (c == -1 || c == delimiter) { // No q value just whitespace @@ -675,7 +723,7 @@ return 0; } // RFC 7231 does not allow whitespace here but be tolerant - skipLws(input); + skipWhitespace(input); c = input.read(); if (c != '=') { // Malformed. Use quality of zero so it is dropped. @@ -684,7 +732,7 @@ } // RFC 7231 does not allow whitespace here but be tolerant - skipLws(input); + skipWhitespace(input); c = input.read(); // Should be no more than 3 decimal places @@ -717,7 +765,7 @@ } if (c == 9 || c == 32) { - skipLws(input); + skipWhitespace(input); c = input.read(); } @@ -943,7 +991,7 @@ static int validatePort(Reader reader, int colonPosition) throws IOException { // Remaining characters should be numeric ... - readLong(reader); + readInteger(reader); // ... followed by EOS if (reader.read() == -1) { return colonPosition; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/LocalStrings.properties tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/LocalStrings.properties --- tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/LocalStrings.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/LocalStrings.properties 2026-05-01 18:56:05.000000000 +0000 @@ -16,6 +16,8 @@ # Do not edit this file directly. # To edit translations see: https://tomcat.apache.org/getinvolved.html#Translations +chunkExtension.invalid=Invalid chunk extension data found + cookie.fallToDebug=\n\ \ Note: further occurrences of this error will be logged at DEBUG level. cookie.invalidCookieValue=A cookie header was received [{0}] that contained an invalid cookie. That cookie will be ignored. diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties --- tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties 2026-05-01 18:56:05.000000000 +0000 @@ -16,6 +16,8 @@ # Do not edit this file directly. # To edit translations see: https://tomcat.apache.org/getinvolved.html#Translations +chunkExtension.invalid=Des données d'extension de chunk invalides ont été rencontrées + cookie.fallToDebug=\n\ \ Note : les occurrences suivantes de cette erreur seront enregistrées au niveau DEBUG. cookie.invalidCookieValue=Un en-tête de cookie a été reçu [{0}] qui contenait un cookie invalide, celui ci sera ignoré diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties --- tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties 2026-05-01 18:56:05.000000000 +0000 @@ -16,6 +16,8 @@ # Do not edit this file directly. # To edit translations see: https://tomcat.apache.org/getinvolved.html#Translations +chunkExtension.invalid=無効なチャンク拡張データが見つかりました + cookie.fallToDebug=\n\ \ 注: 以降のこのエラーの発生はDEBUGレベルでログに出力されます。 cookie.invalidCookieValue=無効なCookieを含むCookieヘッダーが受信されました [{0}]。 そのCookieは無視されます。 diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/Upgrade.java tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/Upgrade.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/http/parser/Upgrade.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/http/parser/Upgrade.java 2026-05-01 18:56:05.000000000 +0000 @@ -69,8 +69,8 @@ Reader r = new StringReader(headerValue); SkipResult skipComma; do { - // Skip any leading LWS - HttpParser.skipLws(r); + // Skip any leading whitespace + HttpParser.skipWhitespace(r); String protocolName = HttpParser.readToken(r); if (protocolName == null || protocolName.isEmpty()) { // Invalid @@ -84,7 +84,7 @@ return null; } } - HttpParser.skipLws(r); + HttpParser.skipWhitespace(r); skipComma = HttpParser.skipConstant(r, ","); if (skipComma == SkipResult.NOT_FOUND) { diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/AbstractEndpoint.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/AbstractEndpoint.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/AbstractEndpoint.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/AbstractEndpoint.java 2026-05-01 18:56:05.000000000 +0000 @@ -68,6 +68,8 @@ import org.apache.tomcat.util.threads.VirtualThreadExecutor; /** + * Abstract endpoint implementation. + * * @param The type used by the socket wrapper associated with this endpoint. Might be the same as U. * @param The type of the underlying socket used by this endpoint. Might be the same as S. */ @@ -407,6 +409,12 @@ */ protected void createSSLContext(SSLHostConfig sslHostConfig) throws IllegalArgumentException { + // Initialize group list + LinkedHashSet groupList = sslHostConfig.getGroupList(); + if (groupList != null && getLog().isDebugEnabled()) { + getLog().debug(sm.getString("endpoint.tls.enabledGroups", groupList)); + } + boolean firstCertificate = true; for (SSLHostConfigCertificate certificate : sslHostConfig.getCertificates(true)) { SSLUtil sslUtil = sslImplementation.getSSLUtil(certificate); @@ -737,7 +745,8 @@ public boolean checkSni(String sniHostName, String protocolHostName) { return (!strictSni || !isSSLEnabled() || (sniHostName != null && sniHostName.equalsIgnoreCase(protocolHostName)) - || getSSLHostConfig(sniHostName) == getSSLHostConfig(protocolHostName)); + || getSSLHostConfig(sniHostName) == getSSLHostConfig( + protocolHostName != null ? protocolHostName.toLowerCase(Locale.ENGLISH) : null)); } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/LocalStrings.properties tomcat11-11.0.22/java/org/apache/tomcat/util/net/LocalStrings.properties --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/LocalStrings.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/LocalStrings.properties 2026-05-01 18:56:05.000000000 +0000 @@ -29,6 +29,7 @@ channel.nio.ssl.expandNetOutBuffer=Expanding network output buffer to [{0}] bytes channel.nio.ssl.foundHttp=Found an plain text HTTP request on what should be an encrypted TLS connection channel.nio.ssl.handshakeError=Handshake error +channel.nio.ssl.handshakeUnwrapBufferUnderflow=BUFFER_UNDERFLOW during handshake unwrap, more data needed from the network channel.nio.ssl.handshakeWrapPending=There is already handshake data waiting to be wrapped channel.nio.ssl.handshakeWrapQueueTooLong=The queue of handshake data to be wrapped has grown too long channel.nio.ssl.incompleteHandshake=Handshake incomplete, you must complete handshake before reading data. @@ -123,6 +124,7 @@ endpoint.timeout.err=Error processing socket timeout endpoint.tls.cert.encodingError=Certificate fingerprints not available endpoint.tls.cert.noCerts=Certificate details not available as the certificate chain returned from the SSLContext was empty +endpoint.tls.enabledGroups=Configured named groups {0} for the TLS connector endpoint.tls.info=Connector [{0}], TLS virtual host [{1}], certificate type [{2}] configured from {3} with trust store [{4}] endpoint.tls.info.cert.keystore=keystore [{0}] using alias [{1}] endpoint.tls.info.cert.keystore.direct=Set directly - location unknown @@ -154,11 +156,15 @@ sslHostConfig.certificate.notype=Multiple certificates were specified and at least one is missing the required attribute type sslHostConfig.certificateVerificationInvalid=The certificate verification value [{0}] is not recognised sslHostConfig.fileNotFound=Configured file [{0}] does not exist +sslHostConfig.handleTls13CiphersuiteInCiphers=The TLS 1.3 cipher suite [{0}] included in the TLS 1.2 and below ciphers list will be removed from the TLS 1.2 ciphers list and added to the end of the TLS 1.3 cipher suite list +sslHostConfig.ignoreNonTls13Ciphersuite=The non-TLS 1.3 cipher suite [{0}] included in the TLS 1.3 cipher suite list will be ignored sslHostConfig.invalid_truststore_password=The provided trust store password could not be used to unlock and/or validate the trust store. Retrying to access the trust store with a null password which will skip validation. sslHostConfig.mismatch=The property [{0}] was set on the SSLHostConfig named [{1}] and is for the [{2}] configuration syntax but the SSLHostConfig is being used with the [{3}] configuration syntax +sslHostConfig.mismatch.trust=The trust configuration property [{0}] was set on the SSLHostConfig named [{1}] and is for the [{2}] configuration syntax but the SSLHostConfig is being used with the [{3}] trust configuration syntax sslHostConfig.opensslconf.alreadyset=Attempt to set another OpenSSLConf ignored sslHostConfig.opensslconf.null=Attempt to set null OpenSSLConf ignored sslHostConfig.prefix_missing=The protocol [{0}] was added to the list of protocols on the SSLHostConfig named [{1}]. Check if a +/- prefix is missing. +sslHostConfig.unknownGroup=Unknown TLS group [{0}] was specified and will not be enabled sslHostConfigCertificate.mismatch=The property [{0}] was set on the SSLHostConfigCertificate named [{1}] and is for certificate storage type [{2}] but the certificate is being used with a storage of type [{3}] diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/LocalStrings_fr.properties tomcat11-11.0.22/java/org/apache/tomcat/util/net/LocalStrings_fr.properties --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/LocalStrings_fr.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/LocalStrings_fr.properties 2026-05-01 18:56:05.000000000 +0000 @@ -123,8 +123,10 @@ endpoint.timeout.err=Erreur en traitant le dépassement de temps d'attente du socket endpoint.tls.cert.encodingError=Les empreintes du certificat ne sont pas disponibles endpoint.tls.cert.noCerts=Les détails du certificat ne sont pas disponibles car la chaîne de certificats retournée par le SSLContext est vide +endpoint.tls.enabledGroups=Les groupes {0} ont été configurés pour le connecteur TLS endpoint.tls.info=Connecteur [{0}], hôte virtuel TLS [{1}], type de certificat [{2}] configuré depuis {3} et la trust store [{4}] endpoint.tls.info.cert.keystore=[{0}] avec l''alias [{1}] +endpoint.tls.info.cert.keystore.direct=Fixé directement - la localisation est inconnue endpoint.tls.info.cert.pem=clé [{0}], certificat [{1}] et chaîne de certificats [{2}] endpoint.unknownSslHostName=Le nom d''hôte SSL [{0}] n''est pas reconnu pour cette terminaison endpoint.warn.executorShutdown=L''exécuteur associé au pool de threads [{0}] n''est pas complètement arrêté, certains threads d''application peuvent toujours être en cours d''exécution @@ -153,11 +155,14 @@ sslHostConfig.certificate.notype=Plusieurs certificats ont été spécifiés et au moins un n'a pas d'attribut type sslHostConfig.certificateVerificationInvalid=La valeur de vérification de certificat [{0}] n''est pas reconnue sslHostConfig.fileNotFound=Le fichier [{0}] configuré n''existe pas. +sslHostConfig.ignoreNonTls13Ciphersuite=Le suite de chiffres non TLS 1.3 [{0}] incluse dans la suite de chiffres TLS 1.3 sera ignorée sslHostConfig.invalid_truststore_password=Le mot de passe de la base de confiance n'a pas pu être utilisé pour déverrouiller et ou valider celle ci, nouvel essai en utilisant un mot de passe null pour passer la validation sslHostConfig.mismatch=La propriété [{0}] a été fixée sur le SSLHostConfig nommé [{1}] et est pour la syntaxe de configuration [{2}] mais le SSLHostConfig est utilisé avec la syntaxe de configuration [{3}] +sslHostConfig.mismatch.trust=La propriété [{0}] de configuration des relations de confiance (trust) a été fixé sur le SSLHostConfig nommé [{1}] est est pour la syntaxe de configuration [{2}] mais le SSLHostConfig est utilisé avec la syntaxe [{3}] de configuration des relations de confiance sslHostConfig.opensslconf.alreadyset=La tentative de définition d'un autre OpenSSLConf est ignorée sslHostConfig.opensslconf.null=L'OpenSSLConf nul a été ignoré sslHostConfig.prefix_missing=Le protocole [{0}] a été ajouté à la liste des protocoles du SSLHostConfig nommé [{1}], vérifier qu''un préfixe +/- ne manque pas +sslHostConfig.unknownGroup=Le groupe TLS [{0}] inconnu a été spécifié et ne sera pas activé sslHostConfigCertificate.mismatch=La propriété [{0}] a été définie sur le SSLHostConfigCertificate nommé [{1}] et est pour un certificat de stockage de type [{2}] mais le certificat est utilisé avec un stockage de type [{3}] diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/LocalStrings_ja.properties tomcat11-11.0.22/java/org/apache/tomcat/util/net/LocalStrings_ja.properties --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/LocalStrings_ja.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/LocalStrings_ja.properties 2026-05-01 18:56:05.000000000 +0000 @@ -123,8 +123,10 @@ endpoint.timeout.err=ソケットタイムアウト処理中のエラー endpoint.tls.cert.encodingError=証明書のフィンガープリントが利用できません endpoint.tls.cert.noCerts=SSLContext から返された証明書チェーンが空だったため、証明書の詳細を利用できません +endpoint.tls.enabledGroups=TLS connector 用に名前付きグループ {0} を設定しました endpoint.tls.info=トラストストア [{4}] を使用して {3} から構成されたコネクタ [{0}]、TLS 仮想ホスト [{1}]、および証明書タイプ [{2}] endpoint.tls.info.cert.keystore=エイリアス [{1}] を使用したキーストア [{0}] +endpoint.tls.info.cert.keystore.direct=直接設定 - 場所が不明 endpoint.tls.info.cert.pem=キー [{0}]、証明書 [{1}]、および証明書チェーン [{2}] endpoint.unknownSslHostName=SSL ホスト名 [{0}] はこのエンドポイントから認識されていません。 endpoint.warn.executorShutdown=スレッドプール [{0}] と関連付けられたエグゼキュータは完全に停止できませんでした。いくつかのアプリケーションスレッドはまだ動作し続けている可能性があります。 @@ -153,11 +155,14 @@ sslHostConfig.certificate.notype=指定された複数の証明書の中に、少なくとも1つは必須要素の存在しない証明書が含まれています。 sslHostConfig.certificateVerificationInvalid=証明書検証値[{0}]が認識されません sslHostConfig.fileNotFound=構成ファイル[{0}]は存在しません +sslHostConfig.ignoreNonTls13Ciphersuite=TLS 1.3暗号スイートリストに含まれる非TLS 1.3暗号スイート [{0}] は無視されます sslHostConfig.invalid_truststore_password=提供されたトラストストアパスワードは、トラストストアのロック解除および検証に使用できませんでした。 検証をスキップするnullパスワードでトラストストアにアクセスしようとしました。 sslHostConfig.mismatch=[{0}] プロパティは [{1}] という名前のSSLHostConfigで設定され、[{2}] 構成構文用ですが、[{3}] 構成構文でSSLHostConfigが使用されています +sslHostConfig.mismatch.trust=プロパティ [{0}] は [{1}] という名前の SSLHostConfig に設定されており、[{2}] 設定用ですが、SSLHostConfig は [{3}] の設定で使用されています sslHostConfig.opensslconf.alreadyset=別のOpenSSLConfを設定しようとすると無視されます sslHostConfig.opensslconf.null=Null OpenSSLConfを設定しようとしましたが無視されました sslHostConfig.prefix_missing=[{1}]というSSLHostConfigのプロトコルのリストにプロトコル[{0}]が追加されました。+/-接頭辞がないか確認してください。 +sslHostConfig.unknownGroup=不明なTLSグループ [{0}] が指定されており、有効になりません sslHostConfigCertificate.mismatch=プロパティ [{0}] は [{1}] という名前のSSLHostConfigCertificateに設定されており、証明書の格納タイプ [{2}] 用ですが、証明書は [{3}] タイプのストレージで使用されています。 diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/Nio2Endpoint.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/Nio2Endpoint.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/Nio2Endpoint.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/Nio2Endpoint.java 2026-05-01 18:56:05.000000000 +0000 @@ -975,6 +975,7 @@ getSocket().close(true); } if (getEndpoint().running) { + getSocket().reset(null, null); if (nioChannels == null || !nioChannels.push(getSocket())) { getSocket().free(); } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/NioChannel.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/NioChannel.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/NioChannel.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/NioChannel.java 2026-05-01 18:56:05.000000000 +0000 @@ -54,7 +54,11 @@ * @throws IOException If a problem was encountered resetting the channel */ public void reset(SocketChannel channel, NioSocketWrapper socketWrapper) throws IOException { - this.sc = channel; + // Don't reset socket on null as it can lead to NPEs + if (channel != null) { + this.sc = channel; + } + // Resetting socketWrapper is possible this.socketWrapper = socketWrapper; bufHandler.reset(); } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/NioEndpoint.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/NioEndpoint.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/NioEndpoint.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/NioEndpoint.java 2026-05-01 18:56:05.000000000 +0000 @@ -1254,6 +1254,7 @@ getSocket().close(true); } if (getEndpoint().running) { + getSocket().reset(null, null); if (nioChannels == null || !nioChannels.push(getSocket())) { getSocket().free(); } @@ -1379,6 +1380,28 @@ } + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=69982 + * + * Similar to socketOrNetworkBufferHasDataLeft(), check the additional buffer for TLS. + */ + @Override + public boolean hasDataToWrite() { + return super.hasDataToWrite() || getSocket().getOutboundRemaining() > 0; + } + + + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=69982 + * + * Similar to socketOrNetworkBufferHasDataLeft(), check the additional buffer for TLS. + */ + @Override + public boolean canWrite() { + return super.canWrite() && getSocket().getOutboundRemaining() == 0; + } + + @Override protected void doWrite(boolean block, ByteBuffer buffer) throws IOException { int n; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/SSLHostConfig.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/SSLHostConfig.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/SSLHostConfig.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/SSLHostConfig.java 2026-05-01 18:56:05.000000000 +0000 @@ -59,7 +59,14 @@ // keys in Maps. protected static final String DEFAULT_SSL_HOST_NAME = "_default_"; protected static final Set SSL_PROTO_ALL_SET = new HashSet<>(); - public static final String DEFAULT_TLS_CIPHERS = "HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!kRSA"; + public static final String DEFAULT_TLS_CIPHERS_12 = "HIGH:!aNULL:!eNULL:!DES:!RC4:!MD5:!kRSA"; + public static final String DEFAULT_TLS_CIPHERS_13 = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256"; + /** + * Default cipher list for TLS 1.2 and below. + * @deprecated Replaced by {@link #DEFAULT_TLS_CIPHERS_12} + */ + @Deprecated + public static final String DEFAULT_TLS_CIPHERS = DEFAULT_TLS_CIPHERS_12; static { /* @@ -73,6 +80,7 @@ } private Type configType = null; + private Type trustConfigType = null; private String hostName = DEFAULT_SSL_HOST_NAME; @@ -103,10 +111,17 @@ private int certificateVerificationDepth = 10; // Used to track if certificateVerificationDepth has been explicitly set private boolean certificateVerificationDepthConfigured = false; - private String ciphers = DEFAULT_TLS_CIPHERS; + private String ciphers = DEFAULT_TLS_CIPHERS_12; + private String cipherSuites = DEFAULT_TLS_CIPHERS_13; + private String cipherSuitesFromCiphers = null; private LinkedHashSet cipherList = null; + private LinkedHashSet cipherSuiteList = null; private List jsseCipherNames = null; private boolean honorCipherOrder = false; + private boolean ocspEnabled = false; + private boolean ocspSoftFail = true; + private int ocspTimeout = 15000; + private int ocspVerifyFlags = 0; private final Set protocols = new HashSet<>(); // Values <0 mean use the implementation default private int sessionCacheSize = -1; @@ -122,7 +137,7 @@ private String truststoreProvider = System.getProperty("javax.net.ssl.trustStoreProvider"); private String truststoreType = System.getProperty("javax.net.ssl.trustStoreType"); private transient KeyStore truststore = null; - private String groups = null; + private String groups = System.getProperty("jdk.tls.namedGroups"); private LinkedHashSet groupList = null; // OpenSSL private String certificateRevocationListPath; @@ -181,7 +196,7 @@ * @param name the property name * @param configType the configuration type * - * @return true if the property belongs to the current configuration, and false otherwise + * @return true if the property belongs to the current configuration type, and false otherwise */ boolean setProperty(String name, Type configType) { if (this.configType == null) { @@ -196,6 +211,28 @@ } + /** + * Set property which belongs to the specified trust configuration type. + * + * @param name the property name + * @param trustConfigType the trust configuration type + * + * @return true if the property belongs to the current trust configuration type, and false otherwise + */ + boolean setTrustProperty(String name, Type trustConfigType) { + if (this.trustConfigType == null) { + this.trustConfigType = trustConfigType; + } else { + if (trustConfigType != this.trustConfigType) { + log.warn(sm.getString("sslHostConfig.mismatch.trust", name, getHostName(), trustConfigType, + this.trustConfigType)); + return false; + } + } + return true; + } + + // ----------------------------------------------------- Internal properties /** @@ -280,7 +317,6 @@ } else if (openSslConf != null) { throw new IllegalArgumentException(sm.getString("sslHostConfig.opensslconf.alreadySet")); } - setProperty("", Type.OPENSSL); openSslConf = conf; } @@ -354,39 +390,76 @@ /** - * Set the new cipher configuration. Note: Regardless of the format used to set the configuration, it is always - * stored in OpenSSL format. + * Set the new cipher (TLSv1.2 and below) configuration. Note: Regardless of the format used to set the + * configuration, it is always stored in OpenSSL format. * * @param ciphersList The new cipher configuration in OpenSSL or JSSE format */ public void setCiphers(String ciphersList) { // Ciphers is stored in OpenSSL format. Convert the provided value if // necessary. - if (ciphersList != null && !ciphersList.contains(":")) { - StringBuilder sb = new StringBuilder(); - // Not obviously in OpenSSL format. Might be a single OpenSSL or JSSE - // cipher name. Might be a comma separated list of cipher names - String[] ciphers = ciphersList.split(","); - for (String cipher : ciphers) { - String trimmed = cipher.trim(); - if (!trimmed.isEmpty()) { - String openSSLName = OpenSSLCipherConfigurationParser.jsseToOpenSSL(trimmed); - if (openSSLName == null) { - // Not a JSSE name. Maybe an OpenSSL name or alias - openSSLName = trimmed; + if (ciphersList != null) { + if (ciphersList.contains(":")) { + // OpenSSL format + StringBuilder sbCiphers = new StringBuilder(); + StringBuilder sbCipherSuitesFromCiphers = new StringBuilder(); + String[] components = ciphersList.split(":"); + // Remove any TLS 1.3 cipher suites + for (String component : components) { + String trimmed = component.trim(); + if (OpenSSLCipherConfigurationParser.isTls13Cipher(trimmed)) { + log.warn(sm.getString("sslHostConfig.handleTls13CiphersuiteInCiphers", trimmed)); + if (!sbCipherSuitesFromCiphers.isEmpty()) { + sbCipherSuitesFromCiphers.append(':'); + } + sbCipherSuitesFromCiphers.append(trimmed); + } else { + if (!sbCiphers.isEmpty()) { + sbCiphers.append(':'); + } + sbCiphers.append(trimmed); } - if (!sb.isEmpty()) { - sb.append(':'); + } + this.ciphers = sbCiphers.toString(); + this.cipherSuitesFromCiphers = sbCipherSuitesFromCiphers.toString(); + } else { + // Not obviously in OpenSSL format. Might be a single OpenSSL or JSSE + // cipher name. Might be a comma separated list of cipher names + StringBuilder sbCiphers = new StringBuilder(); + StringBuilder sbCipherSuitesFromCiphers = new StringBuilder(); + String[] ciphers = ciphersList.split(","); + for (String cipher : ciphers) { + String trimmed = cipher.trim(); + if (!trimmed.isEmpty()) { + if (OpenSSLCipherConfigurationParser.isTls13Cipher(trimmed)) { + log.warn(sm.getString("sslHostConfig.handleTls13CiphersuiteInCiphers", trimmed)); + if (!sbCipherSuitesFromCiphers.isEmpty()) { + sbCipherSuitesFromCiphers.append(':'); + } + sbCipherSuitesFromCiphers.append(trimmed); + } else { + String openSSLName = OpenSSLCipherConfigurationParser.jsseToOpenSSL(trimmed); + if (openSSLName == null) { + // Not a JSSE name. Maybe an OpenSSL name or alias + openSSLName = trimmed; + } + if (!sbCiphers.isEmpty()) { + sbCiphers.append(':'); + } + sbCiphers.append(openSSLName); + } } - sb.append(openSSLName); } + this.ciphers = sbCiphers.toString(); + this.cipherSuitesFromCiphers = sbCipherSuitesFromCiphers.toString(); } - this.ciphers = sb.toString(); } else { - this.ciphers = ciphersList; + this.ciphers = null; + this.cipherSuitesFromCiphers = null; } this.cipherList = null; this.jsseCipherNames = null; + this.cipherSuiteList = null; } @@ -408,18 +481,89 @@ /** * Obtain the list of JSSE cipher names for the current configuration. Ciphers included in the configuration but not - * supported by JSSE will be excluded from this list. + * supported by JSSE will be excluded from this list. TLS 1.3 ciphers will be first in the list. * * @return A list of the JSSE cipher names */ public List getJsseCipherNames() { if (jsseCipherNames == null) { - jsseCipherNames = OpenSSLCipherConfigurationParser.convertForJSSE(getCipherList()); + Set jsseCiphers = new LinkedHashSet<>(); + jsseCiphers.addAll(getCipherSuiteList()); + jsseCiphers.addAll(getCipherList()); + jsseCipherNames = OpenSSLCipherConfigurationParser.convertForJSSE(jsseCiphers); } return jsseCipherNames; } + /** + * Set the cipher suite (TLSv1.3) configuration. + * + * @param cipherSuites The cipher suites to use in a colon-separated, preference order list + */ + public void setCipherSuites(String cipherSuites) { + StringBuilder sb = new StringBuilder(); + String[] values; + if (cipherSuites.contains(":")) { + // OpenSSL format + values = cipherSuites.split(":"); + } else { + // JSSE format or possible a single cipher suite name + values = cipherSuites.split(","); + } + for (String value : values) { + String trimmed = value.trim(); + if (!trimmed.isEmpty()) { + if (!OpenSSLCipherConfigurationParser.isTls13Cipher(trimmed)) { + log.warn(sm.getString("sslHostConfig.ignoreNonTls13Ciphersuite", trimmed)); + continue; + } + /* + * OpenSSL and JSSE names for TLSv1.3 cipher suites are currently (January 2026) the same but handle the + * possible future case where they are not. + */ + String openSSLName = OpenSSLCipherConfigurationParser.jsseToOpenSSL(trimmed); + if (openSSLName == null) { + // Not a JSSE name. Maybe an OpenSSL name or alias + openSSLName = trimmed; + } + if (!sb.isEmpty()) { + sb.append(':'); + } + sb.append(trimmed); + } + } + this.cipherSuites = sb.toString(); + this.cipherSuiteList = null; + this.jsseCipherNames = null; + } + + + /** + * Obtain the current cipher suite (TLSv1.3) configuration. + * + * @return An OpenSSL cipher suite string for the current configuration. + */ + public String getCipherSuites() { + StringBuilder sb = new StringBuilder(cipherSuites); + if (cipherSuitesFromCiphers != null && !cipherSuitesFromCiphers.isEmpty()) { + if (!sb.isEmpty()) { + sb.append(':'); + } + sb.append(cipherSuitesFromCiphers); + } + return sb.toString(); + } + + + private LinkedHashSet getCipherSuiteList() { + if (cipherSuiteList == null) { + cipherSuiteList = OpenSSLCipherConfigurationParser.parse(getCipherSuites()); + } + return cipherSuiteList; + } + + public void setHonorCipherOrder(boolean honorCipherOrder) { this.honorCipherOrder = honorCipherOrder; } @@ -443,6 +587,46 @@ } + public boolean getOcspEnabled() { + return ocspEnabled; + } + + + public void setOcspEnabled(boolean ocspEnabled) { + this.ocspEnabled = ocspEnabled; + } + + + public boolean getOcspSoftFail() { + return ocspSoftFail; + } + + + public void setOcspSoftFail(boolean ocspSoftFail) { + this.ocspSoftFail = ocspSoftFail; + } + + + public int getOcspTimeout() { + return ocspTimeout; + } + + + public void setOcspTimeout(int ocspTimeout) { + this.ocspTimeout = ocspTimeout; + } + + + public int getOcspVerifyFlags() { + return ocspVerifyFlags; + } + + + public void setOcspVerifyFlags(int ocspVerifyFlags) { + this.ocspVerifyFlags = ocspVerifyFlags; + } + + public void setProtocols(String input) { protocols.clear(); explicitlyRequestedProtocols.clear(); @@ -538,19 +722,11 @@ /** * Set the enabled named groups. * - * @param groupsString the case sensitive comma separated list of groups + * @param groups the case sensitive comma separated list of groups */ - public void setGroups(String groupsString) { - if (groupsString != null) { - LinkedHashSet groupList = new LinkedHashSet<>(); - String[] groupNames = groupsString.split(","); - for (String groupName : groupNames) { - Group group = Group.valueOf(groupName.trim()); - groupList.add(group); - } - this.groups = groupsString; - this.groupList = groupList; - } + public void setGroups(String groups) { + this.groups = groups; + this.groupList = null; } @@ -558,6 +734,22 @@ * @return the groupList */ public LinkedHashSet getGroupList() { + if (groupList == null) { + String groups = this.groups; + if (groups != null) { + LinkedHashSet groupList = new LinkedHashSet<>(); + String[] groupNames = groups.split(","); + for (String groupName : groupNames) { + try { + Group group = Group.valueOf(groupName.trim()); + groupList.add(group); + } catch (IllegalArgumentException e) { + log.warn(sm.getString("sslHostConfig.unknownGroup", groupName)); + } + } + this.groupList = groupList; + } + } return this.groupList; } @@ -599,7 +791,7 @@ public void setTrustManagerClassName(String trustManagerClassName) { - setProperty("trustManagerClassName", Type.JSSE); + setTrustProperty("trustManagerClassName", Type.JSSE); this.trustManagerClassName = trustManagerClassName; } @@ -610,7 +802,7 @@ public void setTruststoreAlgorithm(String truststoreAlgorithm) { - setProperty("truststoreAlgorithm", Type.JSSE); + setTrustProperty("truststoreAlgorithm", Type.JSSE); this.truststoreAlgorithm = truststoreAlgorithm; } @@ -621,7 +813,7 @@ public void setTruststoreFile(String truststoreFile) { - setProperty("truststoreFile", Type.JSSE); + setTrustProperty("truststoreFile", Type.JSSE); this.truststoreFile = truststoreFile; } @@ -632,7 +824,7 @@ public void setTruststorePassword(String truststorePassword) { - setProperty("truststorePassword", Type.JSSE); + setTrustProperty("truststorePassword", Type.JSSE); this.truststorePassword = truststorePassword; } @@ -643,7 +835,7 @@ public void setTruststoreProvider(String truststoreProvider) { - setProperty("truststoreProvider", Type.JSSE); + setTrustProperty("truststoreProvider", Type.JSSE); this.truststoreProvider = truststoreProvider; } @@ -662,7 +854,7 @@ public void setTruststoreType(String truststoreType) { - setProperty("truststoreType", Type.JSSE); + setTrustProperty("truststoreType", Type.JSSE); this.truststoreType = truststoreType; } @@ -686,6 +878,7 @@ public void setTrustStore(KeyStore truststore) { + setTrustProperty("trustStore", Type.JSSE); this.truststore = truststore; } @@ -730,7 +923,7 @@ public void setCaCertificateFile(String caCertificateFile) { - if (setProperty("caCertificateFile", Type.OPENSSL)) { + if (setTrustProperty("caCertificateFile", Type.OPENSSL)) { // Reset default JSSE trust store if not a JSSE configuration if (truststoreFile != null) { truststoreFile = null; @@ -746,7 +939,7 @@ public void setCaCertificatePath(String caCertificatePath) { - if (setProperty("caCertificatePath", Type.OPENSSL)) { + if (setTrustProperty("caCertificatePath", Type.OPENSSL)) { // Reset default JSSE trust store if not a JSSE configuration if (truststoreFile != null) { truststoreFile = null; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/SSLSupport.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/SSLSupport.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/SSLSupport.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/SSLSupport.java 2026-05-01 18:56:05.000000000 +0000 @@ -24,11 +24,10 @@ */ public interface SSLSupport { /** - * The Request attribute key for the cipher suite. + * The Request attribute key for the protocol that created the SSL socket. e.g. TLSv1 or TLSv1.2 etc. */ String SECURE_PROTOCOL_KEY = "jakarta.servlet.request.secure_protocol"; - /** * The Request attribute key for the cipher suite. */ diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/SSLUtilBase.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/SSLUtilBase.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/SSLUtilBase.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/SSLUtilBase.java 2026-05-01 18:56:05.000000000 +0000 @@ -28,6 +28,7 @@ import java.security.cert.CRL; import java.security.cert.CRLException; import java.security.cert.CertPathParameters; +import java.security.cert.CertPathValidator; import java.security.cert.CertStore; import java.security.cert.CertStoreParameters; import java.security.cert.Certificate; @@ -37,12 +38,14 @@ import java.security.cert.CertificateNotYetValidException; import java.security.cert.CollectionCertStoreParameters; import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXRevocationChecker; import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.EnumSet; import java.util.Enumeration; import java.util.List; import java.util.Locale; @@ -128,7 +131,7 @@ // OpenSSL profiles cannot be resolved without Java 22 this.enabledCiphers = new String[0]; } else { - boolean warnOnSkip = !sslHostConfig.getCiphers().equals(SSLHostConfig.DEFAULT_TLS_CIPHERS); + boolean warnOnSkip = !sslHostConfig.getCiphers().equals(SSLHostConfig.DEFAULT_TLS_CIPHERS_12); List configuredCiphers = sslHostConfig.getJsseCipherNames(); Set implementedCiphers = getImplementedCiphers(); List enabledCiphers = @@ -520,8 +523,10 @@ * * @throws Exception An error occurred */ - protected CertPathParameters getParameters(String crlf, KeyStore trustStore, boolean revocationEnabled) - throws Exception { + protected CertPathParameters getParameters(final String crlf, final KeyStore trustStore, + final boolean revocationEnabled) throws Exception { + + boolean enableRevocation = revocationEnabled; PKIXBuilderParameters xparams = new PKIXBuilderParameters(trustStore, new X509CertSelector()); if (crlf != null && !crlf.isEmpty()) { @@ -529,11 +534,24 @@ CertStoreParameters csp = new CollectionCertStoreParameters(crls); CertStore store = CertStore.getInstance("Collection", csp); xparams.addCertStore(store); - xparams.setRevocationEnabled(true); - } else { - xparams.setRevocationEnabled(revocationEnabled); + enableRevocation = true; } + + if (sslHostConfig.getOcspEnabled()) { + PKIXRevocationChecker revocationChecker =(PKIXRevocationChecker) CertPathValidator.getInstance("PKIX").getRevocationChecker(); + if (sslHostConfig.getOcspSoftFail()) { + revocationChecker.setOptions(EnumSet.of(PKIXRevocationChecker.Option.SOFT_FAIL)); + } else { + revocationChecker.setOptions(Collections.emptySet()); + } + xparams.addCertPathChecker(revocationChecker); + enableRevocation = true; + } + + xparams.setRevocationEnabled(enableRevocation); + xparams.setMaxPathLength(sslHostConfig.getCertificateVerificationDepth()); + return xparams; } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/SecureNio2Channel.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/SecureNio2Channel.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/SecureNio2Channel.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/SecureNio2Channel.java 2026-05-01 18:56:05.000000000 +0000 @@ -144,13 +144,15 @@ public void reset(AsynchronousSocketChannel channel, SocketWrapperBase socket) throws IOException { super.reset(channel, socket); sslEngine = null; - sniComplete = false; - handshakeComplete = false; - handshakeWrapQueueLength.set(0); - unwrapBeforeRead = true; - closed = false; - closing = false; - netInBuffer.clear(); + if (channel != null) { + sniComplete = false; + handshakeComplete = false; + handshakeWrapQueueLength.set(0); + unwrapBeforeRead = true; + closed = false; + closing = false; + netInBuffer.clear(); + } } @Override @@ -588,6 +590,9 @@ // call unwrap getBufHandler().configureReadBufferForWrite(); result = sslEngine.unwrap(netInBuffer, getBufHandler().getReadBuffer()); + if (log.isDebugEnabled() && result.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW) { + log.debug(sm.getString("channel.nio.ssl.handshakeUnwrapBufferUnderflow")); + } /* * ByteBuffer.compact() is an optional method but netInBuffer is created from either ByteBuffer.allocate() * or ByteBuffer.allocateDirect() and the ByteBuffers returned by those methods do implement compact(). The diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/SecureNioChannel.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/SecureNioChannel.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/SecureNioChannel.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/SecureNioChannel.java 2026-05-01 18:56:05.000000000 +0000 @@ -94,11 +94,13 @@ public void reset(SocketChannel channel, NioSocketWrapper socketWrapper) throws IOException { super.reset(channel, socketWrapper); sslEngine = null; - sniComplete = false; - handshakeComplete = false; - closed = false; - closing = false; - netInBuffer.clear(); + if (channel != null) { + sniComplete = false; + handshakeComplete = false; + closed = false; + closing = false; + netInBuffer.clear(); + } } @Override @@ -490,6 +492,9 @@ // call unwrap getBufHandler().configureReadBufferForWrite(); result = sslEngine.unwrap(netInBuffer, getBufHandler().getReadBuffer()); + if (log.isDebugEnabled() && result.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW) { + log.debug(sm.getString("channel.nio.ssl.handshakeUnwrapBufferUnderflow")); + } /* * ByteBuffer.compact() is an optional method but netInBuffer is created from either ByteBuffer.allocate() * or ByteBuffer.allocateDirect() and the ByteBuffers returned by those methods do implement compact(). The @@ -606,7 +611,8 @@ @Override public int read(ByteBuffer dst) throws IOException { // are we in the middle of closing or closed? - if (closing || closed) { + SSLEngine sslEngine = this.sslEngine; + if (closing || closed || sslEngine == null) { return -1; } // did we finish our handshake? @@ -693,7 +699,8 @@ @Override public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { // are we in the middle of closing or closed? - if (closing || closed) { + SSLEngine sslEngine = this.sslEngine; + if (closing || closed || sslEngine == null) { return -1; } // did we finish our handshake? @@ -824,7 +831,8 @@ return sc.write(src); } else { // Are we closing or closed? - if (closing || closed) { + SSLEngine sslEngine = this.sslEngine; + if (closing || closed || sslEngine == null) { throw new IOException(sm.getString("channel.nio.ssl.closing")); } @@ -867,7 +875,8 @@ public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { checkInterruptStatus(); // Are we closing or closed? - if (closing || closed) { + SSLEngine sslEngine = this.sslEngine; + if (closing || closed || sslEngine == null) { throw new IOException(sm.getString("channel.nio.ssl.closing")); } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/SocketWrapperBase.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/SocketWrapperBase.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/SocketWrapperBase.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/SocketWrapperBase.java 2026-05-01 18:56:05.000000000 +0000 @@ -923,6 +923,8 @@ /** * Internal state tracker for vectored operations. + * + * @param The attachment type */ protected abstract class OperationState implements Runnable { protected final boolean read; @@ -1013,6 +1015,8 @@ /** * Completion handler for vectored operations. This will check the completion of the operation, then either continue * or call the user provided completion handler. + * + * @param The attachment type */ protected class VectoredIOCompletionHandler implements CompletionHandler> { @Override diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java 2026-05-01 18:56:05.000000000 +0000 @@ -142,6 +142,11 @@ } else { implementedCiphers = new HashSet<>(Arrays.asList(implementedCipherSuiteArray)); } + + if (sslHostConfig.getOpenSslConf() != null) { + log.warn(sm.getString("jsseUtil.opensslconf.present")); + } + initialized = true; } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties 2026-05-01 18:56:05.000000000 +0000 @@ -21,6 +21,7 @@ jsseUtil.excludeProtocol=The SSL protocol [{0}] which is supported in this JRE was excluded from the protocols available to Tomcat jsseUtil.noDefaultProtocols=Unable to determine a default for sslEnabledProtocols. Set an explicit value to ensure the connector can start. +jsseUtil.opensslconf.present=A connector is configured to use a JSSE TLS implementation with OpenSSL specific OpenSSLConf configuration elements. The OpenSSLConf configuration elements will be ignored. pemFile.noMultiPrimes=The PKCS#1 certificate is in multi-prime format and Java does not provide an API for constructing an RSA private key object from that format pemFile.noPassword=A password is required to decrypt the private key diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties 2026-05-01 18:56:05.000000000 +0000 @@ -21,6 +21,7 @@ jsseUtil.excludeProtocol=Le protocole SSL [{0}] qui est supporté par ce JRE a été exclu des protocoles disponibles dans Tomcat jsseUtil.noDefaultProtocols=Impossible de déterminer un défaut pour sslEnabledProtocols de [{0}], indiquez une valeur explicite pour permettre le démarrage du connecteur +jsseUtil.opensslconf.present=Un connecteur est configuré pour utiliser une implémentation TLS JSSE avec des éléments de configuration OpenSSLConf spécifiques à OpenSSL, qui seront ignorés pemFile.noMultiPrimes=Le certificat PKCS#1 est dans un format mutli-prime et Java ne fournit pas d'API pour construire une clé privée RSA à partir de ce format pemFile.noPassword=Un mot de passe est requis pour déchiffrer la clé privée diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties 2026-05-01 18:56:05.000000000 +0000 @@ -21,6 +21,7 @@ jsseUtil.excludeProtocol=JRE は SSL プロトコル [{0}] に対応しています。しかし Tomcat の利用可能プロトコルからは除外されています。 jsseUtil.noDefaultProtocols=sslEnableProtocols の既定値を取得できません。コネクターを開始できるよう明示的に値を設定してください。 +jsseUtil.opensslconf.present=ConnectorがOpenSSL固有のOpenSSLConf要素を含むJSSE TLS実装を使用するように設定されています。OpenSSLConf要素は無視されます。 pemFile.noMultiPrimes=PKCS#1 証明書は multi-prime RSA フォーマットですが、Java はそのようなフォーマットに対する RSA 秘密鍵を構築する API を提供していません pemFile.noPassword=秘密鍵を復号するにはパスワードが必要です diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/OpenSSLConfCmd.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLConfCmd.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/OpenSSLConfCmd.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLConfCmd.java 2026-05-01 18:56:05.000000000 +0000 @@ -21,12 +21,26 @@ public class OpenSSLConfCmd implements Serializable { + // Tomcat / Tomcat Native custom commands. Used internally by Tomcat. Not intended for direct use by users. + public static final String NO_OCSP_CHECK = "NO_OCSP_CHECK"; + public static final String OCSP_SOFT_FAIL = "OCSP_SOFT_FAIL"; + public static final String OCSP_TIMEOUT = "OCSP_TIMEOUT"; + public static final String OCSP_VERIFY_FLAGS = "OCSP_VERIFY_FLAGS"; + @Serial private static final long serialVersionUID = 1L; private String name = null; private String value = null; + public OpenSSLConfCmd() { + } + + public OpenSSLConfCmd(String name, String value) { + this.name = name; + this.value = value; + } + public String getName() { return name; } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java 2026-05-01 18:56:05.000000000 +0000 @@ -102,8 +102,18 @@ boolean success = false; try { // Create OpenSSLConfCmd context if used - OpenSSLConf openSslConf = sslHostConfig.getOpenSslConf(); - if (openSslConf != null) { + if (sslHostConfig.getOpenSslConf() == null && sslHostConfig.getTrustManagerClassName() == null && + sslHostConfig.getTruststore() == null) { + /* + * If an instance of OpenSSLConf is required, it must be created here so the reference can be placed in + * the (immutable) OpenSSLState record. + * + * If OpenSSL managed trust is used, an instance of OpenSSLConf is required to pass OCSP configuration + * parameters to Tomcat Native. Create one if one hasn't already been created. + */ + sslHostConfig.setOpenSslConf(new OpenSSLConf()); + } + if (sslHostConfig.getOpenSslConf() != null) { try { if (log.isTraceEnabled()) { log.trace(sm.getString("openssl.makeConf")); @@ -121,8 +131,6 @@ for (String protocol : sslHostConfig.getEnabledProtocols()) { if (Constants.SSL_PROTO_SSLv2Hello.equalsIgnoreCase(protocol)) { // NO-OP. OpenSSL always supports SSLv2Hello - } else if (Constants.SSL_PROTO_SSLv2.equalsIgnoreCase(protocol)) { - value |= SSL.SSL_PROTOCOL_SSLV2; } else if (Constants.SSL_PROTO_SSLv3.equalsIgnoreCase(protocol)) { value |= SSL.SSL_PROTOCOL_SSLV3; } else if (Constants.SSL_PROTO_TLSv1.equalsIgnoreCase(protocol)) { @@ -316,8 +324,9 @@ SSLContext.clearOptions(state.ctx, SSL.SSL_OP_NO_TICKET); } - // List the ciphers that the client is permitted to negotiate + // Configure the ciphers that the client is permitted to negotiate SSLContext.setCipherSuite(state.ctx, sslHostConfig.getCiphers()); + SSLContext.setCipherSuitesEx(state.ctx, sslHostConfig.getCipherSuites()); // If there is no certificate file must be using a KeyStore so a KeyManager is required. // If there is a certificate file a KeyManager is helpful but not strictly necessary. @@ -354,6 +363,14 @@ SSLContext.setCACertificate(state.ctx, SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificateFile()), SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificatePath())); + sslHostConfig.getOpenSslConf().addCmd(new OpenSSLConfCmd(OpenSSLConfCmd.NO_OCSP_CHECK, + Boolean.toString(!sslHostConfig.getOcspEnabled()))); + sslHostConfig.getOpenSslConf().addCmd(new OpenSSLConfCmd(OpenSSLConfCmd.OCSP_SOFT_FAIL, + Boolean.toString(sslHostConfig.getOcspSoftFail()))); + sslHostConfig.getOpenSslConf().addCmd(new OpenSSLConfCmd(OpenSSLConfCmd.OCSP_TIMEOUT, + Integer.toString(sslHostConfig.getOcspTimeout()))); + sslHostConfig.getOpenSslConf().addCmd(new OpenSSLConfCmd(OpenSSLConfCmd.OCSP_VERIFY_FLAGS, + Integer.toString(sslHostConfig.getOcspVerifyFlags()))); } if (negotiableProtocols != null && !negotiableProtocols.isEmpty()) { @@ -404,9 +421,6 @@ if ((opts & SSL.SSL_OP_NO_TLSv1_2) == 0) { enabled.add(Constants.SSL_PROTO_TLSv1_2); } - if ((opts & SSL.SSL_OP_NO_SSLv2) == 0) { - enabled.add(Constants.SSL_PROTO_SSLv2); - } if ((opts & SSL.SSL_OP_NO_SSLv3) == 0) { enabled.add(Constants.SSL_PROTO_SSLv3); } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java 2026-05-01 18:56:05.000000000 +0000 @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.locks.Lock; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; @@ -42,6 +43,7 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.jni.AprStatus; import org.apache.tomcat.jni.Buffer; import org.apache.tomcat.jni.Pool; import org.apache.tomcat.jni.SSL; @@ -99,7 +101,6 @@ HashSet protocols = new HashSet<>(); protocols.add(Constants.SSL_PROTO_SSLv2Hello); - protocols.add(Constants.SSL_PROTO_SSLv2); protocols.add(Constants.SSL_PROTO_SSLv3); protocols.add(Constants.SSL_PROTO_TLSv1); protocols.add(Constants.SSL_PROTO_TLSv1_1); @@ -222,9 +223,9 @@ public synchronized void shutdown() { if (!destroyed) { destroyed = true; - cleanable.clean(); // internal errors can cause shutdown without marking the engine closed isInboundDone = isOutboundDone = engineClosed = true; + cleanable.clean(); ByteBufferUtils.cleanDirectBuffer(buf); } } @@ -811,9 +812,6 @@ if ((opts & SSL.SSL_OP_NO_TLSv1_2) == 0) { enabled.add(Constants.SSL_PROTO_TLSv1_2); } - if ((opts & SSL.SSL_OP_NO_SSLv2) == 0) { - enabled.add(Constants.SSL_PROTO_SSLv2); - } if ((opts & SSL.SSL_OP_NO_SSLv3) == 0) { enabled.add(Constants.SSL_PROTO_SSLv3); } @@ -832,7 +830,6 @@ if (destroyed) { return; } - boolean sslv2 = false; boolean sslv3 = false; boolean tlsv1 = false; boolean tlsv1_1 = false; @@ -842,7 +839,6 @@ throw new IllegalArgumentException(sm.getString("engine.unsupportedProtocol", p)); } switch (p) { - case Constants.SSL_PROTO_SSLv2 -> sslv2 = true; case Constants.SSL_PROTO_SSLv3 -> sslv3 = true; case Constants.SSL_PROTO_TLSv1 -> tlsv1 = true; case Constants.SSL_PROTO_TLSv1_1 -> tlsv1_1 = true; @@ -851,10 +847,8 @@ } // Enable all and then disable what we not want SSL.setOptions(state.ssl, SSL.SSL_OP_ALL); - - if (!sslv2) { - SSL.setOptions(state.ssl, SSL.SSL_OP_NO_SSLv2); - } + // Always disable SSLv2 + SSL.setOptions(state.ssl, SSL.SSL_OP_NO_SSLv2); if (!sslv3) { SSL.setOptions(state.ssl, SSL.SSL_OP_NO_SSLv3); } @@ -1400,11 +1394,19 @@ private record OpenSSLState(long ssl, long networkBIO) implements Runnable { @Override public void run() { - if (networkBIO != 0) { - SSL.freeBIO(networkBIO); - } - if (ssl != 0) { - SSL.freeSSL(ssl); + Lock readLock = AprStatus.getStatusLock().readLock(); + readLock.lock(); + try { + if (AprStatus.isAprInitialized()) { + if (networkBIO != 0) { + SSL.freeBIO(networkBIO); + } + if (ssl != 0) { + SSL.freeSSL(ssl); + } + } + } finally { + readLock.unlock(); } } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java 2026-05-01 18:56:05.000000000 +0000 @@ -38,9 +38,10 @@ private static volatile boolean useOpenSSL = true; private static volatile boolean instanceCreated = false; private static volatile long version = 0; + private static volatile int majorVersion = 0; + private static volatile int minorVersion = 0; private static volatile Name name = Name.UNKNOWN; - public static boolean isLibraryInitialized() { return libraryInitialized; } @@ -96,6 +97,34 @@ } /** + * @return the majorVersion + */ + public static int getMajorVersion() { + return majorVersion; + } + + /** + * @param majorVersion the majorVersion to set + */ + public static void setMajorVersion(int majorVersion) { + OpenSSLStatus.majorVersion = majorVersion; + } + + /** + * @return the minorVersion + */ + public static int getMinorVersion() { + return minorVersion; + } + + /** + * @param minorVersion the minorVersion to set + */ + public static void setMinorVersion(int minorVersion) { + OpenSSLStatus.minorVersion = minorVersion; + } + + /** * @return the library name */ public static Name getName() { @@ -116,4 +145,18 @@ return Name.OPENSSL3.equals(name); } + /** + * @return true if running with BoringSSL + */ + public static boolean isBoringSSL() { + return Name.BORINGSSL.equals(name); + } + + /** + * @return true if running with LibreSSL < 3.5 + */ + public static boolean isLibreSSLPre35() { + return Name.LIBRESSL.equals(name) && ((majorVersion == 3 && minorVersion < 5) || majorVersion < 3); + } + } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/ciphers/Group.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/ciphers/Group.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/ciphers/Group.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/ciphers/Group.java 2026-05-01 18:56:05.000000000 +0000 @@ -41,6 +41,9 @@ ffdhe6144(0x0103), ffdhe8192(0x0104), + // SM2 Curve + curveSM2(0x0029), + // Post-Quantum Key Exchange MLKEM512(0x0200), MLKEM768(0x0201), @@ -49,7 +52,8 @@ // Hybrid Key Exchange SecP256r1MLKEM768(0x11EB), X25519MLKEM768(0x11EC), - SecP384r1MLKEM1024(0x11ED); + SecP384r1MLKEM1024(0x11ED), + curveSM2MLKEM768(0x11EE); private final int id; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java 2026-05-01 18:56:05.000000000 +0000 @@ -72,6 +72,8 @@ */ private static final Map> aliases = new LinkedHashMap<>(); + private static final Set tls13CipherSuiteNames = new HashSet<>(); + /** * the 'NULL' ciphers that is those offering no encryption. Because these offer no encryption at all and are a * security risk they are disabled unless explicitly included. @@ -98,18 +100,6 @@ */ private static final String LOW = "LOW"; /** - * Export encryption algorithms. Including 40 and 56 bits algorithms. - */ - private static final String EXPORT = "EXPORT"; - /** - * 40 bit export encryption algorithms. - */ - private static final String EXPORT40 = "EXPORT40"; - /** - * 56 bit export encryption algorithms. - */ - private static final String EXPORT56 = "EXPORT56"; - /** * Cipher suites using RSA key exchange. */ private static final String kRSA = "kRSA"; @@ -210,22 +200,6 @@ */ private static final String ECDSA = "ECDSA"; /** - * Ciphers suites using FORTEZZA key exchange algorithms. - */ - private static final String kFZA = "kFZA"; - /** - * Ciphers suites using FORTEZZA authentication algorithms. - */ - private static final String aFZA = "aFZA"; - /** - * Ciphers suites using FORTEZZA encryption algorithms. - */ - private static final String eFZA = "eFZA"; - /** - * Ciphers suites using all FORTEZZA algorithms. - */ - private static final String FZA = "FZA"; - /** * Cipher suites using DH, including anonymous DH, ephemeral DH and fixed DH. */ private static final String DH = "DH"; @@ -282,6 +256,10 @@ */ private static final String CAMELLIA = "CAMELLIA"; /** + * Cipher suites using Cipher Block Chaining. + */ + private static final String CBC = "CBC"; + /** * Cipher suites using CHACHA20. */ private static final String CHACHA20 = "CHACHA20"; @@ -330,10 +308,6 @@ */ private static final String SHA384 = "SHA384"; /** - * Cipher suites using KRB5. - */ - private static final String KRB5 = "KRB5"; - /** * Cipher suites using GOST R 34.10 (either 2001 or 94) for authentication. */ private static final String aGOST = "aGOST"; @@ -423,6 +397,16 @@ for (String jsseName : jsseNames) { jsseToOpenSSL.put(jsseName, cipher.getOpenSSLAlias()); } + + if (cipher.getProtocol().equals(Protocol.TLSv1_3)) { + tls13CipherSuiteNames.add(cipher.getOpenSSLAlias()); + /* + * The TLS 1.3 cipher suites do not, currently (January 2026), have any alternative names defined so the + * following two calls are NO-OPs but are implemented in case alternative names are used in the future. + */ + tls13CipherSuiteNames.addAll(cipher.getOpenSSLAltNames()); + tls13CipherSuiteNames.addAll(cipher.getJsseNames()); + } } List allCiphersList = Arrays.asList(Cipher.values()); Collections.reverse(allCiphersList); @@ -434,11 +418,6 @@ addListAlias(HIGH, filterByEncryptionLevel(allCiphers, Collections.singleton(EncryptionLevel.HIGH))); addListAlias(MEDIUM, filterByEncryptionLevel(allCiphers, Collections.singleton(EncryptionLevel.MEDIUM))); addListAlias(LOW, filterByEncryptionLevel(allCiphers, Collections.singleton(EncryptionLevel.LOW))); - addListAlias(EXPORT, filterByEncryptionLevel(allCiphers, - new HashSet<>(Arrays.asList(EncryptionLevel.EXP40, EncryptionLevel.EXP56)))); - aliases.put("EXP", aliases.get(EXPORT)); - addListAlias(EXPORT40, filterByEncryptionLevel(allCiphers, Collections.singleton(EncryptionLevel.EXP40))); - addListAlias(EXPORT56, filterByEncryptionLevel(allCiphers, Collections.singleton(EncryptionLevel.EXP56))); aliases.put("NULL", aliases.get(eNULL)); aliases.put(COMPLEMENTOFALL, aliases.get(eNULL)); addListAlias(aNULL, filterByAuthentication(allCiphers, Collections.singleton(Authentication.aNULL))); @@ -481,17 +460,10 @@ addListAlias(aECDH, filterByAuthentication(allCiphers, Collections.singleton(Authentication.ECDH))); addListAlias(ECDSA, filterByAuthentication(allCiphers, Collections.singleton(Authentication.ECDSA))); aliases.put(aECDSA, aliases.get(ECDSA)); - addListAlias(kFZA, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.FZA))); - addListAlias(aFZA, filterByAuthentication(allCiphers, Collections.singleton(Authentication.FZA))); - addListAlias(eFZA, filterByEncryption(allCiphers, Collections.singleton(Encryption.FZA))); - addListAlias(FZA, filter(allCiphers, null, Collections.singleton(KeyExchange.FZA), - Collections.singleton(Authentication.FZA), Collections.singleton(Encryption.FZA), null, null)); addListAlias(Constants.SSL_PROTO_TLSv1_2, filterByProtocol(allCiphers, Collections.singleton(Protocol.TLSv1_2))); addListAlias(Constants.SSL_PROTO_TLSv1_0, filterByProtocol(allCiphers, Collections.singleton(Protocol.TLSv1))); - addListAlias(Constants.SSL_PROTO_SSLv3, filterByProtocol(allCiphers, Collections.singleton(Protocol.SSLv3))); aliases.put(Constants.SSL_PROTO_TLSv1, aliases.get(Constants.SSL_PROTO_TLSv1_0)); - addListAlias(Constants.SSL_PROTO_SSLv2, filterByProtocol(allCiphers, Collections.singleton(Protocol.SSLv2))); addListAlias(DH, filterByKeyExchange(allCiphers, new HashSet<>(Arrays.asList(KeyExchange.DHr, KeyExchange.DHd, KeyExchange.EDH)))); Set adh = filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.EDH)); @@ -510,6 +482,7 @@ addListAlias(ARIA256, filterByEncryption(allCiphers, Collections.singleton(Encryption.ARIA256GCM))); addListAlias(ARIA, filterByEncryption(allCiphers, new HashSet<>(Arrays.asList(Encryption.ARIA128GCM, Encryption.ARIA256GCM)))); + aliases.put("ARIAGCM", aliases.get(ARIA)); addListAlias(AESGCM, filterByEncryption(allCiphers, new HashSet<>(Arrays.asList(Encryption.AES128GCM, Encryption.AES256GCM)))); addListAlias(AESCCM, filterByEncryption(allCiphers, new HashSet<>(Arrays.asList(Encryption.AES128CCM, @@ -520,6 +493,8 @@ new HashSet<>(Arrays.asList(Encryption.CAMELLIA128, Encryption.CAMELLIA256)))); addListAlias(CAMELLIA128, filterByEncryption(allCiphers, Collections.singleton(Encryption.CAMELLIA128))); addListAlias(CAMELLIA256, filterByEncryption(allCiphers, Collections.singleton(Encryption.CAMELLIA256))); + addListAlias(CBC, filterByEncryption(allCiphers, new HashSet<>( + Arrays.asList(Encryption.AES128, Encryption.AES256, Encryption.CAMELLIA128, Encryption.CAMELLIA256)))); addListAlias(CHACHA20, filterByEncryption(allCiphers, Collections.singleton(Encryption.CHACHA20POLY1305))); addListAlias(TRIPLE_DES, filterByEncryption(allCiphers, Collections.singleton(Encryption.TRIPLE_DES))); addListAlias(DES, filterByEncryption(allCiphers, Collections.singleton(Encryption.DES))); @@ -548,22 +523,16 @@ addListAlias(kRSAPSK, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.RSAPSK))); addListAlias(kECDHEPSK, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.ECDHEPSK))); addListAlias(kDHEPSK, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.DHEPSK))); - addListAlias(KRB5, filter(allCiphers, null, Collections.singleton(KeyExchange.KRB5), - Collections.singleton(Authentication.KRB5), null, null, null)); addListAlias(aSRP, filterByAuthentication(allCiphers, Collections.singleton(Authentication.SRP))); addListAlias(kSRP, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.SRP))); addListAlias(SRP, filterByKeyExchange(allCiphers, Collections.singleton(KeyExchange.SRP))); initialized = true; - // Despite what the OpenSSL docs say, DEFAULT also excludes SSLv2 - addListAlias(DEFAULT, parse( - "ALL:!EXPORT:!eNULL:!aNULL:!SSLv2:!DES:!RC2:!RC4:!DSS:!SEED:!IDEA:!CAMELLIA:!AESCCM:!3DES:!ARIA")); + addListAlias(DEFAULT, parse("ALL:!eNULL:!aNULL:!DES:!RC2:!RC4:!DSS:!SEED:!IDEA:!CAMELLIA:!AESCCM:!3DES:!ARIA")); // COMPLEMENTOFDEFAULT is also not exactly as defined by the docs LinkedHashSet complementOfDefault = filterByKeyExchange(all, new HashSet<>(Arrays.asList(KeyExchange.EDH, KeyExchange.EECDH))); complementOfDefault = filterByAuthentication(complementOfDefault, Collections.singleton(Authentication.aNULL)); aliases.get(eNULL).forEach(complementOfDefault::remove); - complementOfDefault.addAll(aliases.get(Constants.SSL_PROTO_SSLv2)); - complementOfDefault.addAll(aliases.get(EXPORT)); complementOfDefault.addAll(aliases.get(DES)); complementOfDefault.addAll(aliases.get(TRIPLE_DES)); complementOfDefault.addAll(aliases.get(RC2)); @@ -820,6 +789,20 @@ } /** + * Determines if the provided name is the name of a TLS 1.3 cipher suite. + * + * @param cipherSuiteName The name to test + * + * @return {@code true} if the provided String is recognised as the name of a TLS 1.3 cipherSuite. + */ + public static boolean isTls13Cipher(String cipherSuiteName) { + if (!initialized) { + init(); + } + return tls13CipherSuiteNames.contains(cipherSuiteName); + } + + /** * Parse the specified expression according to the OpenSSL syntax and returns a list of standard JSSE cipher names. * * @param expression the openssl expression to define a list of cipher. diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/ciphers/SignatureScheme.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/ciphers/SignatureScheme.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/ciphers/SignatureScheme.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/ciphers/SignatureScheme.java 2026-05-01 18:56:05.000000000 +0000 @@ -58,7 +58,26 @@ // ML-DSA algorithms mldsa44(0x0904, Authentication.MLDSA), mldsa65(0x0905, Authentication.MLDSA), - mldsa87(0x0906, Authentication.MLDSA); + mldsa87(0x0906, Authentication.MLDSA), + + // SLH-DSA algorithms + // Note: Mapped to ML-DSA for now, since not working + slhdsa_sha2_128s(0x0911, Authentication.MLDSA), + slhdsa_sha2_128f(0x0912, Authentication.MLDSA), + slhdsa_sha2_192s(0x0913, Authentication.MLDSA), + slhdsa_sha2_192f(0x0914, Authentication.MLDSA), + slhdsa_sha2_256s(0x0915, Authentication.MLDSA), + slhdsa_sha2_256f(0x0916, Authentication.MLDSA), + slhdsa_shake_128s(0x0917, Authentication.MLDSA), + slhdsa_shake_128f(0x0918, Authentication.MLDSA), + slhdsa_shake_192s(0x0919, Authentication.MLDSA), + slhdsa_shake_192f(0x091a, Authentication.MLDSA), + slhdsa_shake_256s(0x091b, Authentication.MLDSA), + slhdsa_shake_256f(0x091c, Authentication.MLDSA), + + // SM2 algorithms + // Note: Mapped to ML-DSA for now, since not confirmed to be working + sm2sig_sm3(0x0708, Authentication.MLDSA); private final int id; private final Authentication auth; diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java 2026-05-01 18:56:05.000000000 +0000 @@ -123,6 +123,10 @@ private boolean initialized = false; private boolean noOcspCheck = false; + private boolean ocspSoftFail = true; + // 15s default - same as JSSE + private int ocspTimeout = 15000; + private int ocspVerifyFlags = 0; private X509TrustManager x509TrustManager; private final ContextState state; @@ -189,8 +193,6 @@ for (String enabledProtocol : sslHostConfig.getEnabledProtocols()) { if (Constants.SSL_PROTO_SSLv2Hello.equalsIgnoreCase(enabledProtocol)) { // NO-OP. OpenSSL always supports SSLv2Hello - } else if (Constants.SSL_PROTO_SSLv2.equalsIgnoreCase(enabledProtocol)) { - protocol |= SSL_PROTOCOL_SSLV2; } else if (Constants.SSL_PROTO_SSLv3.equalsIgnoreCase(enabledProtocol)) { protocol |= SSL_PROTOCOL_SSLV3; } else if (Constants.SSL_PROTO_TLSv1.equalsIgnoreCase(enabledProtocol)) { @@ -263,8 +265,8 @@ // Set server groups // Note: It is also possible to override setSSLParameters in OpenSSLEngine to set the final - // list of groups per connection, but this is less efficient than setting the configured - // group list on the SSL context and letting OpenSSL figure it out. + // list of groups per connection, but this is less efficient than setting the configured + // group list on the SSL context and letting OpenSSL figure it out. if (sslHostConfig.getGroupList() != null) { StringBuilder sb = new StringBuilder(); boolean first = true; @@ -353,7 +355,13 @@ log.trace(sm.getString("opensslconf.checkCommand", name, value)); } try (var localArena = Arena.ofConfined()) { - if (name.equals("NO_OCSP_CHECK")) { + if (name.equals(OpenSSLConfCmd.NO_OCSP_CHECK)) { + ok = true; + } else if (name.equals(OpenSSLConfCmd.OCSP_SOFT_FAIL)) { + ok = true; + } else if (name.equals(OpenSSLConfCmd.OCSP_TIMEOUT)) { + ok = true; + } else if (name.equals(OpenSSLConfCmd.OCSP_VERIFY_FLAGS)) { ok = true; } else { int code = SSL_CONF_cmd_value_type(state.confCtx, localArena.allocateFrom(name)); @@ -422,8 +430,17 @@ log.trace(sm.getString("opensslconf.applyCommand", name, value)); } try (var localArena = Arena.ofConfined()) { - if (name.equals("NO_OCSP_CHECK")) { - noOcspCheck = Boolean.parseBoolean(value); + if (name.equals(OpenSSLConfCmd.NO_OCSP_CHECK)) { + // Ignore - Tomcat internal - set directly + rc = 1; + } else if (name.equals(OpenSSLConfCmd.OCSP_SOFT_FAIL)) { + // Ignore - Tomcat internal - set directly + rc = 1; + } else if (name.equals(OpenSSLConfCmd.OCSP_TIMEOUT)) { + // Ignore - Tomcat internal - set directly + rc = 1; + } else if (name.equals(OpenSSLConfCmd.OCSP_VERIFY_FLAGS)) { + // Ignore - Tomcat internal - set directly rc = 1; } else { rc = SSL_CONF_cmd(state.confCtx, localArena.allocateFrom(name), localArena.allocateFrom(value)); @@ -502,17 +519,36 @@ openssl_h_Compatibility.SSL_CTX_clear_options(state.sslCtx, SSL_OP_NO_TICKET()); } + boolean ciphersSet = false; + String tls12Warning = null; + String tls13Warning = null; // List the ciphers that the client is permitted to negotiate if (minTlsVersion <= TLS1_2_VERSION()) { if (SSL_CTX_set_cipher_list(state.sslCtx, localArena.allocateFrom(sslHostConfig.getCiphers())) <= 0) { - log.warn(sm.getString("engine.failedCipherList", sslHostConfig.getCiphers())); + tls12Warning = sm.getString("engine.failedCipherList", sslHostConfig.getCiphers()); + } else { + ciphersSet = true; + } + } + if (maxTlsVersion >= TLS1_3_VERSION()) { + try { + if (SSL_CTX_set_ciphersuites(state.sslCtx, + localArena.allocateFrom(sslHostConfig.getCipherSuites())) <= 0) { + tls13Warning = sm.getString("engine.failedCipherSuite", sslHostConfig.getCipherSuites()); + } else { + ciphersSet = true; + } + } catch (NoClassDefFoundError | UnsatisfiedLinkError e) { + // Ignore unavailable TLS 1.3 call, which might be compiled out sometimes on LibreSSL + tls13Warning = sm.getString("engine.failedCipherSuite", sslHostConfig.getCipherSuites()); } } - // Check if the ciphers have been changed from the defaults - if (maxTlsVersion >= TLS1_3_VERSION() && - (sslHostConfig.getCiphers() != SSLHostConfig.DEFAULT_TLS_CIPHERS)) { - if (SSL_CTX_set_ciphersuites(state.sslCtx, localArena.allocateFrom(sslHostConfig.getCiphers())) <= 0) { - log.warn(sm.getString("engine.failedCipherSuite", sslHostConfig.getCiphers())); + if (!ciphersSet) { + if (tls12Warning != null) { + log.warn(tls12Warning); + } + if (tls13Warning != null) { + log.warn(tls13Warning); } } @@ -531,15 +567,18 @@ case REQUIRED -> SSL_VERIFY_FAIL_IF_NO_PEER_CERT(); }; - if (value == OPTIONAL_NO_CA) { + if (value == OPTIONAL_NO_CA || !sslHostConfig.getOcspEnabled()) { noOcspCheck = true; } + ocspSoftFail = sslHostConfig.getOcspSoftFail(); + ocspTimeout = sslHostConfig.getOcspTimeout(); + ocspVerifyFlags = sslHostConfig.getOcspVerifyFlags(); // Set int verify_callback(int preverify_ok, X509_STORE_CTX *x509_ctx) callback SSL_CTX_set_verify(state.sslCtx, value, SSL_CTX_set_verify$callback.allocate(new OpenSSLEngine.VerifyCallback(), contextArena)); - // Trust and certificate verification + // Trust and certificate verification (optional - may not be configured) if (tms != null) { // Client certificate verification based on custom trust managers x509TrustManager = chooseTrustManager(tms); @@ -561,8 +600,9 @@ } else if (log.isDebugEnabled()) { log.debug(sm.getString("openssl.addedClientCaCert", caCert.toString())); } + X509_free(x509CACert); } - } else { + } else if (sslHostConfig.getCaCertificateFile() != null || sslHostConfig.getCaCertificatePath() != null) { // Client certificate verification based on trusted CA files and dirs MemorySegment caCertificateFileNative = sslHostConfig.getCaCertificateFile() != null ? localArena @@ -572,9 +612,8 @@ localArena .allocateFrom(SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificatePath())) : MemorySegment.NULL; - if ((sslHostConfig.getCaCertificateFile() != null || sslHostConfig.getCaCertificatePath() != null) && - SSL_CTX_load_verify_locations(state.sslCtx, caCertificateFileNative, - caCertificatePathNative) <= 0) { + if (SSL_CTX_load_verify_locations(state.sslCtx, caCertificateFileNative, + caCertificatePathNative) <= 0) { logLastError("openssl.errorConfiguringLocations"); } else { var caCerts = SSL_CTX_get_client_CA_list(state.sslCtx); @@ -650,9 +689,6 @@ if ((opts & SSL_OP_NO_TLSv1_3()) == 0) { enabled.add(Constants.SSL_PROTO_TLSv1_3); } - if ((opts & SSL_OP_NO_SSLv2()) == 0) { - enabled.add(Constants.SSL_PROTO_SSLv2); - } if ((opts & SSL_OP_NO_SSLv3()) == 0) { enabled.add(Constants.SSL_PROTO_SSLv3); } @@ -773,18 +809,7 @@ int len = openssl_h_Compatibility.OPENSSL_sk_num(sk); byte[][] certificateChain = new byte[len][]; try (var localArena = Arena.ofConfined()) { - for (int i = 0; i < len; i++) { - MemorySegment/* (X509*) */ x509 = openssl_h_Compatibility.OPENSSL_sk_value(sk, i); - MemorySegment bufPointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); - int length = i2d_X509(x509, bufPointer); - if (length < 0) { - certificateChain[i] = new byte[0]; - continue; - } - MemorySegment buf = bufPointer.get(ValueLayout.ADDRESS, 0); - certificateChain[i] = buf.reinterpret(length, localArena, null).toArray(ValueLayout.JAVA_BYTE); - OPENSSL_free(buf); - } + OpenSSLLibrary.populateCertificateChain(localArena, sk, certificateChain); MemorySegment cipher = SSL_get_current_cipher(ssl); String authMethod = (MemorySegment.NULL.equals(cipher)) ? "UNKNOWN" : getCipherAuthenticationMethod(SSL_CIPHER_get_auth_nid(cipher), SSL_CIPHER_get_kx_nid(cipher)); @@ -1035,17 +1060,23 @@ if (MemorySegment.NULL.equals(cert)) { log.error(sm.getString("openssl.errorLoadingCertificateWithError", certificate.getCertificateFile(), OpenSSLLibrary.getLastError())); + EVP_PKEY_free(key); return false; } } if (SSL_CTX_use_certificate(state.sslCtx, cert) <= 0) { logLastError("openssl.errorLoadingCertificate"); + EVP_PKEY_free(key); + X509_free(cert); return false; } + X509_free(cert); if (SSL_CTX_use_PrivateKey(state.sslCtx, key) <= 0) { logLastError("openssl.errorLoadingPrivateKey"); + EVP_PKEY_free(key); return false; } + EVP_PKEY_free(key); if (SSL_CTX_check_private_key(state.sslCtx) <= 0) { logLastError("openssl.errorPrivateKeyCheck"); return false; @@ -1077,7 +1108,7 @@ log.debug(sm.getString("openssl.errorReadingPEMParameters", errMessage, certificate.getCertificateFile())); } - SSL_CTX_ctrl(state.sslCtx, SSL_CTRL_SET_DH_AUTO(), 1, MemorySegment.NULL); + SSL_CTX_set_dh_auto(state.sslCtx, 1); } } } @@ -1143,6 +1174,7 @@ if (SSL_CTX_add0_chain_cert(state.sslCtx, certChainEntry) <= 0) { log.error(sm.getString("openssl.errorLoadingCertificateWithError", certificate.getCertificateChainFile(), OpenSSLLibrary.getLastError())); + X509_free(certChainEntry); } certChainEntry = PEM_read_bio_X509_AUX(certificateChainBIO, MemorySegment.NULL, MemorySegment.NULL, MemorySegment.NULL); @@ -1212,16 +1244,22 @@ PEM_read_bio_PrivateKey(keyBIO, MemorySegment.NULL, MemorySegment.NULL, MemorySegment.NULL); if (MemorySegment.NULL.equals(privateKeyAddress)) { logLastError("openssl.errorLoadingPrivateKey"); + X509_free(x509cert); return false; } if (SSL_CTX_use_certificate(state.sslCtx, x509cert) <= 0) { logLastError("openssl.errorLoadingCertificate"); + EVP_PKEY_free(privateKeyAddress); + X509_free(x509cert); return false; } + X509_free(x509cert); if (SSL_CTX_use_PrivateKey(state.sslCtx, privateKeyAddress) <= 0) { logLastError("openssl.errorLoadingPrivateKey"); + EVP_PKEY_free(privateKeyAddress); return false; } + EVP_PKEY_free(privateKeyAddress); if (SSL_CTX_check_private_key(state.sslCtx) <= 0) { logLastError("openssl.errorPrivateKeyCheck"); return false; @@ -1247,7 +1285,7 @@ log.debug(sm.getString("openssl.errorReadingPEMParameters", errMessage, x509KeyManager.toString())); } - SSL_CTX_ctrl(state.sslCtx, SSL_CTRL_SET_DH_AUTO(), 1, MemorySegment.NULL); + SSL_CTX_set_dh_auto(state.sslCtx, 1); } } for (int i = 1; i < chain.length; i++) { @@ -1261,6 +1299,7 @@ } if (SSL_CTX_add0_chain_cert(state.sslCtx, x509certChain) <= 0) { logLastError("openssl.errorAddingCertificate"); + X509_free(x509certChain); return false; } } @@ -1333,7 +1372,8 @@ public SSLEngine createSSLEngine() { return new OpenSSLEngine(cleaner, state.sslCtx, defaultProtocol, false, sessionContext, alpn, initialized, sslHostConfig.getCertificateVerificationDepth(), - sslHostConfig.getCertificateVerification() == CertificateVerification.OPTIONAL_NO_CA, noOcspCheck); + sslHostConfig.getCertificateVerification() == CertificateVerification.OPTIONAL_NO_CA, noOcspCheck, + ocspSoftFail, ocspTimeout, ocspVerifyFlags); } @Override diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java 2026-05-01 18:56:05.000000000 +0000 @@ -17,6 +17,7 @@ package org.apache.tomcat.util.net.openssl.panama; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; @@ -86,14 +87,17 @@ final Set availableCipherSuites = new LinkedHashSet<>(128); availableCipherSuites.addAll(OpenSSLLibrary.findCiphers("ALL")); AVAILABLE_CIPHER_SUITES = Collections.unmodifiableSet(availableCipherSuites); - IMPLEMENTED_PROTOCOLS_SET = Set.of(Constants.SSL_PROTO_SSLv2Hello, Constants.SSL_PROTO_SSLv2, - Constants.SSL_PROTO_SSLv3, Constants.SSL_PROTO_TLSv1, Constants.SSL_PROTO_TLSv1_1, - Constants.SSL_PROTO_TLSv1_2, Constants.SSL_PROTO_TLSv1_3); + IMPLEMENTED_PROTOCOLS_SET = + Set.of(Constants.SSL_PROTO_SSLv2Hello, Constants.SSL_PROTO_SSLv3, Constants.SSL_PROTO_TLSv1, + Constants.SSL_PROTO_TLSv1_1, Constants.SSL_PROTO_TLSv1_2, Constants.SSL_PROTO_TLSv1_3); } private static final int MAX_PLAINTEXT_LENGTH = 16 * 1024; // 2^14 private static final int MAX_COMPRESSED_LENGTH = MAX_PLAINTEXT_LENGTH + 1024; private static final int MAX_CIPHERTEXT_LENGTH = MAX_COMPRESSED_LENGTH + 1024; + // 15 minutes aligns with JSSE + private static final int OCSP_MAX_SKEW = 60 * 15; + private static final int OCSP_MAX_RESPONSE_SIZE = 64 * 1024; // Header (5) + Data (2^14) + Compression (1024) + Encryption (1024) + MAC (20) + Padding (256) private static final int MAX_ENCRYPTED_PACKET_LENGTH = MAX_CIPHERTEXT_LENGTH + 5 + 20 + 256; @@ -174,10 +178,15 @@ * verification from the {@code SSL_CTX} {@code sslCtx} * @param certificateVerificationDepth Certificate verification depth * @param certificateVerificationOptionalNoCA Skip CA verification in optional mode + * @param noOcspCheck Enable OCSP if true + * @param ocspSoftFail Allow OCSP checks to pass if the responder can't be contacted + * @param ocspTimeout Timout in ms to use for OCSP requests + * @param ocspVerifyFlags Verification flags for OCSP */ OpenSSLEngine(Cleaner cleaner, MemorySegment sslCtx, String fallbackApplicationProtocol, boolean clientMode, OpenSSLSessionContext sessionContext, boolean alpn, boolean initialized, int certificateVerificationDepth, - boolean certificateVerificationOptionalNoCA, boolean noOcspCheck) { + boolean certificateVerificationOptionalNoCA, boolean noOcspCheck, boolean ocspSoftFail, int ocspTimeout, + int ocspVerifyFlags) { if (sslCtx == null) { throw new IllegalArgumentException(sm.getString("engine.noSSLContext")); } @@ -200,7 +209,8 @@ var internalBIO = internalBIOPointer.get(ValueLayout.ADDRESS, 0); var networkBIO = networkBIOPointer.get(ValueLayout.ADDRESS, 0); SSL_set_bio(ssl, internalBIO, internalBIO); - state = new EngineState(ssl, networkBIO, certificateVerificationDepth, noOcspCheck); + state = new EngineState(ssl, networkBIO, certificateVerificationDepth, noOcspCheck, ocspSoftFail, + ocspTimeout, ocspVerifyFlags); } this.fallbackApplicationProtocol = fallbackApplicationProtocol; this.clientMode = clientMode; @@ -403,7 +413,7 @@ } if (bytesWritten == 0) { - throw new IllegalStateException(sm.getString("engine.failedToWriteBytes")); + throw new SSLException(sm.getString("engine.failedToWriteBytes")); } // Check to see if the engine wrote data into the network BIO @@ -531,7 +541,7 @@ if (bytesRead == 0) { // This should not be possible. pendingApp is positive // therefore the read should have read at least one byte. - throw new IllegalStateException(sm.getString("engine.failedToReadAvailableBytes")); + throw new SSLException(sm.getString("engine.failedToReadAvailableBytes")); } bytesProduced += bytesRead; @@ -730,9 +740,6 @@ if ((opts & SSL_OP_NO_TLSv1_3()) == 0) { enabled.add(Constants.SSL_PROTO_TLSv1_3); } - if ((opts & SSL_OP_NO_SSLv2()) == 0) { - enabled.add(Constants.SSL_PROTO_SSLv2); - } if ((opts & SSL_OP_NO_SSLv3()) == 0) { enabled.add(Constants.SSL_PROTO_SSLv3); } @@ -752,7 +759,6 @@ if (destroyed) { return; } - boolean sslv2 = false; boolean sslv3 = false; boolean tlsv1 = false; boolean tlsv1_1 = false; @@ -763,7 +769,6 @@ throw new IllegalArgumentException(sm.getString("engine.unsupportedProtocol", p)); } switch (p) { - case Constants.SSL_PROTO_SSLv2 -> sslv2 = true; case Constants.SSL_PROTO_SSLv3 -> sslv3 = true; case Constants.SSL_PROTO_TLSv1 -> tlsv1 = true; case Constants.SSL_PROTO_TLSv1_1 -> tlsv1_1 = true; @@ -773,10 +778,8 @@ } // Enable all and then disable what we not want openssl_h_Compatibility.SSL_set_options(state.ssl, SSL_OP_ALL()); - - if (!sslv2) { - openssl_h_Compatibility.SSL_set_options(state.ssl, SSL_OP_NO_SSLv2()); - } + // Always disable SSLv2 + openssl_h_Compatibility.SSL_set_options(state.ssl, SSL_OP_NO_SSLv2()); if (!sslv3) { openssl_h_Compatibility.SSL_set_options(state.ssl, SSL_OP_NO_SSLv3()); } @@ -827,15 +830,23 @@ private byte[] getPeerCertificate() { try (var localArena = Arena.ofConfined()) { - MemorySegment/* (X509*) */ x509 = openssl_h_Compatibility.SSL_get_peer_certificate(state.ssl); + // Use the new SSL_get0_peer_certificate call for OpenSSL 3+ to avoid having to call free + MemorySegment/* (X509*) */ x509 = + (openssl_h_Compatibility.OPENSSL3) ? SSL_get0_peer_certificate(state.ssl) : + openssl_h_Compatibility.SSL_get_peer_certificate(state.ssl); MemorySegment bufPointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); int length = i2d_X509(x509, bufPointer); if (length <= 0) { + if (!openssl_h_Compatibility.OPENSSL3) { + X509_free(x509); + } return null; } MemorySegment buf = bufPointer.get(ValueLayout.ADDRESS, 0); byte[] certificate = buf.reinterpret(length, localArena, null).toArray(ValueLayout.JAVA_BYTE); - X509_free(x509); + if (!openssl_h_Compatibility.OPENSSL3) { + X509_free(x509); + } OPENSSL_free(buf); return certificate; } @@ -849,19 +860,7 @@ } byte[][] certificateChain = new byte[len][]; try (var localArena = Arena.ofConfined()) { - for (int i = 0; i < len; i++) { - MemorySegment/* (X509*) */ x509 = openssl_h_Compatibility.OPENSSL_sk_value(sk, i); - MemorySegment bufPointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); - int length = i2d_X509(x509, bufPointer); - if (length < 0) { - certificateChain[i] = new byte[0]; - continue; - } - MemorySegment buf = bufPointer.get(ValueLayout.ADDRESS, 0); - byte[] certificate = buf.reinterpret(length, localArena, null).toArray(ValueLayout.JAVA_BYTE); - certificateChain[i] = certificate; - OPENSSL_free(buf); - } + OpenSSLLibrary.populateCertificateChain(localArena, sk, certificateChain); return certificateChain; } } @@ -1134,7 +1133,8 @@ (errnum == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN()) || (errnum == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY()) || (errnum == X509_V_ERR_CERT_UNTRUSTED()) || (errnum == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE()); - if (verifyErrorIsOptional && (state.certificateVerifyMode == OpenSSLContext.OPTIONAL_NO_CA)) { + if ((verifyErrorIsOptional || errnum == X509_V_OK()) && + (state.certificateVerifyMode == OpenSSLContext.OPTIONAL_NO_CA)) { ok = 1; openssl_h_Compatibility.SSL_set_verify_result(state.ssl, X509_V_OK()); } @@ -1161,21 +1161,18 @@ * issuer may be missing/untrusted. Fail in that case. */ if (verifyErrorIsOptional) { - if (state.certificateVerifyMode != OpenSSLContext.OPTIONAL_NO_CA) { - X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_APPLICATION_VERIFICATION()); - errnum = X509_V_ERR_APPLICATION_VERIFICATION(); - ok = 0; - } + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_APPLICATION_VERIFICATION()); + errnum = X509_V_ERR_APPLICATION_VERIFICATION(); + ok = 0; } else { - int ocspResponse = processOCSP(x509ctx); + int ocspResponse = processOCSP(state, x509ctx); if (ocspResponse == V_OCSP_CERTSTATUS_REVOKED()) { ok = 0; errnum = X509_STORE_CTX_get_error(x509ctx); X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_CERT_REVOKED()); } else if (ocspResponse == V_OCSP_CERTSTATUS_UNKNOWN()) { errnum = X509_STORE_CTX_get_error(x509ctx); - if (errnum != X509_V_ERR_UNABLE_TO_GET_CRL() && - (errnum == X509_V_ERR_APPLICATION_VERIFICATION() || errnum != 0)) { + if (errnum != 0 && !(state.ocspSoftFail && errnum == X509_V_ERR_UNABLE_TO_GET_CRL())) { ok = 0; } } @@ -1190,7 +1187,7 @@ } } - private static int processOCSP(MemorySegment /* X509_STORE_CTX */ x509ctx) { + private static int processOCSP(EngineState state, MemorySegment /* X509_STORE_CTX */ x509ctx) { int ocspResponse = V_OCSP_CERTSTATUS_UNKNOWN(); MemorySegment x509 = X509_STORE_CTX_get_current_cert(x509ctx); if (!MemorySegment.NULL.equals(x509)) { @@ -1239,7 +1236,8 @@ for (String urlString : urls) { try { URL url = (new URI(urlString)).toURL(); - ocspResponse = processOCSPRequest(url, issuer, x509, x509ctx, localArena); + ocspResponse = + processOCSPRequest(state, url, issuer, x509, x509ctx, localArena); if (log.isDebugEnabled()) { log.debug(sm.getString("engine.ocspResponse", urlString, Integer.toString(ocspResponse))); @@ -1292,9 +1290,10 @@ } } - private static int processOCSPRequest(URL url, MemorySegment issuer, MemorySegment x509, + private static int processOCSPRequest(EngineState state, URL url, MemorySegment issuer, MemorySegment x509, MemorySegment /* X509_STORE_CTX */ x509ctx, Arena localArena) { - if (openssl_h_Compatibility.BORINGSSL) { + if (openssl_h_Compatibility.BORINGSSL || openssl_h_Compatibility.isLibreSSLPre35()) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_UNABLE_TO_GET_CRL()); return V_OCSP_CERTSTATUS_UNKNOWN(); } MemorySegment ocspRequest = MemorySegment.NULL; @@ -1307,19 +1306,24 @@ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { ocspRequest = OCSP_REQUEST_new(); if (MemorySegment.NULL.equals(ocspRequest)) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_UNABLE_TO_GET_CRL()); return V_OCSP_CERTSTATUS_UNKNOWN(); } id = OCSP_cert_to_id(MemorySegment.NULL, x509, issuer); if (MemorySegment.NULL.equals(id)) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_UNABLE_TO_GET_CRL()); return V_OCSP_CERTSTATUS_UNKNOWN(); } ocspOneReq = OCSP_request_add0_id(ocspRequest, id); if (MemorySegment.NULL.equals(ocspOneReq)) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_UNABLE_TO_GET_CRL()); return V_OCSP_CERTSTATUS_UNKNOWN(); } + OCSP_request_add1_nonce(ocspRequest, (char) 0, -1); MemorySegment bufPointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); int requestLength = i2d_OCSP_REQUEST(ocspRequest, bufPointer); if (requestLength <= 0) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_UNABLE_TO_GET_CRL()); return V_OCSP_CERTSTATUS_UNKNOWN(); } MemorySegment buf = bufPointer.get(ValueLayout.ADDRESS, 0); @@ -1330,6 +1334,8 @@ // Content-Length: ocspRequestData.length byte[] ocspRequestData = buf.reinterpret(requestLength, localArena, null).toArray(ValueLayout.JAVA_BYTE); connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(state.ocspTimeout); + connection.setReadTimeout(state.ocspTimeout); connection.setRequestMethod(Method.POST); connection.setDoInput(true); connection.setDoOutput(true); @@ -1339,39 +1345,74 @@ connection.getOutputStream().write(ocspRequestData); int responseCode = connection.getResponseCode(); if (responseCode != HttpURLConnection.HTTP_OK) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_UNABLE_TO_GET_CRL()); return V_OCSP_CERTSTATUS_UNKNOWN(); } InputStream is = connection.getInputStream(); int read; byte[] responseBuf = new byte[1024]; while ((read = is.read(responseBuf)) > 0) { + if (baos.size() > OCSP_MAX_RESPONSE_SIZE) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_UNABLE_TO_GET_CRL()); + return V_OCSP_CERTSTATUS_UNKNOWN(); + } baos.write(responseBuf, 0, read); } byte[] responseData = baos.toByteArray(); var nativeResponseData = localArena.allocateFrom(ValueLayout.JAVA_BYTE, responseData); var nativeResponseDataPointer = localArena.allocateFrom(ValueLayout.ADDRESS, nativeResponseData); ocspResponse = d2i_OCSP_RESPONSE(MemorySegment.NULL, nativeResponseDataPointer, responseData.length); - if (!MemorySegment.NULL.equals(ocspResponse)) { + if (MemorySegment.NULL.equals(ocspResponse)) { + // Failed to get a valid response + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_APPLICATION_VERIFICATION()); + } else { if (OCSP_response_status(ocspResponse) == OCSP_RESPONSE_STATUS_SUCCESSFUL()) { basicResponse = OCSP_response_get1_basic(ocspResponse); + if (OCSP_check_nonce(ocspRequest, basicResponse) == 0) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_OCSP_RESP_INVALID()); + return V_OCSP_CERTSTATUS_UNKNOWN(); + } + MemorySegment certStack = OCSP_resp_get0_certs(basicResponse); + if (OCSP_basic_verify(basicResponse, certStack, X509_STORE_CTX_get0_store(x509ctx), + state.ocspVerifyFlags) <= 0) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_OCSP_SIGNATURE_FAILURE()); + return V_OCSP_CERTSTATUS_UNKNOWN(); + } certId = OCSP_cert_to_id(MemorySegment.NULL, x509, issuer); if (MemorySegment.NULL.equals(certId)) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_OCSP_RESP_INVALID()); return V_OCSP_CERTSTATUS_UNKNOWN(); } // Find by serial number and get the matching response MemorySegment singleResponse = OCSP_resp_get0(basicResponse, OCSP_resp_find(basicResponse, certId, -1)); - return OCSP_single_get0_status(singleResponse, MemorySegment.NULL, MemorySegment.NULL, - MemorySegment.NULL, MemorySegment.NULL); + MemorySegment thisUpdatePointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); + MemorySegment nextUpdatePointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); + int status = OCSP_single_get0_status(singleResponse, MemorySegment.NULL, MemorySegment.NULL, + thisUpdatePointer, nextUpdatePointer); + if (OCSP_check_validity(thisUpdatePointer.get(ValueLayout.ADDRESS, 0), + nextUpdatePointer.get(ValueLayout.ADDRESS, 0), OCSP_MAX_SKEW, -1) <= 0) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_OCSP_NOT_YET_VALID()); + return V_OCSP_CERTSTATUS_UNKNOWN(); + } + if (OCSP_check_validity(thisUpdatePointer.get(ValueLayout.ADDRESS, 0), + nextUpdatePointer.get(ValueLayout.ADDRESS, 0), OCSP_MAX_SKEW, OCSP_MAX_SKEW) <= 0) { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_OCSP_HAS_EXPIRED()); + return V_OCSP_CERTSTATUS_UNKNOWN(); + } + return status; + } else { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_UNABLE_TO_GET_CRL()); } } + } catch (IOException ioe) { + // Timeout or network error. Responder is not available. + log.warn(sm.getString("engine.ocspRequestError", url.toString()), ioe); + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_UNABLE_TO_GET_CRL()); } catch (Exception e) { log.warn(sm.getString("engine.ocspRequestError", url.toString()), e); + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_APPLICATION_VERIFICATION()); } finally { - if (MemorySegment.NULL.equals(ocspResponse)) { - // Failed to get a valid response - X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_APPLICATION_VERIFICATION()); - } OCSP_CERTID_free(certId); OCSP_BASICRESP_free(basicResponse); OCSP_RESPONSE_free(ocspResponse); @@ -1675,16 +1716,22 @@ private final MemorySegment networkBIO; private final int certificateVerificationDepth; private final boolean noOcspCheck; + private final boolean ocspSoftFail; + private final int ocspTimeout; + private final int ocspVerifyFlags; private PHAState phaState = PHAState.NONE; private int certificateVerifyMode = 0; private int handshakeCount = 0; private EngineState(MemorySegment ssl, MemorySegment networkBIO, int certificateVerificationDepth, - boolean noOcspCheck) { + boolean noOcspCheck, boolean ocspSoftFail, int ocspTimeout, int ocspVerifyFlags) { states.put(Long.valueOf(ssl.address()), this); this.certificateVerificationDepth = certificateVerificationDepth; this.noOcspCheck = noOcspCheck; + this.ocspSoftFail = ocspSoftFail; + this.ocspTimeout = ocspTimeout; + this.ocspVerifyFlags = ocspVerifyFlags; // Use another arena to avoid keeping a reference through segments // This also allows making further accesses to the main pointers safer this.ssl = ssl.reinterpret(ValueLayout.ADDRESS.byteSize(), stateArena, openssl_h::SSL_free); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java 2026-05-01 18:56:05.000000000 +0000 @@ -26,6 +26,7 @@ import static org.apache.tomcat.util.openssl.openssl_h.*; import static org.apache.tomcat.util.openssl.openssl_h_Compatibility.*; +import static org.apache.tomcat.util.openssl.openssl_h_Macros.*; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.net.openssl.OpenSSLStatus; @@ -178,6 +179,8 @@ initLibrary(); OpenSSLStatus.setVersion(OpenSSL_version_num()); + OpenSSLStatus.setMajorVersion(openssl_h_Compatibility.MAJOR); + OpenSSLStatus.setMinorVersion(openssl_h_Compatibility.MINOR); if (openssl_h_Compatibility.OPENSSL3) { OpenSSLStatus.setName(OpenSSLStatus.Name.OPENSSL3); } else if (openssl_h_Compatibility.OPENSSL) { @@ -425,6 +428,18 @@ 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()) { @@ -502,5 +517,19 @@ return sslError; } - + static void populateCertificateChain(Arena localArena, MemorySegment /* STACK_OF(X509) */ sk, + byte[][] certificateChain) { + for (int i = 0; i < certificateChain.length; i++) { + MemorySegment/* (X509*) */ x509 = openssl_h_Compatibility.OPENSSL_sk_value(sk, i); + MemorySegment bufPointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); + int length = i2d_X509(x509, bufPointer); + if (length <= 0) { + certificateChain[i] = new byte[0]; + continue; + } + MemorySegment buf = bufPointer.get(ValueLayout.ADDRESS, 0); + certificateChain[i] = buf.reinterpret(length, localArena, null).toArray(ValueLayout.JAVA_BYTE); + OPENSSL_free(buf); + } + } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java 2026-05-01 18:56:05.000000000 +0000 @@ -105,6 +105,4 @@ throw e; } } - - } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/openssl/openssl_h.java tomcat11-11.0.22/java/org/apache/tomcat/util/openssl/openssl_h.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/openssl/openssl_h.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/openssl/openssl_h.java 2026-05-01 18:56:05.000000000 +0000 @@ -196,6 +196,16 @@ return X509_V_OK; } + private static final int X509_V_ERR_UNABLE_TO_GET_CRL = (int) 3L; + + /** + * {@snippet lang = c : * #define X509_V_ERR_UNABLE_TO_GET_CRL 3 + * } + */ + public static int X509_V_ERR_UNABLE_TO_GET_CRL() { + return X509_V_ERR_UNABLE_TO_GET_CRL; + } + private static final int X509_V_ERR_CRL_HAS_EXPIRED = (int) 12L; /** @@ -226,26 +236,6 @@ return X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN; } - private static final int X509_V_ERR_CERT_REVOKED = (int) 23L; - - /** - * {@snippet lang = c : * #define X509_V_ERR_CERT_REVOKED 23 - * } - */ - public static int X509_V_ERR_CERT_REVOKED() { - return X509_V_ERR_CERT_REVOKED; - } - - private static final int X509_V_ERR_UNABLE_TO_GET_CRL = (int) 3L; - - /** - * {@snippet lang = c : * #define X509_V_ERR_UNABLE_TO_GET_CRL 3 - * } - */ - public static int X509_V_ERR_UNABLE_TO_GET_CRL() { - return X509_V_ERR_UNABLE_TO_GET_CRL; - } - private static final int X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY = (int) 20L; /** @@ -266,6 +256,16 @@ return X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE; } + private static final int X509_V_ERR_CERT_REVOKED = (int) 23L; + + /** + * {@snippet lang = c : * #define X509_V_ERR_CERT_REVOKED 23 + * } + */ + public static int X509_V_ERR_CERT_REVOKED() { + return X509_V_ERR_CERT_REVOKED; + } + private static final int X509_V_ERR_CERT_UNTRUSTED = (int) 27L; /** @@ -286,6 +286,46 @@ return X509_V_ERR_APPLICATION_VERIFICATION; } + private static final int X509_V_ERR_OCSP_RESP_INVALID = (int) 96L; + + /** + * {@snippet lang = c : * #define X509_V_ERR_OCSP_RESP_INVALID 96 + * } + */ + public static int X509_V_ERR_OCSP_RESP_INVALID() { + return X509_V_ERR_OCSP_RESP_INVALID; + } + + private static final int X509_V_ERR_OCSP_SIGNATURE_FAILURE = (int) 97L; + + /** + * {@snippet lang = c : * #define X509_V_ERR_OCSP_SIGNATURE_FAILURE 97 + * } + */ + public static int X509_V_ERR_OCSP_SIGNATURE_FAILURE() { + return X509_V_ERR_OCSP_SIGNATURE_FAILURE; + } + + private static final int X509_V_ERR_OCSP_NOT_YET_VALID = (int) 98L; + + /** + * {@snippet lang = c : * #define X509_V_ERR_OCSP_NOT_YET_VALID 98 + * } + */ + public static int X509_V_ERR_OCSP_NOT_YET_VALID() { + return X509_V_ERR_OCSP_NOT_YET_VALID; + } + + private static final int X509_V_ERR_OCSP_HAS_EXPIRED = (int) 99L; + + /** + * {@snippet lang = c : * #define X509_V_ERR_OCSP_HAS_EXPIRED 99 + * } + */ + public static int X509_V_ERR_OCSP_HAS_EXPIRED() { + return X509_V_ERR_OCSP_HAS_EXPIRED; + } + private static final int X509_V_FLAG_CRL_CHECK = (int) 4L; /** @@ -3091,6 +3131,66 @@ } } + private static class X509_STORE_CTX_get0_store { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + public static final MemorySegment ADDR = openssl_h.findOrThrow("X509_STORE_CTX_get0_store"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * Function descriptor for: + * {@snippet lang=c : + * X509_STORE *X509_STORE_CTX_get0_store(const X509_STORE_CTX *ctx) + * } + */ + public static FunctionDescriptor X509_STORE_CTX_get0_store$descriptor() { + return X509_STORE_CTX_get0_store.DESC; + } + + /** + * Downcall method handle for: + * {@snippet lang=c : + * X509_STORE *X509_STORE_CTX_get0_store(const X509_STORE_CTX *ctx) + * } + */ + public static MethodHandle X509_STORE_CTX_get0_store$handle() { + return X509_STORE_CTX_get0_store.HANDLE; + } + + /** + * Address for: + * {@snippet lang=c : + * X509_STORE *X509_STORE_CTX_get0_store(const X509_STORE_CTX *ctx) + * } + */ + public static MemorySegment X509_STORE_CTX_get0_store$address() { + return X509_STORE_CTX_get0_store.ADDR; + } + + /** + * {@snippet lang=c : + * X509_STORE *X509_STORE_CTX_get0_store(const X509_STORE_CTX *ctx) + * } + */ + public static MemorySegment X509_STORE_CTX_get0_store(MemorySegment ctx) { + var mh$ = X509_STORE_CTX_get0_store.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("X509_STORE_CTX_get0_store", ctx); + } + return (MemorySegment)mh$.invokeExact(ctx); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + private static class X509_STORE_CTX_get0_untrusted { public static final FunctionDescriptor DESC = FunctionDescriptor.of(openssl_h.C_POINTER, openssl_h.C_POINTER); @@ -5846,50 +5946,50 @@ } } - private static class SSL_get1_peer_certificate { + private static class SSL_get0_peer_certificate { public static final FunctionDescriptor DESC = FunctionDescriptor.of(openssl_h.C_POINTER, openssl_h.C_POINTER); - public static final MemorySegment ADDR = openssl_h.findOrThrow("SSL_get1_peer_certificate"); + public static final MemorySegment ADDR = openssl_h.findOrThrow("SSL_get0_peer_certificate"); public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } /** * Function descriptor for: - * {@snippet lang = c : * X509 *SSL_get1_peer_certificate(const SSL *s) + * {@snippet lang = c : * X509 *SSL_get0_peer_certificate(const SSL *s) * } */ - public static FunctionDescriptor SSL_get1_peer_certificate$descriptor() { - return SSL_get1_peer_certificate.DESC; + public static FunctionDescriptor SSL_get0_peer_certificate$descriptor() { + return SSL_get0_peer_certificate.DESC; } /** * Downcall method handle for: - * {@snippet lang = c : * X509 *SSL_get1_peer_certificate(const SSL *s) + * {@snippet lang = c : * X509 *SSL_get0_peer_certificate(const SSL *s) * } */ - public static MethodHandle SSL_get1_peer_certificate$handle() { - return SSL_get1_peer_certificate.HANDLE; + public static MethodHandle SSL_get0_peer_certificate$handle() { + return SSL_get0_peer_certificate.HANDLE; } /** * Address for: - * {@snippet lang = c : * X509 *SSL_get1_peer_certificate(const SSL *s) + * {@snippet lang = c : * X509 *SSL_get0_peer_certificate(const SSL *s) * } */ - public static MemorySegment SSL_get1_peer_certificate$address() { - return SSL_get1_peer_certificate.ADDR; + public static MemorySegment SSL_get0_peer_certificate$address() { + return SSL_get0_peer_certificate.ADDR; } /** - * {@snippet lang = c : * X509 *SSL_get1_peer_certificate(const SSL *s) + * {@snippet lang = c : * X509 *SSL_get0_peer_certificate(const SSL *s) * } */ - public static MemorySegment SSL_get1_peer_certificate(MemorySegment s) { - var mh$ = SSL_get1_peer_certificate.HANDLE; + public static MemorySegment SSL_get0_peer_certificate(MemorySegment s) { + var mh$ = SSL_get0_peer_certificate.HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall("SSL_get1_peer_certificate", s); + traceDowncall("SSL_get0_peer_certificate", s); } return (MemorySegment) mh$.invokeExact(s); } catch (Throwable ex$) { @@ -8792,6 +8892,315 @@ } } + private static class OCSP_resp_get0_certs { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + public static final MemorySegment ADDR = openssl_h.findOrThrow("OCSP_resp_get0_certs"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * Function descriptor for: + * {@snippet lang=c : + * const struct stack_st_X509 *OCSP_resp_get0_certs(const OCSP_BASICRESP *bs) + * } + */ + public static FunctionDescriptor OCSP_resp_get0_certs$descriptor() { + return OCSP_resp_get0_certs.DESC; + } + + /** + * Downcall method handle for: + * {@snippet lang=c : + * const struct stack_st_X509 *OCSP_resp_get0_certs(const OCSP_BASICRESP *bs) + * } + */ + public static MethodHandle OCSP_resp_get0_certs$handle() { + return OCSP_resp_get0_certs.HANDLE; + } + + /** + * Address for: + * {@snippet lang=c : + * const struct stack_st_X509 *OCSP_resp_get0_certs(const OCSP_BASICRESP *bs) + * } + */ + public static MemorySegment OCSP_resp_get0_certs$address() { + return OCSP_resp_get0_certs.ADDR; + } + + /** + * {@snippet lang=c : + * const struct stack_st_X509 *OCSP_resp_get0_certs(const OCSP_BASICRESP *bs) + * } + */ + public static MemorySegment OCSP_resp_get0_certs(MemorySegment bs) { + var mh$ = OCSP_resp_get0_certs.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_resp_get0_certs", bs); + } + return (MemorySegment)mh$.invokeExact(bs); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static class OCSP_basic_verify { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_LONG + ); + + public static final MemorySegment ADDR = openssl_h.findOrThrow("OCSP_basic_verify"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * Function descriptor for: + * {@snippet lang=c : + * int OCSP_basic_verify(OCSP_BASICRESP *bs, struct stack_st_X509 *certs, X509_STORE *st, unsigned long flags) + * } + */ + public static FunctionDescriptor OCSP_basic_verify$descriptor() { + return OCSP_basic_verify.DESC; + } + + /** + * Downcall method handle for: + * {@snippet lang=c : + * int OCSP_basic_verify(OCSP_BASICRESP *bs, struct stack_st_X509 *certs, X509_STORE *st, unsigned long flags) + * } + */ + public static MethodHandle OCSP_basic_verify$handle() { + return OCSP_basic_verify.HANDLE; + } + + /** + * Address for: + * {@snippet lang=c : + * int OCSP_basic_verify(OCSP_BASICRESP *bs, struct stack_st_X509 *certs, X509_STORE *st, unsigned long flags) + * } + */ + public static MemorySegment OCSP_basic_verify$address() { + return OCSP_basic_verify.ADDR; + } + + /** + * {@snippet lang=c : + * int OCSP_basic_verify(OCSP_BASICRESP *bs, struct stack_st_X509 *certs, X509_STORE *st, unsigned long flags) + * } + */ + public static int OCSP_basic_verify(MemorySegment bs, MemorySegment certs, MemorySegment st, long flags) { + var mh$ = OCSP_basic_verify.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_basic_verify", bs, certs, st, flags); + } + return (int)mh$.invokeExact(bs, certs, st, flags); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static class OCSP_check_validity { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER, + openssl_h.C_LONG, + openssl_h.C_LONG + ); + + public static final MemorySegment ADDR = openssl_h.findOrThrow("OCSP_check_validity"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * Function descriptor for: + * {@snippet lang=c : + * int OCSP_check_validity(ASN1_GENERALIZEDTIME *thisupd, ASN1_GENERALIZEDTIME *nextupd, long sec, long maxsec) + * } + */ + public static FunctionDescriptor OCSP_check_validity$descriptor() { + return OCSP_check_validity.DESC; + } + + /** + * Downcall method handle for: + * {@snippet lang=c : + * int OCSP_check_validity(ASN1_GENERALIZEDTIME *thisupd, ASN1_GENERALIZEDTIME *nextupd, long sec, long maxsec) + * } + */ + public static MethodHandle OCSP_check_validity$handle() { + return OCSP_check_validity.HANDLE; + } + + /** + * Address for: + * {@snippet lang=c : + * int OCSP_check_validity(ASN1_GENERALIZEDTIME *thisupd, ASN1_GENERALIZEDTIME *nextupd, long sec, long maxsec) + * } + */ + public static MemorySegment OCSP_check_validity$address() { + return OCSP_check_validity.ADDR; + } + + /** + * {@snippet lang=c : + * int OCSP_check_validity(ASN1_GENERALIZEDTIME *thisupd, ASN1_GENERALIZEDTIME *nextupd, long sec, long maxsec) + * } + */ + public static int OCSP_check_validity(MemorySegment thisupd, MemorySegment nextupd, long sec, long maxsec) { + var mh$ = OCSP_check_validity.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_check_validity", thisupd, nextupd, sec, maxsec); + } + return (int)mh$.invokeExact(thisupd, nextupd, sec, maxsec); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static class OCSP_request_add1_nonce { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + ValueLayout.JAVA_CHAR, + openssl_h.C_INT + ); + + public static final MemorySegment ADDR = openssl_h.findOrThrow("OCSP_request_add1_nonce"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * Function descriptor for: + * {@snippet lang=c : + * int OCSP_request_add1_nonce(OCSP_REQUEST *req, unsigned char *val, int len) + * } + */ + public static FunctionDescriptor OCSP_request_add1_nonce$descriptor() { + return OCSP_request_add1_nonce.DESC; + } + + /** + * Downcall method handle for: + * {@snippet lang=c : + * int OCSP_request_add1_nonce(OCSP_REQUEST *req, unsigned char *val, int len) + * } + */ + public static MethodHandle OCSP_request_add1_nonce$handle() { + return OCSP_request_add1_nonce.HANDLE; + } + + /** + * Address for: + * {@snippet lang=c : + * int OCSP_request_add1_nonce(OCSP_REQUEST *req, unsigned char *val, int len) + * } + */ + public static MemorySegment OCSP_request_add1_nonce$address() { + return OCSP_request_add1_nonce.ADDR; + } + + /** + * {@snippet lang=c : + * int OCSP_request_add1_nonce(OCSP_REQUEST *req, unsigned char *val, int len) + * } + */ + public static int OCSP_request_add1_nonce(MemorySegment req, char val, int len) { + var mh$ = OCSP_request_add1_nonce.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_request_add1_nonce", req, val, len); + } + return (int)mh$.invokeExact(req, val, len); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static class OCSP_check_nonce { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + openssl_h.C_INT, + openssl_h.C_POINTER, + openssl_h.C_POINTER + ); + + public static final MemorySegment ADDR = openssl_h.findOrThrow("OCSP_check_nonce"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * Function descriptor for: + * {@snippet lang=c : + * int OCSP_check_nonce(OCSP_REQUEST *req, OCSP_BASICRESP *bs) + * } + */ + public static FunctionDescriptor OCSP_check_nonce$descriptor() { + return OCSP_check_nonce.DESC; + } + + /** + * Downcall method handle for: + * {@snippet lang=c : + * int OCSP_check_nonce(OCSP_REQUEST *req, OCSP_BASICRESP *bs) + * } + */ + public static MethodHandle OCSP_check_nonce$handle() { + return OCSP_check_nonce.HANDLE; + } + + /** + * Address for: + * {@snippet lang=c : + * int OCSP_check_nonce(OCSP_REQUEST *req, OCSP_BASICRESP *bs) + * } + */ + public static MemorySegment OCSP_check_nonce$address() { + return OCSP_check_nonce.ADDR; + } + + /** + * {@snippet lang=c : + * int OCSP_check_nonce(OCSP_REQUEST *req, OCSP_BASICRESP *bs) + * } + */ + public static int OCSP_check_nonce(MemorySegment req, MemorySegment bs) { + var mh$ = OCSP_check_nonce.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("OCSP_check_nonce", req, bs); + } + return (int)mh$.invokeExact(req, bs); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + private static class OCSP_cert_to_id { public static final FunctionDescriptor DESC = FunctionDescriptor.of(openssl_h.C_POINTER, openssl_h.C_POINTER, openssl_h.C_POINTER, openssl_h.C_POINTER); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java tomcat11-11.0.22/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java 2026-05-01 18:56:05.000000000 +0000 @@ -22,7 +22,6 @@ import static java.lang.foreign.ValueLayout.*; import static org.apache.tomcat.util.openssl.openssl_h.OpenSSL_version; import static org.apache.tomcat.util.openssl.openssl_h.OpenSSL_version_num; -import static org.apache.tomcat.util.openssl.openssl_h.SSL_get1_peer_certificate; /** * Methods used present in older OpenSSL versions but not in the current major version or OpenSSL derivatives. @@ -31,19 +30,50 @@ public class openssl_h_Compatibility { public static final boolean OPENSSL; + public static final boolean OPENSSL1; public static final boolean OPENSSL3; public static final boolean BORINGSSL; public static final boolean LIBRESSL; + + public static final int MAJOR; + public static final int MINOR; + static { String versionString = OpenSSL_version(0).getString(0); OPENSSL = versionString.contains("OpenSSL"); + OPENSSL1 = OPENSSL && OpenSSL_version_num() < 0x3000000fL; OPENSSL3 = OPENSSL && OpenSSL_version_num() >= 0x3000000fL; BORINGSSL = versionString.contains("BoringSSL"); LIBRESSL = versionString.contains("LibreSSL"); + int majorVersion = 0; + int minorVersion = 0; + try { + String[] blocks = versionString.split("\\s"); + if (blocks.length >= 2) { + versionString = blocks[1]; + } + String[] versionNumberStrings = versionString.split("\\."); + if (versionNumberStrings.length >= 2) { + majorVersion = Integer.parseInt(versionNumberStrings[0]); + minorVersion = Integer.parseInt(versionNumberStrings[1]); + } + } catch (Exception e) { + // Ignore, default to 0 + } finally { + MAJOR = majorVersion; + MINOR = minorVersion; + } + } + + public static boolean isLibreSSLPre35() { + return LIBRESSL && ((MAJOR == 3 && MINOR < 5) || MAJOR < 3); } // OpenSSL 1.1 FIPS_mode public static int FIPS_mode() { + if (isLibreSSLPre35()) { + return 0; + } class Holder { static final String NAME = "FIPS_mode"; static final FunctionDescriptor DESC = FunctionDescriptor.of(JAVA_INT); @@ -62,6 +92,9 @@ // OpenSSL 1.1 FIPS_mode_set public static int FIPS_mode_set(int r) { + if (isLibreSSLPre35()) { + return 0; + } class Holder { static final String NAME = "FIPS_mode_set"; static final FunctionDescriptor DESC = FunctionDescriptor.of(JAVA_INT, JAVA_INT); @@ -117,22 +150,23 @@ // OpenSSL 1.1 SSL_get_peer_certificate public static MemorySegment SSL_get_peer_certificate(MemorySegment s) { if (OPENSSL3) { - return SSL_get1_peer_certificate(s); - } else { - class Holder { - static final String NAME = "SSL_get_peer_certificate"; - static final FunctionDescriptor DESC = FunctionDescriptor.of(openssl_h.C_POINTER, openssl_h.C_POINTER); - static final MethodHandle MH = Linker.nativeLinker().downcallHandle(openssl_h.findOrThrow(NAME), DESC); - } - var mh$ = Holder.MH; - try { - if (openssl_h.TRACE_DOWNCALLS) { - openssl_h.traceDowncall(Holder.NAME, s); - } - return (java.lang.foreign.MemorySegment) mh$.invokeExact(s); - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); + // This could be using SSL_get1_peer_certificate instead, as all the other implementations + // use SSL_get_peer_certificate which is equivalent to SSL_get1_peer_certificate + return MemorySegment.NULL; + } + class Holder { + static final String NAME = "SSL_get_peer_certificate"; + static final FunctionDescriptor DESC = FunctionDescriptor.of(openssl_h.C_POINTER, openssl_h.C_POINTER); + static final MethodHandle MH = Linker.nativeLinker().downcallHandle(openssl_h.findOrThrow(NAME), DESC); + } + var mh$ = Holder.MH; + try { + if (openssl_h.TRACE_DOWNCALLS) { + openssl_h.traceDowncall(Holder.NAME, s); } + return (java.lang.foreign.MemorySegment) mh$.invokeExact(s); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); } } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java tomcat11-11.0.22/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java 2026-05-01 18:56:05.000000000 +0000 @@ -371,6 +371,24 @@ /** + * Set automatic dh. + * {@snippet lang = c : # define SSL_CTX_set_dh_auto(ctx, onoff) \ + * SSL_CTX_ctrl(ctx,SSL_CTRL_SET_DH_AUTO,onoff,NULL) + * } + * @param sslCtx the SSL context + * @param onoff 1 to enable + * @return > 0 if successful + */ + public static long SSL_CTX_set_dh_auto(MemorySegment sslCtx, int onoff) { + if (openssl_h_Compatibility.BORINGSSL) { + return 1; + } else { + return SSL_CTX_ctrl(sslCtx, SSL_CTRL_SET_DH_AUTO(), 1, MemorySegment.NULL); + } + } + + + /** * Free memory. * {@snippet lang = c : # define OPENSSL_free(addr) CRYPTO_free(addr, OPENSSL_FILE, OPENSSL_LINE) * } diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/util/security/ConstantTime.java tomcat11-11.0.22/java/org/apache/tomcat/util/security/ConstantTime.java --- tomcat11-11.0.15/java/org/apache/tomcat/util/security/ConstantTime.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/util/security/ConstantTime.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,142 @@ +/* + * 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.util.security; + +import java.security.MessageDigest; + +import org.apache.tomcat.util.buf.ByteChunk; + +/** + * Utility class for methods that, for security reasons, need to run in - as far as practical - constant time. + */ +public class ConstantTime { + + private ConstantTime() { + // Hide default constructor for this utility class + } + + + /** + * Implements String equality which always compares all characters in the string, without stopping early if any + * characters do not match. + *

    + * Note: This implementation was adapted from {@link MessageDigest#isEqual} which we assume is as + * optimizer-defeating as possible. + * + * @param s1 The first string to compare. + * @param s2 The second string to compare. + * @param ignoreCase true if the strings should be compared without regard to case. Note that "true" + * here is only guaranteed to work with plain ASCII characters. + * + * @return true if the strings are equal to each other, false otherwise. + */ + public static boolean equals(final String s1, final String s2, final boolean ignoreCase) { + if (s1 == s2) { + return true; + } + if (s1 == null || s2 == null) { + return false; + } + + final int len1 = s1.length(); + final int len2 = s2.length(); + + if (len2 == 0) { + return len1 == 0; + } + + int result = 0; + result |= len1 - len2; + + // time-constant comparison + for (int i = 0; i < len1; i++) { + // If i >= len2, index2 is 0; otherwise, i. + final int index2 = ((i - len2) >>> 31) * i; + char c1 = s1.charAt(i); + char c2 = s2.charAt(index2); + if (ignoreCase) { + c1 = Character.toLowerCase(c1); + c2 = Character.toLowerCase(c2); + } + result |= c1 ^ c2; + } + return result == 0; + } + + + /** + * Implements ByteChunk / String equality which always compares all characters, without stopping early if any + * characters do not match. + *

    + * Note: This implementation was adapted from {@link MessageDigest#isEqual} which we assume is as + * optimizer-defeating as possible. + * + * @param bc The ByteChunk to compare. + * @param s The string to compare. + * + * @return true if the strings are equal to each other, false otherwise. + */ + public static boolean equals(final ByteChunk bc, final String s) { + if (bc == null && s == null) { + return true; + } + if (bc == null || s == null) { + return false; + } + + final int len1 = bc.getLength(); + final int len2 = s.length(); + + byte[] bytes = bc.getBytes(); + + if (len2 == 0) { + return len1 == 0; + } + + int result = 0; + result |= len1 - len2; + + // time-constant comparison + for (int i = 0; i < len1; i++) { + // If i >= len2, index2 is 0; otherwise, i. + final int index2 = ((i - len2) >>> 31) * i; + byte b = bytes[bc.getStart() + i]; + char c = s.charAt(index2); + result |= (b & 0xFF) ^ c; + } + return result == 0; + } + + + /** + * Implements byte-array equality which always compares all bytes in the array, without stopping early if any bytes + * do not match. + *

    + * Note: Implementation note: this method delegates to {@link MessageDigest#isEqual} under the assumption + * that it provides a constant-time comparison of the bytes in the arrays. Java 7+ has such an implementation, but + * neither the Javadoc nor any specification requires it. Therefore, Tomcat should continue to use this + * method internally in case the JDK implementation changes so this method can be re-implemented properly. + * + * @param b1 The first array to compare. + * @param b2 The second array to compare. + * + * @return true if the arrays are equal to each other, false otherwise. + */ + public static boolean equals(final byte[] b1, final byte[] b2) { + return MessageDigest.isEqual(b1, b2); + } +} diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/websocket/Authenticator.java tomcat11-11.0.22/java/org/apache/tomcat/websocket/Authenticator.java --- tomcat11-11.0.15/java/org/apache/tomcat/websocket/Authenticator.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/websocket/Authenticator.java 2026-05-01 18:56:05.000000000 +0000 @@ -21,6 +21,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.tomcat.util.http.Method; import org.apache.tomcat.util.res.StringManager; /** @@ -46,9 +47,34 @@ * @return The generated authorization header value * * @throws AuthenticationException When an error occurs + * + * @deprecated Unused. Will be remove in Tomcat 12. Use + * {@link #getAuthorization(String, String, String, String, String, String)} + */ + @Deprecated + public String getAuthorization(String requestUri, String authenticateHeader, String userName, String userPassword, + String userRealm) throws AuthenticationException { + return getAuthorization(Method.GET, requestUri, authenticateHeader, userName, userPassword, userRealm); + } + + + /** + * Generate the authorization header value that will be sent to the server. + * + * @param method The request method + * @param requestUri The request URI + * @param authenticateHeader The server authentication header received + * @param userName The username + * @param userPassword The user password + * @param userRealm The realm for which the provided username and password are valid. {@code null} to + * indicate all realms. + * + * @return The generated authorization header value + * + * @throws AuthenticationException When an error occurs */ - public abstract String getAuthorization(String requestUri, String authenticateHeader, String userName, - String userPassword, String userRealm) throws AuthenticationException; + public abstract String getAuthorization(String method, String requestUri, String authenticateHeader, + String userName, String userPassword, String userRealm) throws AuthenticationException; /** diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/websocket/BasicAuthenticator.java tomcat11-11.0.22/java/org/apache/tomcat/websocket/BasicAuthenticator.java --- tomcat11-11.0.15/java/org/apache/tomcat/websocket/BasicAuthenticator.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/websocket/BasicAuthenticator.java 2026-05-01 18:56:05.000000000 +0000 @@ -30,8 +30,8 @@ public static final String charsetparam = "charset"; @Override - public String getAuthorization(String requestUri, String authenticateHeader, String userName, String userPassword, - String userRealm) throws AuthenticationException { + public String getAuthorization(String method, String requestUri, String authenticateHeader, String userName, + String userPassword, String userRealm) throws AuthenticationException { validateUsername(userName); validatePassword(userPassword); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/websocket/DigestAuthenticator.java tomcat11-11.0.22/java/org/apache/tomcat/websocket/DigestAuthenticator.java --- tomcat11-11.0.15/java/org/apache/tomcat/websocket/DigestAuthenticator.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/websocket/DigestAuthenticator.java 2026-05-01 18:56:05.000000000 +0000 @@ -40,8 +40,8 @@ private long cNonce; @Override - public String getAuthorization(String requestUri, String authenticateHeader, String userName, String userPassword, - String userRealm) throws AuthenticationException { + public String getAuthorization(String method, String requestUri, String authenticateHeader, String userName, + String userPassword, String userRealm) throws AuthenticationException { validateUsername(userName); validatePassword(userPassword); @@ -72,15 +72,15 @@ } challenge.append("Digest "); - challenge.append("username =\"").append(userName).append("\","); + challenge.append("username=\"").append(userName).append("\","); challenge.append("realm=\"").append(realm).append("\","); challenge.append("nonce=\"").append(nonce).append("\","); challenge.append("uri=\"").append(requestUri).append("\","); try { challenge.append("response=\""); - challenge.append( - calculateRequestDigest(requestUri, userName, userPassword, realm, nonce, messageQop, algorithm)); + challenge.append(calculateRequestDigest(method, requestUri, userName, userPassword, realm, nonce, + messageQop, algorithm)); challenge.append("\","); } @@ -89,7 +89,9 @@ } challenge.append("algorithm=").append(algorithm).append(","); - challenge.append("opaque=\"").append(opaque).append("\","); + if (opaque != null) { + challenge.append("opaque=\"").append(opaque).append("\","); + } if (!messageQop.isEmpty()) { challenge.append("qop=\"").append(messageQop).append("\""); @@ -101,8 +103,8 @@ } - private String calculateRequestDigest(String requestUri, String userName, String password, String realm, - String nonce, String qop, String algorithm) throws NoSuchAlgorithmException { + private String calculateRequestDigest(String method, String requestUri, String userName, String password, + String realm, String nonce, String qop, String algorithm) throws NoSuchAlgorithmException { boolean session = false; if (algorithm.endsWith("-sess")) { @@ -123,7 +125,7 @@ * If the "qop" value is "auth-int", then A2 is: A2 = Method ":" digest-uri-value ":" H(entity-body) since we do * not have an entity-body, A2 = Method ":" digest-uri-value for auth and auth_int */ - String A2 = "GET:" + requestUri; + String A2 = method + ":" + requestUri; preDigest.append(encode(algorithm, A1)); preDigest.append(':'); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java tomcat11-11.0.22/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java --- tomcat11-11.0.15/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java 2026-05-01 18:56:05.000000000 +0000 @@ -978,9 +978,7 @@ @Override public void write(int b) throws IOException { - if (closed) { - throw new IllegalStateException(sm.getString("wsRemoteEndpoint.closedOutputStream")); - } + checkOpen(); used = true; if (buffer.remaining() == 0) { @@ -991,9 +989,7 @@ @Override public void write(byte[] b, int off, int len) throws IOException { - if (closed) { - throw new IllegalStateException(sm.getString("wsRemoteEndpoint.closedOutputStream")); - } + checkOpen(); if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } @@ -1021,9 +1017,7 @@ @Override public void flush() throws IOException { - if (closed) { - throw new IllegalStateException(sm.getString("wsRemoteEndpoint.closedOutputStream")); - } + checkOpen(); // Optimisation. If there is no data to flush then do not send an // empty message. @@ -1044,6 +1038,12 @@ doWrite(true); } + private void checkOpen() throws IOException { + if (closed) { + throw new IOException(sm.getString("wsRemoteEndpoint.closedOutputStream")); + } + } + private void doWrite(boolean last) throws IOException { if (used) { buffer.flip(); @@ -1069,9 +1069,7 @@ @Override public void write(char[] cbuf, int off, int len) throws IOException { - if (closed) { - throw new IllegalStateException(sm.getString("wsRemoteEndpoint.closedWriter")); - } + checkOpen(); if ((off < 0) || (off > cbuf.length) || (len < 0) || ((off + len) > cbuf.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } @@ -1099,9 +1097,7 @@ @Override public void flush() throws IOException { - if (closed) { - throw new IllegalStateException(sm.getString("wsRemoteEndpoint.closedWriter")); - } + checkOpen(); if (buffer.position() > 0) { doWrite(false); @@ -1120,6 +1116,12 @@ doWrite(true); } + private void checkOpen() throws IOException { + if (closed) { + throw new IOException(sm.getString("wsRemoteEndpoint.closedWriter")); + } + } + private void doWrite(boolean last) throws IOException { if (used) { buffer.flip(); diff -Nru tomcat11-11.0.15/java/org/apache/tomcat/websocket/WsWebSocketContainer.java tomcat11-11.0.22/java/org/apache/tomcat/websocket/WsWebSocketContainer.java --- tomcat11-11.0.15/java/org/apache/tomcat/websocket/WsWebSocketContainer.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/java/org/apache/tomcat/websocket/WsWebSocketContainer.java 2026-05-01 18:56:05.000000000 +0000 @@ -69,6 +69,7 @@ import org.apache.tomcat.InstanceManagerBindings; import org.apache.tomcat.util.buf.StringUtils; import org.apache.tomcat.util.collections.CaseInsensitiveKeyMap; +import org.apache.tomcat.util.http.Method; import org.apache.tomcat.util.res.StringManager; public class WsWebSocketContainer implements WebSocketContainer, BackgroundProcess { @@ -173,40 +174,41 @@ private Session connectToServerRecursive(ClientEndpointHolder clientEndpointHolder, - ClientEndpointConfig clientEndpointConfiguration, URI path, Set redirectSet) + ClientEndpointConfig clientEndpointConfiguration, URI serverEndpointUri, Set redirectSet) throws DeploymentException { if (log.isTraceEnabled()) { - log.trace(sm.getString("wsWebSocketContainer.connect.entry", clientEndpointHolder.getClassName(), path)); + log.trace(sm.getString("wsWebSocketContainer.connect.entry", clientEndpointHolder.getClassName(), + serverEndpointUri)); } boolean secure = false; ByteBuffer proxyConnect = null; - URI proxyPath; + URI proxyUri; // Validate scheme (and build proxyPath) - String scheme = path.getScheme(); + String scheme = serverEndpointUri.getScheme(); if ("ws".equalsIgnoreCase(scheme)) { - proxyPath = URI.create("http" + path.toString().substring(2)); + proxyUri = URI.create("http" + serverEndpointUri.toString().substring(2)); } else if ("wss".equalsIgnoreCase(scheme)) { - proxyPath = URI.create("https" + path.toString().substring(3)); + proxyUri = URI.create("https" + serverEndpointUri.toString().substring(3)); secure = true; } else { throw new DeploymentException(sm.getString("wsWebSocketContainer.pathWrongScheme", scheme)); } - // Validate host - String host = path.getHost(); - if (host == null) { + // Validate server endpoint host + String serverEndpointHost = serverEndpointUri.getHost(); + if (serverEndpointHost == null) { throw new DeploymentException(sm.getString("wsWebSocketContainer.pathNoHost")); } - int port = path.getPort(); + int serverEndpointPort = serverEndpointUri.getPort(); SocketAddress sa = null; // Check to see if a proxy is configured. Javadoc indicates return value // will never be null - List proxies = ProxySelector.getDefault().select(proxyPath); + List proxies = ProxySelector.getDefault().select(proxyUri); Proxy selectedProxy = null; for (Proxy proxy : proxies) { if (proxy.type().equals(Proxy.Type.HTTP)) { @@ -223,12 +225,12 @@ // If the port is not explicitly specified, compute it based on the // scheme - if (port == -1) { + if (serverEndpointPort == -1) { if ("ws".equalsIgnoreCase(scheme)) { - port = 80; + serverEndpointPort = 80; } else { // Must be wss due to scheme validation above - port = 443; + serverEndpointPort = 443; } } @@ -236,21 +238,23 @@ // If sa is null, no proxy is configured so need to create sa if (sa == null) { - sa = new InetSocketAddress(host, port); + sa = new InetSocketAddress(serverEndpointHost, serverEndpointPort); } else { - proxyConnect = createProxyRequest(host, port, + proxyConnect = createProxyRequest(serverEndpointHost, serverEndpointPort, (String) userProperties.get(Constants.PROXY_AUTHORIZATION_HEADER_NAME)); } // Create the initial HTTP request to open the WebSocket connection - Map> reqHeaders = createRequestHeaders(host, port, secure, clientEndpointConfiguration); - clientEndpointConfiguration.getConfigurator().beforeRequest(reqHeaders); - if (Constants.DEFAULT_ORIGIN_HEADER_VALUE != null && !reqHeaders.containsKey(Constants.ORIGIN_HEADER_NAME)) { + Map> upgradeRequestHeaders = + createRequestHeaders(serverEndpointHost, serverEndpointPort, secure, clientEndpointConfiguration); + clientEndpointConfiguration.getConfigurator().beforeRequest(upgradeRequestHeaders); + if (Constants.DEFAULT_ORIGIN_HEADER_VALUE != null && + !upgradeRequestHeaders.containsKey(Constants.ORIGIN_HEADER_NAME)) { List originValues = new ArrayList<>(1); originValues.add(Constants.DEFAULT_ORIGIN_HEADER_VALUE); - reqHeaders.put(Constants.ORIGIN_HEADER_NAME, originValues); + upgradeRequestHeaders.put(Constants.ORIGIN_HEADER_NAME, originValues); } - ByteBuffer request = createRequest(path, reqHeaders); + ByteBuffer upgradeRequest = createRequest(serverEndpointUri, upgradeRequestHeaders); // Get the connection timeout long timeout = Constants.IO_TIMEOUT_MS_DEFAULT; @@ -286,19 +290,23 @@ writeRequest(channel, proxyConnect, timeout); HttpResponse httpResponse = processResponse(response, channel, timeout); if (httpResponse.status == Constants.PROXY_AUTHENTICATION_REQUIRED) { - return processAuthenticationChallenge(clientEndpointHolder, clientEndpointConfiguration, path, - redirectSet, userProperties, request, httpResponse, AuthenticationType.PROXY); + return processAuthenticationChallenge(clientEndpointHolder, clientEndpointConfiguration, + serverEndpointUri, redirectSet, userProperties, Method.CONNECT, + serverEndpointHost + ":" + serverEndpointPort, httpResponse, AuthenticationType.PROXY); } else if (httpResponse.status() != 200) { throw new DeploymentException(sm.getString("wsWebSocketContainer.proxyConnectFail", selectedProxy, Integer.toString(httpResponse.status()))); } + // Proxy authentication either successful or not required. + userProperties.remove(Constants.PROXY_AUTHORIZATION_HEADER_NAME); } if (secure) { // Regardless of whether a non-secure wrapper was created for a // proxy CONNECT, need to use TLS from this point on so wrap the // original AsynchronousSocketChannel - SSLEngine sslEngine = createSSLEngine(clientEndpointConfiguration, host, port); + SSLEngine sslEngine = + createSSLEngine(clientEndpointConfiguration, serverEndpointHost, serverEndpointPort); channel = new AsyncChannelWrapperSecure(socketChannel, sslEngine); } else if (channel == null) { // Only need to wrap as this point if it wasn't wrapped to process a @@ -318,10 +326,10 @@ } catch (IOException ioe) { // Ignore } - log.trace(sm.getString("wsWebSocketContainer.connect.write", Integer.valueOf(request.position()), - Integer.valueOf(request.limit()), localAddress)); + log.trace(sm.getString("wsWebSocketContainer.connect.write", Integer.valueOf(upgradeRequest.position()), + Integer.valueOf(upgradeRequest.limit()), localAddress)); } - writeRequest(channel, request, timeout); + writeRequest(channel, upgradeRequest, timeout); HttpResponse httpResponse = processResponse(response, channel, timeout); @@ -334,6 +342,9 @@ if (httpResponse.status != 101) { if (isRedirectStatus(httpResponse.status)) { + // HTTP redirect. Authentication either successful or not required. + userProperties.remove(Constants.AUTHORIZATION_HEADER_NAME); + List locationHeader = httpResponse.handshakeResponse().getHeaders().get(Constants.LOCATION_HEADER_NAME); @@ -346,7 +357,7 @@ URI redirectLocation = URI.create(locationHeader.get(0)).normalize(); if (!redirectLocation.isAbsolute()) { - redirectLocation = path.resolve(redirectLocation); + redirectLocation = serverEndpointUri.resolve(redirectLocation); } String redirectScheme = redirectLocation.getScheme().toLowerCase(Locale.ENGLISH); @@ -367,14 +378,20 @@ redirectSet); } else if (httpResponse.status == Constants.UNAUTHORIZED) { - return processAuthenticationChallenge(clientEndpointHolder, clientEndpointConfiguration, path, - redirectSet, userProperties, request, httpResponse, AuthenticationType.WWW); + String authenticationUri = + new String(upgradeRequest.array(), StandardCharsets.ISO_8859_1).split("\\s", 3)[1]; + return processAuthenticationChallenge(clientEndpointHolder, clientEndpointConfiguration, + serverEndpointUri, redirectSet, userProperties, Method.GET, authenticationUri, httpResponse, + AuthenticationType.WWW); } else { throw new DeploymentException( sm.getString("wsWebSocketContainer.invalidStatus", Integer.toString(httpResponse.status))); } } + // HTTP upgrade successful. Authentication either successful or not required. + userProperties.remove(Constants.AUTHORIZATION_HEADER_NAME); + HandshakeResponse handshakeResponse = httpResponse.handshakeResponse(); clientEndpointConfiguration.getConfigurator().afterResponse(handshakeResponse); @@ -417,7 +434,7 @@ success = true; } catch (ExecutionException | InterruptedException | SSLException | EOFException | TimeoutException | URISyntaxException | AuthenticationException e) { - throw new DeploymentException(sm.getString("wsWebSocketContainer.httpRequestFailed", path), e); + throw new DeploymentException(sm.getString("wsWebSocketContainer.httpRequestFailed", serverEndpointUri), e); } finally { if (!success) { if (channel != null) { @@ -461,9 +478,10 @@ private Session processAuthenticationChallenge(ClientEndpointHolder clientEndpointHolder, - ClientEndpointConfig clientEndpointConfiguration, URI path, Set redirectSet, - Map userProperties, ByteBuffer request, HttpResponse httpResponse, - AuthenticationType authenticationType) throws DeploymentException, AuthenticationException { + ClientEndpointConfig clientEndpointConfiguration, URI serverEndpointUri, Set redirectSet, + Map userProperties, String authenticationMethod, String authenticationUri, + HttpResponse httpResponse, AuthenticationType authenticationType) + throws DeploymentException, AuthenticationException { if (userProperties.get(authenticationType.getAuthorizationHeaderName()) != null) { throw new DeploymentException(sm.getString("wsWebSocketContainer.failedAuthentication", @@ -488,15 +506,14 @@ Integer.valueOf(httpResponse.status), authScheme)); } - String requestUri = new String(request.array(), StandardCharsets.ISO_8859_1).split("\\s", 3)[1]; - userProperties.put(authenticationType.getAuthorizationHeaderName(), - auth.getAuthorization(requestUri, authenticateHeaders.get(0), + auth.getAuthorization(authenticationMethod, authenticationUri, authenticateHeaders.get(0), (String) userProperties.get(authenticationType.getUserNameProperty()), (String) userProperties.get(authenticationType.getUserPasswordProperty()), (String) userProperties.get(authenticationType.getUserRealmProperty()))); - return connectToServerRecursive(clientEndpointHolder, clientEndpointConfiguration, path, redirectSet); + return connectToServerRecursive(clientEndpointHolder, clientEndpointConfiguration, serverEndpointUri, + redirectSet); } diff -Nru tomcat11-11.0.15/modules/cxf/pom.xml tomcat11-11.0.22/modules/cxf/pom.xml --- tomcat11-11.0.15/modules/cxf/pom.xml 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/modules/cxf/pom.xml 2026-05-01 18:56:05.000000000 +0000 @@ -29,14 +29,14 @@ Apache CXF for Apache Tomcat CDI Apache CXF packaged for Apache Tomcat CDI - 4.1.1 + 4.1.4 jar 4.0.1 2.1.3 3.0.1 - 2.0.1 + 2.0.2 diff -Nru tomcat11-11.0.15/modules/jdbc-pool/NOTICE tomcat11-11.0.22/modules/jdbc-pool/NOTICE --- tomcat11-11.0.15/modules/jdbc-pool/NOTICE 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/modules/jdbc-pool/NOTICE 2026-05-01 18:56:05.000000000 +0000 @@ -1,5 +1,5 @@ Apache Tomcat JDBC Pool -Copyright 2008-2025 The Apache Software Foundation +Copyright 2008-2026 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). diff -Nru tomcat11-11.0.15/res/bnd/jasper.jar.tmp.bnd tomcat11-11.0.22/res/bnd/jasper.jar.tmp.bnd --- tomcat11-11.0.15/res/bnd/jasper.jar.tmp.bnd 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/bnd/jasper.jar.tmp.bnd 2026-05-01 18:56:05.000000000 +0000 @@ -24,7 +24,6 @@ org.apache.jasper.el,\ org.apache.jasper.optimizations,\ org.apache.jasper.runtime,\ - org.apache.jasper.security,\ org.apache.jasper.servlet,\ org.apache.jasper.tagplugins.jstl,\ org.apache.jasper.tagplugins.jstl.core,\ diff -Nru tomcat11-11.0.15/res/bnd/tomcat-embed-jasper.jar.tmp.bnd tomcat11-11.0.22/res/bnd/tomcat-embed-jasper.jar.tmp.bnd --- tomcat11-11.0.15/res/bnd/tomcat-embed-jasper.jar.tmp.bnd 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/bnd/tomcat-embed-jasper.jar.tmp.bnd 2026-05-01 18:56:05.000000000 +0000 @@ -25,7 +25,6 @@ org.apache.jasper.el,\ org.apache.jasper.optimizations,\ org.apache.jasper.runtime,\ - org.apache.jasper.security,\ org.apache.jasper.servlet,\ org.apache.jasper.tagplugins.jstl,\ org.apache.jasper.tagplugins.jstl.core,\ diff -Nru tomcat11-11.0.15/res/checkstyle/header-al2.txt tomcat11-11.0.22/res/checkstyle/header-al2.txt --- tomcat11-11.0.15/res/checkstyle/header-al2.txt 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/checkstyle/header-al2.txt 2026-05-01 18:56:05.000000000 +0000 @@ -9,7 +9,7 @@ ^(rem)?\W*\(the "License"\); you may not use this file except in compliance with$ ^(rem)?\W*the License\. You may obtain a copy of the License at$ ^(rem)?\W*$ -^(rem)?\W*http://www.apache.org/licenses/LICENSE-2\.0$ +^(rem)?\W*http(s)?://www.apache.org/licenses/LICENSE-2\.0$ ^(rem)?\W*$ ^(rem)?\W*Unless required by applicable law or agreed to in writing, software$ ^(rem)?\W*distributed under the License is distributed on an "AS IS" BASIS,$ diff -Nru tomcat11-11.0.15/res/ide-support/eclipse/eclipse.classpath tomcat11-11.0.22/res/ide-support/eclipse/eclipse.classpath --- tomcat11-11.0.15/res/ide-support/eclipse/eclipse.classpath 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/ide-support/eclipse/eclipse.classpath 2026-05-01 18:56:05.000000000 +0000 @@ -38,5 +38,8 @@ + + + diff -Nru tomcat11-11.0.15/res/ide-support/eclipse/java-compiler-errors-warnings.txt tomcat11-11.0.22/res/ide-support/eclipse/java-compiler-errors-warnings.txt --- tomcat11-11.0.15/res/ide-support/eclipse/java-compiler-errors-warnings.txt 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/ide-support/eclipse/java-compiler-errors-warnings.txt 2026-05-01 18:56:05.000000000 +0000 @@ -79,6 +79,9 @@ Annotations - All - W ([x] on all additional check boxes) + except the following: + + - Unhandled token in '@SupressWar... - I Null analysis - Null pointer access - W diff -Nru tomcat11-11.0.15/res/ide-support/idea/tomcat.iml tomcat11-11.0.22/res/ide-support/idea/tomcat.iml --- tomcat11-11.0.15/res/ide-support/idea/tomcat.iml 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/ide-support/idea/tomcat.iml 2026-05-01 18:56:05.000000000 +0000 @@ -126,6 +126,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru tomcat11-11.0.15/res/ide-support/netbeans/nb-tomcat-build.properties tomcat11-11.0.22/res/ide-support/netbeans/nb-tomcat-build.properties --- tomcat11-11.0.15/res/ide-support/netbeans/nb-tomcat-build.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/ide-support/netbeans/nb-tomcat-build.properties 2026-05-01 18:56:05.000000000 +0000 @@ -37,7 +37,7 @@ # it is not possible to retrieve the classpaths from the build to # use in the NetBeans targets, so they must be explicitly declared -nb-test.classpath=${test.classes}:${tomcat.build}/webapps/examples/WEB-INF/classes:@JUNIT_JAR@:@EASYMOCK_JAR@:@BYTEBUDDY_JAR@:@OBJENESIS_JAR@:@HAMCREST_JAR@:@ECJ_JAR@:@UNBOUNDID_JAR@:${tomcat.classes} +nb-test.classpath=${test.classes}:${tomcat.build}/webapps/examples/WEB-INF/classes:@JUNIT_JAR@:@EASYMOCK_JAR@:@BYTEBUDDY_JAR@:@OBJENESIS_JAR@:@HAMCREST_JAR@:@ECJ_JAR@:@UNBOUNDID_JAR@:@BC_PROVIDER_JAR@:@BC_PKIX_JAR@:@BC_UTIL_JAR@:${tomcat.classes} # Extra properties used by the Tomcat project additional NetBeans targets. diff -Nru tomcat11-11.0.15/res/ide-support/netbeans/project.xml tomcat11-11.0.22/res/ide-support/netbeans/project.xml --- tomcat11-11.0.15/res/ide-support/netbeans/project.xml 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/ide-support/netbeans/project.xml 2026-05-01 18:56:05.000000000 +0000 @@ -189,7 +189,7 @@ test - output/classes:output/testclasses:output/build/webapps/examples/WEB-INF/classes:@JUNIT_JAR@:@EASYMOCK_JAR@:@BYTEBUDDY_JAR@:@OBJENESIS_JAR@:@HAMCREST_JAR@:@UNBOUNDID_JAR@ + output/classes:output/testclasses:output/build/webapps/examples/WEB-INF/classes:@JUNIT_JAR@:@EASYMOCK_JAR@:@BYTEBUDDY_JAR@:@OBJENESIS_JAR@:@HAMCREST_JAR@:@UNBOUNDID_JAR@:@BC_PROVIDER_JAR@:@BC_PKIX_JAR@:@BC_UTIL_JAR@ @BUILD_JAVA_VERSION@ Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/res/install-win/Uninstall.exe.sig and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/res/install-win/Uninstall.exe.sig differ Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/res/install-win/tomcat-installer.exe.sig and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/res/install-win/tomcat-installer.exe.sig differ diff -Nru tomcat11-11.0.15/res/maven/mvn.properties.default tomcat11-11.0.22/res/maven/mvn.properties.default --- tomcat11-11.0.15/res/maven/mvn.properties.default 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/maven/mvn.properties.default 2026-05-01 18:56:05.000000000 +0000 @@ -39,7 +39,7 @@ maven.asf.release.repo.repositoryId=apache.releases.https # Release version info -maven.asf.release.deploy.version=11.0.15 +maven.asf.release.deploy.version=11.0.22 #Where do we load the libraries from tomcat.lib.path=../../output/build/lib diff -Nru tomcat11-11.0.15/res/maven/mvn.properties.release tomcat11-11.0.22/res/maven/mvn.properties.release --- tomcat11-11.0.15/res/maven/mvn.properties.release 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/maven/mvn.properties.release 2026-05-01 18:56:05.000000000 +0000 @@ -18,7 +18,7 @@ # This file was auto-generated by the pre-release Ant target. # Remove "-dev" from the version since this is not a development release. -maven.asf.release.deploy.version=11.0.15 +maven.asf.release.deploy.version=11.0.22 # Re-use the same GPG executable. gpg.exec=C:/Program Files (x86)/GnuPG/bin/gpg.exe diff -Nru tomcat11-11.0.15/res/maven/tomcat-embed-jasper.pom tomcat11-11.0.22/res/maven/tomcat-embed-jasper.pom --- tomcat11-11.0.15/res/maven/tomcat-embed-jasper.pom 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/maven/tomcat-embed-jasper.pom 2026-05-01 18:56:05.000000000 +0000 @@ -48,7 +48,7 @@ org.eclipse.jdt ecj - 3.43.0 + 3.45.0 diff -Nru tomcat11-11.0.15/res/maven/tomcat-jasper.pom tomcat11-11.0.22/res/maven/tomcat-jasper.pom --- tomcat11-11.0.15/res/maven/tomcat-jasper.pom 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/maven/tomcat-jasper.pom 2026-05-01 18:56:05.000000000 +0000 @@ -60,7 +60,7 @@ org.eclipse.jdt ecj - 3.43.0 + 3.45.0 org.apache.tomcat diff -Nru tomcat11-11.0.15/res/maven/tomcat-servlet-api.pom tomcat11-11.0.22/res/maven/tomcat-servlet-api.pom --- tomcat11-11.0.15/res/maven/tomcat-servlet-api.pom 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/maven/tomcat-servlet-api.pom 2026-05-01 18:56:05.000000000 +0000 @@ -34,7 +34,7 @@ http://www.apache.org/licenses/LICENSE-2.0.txt and - http://www.opensource.org/licenses/cddl1.txt and + https://opensource.org/license/CDDL-1.0 and https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt repo diff -Nru tomcat11-11.0.15/res/openssl/openssl-tomcat.conf tomcat11-11.0.22/res/openssl/openssl-tomcat.conf --- tomcat11-11.0.15/res/openssl/openssl-tomcat.conf 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/openssl/openssl-tomcat.conf 2026-05-01 18:56:05.000000000 +0000 @@ -108,12 +108,17 @@ --include-function OCSP_REQUEST_free # header: /usr/include/openssl/ocsp.h --include-function OCSP_REQUEST_new # header: /usr/include/openssl/ocsp.h --include-function OCSP_RESPONSE_free # header: /usr/include/openssl/ocsp.h +--include-function OCSP_basic_verify # header: /usr/include/openssl/ocsp.h --include-function OCSP_cert_to_id # header: /usr/include/openssl/ocsp.h +--include-function OCSP_check_validity # header: /usr/include/openssl/ocsp.h +--include-function OCSP_check_nonce # header: /usr/include/openssl/ocsp.h --include-function OCSP_request_add0_id # header: /usr/include/openssl/ocsp.h +--include-function OCSP_request_add1_nonce # header: /usr/include/openssl/ocsp.h --include-function OCSP_response_get1_basic # header: /usr/include/openssl/ocsp.h --include-function OCSP_response_status # header: /usr/include/openssl/ocsp.h --include-function OCSP_resp_find # header: /usr/include/openssl/ocsp.h --include-function OCSP_resp_get0 # header: /usr/include/openssl/ocsp.h +--include-function OCSP_resp_get0_certs # header: /usr/include/openssl/ocsp.h --include-function OCSP_single_get0_status # header: /usr/include/openssl/ocsp.h --include-function d2i_OCSP_RESPONSE # header: /usr/include/openssl/ocsp.h --include-function i2d_OCSP_REQUEST # header: /usr/include/openssl/ocsp.h @@ -215,7 +220,7 @@ --include-function SSL_get_shutdown # header: /usr/include/openssl/ssl.h --include-function SSL_get_version # header: /usr/include/openssl/ssl.h --include-function SSL_get0_alpn_selected # header: /usr/include/openssl/ssl.h ---include-function SSL_get1_peer_certificate # header: /usr/include/openssl/ssl.h +--include-function SSL_get0_peer_certificate # header: /usr/include/openssl/ssl.h --include-function SSL_in_init # header: /usr/include/openssl/ssl.h --include-function SSL_load_client_CA_file # header: /usr/include/openssl/ssl.h --include-function SSL_new # header: /usr/include/openssl/ssl.h @@ -342,22 +347,26 @@ --include-function X509_STORE_CTX_get_error # header: /usr/include/openssl/x509_vfy.h --include-function X509_STORE_CTX_get_error_depth # header: /usr/include/openssl/x509_vfy.h --include-function X509_STORE_CTX_get_ex_data # header: /usr/include/openssl/x509_vfy.h +--include-function X509_STORE_CTX_get0_store # header: /usr/include/openssl/x509_vfy.h --include-function X509_STORE_CTX_get0_untrusted # header: /usr/include/openssl/x509_vfy.h --include-function X509_STORE_CTX_get1_issuer # header: /usr/include/openssl/x509_vfy.h --include-function X509_STORE_CTX_set_error # header: /usr/include/openssl/x509_vfy.h --include-function X509_STORE_set_flags # header: /usr/include/openssl/x509_vfy.h --include-constant X509_L_ADD_DIR # header: /usr/include/openssl/x509_vfy.h --include-constant X509_L_FILE_LOAD # header: /usr/include/openssl/x509_vfy.h ---include-constant X509_V_ERR_APPLICATION_VERIFICATION # header: /usr/include/openssl/x509_vfy.h ---include-constant X509_V_ERR_CERT_UNTRUSTED # header: /usr/include/openssl/x509_vfy.h ---include-constant X509_V_ERR_CERT_REVOKED # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_OK # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_UNABLE_TO_GET_CRL # header: /usr/include/openssl/x509_vfy.h --include-constant X509_V_ERR_CRL_HAS_EXPIRED # header: /usr/include/openssl/x509_vfy.h --include-constant X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT # header: /usr/include/openssl/x509_vfy.h --include-constant X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN # header: /usr/include/openssl/x509_vfy.h ---include-constant X509_V_ERR_UNABLE_TO_GET_CRL # header: /usr/include/openssl/x509_vfy.h --include-constant X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY # header: /usr/include/openssl/x509_vfy.h --include-constant X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_CERT_REVOKED # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_CERT_UNTRUSTED # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_APPLICATION_VERIFICATION # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_OCSP_RESP_INVALID # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_OCSP_SIGNATURE_FAILURE # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_OCSP_NOT_YET_VALID # header: /usr/include/openssl/x509_vfy.h +--include-constant X509_V_ERR_OCSP_HAS_EXPIRED # header: /usr/include/openssl/x509_vfy.h --include-constant X509_V_FLAG_CRL_CHECK # header: /usr/include/openssl/x509_vfy.h --include-constant X509_V_FLAG_CRL_CHECK_ALL # header: /usr/include/openssl/x509_vfy.h ---include-constant X509_V_OK # header: /usr/include/openssl/x509_vfy.h - diff -Nru tomcat11-11.0.15/res/rat/rat-excludes.txt tomcat11-11.0.22/res/rat/rat-excludes.txt --- tomcat11-11.0.15/res/rat/rat-excludes.txt 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/res/rat/rat-excludes.txt 2026-05-01 18:56:05.000000000 +0000 @@ -30,7 +30,8 @@ - Files in API documentation (javadoc) that are generated - test files, such as trivial textual files containing only "OK' string, - files in unusual encodings or compressed files are also excluded. + files in that cannot support license headers and/or use unusual encodings + and/or are compressed files are also excluded. - JSON files (RFC7159) are data and cannot contain comments @@ -87,6 +88,9 @@ output/dist/webapps/docs/*/tag-search-index.js output/dist/webapps/docs/*/type-search-index.js +output/dist/src/test/org/apache/tomcat/util/net/*.pem +output/dist/src/test/org/apache/tomcat/util/net/index.db +output/dist/src/test/org/apache/tomcat/util/net/jsse/*.pem output/dist/src/test/org/apache/tomcat/util/net/jsse/key-password output/dist/src/test/org/apache/tomcat/util/net/key-password output/dist/src/test/org/apache/tomcat/util/net/keystore-password @@ -106,6 +110,9 @@ output/dist/src/test/webresources/dir1/d1/d1-f1.txt output/dist/src/test/webresources/dir1/d2/d2-f1.txt output/dist/src/test/webresources/dir1/*.txt +test/org/apache/tomcat/util/net/*.pem +test/org/apache/tomcat/util/net/index.db +test/org/apache/tomcat/util/net/jsse/*.pem test/org/apache/tomcat/util/net/jsse/key-password test/org/apache/tomcat/util/net/key-password test/org/apache/tomcat/util/net/keystore-password diff -Nru tomcat11-11.0.15/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java tomcat11-11.0.22/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java --- tomcat11-11.0.15/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java 2026-05-01 18:56:05.000000000 +0000 @@ -194,21 +194,21 @@ } - protected static String getNonce(String authHeader) { + private static String getNonce(String authHeader) { int start = authHeader.indexOf("nonce=\"") + 7; int end = authHeader.indexOf('\"', start); return authHeader.substring(start, end); } - protected static String getOpaque(String authHeader) { + private static String getOpaque(String authHeader) { int start = authHeader.indexOf("opaque=\"") + 8; int end = authHeader.indexOf('\"', start); return authHeader.substring(start, end); } - private static String buildDigestResponse(String user, String pwd, String uri, String realm, AuthDigest algorithm, + static String buildDigestResponse(String user, String pwd, String uri, String realm, AuthDigest algorithm, List authHeaders, String nc, String cnonce, String qop) { // Find auth header with correct algorithm diff -Nru tomcat11-11.0.15/test/org/apache/catalina/authenticator/TestDigestAuthenticatorB.java tomcat11-11.0.22/test/org/apache/catalina/authenticator/TestDigestAuthenticatorB.java --- tomcat11-11.0.15/test/org/apache/catalina/authenticator/TestDigestAuthenticatorB.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/authenticator/TestDigestAuthenticatorB.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,135 @@ +/* + * 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.authenticator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.authenticator.DigestAuthenticator.AuthDigest; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; + +@RunWith(Parameterized.class) +public class TestDigestAuthenticatorB extends TomcatBaseTest { + + private static final String targetURI = "/test"; + private static final String validUser = "user"; + private static final String validPassword = "password"; + private static final String validRole = "role"; + private static final String realmName = "realm"; + private static final String clientNonce = "cnonce"; + + + @Parameterized.Parameters(name = "{index}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { validRole, validUser, validPassword, Boolean.TRUE }); + parameterSets.add(new Object[] { "**", validUser, validPassword, Boolean.TRUE }); + parameterSets.add(new Object[] { "**", validUser, "null", Boolean.FALSE }); + parameterSets.add(new Object[] { "**", "invalid", "null", Boolean.FALSE }); + return parameterSets; + } + + @Parameter(0) + public String serverPermittedRole; + + @Parameter(1) + public String clientUserName; + + @Parameter(2) + public String clientPassword; + + @Parameter(3) + public boolean validCredentials; + + + @Test + public void testDigestAuthentication() throws Exception { + // Configure a context with digest authentication and a single protected resource + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctxt = getProgrammaticRootContext(); + + // Add protected servlet + Tomcat.addServlet(ctxt, "TesterServlet", new TesterServlet()); + ctxt.addServletMappingDecoded(targetURI, "TesterServlet"); + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded(targetURI); + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole(serverPermittedRole); + sc.addCollection(collection); + ctxt.addConstraint(sc); + + // Configure the Realm + TesterMapRealm realm = new TesterMapRealm(); + realm.addUser(validUser, validPassword); + realm.addUserRole(validUser, validRole); + + ctxt.setRealm(realm); + + // Configure the authenticator + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("DIGEST"); + lc.setRealmName(realmName); + ctxt.setLoginConfig(lc); + DigestAuthenticator digestAuthenticator = new DigestAuthenticator(); + ctxt.getPipeline().addValve(digestAuthenticator); + + tomcat.start(); + + // The first request will always fail - but we need the challenge + Map> respHeaders = new HashMap<>(); + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + targetURI, bc, respHeaders); + Assert.assertEquals(401, rc); + Assert.assertTrue(bc.getLength() > 0); + bc.recycle(); + + // Second request should + List auth = new ArrayList<>(); + auth.add(TestDigestAuthenticatorAlgorithms.buildDigestResponse(clientUserName, clientPassword, targetURI, + realmName, AuthDigest.SHA_256, respHeaders.get(AuthenticatorBase.AUTH_HEADER_NAME), "00000001", + clientNonce, DigestAuthenticator.QOP)); + Map> reqHeaders = new HashMap<>(); + reqHeaders.put("authorization", auth); + rc = getUrl("http://localhost:" + getPort() + targetURI, bc, reqHeaders, null); + + if (validCredentials) { + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", bc.toString()); + } else { + Assert.assertEquals(401, rc); + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java tomcat11-11.0.22/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java --- tomcat11-11.0.15/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,40 @@ +/* + * 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.authenticator; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.net.TesterSupport; + +public class TestSSLAuthenticator extends TomcatBaseTest { + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=65991 + @Test + public void testBindOnInitFalseNoNPE() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + TesterSupport.configureClientCertContext(tomcat); + Assert.assertTrue(tomcat.getConnector().setProperty("bindOnInit", "false")); + + tomcat.start(); + tomcat.stop(); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/connector/TestResponse.java tomcat11-11.0.22/test/org/apache/catalina/connector/TestResponse.java --- tomcat11-11.0.15/test/org/apache/catalina/connector/TestResponse.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/connector/TestResponse.java 2026-05-01 18:56:05.000000000 +0000 @@ -737,6 +737,24 @@ @Test + public void testSetContentLengthHeader() { + Response response = setupResponse(); + + response.setContentLength(10); + Assert.assertEquals("10", response.getHeader("Content-Length")); + } + + + @Test + public void testSetContentTypeHeader() { + Response response = setupResponse(); + + response.setContentType(TEXT_UTF_8); + Assert.assertEquals(TEXT_UTF_8, response.getHeader("Content-Type")); + } + + + @Test public void testSetContentType01() { Response response = setupResponse(); @@ -1014,4 +1032,31 @@ } } } + + + @Test + public void testSpecialHeaderContentLength() throws Exception { + Response response = setupResponse(); + + // Valid + response.setHeader("Content-Length", "10"); + Assert.assertEquals(10, response.getContentLength()); + Assert.assertEquals("10", response.getHeader("Content-Length")); + Assert.assertEquals(1, response.getHeaderNames().stream().filter(s -> s.equalsIgnoreCase("Content-Length")).count()); + Assert.assertEquals(1, response.getHeaders("Content-Length").size()); + + // Invalid + response.setHeader("Content-Length", "zzz"); + Assert.assertEquals(-1, response.getContentLength()); + Assert.assertNull(response.getHeader("Content-Length")); + Assert.assertEquals(0, response.getHeaderNames().stream().filter(s -> s.equalsIgnoreCase("Content-Length")).count()); + Assert.assertEquals(0, response.getHeaders("Content-Length").size()); + + // Valid + response.setHeader("Content-Length", "20"); + Assert.assertEquals(20, response.getContentLength()); + Assert.assertEquals("20", response.getHeader("Content-Length")); + Assert.assertEquals(1, response.getHeaderNames().stream().filter(s -> s.equalsIgnoreCase("Content-Length")).count()); + Assert.assertEquals(1, response.getHeaders("Content-Length").size()); + } } diff -Nru tomcat11-11.0.15/test/org/apache/catalina/connector/TestValidateClientSessionId.java tomcat11-11.0.22/test/org/apache/catalina/connector/TestValidateClientSessionId.java --- tomcat11-11.0.15/test/org/apache/catalina/connector/TestValidateClientSessionId.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/connector/TestValidateClientSessionId.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,89 @@ +/* + * 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.connector; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestValidateClientSessionId extends TomcatBaseTest { + + @Test + public void testMaliciousSessionIdRejected() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + Tomcat.addServlet(ctx, "snoop", new SnoopServlet()); + ctx.addServletMappingDecoded("/", "snoop"); + + tomcat.start(); + + Map> reqHead = new HashMap<>(); + reqHead.put("Cookie", List.of("JSESSIONID=DUMMY_SESSION_ID")); + + ByteChunk res = new ByteChunk(); + getUrl("http://localhost:" + getPort() + "/?createSession=true", res, reqHead, null); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + String actualSessionId = requestDesc.getRequestInfo("SESSION-ID"); + Assert.assertNotEquals("DUMMY_SESSION_ID", actualSessionId); + } + + @Test + public void testValidSessionIdAcceptedAcrossContexts() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx1 = tomcat.addContext("/app1", null); + ctx1.setSessionCookiePath("/"); + Tomcat.addServlet(ctx1, "snoop", new SnoopServlet()); + ctx1.addServletMappingDecoded("/", "snoop"); + + Context ctx2 = tomcat.addContext("/app2", null); + ctx2.setSessionCookiePath("/"); + Tomcat.addServlet(ctx2, "snoop", new SnoopServlet()); + ctx2.addServletMappingDecoded("/", "snoop"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + Map> resHead = new HashMap<>(); + getUrl("http://localhost:" + getPort() + "/app1/?createSession=true", res, null, resHead); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + String sessionId1 = requestDesc.getRequestInfo("SESSION-ID"); + + Map> reqHead = new HashMap<>(); + reqHead.put("Cookie", List.of("JSESSIONID=" + sessionId1)); + + getUrl("http://localhost:" + getPort() + "/app2/?createSession=true", res, reqHead, null); + + requestDesc = SnoopResult.parse(res.toString()); + String sessionId2 = requestDesc.getRequestInfo("SESSION-ID"); + Assert.assertEquals(sessionId1, sessionId2); + } + +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/core/TestStandardContext.java tomcat11-11.0.22/test/org/apache/catalina/core/TestStandardContext.java --- tomcat11-11.0.15/test/org/apache/catalina/core/TestStandardContext.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/core/TestStandardContext.java 2026-05-01 18:56:05.000000000 +0000 @@ -19,9 +19,13 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.atomic.AtomicReference; import jakarta.servlet.FilterChain; import jakarta.servlet.GenericFilter; @@ -1038,6 +1042,56 @@ Assert.assertTrue(lifecycleListenerOk); } + @Test + public void testGetServletContextReturnsSameInstanceUnderConcurrency() throws Exception { + Tomcat tomcat = getTomcatInstance(); + File appDir = new File(tomcat.getHost().getAppBaseFile(), "ROOT"); + if (!appDir.mkdirs() && !appDir.isDirectory()) { + Assert.fail("Unable to create appDir"); + } + + StandardContext standardContext = (StandardContext) tomcat.addContext("", appDir.getAbsolutePath()); + tomcat.start(); + + // Null the context field to simulate the window during reload + standardContext.context = null; + + int numThreads = 20; + CyclicBarrier barrier = new CyclicBarrier(numThreads); + List threads = new ArrayList<>(); + ServletContext[] results = new ServletContext[numThreads]; + AtomicReference failure = new AtomicReference<>(); + + for (int numOfThread = 0; numOfThread < numThreads; numOfThread++) { + final int index = numOfThread; + Thread thread = new Thread(() -> { + try { + barrier.await(); + results[index] = standardContext.getServletContext(); + } catch (Throwable ex) { + failure.set(ex); + } + }); + thread.start(); + threads.add(thread); + } + + for (Thread thread : threads) { + thread.join(5000); + } + + if (failure.get() != null) { + Assert.fail("Thread failed: " + failure.get()); + } + + ServletContext first = results[0]; + Assert.assertNotNull(first); + for (int i = 1; i < numThreads; i++) { + Assert.assertSame(first, results[i]); + } + } + + private static boolean customWrapperClassOk = false; public static class MyWrapperClass extends StandardWrapper { diff -Nru tomcat11-11.0.15/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java tomcat11-11.0.22/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java --- tomcat11-11.0.15/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java 2026-05-01 18:56:05.000000000 +0000 @@ -25,6 +25,8 @@ import java.util.Collections; import java.util.Iterator; import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import jakarta.servlet.http.HttpServletResponse; @@ -196,6 +198,106 @@ Assert.assertEquals("/foo/images/home.jpg", response.encodeURL("/foo/images/home.jpg")); } + @Test + public void testMultipleTokens() { + String nonce = "TESTNONCE"; + String testURL = "/foo/bar?" + Constants.CSRF_NONCE_SESSION_ATTR_NAME + "=sample"; + CsrfPreventionFilter.CsrfResponseWrapper response = new CsrfPreventionFilter.CsrfResponseWrapper(new NonEncodingResponse(), + Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonce, null); + + Assert.assertTrue("Original URL does not contain CSRF token", + testURL.contains(Constants.CSRF_NONCE_SESSION_ATTR_NAME)); + + String result = response.encodeURL(testURL); + + int pos = result.indexOf(Constants.CSRF_NONCE_SESSION_ATTR_NAME); + Assert.assertTrue("Result URL does not contain CSRF token", + pos >= 0); + pos = result.indexOf(Constants.CSRF_NONCE_SESSION_ATTR_NAME, pos + 1); + Assert.assertFalse("Result URL contains multiple CSRF tokens: " + result, + pos >= 0); + } + + @Test + public void testURLCleansing() { + String[] urls = new String[] { + "/foo/bar", + "/foo/bar?", + "/foo/bar?csrf", + "/foo/bar?csrf&", + "/foo/bar?csrf=", + "/foo/bar?csrf=&", + "/foo/bar?csrf=abc", + "/foo/bar?csrf=abc&bar=foo", + "/foo/bar?bar=foo&csrf=abc", + "/foo/bar?bar=foo&csrf=abc&foo=bar", + "/foo/bar?csrfx=foil&bar=foo&csrf=abc&foo=bar", + "/foo/bar?csrfx=foil&bar=foo&csrf=abc&foo=bar&csrf=def", + "/foo/bar?csrf=&csrf&csrf&csrf&csrf=abc&csrf=", + "/foo/bar?xcsrf=&xcsrf&xcsrf&xcsrf&xcsrf=abc&xcsrf=", + "/foo/bar?xcsrf=&xcsrf&xcsrf&csrf=foo&xcsrf&xcsrf=abc&csrf=bar&xcsrf=&", + "/foo/bar?bar=fo?&csrf=abc", + "/foo/bar?bar=fo?&csrf=abc&baz=boh", + }; + + String csrfParameterName = "csrf"; + + for(String url : urls) { + String result = CsrfPreventionFilter.CsrfResponseWrapper.removeQueryParameters(url, csrfParameterName); + + Assert.assertEquals("Failed to cleanse URL '" + url + "' properly", dumbButAccurateCleanse(url, csrfParameterName), result); + } + + } + + private static String dumbButAccurateCleanse(String url, String csrfParameterName) { + // Get rid of [&csrf=value] with optional =value + Pattern p = Pattern.compile(Pattern.quote("&") + Pattern.quote(csrfParameterName) + "(=[^&]*)?(&|$)"); + + // Do this iteratively to get everything + Matcher m = p.matcher(url); + + while (m.find()) { + url = m.replaceFirst("$2"); + m = p.matcher(url); + } + + // Get rid of [?csrf=value] with optional =value + url = url.replaceAll(Pattern.quote("?") + csrfParameterName + "(=[^&]*)?(&|$)", "?"); + + if (url.endsWith("?")) { + // Special case: it's possible to have multiple ? in a URL: + // the query-string is technically allowed to contain ? characters. + // Only trim the trailing ? if it is actually the quest-string + // separator. + if(url.lastIndexOf('?', url.length() - 2) < 0) { + url = url.substring(0, url.length() - 1); + } + } + + return url; + } + + @Test + public void testDuplicateTokens() { + String nonce = "TESTNONCE"; + String testURL = "/foo/bar?" + Constants.CSRF_NONCE_SESSION_ATTR_NAME + "=" + nonce; + CsrfPreventionFilter.CsrfResponseWrapper response = new CsrfPreventionFilter.CsrfResponseWrapper(new NonEncodingResponse(), + Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonce, null); + + Assert.assertTrue("Original URL does not contain CSRF token", + testURL.contains(Constants.CSRF_NONCE_SESSION_ATTR_NAME)); + + String result = response.encodeURL(testURL); + + int pos = result.indexOf(Constants.CSRF_NONCE_SESSION_ATTR_NAME); + Assert.assertTrue("Result URL does not contain CSRF token", + pos >= 0); + pos = result.indexOf(Constants.CSRF_NONCE_SESSION_ATTR_NAME, pos + 1); + Assert.assertFalse("Result URL contains multiple CSRF tokens: " + result, + pos >= 0); + } + private static class MimeTypeServletContext extends TesterServletContext { private String mimeType; diff -Nru tomcat11-11.0.15/test/org/apache/catalina/filters/TestHttpHeaderSecurityFilter.java tomcat11-11.0.22/test/org/apache/catalina/filters/TestHttpHeaderSecurityFilter.java --- tomcat11-11.0.15/test/org/apache/catalina/filters/TestHttpHeaderSecurityFilter.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/filters/TestHttpHeaderSecurityFilter.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,191 @@ +/* + * 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.filters; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.descriptor.web.FilterDef; + +public class TestHttpHeaderSecurityFilter { + + private final FilterChain filterChain = new TesterFilterChain(); + + @Test + public void testDefaultsNonSecure() throws IOException, ServletException { + HttpServletResponse response = doFilter(new FilterDef(), false); + + Assert.assertNull(response.getHeader("Strict-Transport-Security")); + Assert.assertEquals("DENY", response.getHeader("X-Frame-Options")); + Assert.assertEquals("nosniff", response.getHeader("X-Content-Type-Options")); + } + + @Test + public void testDefaultsSecure() throws IOException, ServletException { + HttpServletResponse response = doFilter(new FilterDef(), true); + + Assert.assertEquals("max-age=0", response.getHeader("Strict-Transport-Security")); + Assert.assertEquals("DENY", response.getHeader("X-Frame-Options")); + Assert.assertEquals("nosniff", response.getHeader("X-Content-Type-Options")); + } + + @Test + public void testHstsMaxAgeAndSubDomains() throws IOException, ServletException { + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("hstsMaxAgeSeconds", "63072000"); + filterDef.addInitParameter("hstsIncludeSubDomains", "true"); + + HttpServletResponse response = doFilter(filterDef, true); + + Assert.assertEquals("max-age=63072000;includeSubDomains", response.getHeader("Strict-Transport-Security")); + } + + @Test + public void testHstsPreload() throws IOException, ServletException { + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("hstsMaxAgeSeconds", "63072000"); + filterDef.addInitParameter("hstsIncludeSubDomains", "true"); + filterDef.addInitParameter("hstsPreload", "true"); + + HttpServletResponse response = doFilter(filterDef, true); + + Assert.assertEquals("max-age=63072000;includeSubDomains;preload", response.getHeader("Strict-Transport-Security")); + } + + @Test + public void testHstsDisabled() throws IOException, ServletException { + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("hstsEnabled", "false"); + + HttpServletResponse response = doFilter(filterDef, true); + + Assert.assertNull(response.getHeader("Strict-Transport-Security")); + } + + @Test + public void testHstsNegativeMaxAge() throws IOException, ServletException { + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("hstsMaxAgeSeconds", "-1"); + + HttpServletResponse response = doFilter(filterDef, true); + + Assert.assertEquals("max-age=0", response.getHeader("Strict-Transport-Security")); + } + + @Test + public void testAntiClickJackingSameOrigin() throws IOException, ServletException { + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("antiClickJackingOption", "SAMEORIGIN"); + + HttpServletResponse response = doFilter(filterDef, false); + + Assert.assertEquals("SAMEORIGIN", response.getHeader("X-Frame-Options")); + } + + @Test + public void testAntiClickJackingAllowFrom() throws IOException, ServletException { + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("antiClickJackingOption", "ALLOW-FROM"); + filterDef.addInitParameter("antiClickJackingUri", "https://example.com"); + + HttpServletResponse response = doFilter(filterDef, false); + + Assert.assertEquals("ALLOW-FROM https://example.com", response.getHeader("X-Frame-Options")); + } + + @Test + public void testAntiClickJackingDisabled() throws IOException, ServletException { + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("antiClickJackingEnabled", "false"); + + HttpServletResponse response = doFilter(filterDef, false); + + Assert.assertNull(response.getHeader("X-Frame-Options")); + } + + @Test + public void testBlockContentTypeSniffingDisabled() throws IOException, ServletException { + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("blockContentTypeSniffingEnabled", "false"); + + HttpServletResponse response = doFilter(filterDef, false); + + Assert.assertNull(response.getHeader("X-Content-Type-Options")); + } + + @Test + public void testAllDisabled() throws IOException, ServletException { + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("hstsEnabled", "false"); + filterDef.addInitParameter("antiClickJackingEnabled", "false"); + filterDef.addInitParameter("blockContentTypeSniffingEnabled", "false"); + + HttpServletResponse response = doFilter(filterDef, true); + + Assert.assertNull(response.getHeader("Strict-Transport-Security")); + Assert.assertNull(response.getHeader("X-Frame-Options")); + Assert.assertNull(response.getHeader("X-Content-Type-Options")); + } + + @Test(expected = ServletException.class) + public void testAntiClickJackingInvalidOption() throws ServletException { + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("antiClickJackingOption", "INVALID"); + + HttpHeaderSecurityFilter filter = new HttpHeaderSecurityFilter(); + filterDef.setFilterName(HttpHeaderSecurityFilter.class.getName()); + filterDef.setFilterClass(HttpHeaderSecurityFilter.class.getName()); + filter.init(TesterFilterConfigs.generateFilterConfig(filterDef)); + } + + @Test(expected = ServletException.class) + public void testAntiClickJackingInvalidUri() throws ServletException { + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("antiClickJackingOption", "ALLOW-FROM"); + filterDef.addInitParameter("antiClickJackingUri", "not a valid uri :{}"); + + HttpHeaderSecurityFilter filter = new HttpHeaderSecurityFilter(); + filterDef.setFilterName(HttpHeaderSecurityFilter.class.getName()); + filterDef.setFilterClass(HttpHeaderSecurityFilter.class.getName()); + filter.init(TesterFilterConfigs.generateFilterConfig(filterDef)); + } + + private HttpServletResponse doFilter(FilterDef filterDef, boolean secure) + throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setSecure(secure); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + HttpHeaderSecurityFilter filter = new HttpHeaderSecurityFilter(); + filterDef.setFilterName(HttpHeaderSecurityFilter.class.getName()); + filterDef.setFilterClass(HttpHeaderSecurityFilter.class.getName()); + filter.init(TesterFilterConfigs.generateFilterConfig(filterDef)); + filter.doFilter(request, response, filterChain); + + return response; + } + + + +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/filters/TestRateLimitFilter.java tomcat11-11.0.22/test/org/apache/catalina/filters/TestRateLimitFilter.java --- tomcat11-11.0.15/test/org/apache/catalina/filters/TestRateLimitFilter.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/filters/TestRateLimitFilter.java 2026-05-01 18:56:05.000000000 +0000 @@ -14,26 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.catalina.filters; -import java.io.IOException; -import java.time.Instant; - import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletResponse; import org.junit.Assert; import org.junit.Test; import org.apache.catalina.Context; import org.apache.catalina.filters.TestRemoteIpFilter.MockFilterChain; -import org.apache.catalina.filters.TestRemoteIpFilter.MockHttpServletRequest; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; import org.apache.catalina.util.FastRateLimiter; -import org.apache.tomcat.unittest.TesterResponse; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap; @@ -52,11 +45,12 @@ Tomcat tomcat = getTomcatInstance(); Context root = tomcat.addContext("", TEMP_DIR); - tomcat.start(); MockFilterChain filterChain = new MockFilterChain(); RateLimitFilter rateLimitFilter = testRateLimitFilter(filterDef, root); + tomcat.start(); + FastRateLimiter fastRateLimiter = (FastRateLimiter) rateLimitFilter.rateLimiter; int allowedRequests = fastRateLimiter.getRequests(); @@ -79,16 +73,22 @@ count++; } - Assert.assertEquals(200, tc1.results[24]); // only 25 requests made, all allowed + // only 25 requests made, all allowed + Assert.assertEquals(HttpServletResponse.SC_OK, tc1.results[24]); - Assert.assertEquals(200, tc2.results[49]); // only 25 requests made, all allowed + // only 25 requests made, all allowed + Assert.assertEquals(HttpServletResponse.SC_OK, tc2.results[49]); - Assert.assertEquals(200, tc3.results[allowedRequests - 1]); // first allowedRequests allowed + // first allowedRequests allowed + Assert.assertEquals(HttpServletResponse.SC_OK, tc3.results[allowedRequests - 1]); - Assert.assertEquals(200, tc4.results[allowedRequests - 1]); // first allowedRequests allowed + // first allowedRequests allowed + Assert.assertEquals(HttpServletResponse.SC_OK, tc4.results[allowedRequests - 1]); if (enforce) { - Assert.assertEquals(429, tc3.results[allowedRequests]); // subsequent requests dropped - Assert.assertEquals(429, tc4.results[allowedRequests]); // subsequent requests dropped + // subsequent requests dropped + Assert.assertEquals(429, tc3.results[allowedRequests]); + // subsequent requests dropped + Assert.assertEquals(429, tc4.results[allowedRequests]); } if (exposeHeaders) { Assert.assertTrue(tc3.rlpHeader[24].contains("q=" + allowedRequests)); @@ -126,7 +126,7 @@ testRateLimitWith4Clients(false, true); } - private RateLimitFilter testRateLimitFilter(FilterDef filterDef, Context root) throws ServletException { + private RateLimitFilter testRateLimitFilter(FilterDef filterDef, Context root) { RateLimitFilter rateLimitFilter = new RateLimitFilter(); filterDef.setFilterClass(RateLimitFilter.class.getName()); @@ -139,75 +139,21 @@ filterMap.addURLPatternDecoded("*"); root.addFilterMap(filterMap); - FilterConfig filterConfig = TesterFilterConfigs.generateFilterConfig(filterDef); - - rateLimitFilter.init(filterConfig); - return rateLimitFilter; } - static class TestClient extends Thread { - RateLimitFilter filter; - FilterChain filterChain; - String ip; + static class TestClient extends TesterRateLimitClientBase { - int requests; int sleep; - int[] results; - volatile String[] rlpHeader; - volatile String[] rlHeader; - TestClient(RateLimitFilter filter, FilterChain filterChain, String ip, int requests, int rps) { - this.filter = filter; - this.filterChain = filterChain; - this.ip = ip; - this.requests = requests; + super(filter, filterChain, ip, requests); this.sleep = 1000 / rps; - this.results = new int[requests]; - this.rlpHeader = new String[requests]; - this.rlHeader = new String[requests]; - super.setDaemon(true); - super.start(); } @Override - public void run() { - try { - for (int i = 0; i < requests; i++) { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRemoteAddr(ip); - TesterResponse response = new TesterResponseWithStatus(); - response.setRequest(request); - filter.doFilter(request, response, filterChain); - results[i] = response.getStatus(); - rlpHeader[i] = response.getHeader(RateLimitFilter.HEADER_RATE_LIMIT_POLICY); - rlHeader[i] = response.getHeader(RateLimitFilter.HEADER_RATE_LIMIT); - System.out.printf("%s %s: %s %d, Policy:%s, Current:%s\n", ip, Instant.now(), - Integer.valueOf(i + 1), Integer.valueOf(response.getStatus()), rlpHeader[i], rlHeader[i]); - sleep(sleep); - } - } catch (Exception ex) { - ex.printStackTrace(); - } + void waitForNextRequest(long start, int requestIndex) throws Exception { + sleep(sleep); } } - - static class TesterResponseWithStatus extends TesterResponse { - - int status = 200; - String message = "OK"; - - @Override - public void sendError(int status, String message) throws IOException { - this.status = status; - this.message = message; - } - - @Override - public int getStatus() { - return status; - } - } - } diff -Nru tomcat11-11.0.15/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java tomcat11-11.0.22/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java --- tomcat11-11.0.15/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java 2026-05-01 18:56:05.000000000 +0000 @@ -16,25 +16,23 @@ */ package org.apache.catalina.filters; -import java.io.IOException; - import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletResponse; import org.junit.Assert; import org.junit.Test; import org.apache.catalina.Context; import org.apache.catalina.filters.TestRemoteIpFilter.MockFilterChain; -import org.apache.catalina.filters.TestRemoteIpFilter.MockHttpServletRequest; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; import org.apache.catalina.util.ExactRateLimiter; -import org.apache.tomcat.unittest.TesterResponse; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap; public class TestRateLimitFilterWithExactRateLimiter extends TomcatBaseTest { - private void testRateLimitWith1Clients(boolean exposeHeaders, boolean enforce) throws Exception { + + private void testRateLimitWith4Clients(boolean exposeHeaders, boolean enforce) throws Exception { int bucketRequests = 40; int bucketDuration = 4; @@ -51,6 +49,7 @@ MockFilterChain filterChain = new MockFilterChain(); RateLimitFilter rateLimitFilter = testRateLimitFilter(filterDef, root); + tomcat.start(); ExactRateLimiter exactRateLimiter = (ExactRateLimiter) rateLimitFilter.rateLimiter; @@ -69,24 +68,30 @@ tc2.join(); tc3.join(); tc4.join(); - Assert.assertEquals(200, tc1.results[24]); // only 25 requests made in 5 seconds, all allowed + // only 25 requests made in 5 seconds, all allowed + Assert.assertEquals(HttpServletResponse.SC_OK, tc1.results[24]); - Assert.assertEquals(200, tc2.results[49]); // only 50 requests made in 5 seconds, all allowed + // only 50 requests made in 5 seconds, all allowed + Assert.assertEquals(HttpServletResponse.SC_OK, tc2.results[49]); - Assert.assertEquals(200, tc3.results[39]); // first allowedRequests allowed + // first allowedRequests allowed + Assert.assertEquals(HttpServletResponse.SC_OK, tc3.results[39]); if (enforce) { - Assert.assertEquals(429, tc3.results[allowedRequests]); // subsequent requests dropped + // subsequent requests dropped + Assert.assertEquals(429, tc3.results[allowedRequests]); } else { - Assert.assertEquals(200, tc3.results[allowedRequests]); + Assert.assertEquals(HttpServletResponse.SC_OK, tc3.results[allowedRequests]); } - Assert.assertEquals(200, tc4.results[allowedRequests - 1]); // first allowedRequests allowed + // first allowedRequests allowed + Assert.assertEquals(HttpServletResponse.SC_OK, tc4.results[allowedRequests - 1]); if (enforce) { - Assert.assertEquals(429, tc4.results[allowedRequests]); // subsequent requests dropped + // subsequent requests dropped + Assert.assertEquals(429, tc4.results[allowedRequests]); } else { - Assert.assertEquals(200, tc4.results[allowedRequests]); + Assert.assertEquals(HttpServletResponse.SC_OK, tc4.results[allowedRequests]); } if (exposeHeaders) { @@ -108,22 +113,22 @@ @Test public void testExposeHeaderAndReferenceRateLimitWith4Clients() throws Exception { - testRateLimitWith1Clients(true, false); + testRateLimitWith4Clients(true, false); } @Test public void testUnexposeHeaderAndReferenceRateLimitWith4Clients() throws Exception { - testRateLimitWith1Clients(false, false); + testRateLimitWith4Clients(false, false); } @Test public void testExposeHeaderAndEnforceRateLimitWith4Clients() throws Exception { - testRateLimitWith1Clients(true, true); + testRateLimitWith4Clients(true, true); } @Test public void testUnexposeHeaderAndEnforceRateLimitWith4Clients() throws Exception { - testRateLimitWith1Clients(false, true); + testRateLimitWith4Clients(false, true); } private RateLimitFilter testRateLimitFilter(FilterDef filterDef, Context root) { @@ -142,80 +147,26 @@ return rateLimitFilter; } - static class TestClient extends Thread { - RateLimitFilter filter; - FilterChain filterChain; - String ip; + static class TestClient extends TesterRateLimitClientBase { - int requests; int timePerRequest; - int[] results; - volatile String[] rlpHeader; - volatile String[] rlHeader; - TestClient(RateLimitFilter filter, FilterChain filterChain, String ip, int requests, int rps) { - this.filter = filter; - this.filterChain = filterChain; - this.ip = ip; - this.requests = requests; + super(filter, filterChain, ip, requests); this.timePerRequest = 1000 / rps; - this.results = new int[requests]; - this.rlpHeader = new String[requests]; - this.rlHeader = new String[requests]; - super.setDaemon(true); - super.start(); } @Override - public void run() { - long start = System.nanoTime(); - - try { - for (int i = 0; i < requests; i++) { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRemoteAddr(ip); - TesterResponse response = new TesterResponseWithStatus(); - response.setRequest(request); - filter.doFilter(request, response, filterChain); - results[i] = response.getStatus(); - - rlpHeader[i] = response.getHeader(RateLimitFilter.HEADER_RATE_LIMIT_POLICY); - rlHeader[i] = response.getHeader(RateLimitFilter.HEADER_RATE_LIMIT); - - if (results[i] != 200) { - break; - } - /* - * Ensure requests are evenly spaced through time irrespective of how long each request takes to - * complete. Do comparisons in milliseconds. - */ - long expectedDuration = (i + 1) * timePerRequest; - long duration = (System.nanoTime() - start) / 1000000; - if (expectedDuration > duration) { - sleep(expectedDuration - duration); - } - } - } catch (Exception ex) { - ex.printStackTrace(); + void waitForNextRequest(long start, int requestIndex) throws Exception { + /* + * Ensure requests are evenly spaced through time irrespective of how long each request takes to + * complete. Do comparisons in milliseconds. + */ + long expectedDuration = (requestIndex + 1) * timePerRequest; + long duration = (System.nanoTime() - start) / 1000000; + if (expectedDuration > duration) { + sleep(expectedDuration - duration); } } } - - static class TesterResponseWithStatus extends TesterResponse { - - int status = 200; - String message = "OK"; - - @Override - public void sendError(int status, String message) throws IOException { - this.status = status; - this.message = message; - } - - @Override - public int getStatus() { - return status; - } - } } diff -Nru tomcat11-11.0.15/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java tomcat11-11.0.22/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java --- tomcat11-11.0.15/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java 2026-05-01 18:56:05.000000000 +0000 @@ -30,6 +30,7 @@ import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; import org.apache.tomcat.unittest.TesterResponse; +import org.apache.tomcat.unittest.TesterResponseWithStatus; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap; @@ -56,7 +57,7 @@ for (int j = 0; j < 256; j += 11) { ipAddr = String.format("192.168.%s.%s", Integer.valueOf(i), Integer.valueOf(j)); request = new TestRemoteIpFilter.MockHttpServletRequest(ipAddr); - response = new TestRateLimitFilter.TesterResponseWithStatus(); + response = new TesterResponseWithStatus(); expected = (i == 10 || i == 20) ? HttpServletResponse.SC_OK : HttpServletResponse.SC_FORBIDDEN; filter.doFilter(request, response, filterChain); Assert.assertEquals(expected, response.getStatus()); @@ -85,7 +86,7 @@ for (int j = 0; j < 256; j += 11) { ipAddr = String.format("192.168.%s.%s", Integer.valueOf(i), Integer.valueOf(j)); request = new TestRemoteIpFilter.MockHttpServletRequest(ipAddr); - response = new TestRateLimitFilter.TesterResponseWithStatus(); + response = new TesterResponseWithStatus(); expected = (i != 10 && i != 20) ? HttpServletResponse.SC_OK : HttpServletResponse.SC_FORBIDDEN; filter.doFilter(request, response, filterChain); Assert.assertEquals(expected, response.getStatus()); @@ -115,7 +116,7 @@ for (int j = 0; j < 256; j += 11) { ipAddr = String.format("10.10.%s.%s", Integer.valueOf(i), Integer.valueOf(j)); request = new TestRemoteIpFilter.MockHttpServletRequest(ipAddr); - response = new TestRateLimitFilter.TesterResponseWithStatus(); + response = new TesterResponseWithStatus(); expected = (i != 10 && i != 20) ? HttpServletResponse.SC_OK : HttpServletResponse.SC_FORBIDDEN; filter.doFilter(request, response, filterChain); Assert.assertEquals(expected, response.getStatus()); @@ -145,7 +146,7 @@ for (int j = 0; j < 256; j += 11) { ipAddr = String.format("192.168.%s.%s", Integer.valueOf(i), Integer.valueOf(j)); request = new TestRemoteIpFilter.MockHttpServletRequest(ipAddr); - response = new TestRateLimitFilter.TesterResponseWithStatus(); + response = new TesterResponseWithStatus(); expected = HttpServletResponse.SC_FORBIDDEN; filter.doFilter(request, response, filterChain); Assert.assertEquals(expected, response.getStatus()); @@ -179,7 +180,7 @@ for (int j = 0; j < 256; j += 11) { ipAddr = String.format("192.168.%s.%s", Integer.valueOf(i), Integer.valueOf(j)); request = new TestRemoteIpFilter.MockHttpServletRequest(ipAddr); - response = new TestRateLimitFilter.TesterResponseWithStatus(); + response = new TesterResponseWithStatus(); expected = HttpServletResponse.SC_FORBIDDEN; filter.doFilter(request, response, filterChain); Assert.assertEquals(expected, response.getStatus()); diff -Nru tomcat11-11.0.15/test/org/apache/catalina/filters/TesterHttpServletRequest.java tomcat11-11.0.22/test/org/apache/catalina/filters/TesterHttpServletRequest.java --- tomcat11-11.0.15/test/org/apache/catalina/filters/TesterHttpServletRequest.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/filters/TesterHttpServletRequest.java 2026-05-01 18:56:05.000000000 +0000 @@ -56,6 +56,7 @@ private String serverName; private int serverPort; private String contentType; + private boolean secure = false; @Override public Object getAttribute(String name) { @@ -189,14 +190,13 @@ throw new RuntimeException("Not implemented"); } - /** - * {@inheritDoc} - *

    - * This test implementation is hard coded to return false. - */ + public void setSecure(boolean secure) { + this.secure = secure; + } + @Override public boolean isSecure() { - return false; + return secure; } @Override diff -Nru tomcat11-11.0.15/test/org/apache/catalina/filters/TesterRateLimitClientBase.java tomcat11-11.0.22/test/org/apache/catalina/filters/TesterRateLimitClientBase.java --- tomcat11-11.0.15/test/org/apache/catalina/filters/TesterRateLimitClientBase.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/filters/TesterRateLimitClientBase.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,73 @@ +/* + * 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.filters; + +import jakarta.servlet.FilterChain; + +import org.apache.catalina.filters.TestRemoteIpFilter.MockHttpServletRequest; +import org.apache.tomcat.unittest.TesterResponse; +import org.apache.tomcat.unittest.TesterResponseWithStatus; + +public abstract class TesterRateLimitClientBase extends Thread { + RateLimitFilter filter; + FilterChain filterChain; + String ip; + + int requests; + + int[] results; + volatile String[] rlpHeader; + volatile String[] rlHeader; + + TesterRateLimitClientBase(RateLimitFilter filter, FilterChain filterChain, String ip, int requests) { + this.filter = filter; + this.filterChain = filterChain; + this.ip = ip; + this.requests = requests; + this.results = new int[requests]; + this.rlpHeader = new String[requests]; + this.rlHeader = new String[requests]; + super.setDaemon(true); + super.start(); + } + + @Override + public void run() { + long start = System.nanoTime(); + try { + for (int i = 0; i < requests; i++) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr(ip); + TesterResponse response = new TesterResponseWithStatus(); + response.setRequest(request); + filter.doFilter(request, response, filterChain); + results[i] = response.getStatus(); + rlpHeader[i] = response.getHeader(RateLimitFilter.HEADER_RATE_LIMIT_POLICY); + rlHeader[i] = response.getHeader(RateLimitFilter.HEADER_RATE_LIMIT); + //System.out.printf("%s %s: %s %d, Policy:%s, Current:%s\n", ip, Instant.now(), + // Integer.valueOf(i + 1), Integer.valueOf(response.getStatus()), rlpHeader[i], rlHeader[i]); + + waitForNextRequest(start, i); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + + abstract void waitForNextRequest(long start, int requestIndex) throws Exception; +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/loader/TestVirtualWebappLoader.java tomcat11-11.0.22/test/org/apache/catalina/loader/TestVirtualWebappLoader.java --- tomcat11-11.0.15/test/org/apache/catalina/loader/TestVirtualWebappLoader.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/loader/TestVirtualWebappLoader.java 2026-05-01 18:56:05.000000000 +0000 @@ -86,7 +86,7 @@ loader.start(); String[] repos = loader.getLoaderRepositories(); - Assert.assertEquals(5, repos.length); + Assert.assertEquals(6, repos.length); loader.stop(); repos = loader.getLoaderRepositories(); @@ -95,7 +95,7 @@ // no leak loader.start(); repos = loader.getLoaderRepositories(); - Assert.assertEquals(5, repos.length); + Assert.assertEquals(6, repos.length); // clear loader ctx.setLoader(null); diff -Nru tomcat11-11.0.15/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java tomcat11-11.0.22/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java --- tomcat11-11.0.15/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java 2026-05-01 18:56:05.000000000 +0000 @@ -599,7 +599,7 @@ public void testNonBlockingReadChunkedSplitMaximum() throws Exception { // @formatter:off String requestBody = new String( - "14" + CRLF + + "14;a=b;c" + CRLF + "012345678901FINISHED" + CRLF + "0" + CRLF + TRAILER_HEADER_NAME + ": " + TRAILER_HEADER_VALUE + CRLF + diff -Nru tomcat11-11.0.15/test/org/apache/catalina/realm/TestLockoutRealm.java tomcat11-11.0.22/test/org/apache/catalina/realm/TestLockoutRealm.java --- tomcat11-11.0.15/test/org/apache/catalina/realm/TestLockoutRealm.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/realm/TestLockoutRealm.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,117 @@ +/* + * 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.realm; + +import java.security.Principal; +import java.util.Locale; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.tomcat.unittest.TesterContext; + +public class TestLockoutRealm { + + private static final String USER_NAME = "user"; + private static final String PASSWORD = "password"; + + private LockOutRealm realm; + + + @Before + public void init() throws Exception { + Context context = new TesterContext(); + TesterMapRealm tmr = new TesterMapRealm(); + tmr.setContainer(context); + MessageDigestCredentialHandler ch = new MessageDigestCredentialHandler(); + tmr.setCredentialHandler(ch); + tmr.addUser(USER_NAME, PASSWORD); + tmr.start(); + + realm = new LockOutRealm(); + realm.setContainer(context); + realm.addRealm(tmr); + realm.setFailureCount(2); + realm.start(); + } + + + @Test + public void testLockoutAfterFailure() { + Principal p = realm.authenticate(USER_NAME, PASSWORD); + Assert.assertNotNull(p); + + p = realm.authenticate(USER_NAME, "wrong"); + p = realm.authenticate(USER_NAME, "wrong"); + // Should be locked now + p = realm.authenticate(USER_NAME, PASSWORD); + Assert.assertNull(p); + } + + + @Test + public void testLockoutAfterFailureCaseSensitiveDefault() { + Principal p = realm.authenticate(USER_NAME, PASSWORD); + Assert.assertNotNull(p); + + p = realm.authenticate(USER_NAME, "wrong"); + p = realm.authenticate(USER_NAME.toUpperCase(Locale.ENGLISH), "wrong"); + // Should be locked now + p = realm.authenticate(USER_NAME, PASSWORD); + Assert.assertNull(p); + } + + + @Test + public void testLockoutAfterFailureCaseSensitiveFalse() { + realm.setCaseSensitive(false); + + Principal p = realm.authenticate(USER_NAME, PASSWORD); + Assert.assertNotNull(p); + + p = realm.authenticate(USER_NAME, "wrong"); + p = realm.authenticate(USER_NAME.toUpperCase(Locale.ENGLISH), "wrong"); + // Should be locked now + p = realm.authenticate(USER_NAME, PASSWORD); + Assert.assertNull(p); + } + + + @Test + public void testLockoutAfterFailureCaseSensitiveTrue() { + realm.setCaseSensitive(true); + + Principal p = realm.authenticate(USER_NAME, PASSWORD); + Assert.assertNotNull(p); + + p = realm.authenticate(USER_NAME, "wrong"); + p = realm.authenticate(USER_NAME.toUpperCase(Locale.ENGLISH), "wrong"); + // Should not be locked yet + p = realm.authenticate(USER_NAME, PASSWORD); + Assert.assertNotNull(p); + + p = realm.authenticate(USER_NAME, "wrong"); + p = realm.authenticate(USER_NAME, "wrong"); + + // Both should be locked now + p = realm.authenticate(USER_NAME, PASSWORD); + Assert.assertNull(p); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/realm/TestRealmBase.java tomcat11-11.0.22/test/org/apache/catalina/realm/TestRealmBase.java --- tomcat11-11.0.15/test/org/apache/catalina/realm/TestRealmBase.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/realm/TestRealmBase.java 2026-05-01 18:56:05.000000000 +0000 @@ -790,4 +790,86 @@ Assert.assertFalse(mapRealm.hasResourcePermission( request, response, constraintsDelete, null)); } + + + @Test + public void testUncoveredMethods() throws IOException { + // Create a constraint for ROLE1 + SecurityConstraint constraint = new SecurityConstraint(); + constraint.addAuthRole(ROLE1); + // Add a collection for GET + SecurityCollection getCollection = new SecurityCollection(); + getCollection.addMethod(Method.GET); + getCollection.addPatternDecoded("*.html"); + constraint.addCollection(getCollection); + // Add a collection for POST + SecurityCollection postCollection = new SecurityCollection(); + postCollection.addMethod(Method.POST); + postCollection.addPatternDecoded("*.html"); + constraint.addCollection(postCollection); + + TesterMapRealm mapRealm = new TesterMapRealm(); + + // Set up the mock request and response + TesterRequest request = new TesterRequest(); + Response response = new TesterResponse(); + Context context = request.getContext(); + context.addSecurityRole(ROLE1); + context.addSecurityRole(ROLE2); + request.getMappingData().context = context; + + // Create the principals + List userRoles1 = new ArrayList<>(); + userRoles1.add(ROLE1); + GenericPrincipal gp1 = new GenericPrincipal(USER1, userRoles1); + + List userRoles2 = new ArrayList<>(); + userRoles2.add(ROLE2); + GenericPrincipal gp2 = new GenericPrincipal(USER2, userRoles2); + + List userRoles99 = new ArrayList<>(); + GenericPrincipal gp99 = new GenericPrincipal(USER99, userRoles99); + + // Add the constraint to the context + context.addConstraint(constraint); + + + // Only user1 should be able to perform a GET + request.setMethod(Method.GET); + + SecurityConstraint[] constraintsGet = + mapRealm.findSecurityConstraints(request, context); + + request.setUserPrincipal(null); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsGet, null)); + request.setUserPrincipal(gp1); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsGet, null)); + request.setUserPrincipal(gp2); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsGet, null)); + request.setUserPrincipal(gp99); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsGet, null)); + + // Only user1 should be able to perform a POST + request.setMethod(Method.POST); + + SecurityConstraint[] constraintsPost = + mapRealm.findSecurityConstraints(request, context); + + request.setUserPrincipal(null); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsPost, null)); + request.setUserPrincipal(gp1); + Assert.assertTrue(mapRealm.hasResourcePermission( + request, response, constraintsPost, null)); + request.setUserPrincipal(gp2); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsPost, null)); + request.setUserPrincipal(gp99); + Assert.assertFalse(mapRealm.hasResourcePermission( + request, response, constraintsPost, null)); + } } diff -Nru tomcat11-11.0.15/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java tomcat11-11.0.22/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java --- tomcat11-11.0.15/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,121 @@ +/* + * 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.servlets; + +import java.io.IOException; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.servlets.WebdavServlet.BoundedByteArrayOutputStream; + +public class TestWebdavBoundedByteArrayOutputStream { + + private static final int TEST_LIMIT = 10; + private static final byte[] ONE_BYTE_ARRAY = new byte[] { 0 }; + + + @Test + public void testWriteByte() { + BoundedByteArrayOutputStream bbaos = new BoundedByteArrayOutputStream(TEST_LIMIT); + + for (int i = 0; i < TEST_LIMIT; i++) { + bbaos.write(0); + } + + try { + bbaos.write(0); + Assert.fail("Writing 11th byte failed to trigger error"); + } catch (ArrayIndexOutOfBoundsException e) { + // Pass + } + } + + + @Test + public void testWriteByteArray() throws IOException { + BoundedByteArrayOutputStream bbaos = new BoundedByteArrayOutputStream(TEST_LIMIT); + + for (int i = 0; i < TEST_LIMIT; i++) { + bbaos.write(ONE_BYTE_ARRAY); + } + + try { + bbaos.write(ONE_BYTE_ARRAY); + Assert.fail("Writing 11th byte failed to trigger error"); + } catch (ArrayIndexOutOfBoundsException e) { + // Pass + } + } + + + @Test + public void testWriteByteSubArray() { + BoundedByteArrayOutputStream bbaos = new BoundedByteArrayOutputStream(TEST_LIMIT); + + for (int i = 0; i < TEST_LIMIT; i++) { + bbaos.write(ONE_BYTE_ARRAY, 0, 1); + } + + try { + bbaos.write(ONE_BYTE_ARRAY, 0, 1); + Assert.fail("Writing 11th byte failed to trigger error"); + } catch (ArrayIndexOutOfBoundsException e) { + // Pass + } + } + + + @Test + public void testWriteBytes() { + BoundedByteArrayOutputStream bbaos = new BoundedByteArrayOutputStream(TEST_LIMIT); + + for (int i = 0; i < TEST_LIMIT; i++) { + bbaos.writeBytes(ONE_BYTE_ARRAY); + } + + try { + bbaos.writeBytes(ONE_BYTE_ARRAY); + Assert.fail("Writing 11th byte failed to trigger error"); + } catch (ArrayIndexOutOfBoundsException e) { + // Pass + } + } + + + @Test + public void testReset() throws IOException { + BoundedByteArrayOutputStream bbaos = new BoundedByteArrayOutputStream(TEST_LIMIT); + + for (int i = 0; i < TEST_LIMIT; i++) { + bbaos.write(ONE_BYTE_ARRAY); + } + + bbaos.reset(); + + for (int i = 0; i < TEST_LIMIT; i++) { + bbaos.write(ONE_BYTE_ARRAY); + } + + try { + bbaos.write(ONE_BYTE_ARRAY); + Assert.fail("Writing 11th byte failed to trigger error"); + } catch (ArrayIndexOutOfBoundsException e) { + // Pass + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/servlets/TestWebdavServlet.java tomcat11-11.0.22/test/org/apache/catalina/servlets/TestWebdavServlet.java --- tomcat11-11.0.15/test/org/apache/catalina/servlets/TestWebdavServlet.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/servlets/TestWebdavServlet.java 2026-05-01 18:56:05.000000000 +0000 @@ -75,6 +75,7 @@ // Create a temp webapp that can be safely written to File tempWebapp = new File(getTemporaryDirectory(), "webdav-specialpath"+UUID.randomUUID()); + tempWebapp.deleteOnExit(); Assert.assertTrue("Failed to mkdirs on "+tempWebapp.getCanonicalPath(),tempWebapp.mkdirs()); Assert.assertTrue(new File(tempWebapp,"WEB-INF").mkdir()); Assert.assertTrue(new File(tempWebapp,"META-INF").mkdir()); @@ -297,6 +298,7 @@ // Create a temp webapp that can be safely written to File tempWebapp = new File(getTemporaryDirectory(), "webdav-properties"); + tempWebapp.deleteOnExit(); Assert.assertTrue(tempWebapp.mkdirs()); Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath()); Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet()); @@ -441,6 +443,7 @@ // Create a temp webapp that can be safely written to File tempWebapp = new File(getTemporaryDirectory(), "webdav-webapp"); + tempWebapp.deleteOnExit(); Assert.assertTrue(tempWebapp.mkdirs()); Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath()); Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet()); @@ -922,6 +925,7 @@ // Create a temp webapp that can be safely written to File tempWebapp = new File(getTemporaryDirectory(), "webdav-subpath"); + tempWebapp.deleteOnExit(); File subPath = new File(tempWebapp, "aaa"); Assert.assertTrue(subPath.mkdirs()); @@ -1018,6 +1022,7 @@ // Create a temp webapp that can be safely written to File tempWebapp = new File(getTemporaryDirectory(), "webdav-lock"); + tempWebapp.deleteOnExit(); Assert.assertTrue(tempWebapp.mkdirs()); Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath()); Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet()); @@ -1405,6 +1410,7 @@ // Create a temp webapp that can be safely written to File tempWebapp = new File(getTemporaryDirectory(), "webdav-if"); + tempWebapp.deleteOnExit(); File folder = new File(tempWebapp, "/myfolder/myfolder2/myfolder4/myfolder5"); Assert.assertTrue(folder.mkdirs()); File file = new File(folder, "myfile.txt"); @@ -1535,6 +1541,7 @@ // Create a temp webapp that can be safely written to File tempWebapp = new File(getTemporaryDirectory(), "webdav-store"); + tempWebapp.deleteOnExit(); Assert.assertTrue(tempWebapp.mkdirs()); Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath()); Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet()); @@ -1566,6 +1573,82 @@ validateXml(client.getResponseBody()); } + + /* + * Only tests LOCK bodies exceeding limit. Other tests cover valid LOCK bodies. + */ + @Test + public void testLockBodyLimit() throws Exception { + doTestLimit("LOCK", LOCK_BODY); + } + + + /* + * Only tests PROPFIND bodies exceeding limit. Other tests cover valid PROPFIND bodies. + */ + @Test + public void testPropFindBodyLimit() throws Exception { + doTestLimit("PROPFIND", PROPFIND_PROP); + } + + + private void doTestLimit(String method, String requestBody) throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File("test/webapp"); + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + + Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet()); + webdavServlet.addInitParameter("listings", "true"); + webdavServlet.addInitParameter("secret", "foo"); + webdavServlet.addInitParameter("readonly", "false"); + webdavServlet.addInitParameter("useStrongETags", "true"); + webdavServlet.addInitParameter("maxRequestBodySize", "10"); + + ctxt.addServletMappingDecoded("/*", "webdav"); + tomcat.start(); + + // With content length + Client client = new Client(); + client.setPort(getPort()); + + // @formatter:off + client.setRequest(new String[] { + method + " / HTTP/1.1" + CRLF + + "Host: localhost:" + getPort() + CRLF + + "Content-Length: " + requestBody.length() + CRLF + + "Connection: Close" + CRLF + + CRLF + + requestBody + }); + // @formatter:on + client.connect(); + client.processRequest(true); + Assert.assertEquals(WebdavStatus.SC_REQUEST_TOO_LONG, client.getStatusCode()); + + // Without content length + client.reset(); + + // @formatter:off + client.setRequest(new String[] { + method + " / HTTP/1.1" + CRLF + + "Host: localhost:" + getPort() + CRLF + + "Transfer-Encoding: chunked" + CRLF + + "Connection: Close" + CRLF + + CRLF + + Integer.toHexString(requestBody.length()) + CRLF + + requestBody + CRLF + + "0" + CRLF + + CRLF + }); + // @formatter:on + client.connect(); + client.processRequest(true); + Assert.assertEquals(WebdavStatus.SC_REQUEST_TOO_LONG, client.getStatusCode()); + } + + public static class CustomPropertyStore implements PropertyStore { private String propertyName = null; diff -Nru tomcat11-11.0.15/test/org/apache/catalina/session/FileStoreTest.java tomcat11-11.0.22/test/org/apache/catalina/session/FileStoreTest.java --- tomcat11-11.0.15/test/org/apache/catalina/session/FileStoreTest.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/session/FileStoreTest.java 1970-01-01 00:00:00.000000000 +0000 @@ -1,100 +0,0 @@ -/* - * 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.session; - -import java.io.File; -import java.io.IOException; - -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -import org.apache.catalina.Manager; -import org.apache.catalina.startup.ExpandWar; -import org.apache.tomcat.unittest.TesterContext; -import org.apache.tomcat.unittest.TesterServletContext; - -public class FileStoreTest { - - private static final String SESS_TEMPPATH = "SESS_TEMP"; - private static final File dir = new File(SESS_TEMPPATH); - private static FileStore fileStore; - private static File file1 = new File(SESS_TEMPPATH + "/tmp1.session"); - private static File file2 = new File(SESS_TEMPPATH + "/tmp2.session"); - private static Manager manager = new StandardManager(); - - - @BeforeClass - public static void setup() { - TesterContext testerContext = new TesterContext(); - testerContext.setServletContext(new TesterServletContext()); - manager.setContext(testerContext); - fileStore = new FileStore(); - fileStore.setManager(manager); - } - - - @AfterClass - public static void cleanup() { - ExpandWar.delete(dir); - } - - - @Before - public void beforeEachTest() throws IOException { - fileStore.setDirectory(SESS_TEMPPATH); - if (!dir.exists() && !dir.mkdir()) { - Assert.fail(dir.getAbsolutePath()); - } - if (!file1.exists() && !file1.createNewFile()) { - Assert.fail(); - } - if (!file2.exists() && !file2.createNewFile()) { - Assert.fail(); - } - } - - - @Test - public void getSize() throws Exception { - Assert.assertEquals(2, fileStore.getSize()); - } - - - @Test - public void clear() throws Exception { - fileStore.clear(); - Assert.assertEquals(0, fileStore.getSize()); - } - - - @Test - public void keys() throws Exception { - Assert.assertArrayEquals(new String[]{"tmp1", "tmp2"}, fileStore.keys()); - fileStore.clear(); - Assert.assertArrayEquals(new String[]{}, fileStore.keys()); - } - - - @Test - public void removeTest() throws Exception { - fileStore.remove("tmp1"); - Assert.assertEquals(1, fileStore.getSize()); - } -} \ No newline at end of file diff -Nru tomcat11-11.0.15/test/org/apache/catalina/session/TestFileStore.java tomcat11-11.0.22/test/org/apache/catalina/session/TestFileStore.java --- tomcat11-11.0.15/test/org/apache/catalina/session/TestFileStore.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/session/TestFileStore.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,130 @@ +/* + * 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.session; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.startup.ExpandWar; +import org.apache.tomcat.unittest.TesterContext; +import org.apache.tomcat.unittest.TesterServletContext; + +public class TestFileStore { + + private static final String SESS_TEMPPATH = "SESS_TEMP"; + private static final File dir = new File(SESS_TEMPPATH); + private static FileStore fileStore; + private static final File file1 = new File(SESS_TEMPPATH + "/tmp1.session"); + private static final File file2 = new File(SESS_TEMPPATH + "/tmp2.session"); + private static final Manager manager = new StandardManager(); + + + @BeforeClass + public static void setup() { + TesterContext testerContext = new TesterContext(); + testerContext.setServletContext(new TesterServletContext()); + manager.setContext(testerContext); + fileStore = new FileStore(); + fileStore.setManager(manager); + } + + + @AfterClass + public static void cleanup() { + ExpandWar.delete(dir); + } + + + @Before + public void beforeEachTest() throws IOException { + fileStore.setDirectory(SESS_TEMPPATH); + if (!dir.exists() && !dir.mkdir()) { + Assert.fail(dir.getAbsolutePath()); + } + if (!file1.exists() && !file1.createNewFile()) { + Assert.fail(); + } + if (!file2.exists() && !file2.createNewFile()) { + Assert.fail(); + } + } + + + @Test + public void getSize() throws Exception { + Assert.assertEquals(2, fileStore.getSize()); + } + + + @Test + public void clear() throws Exception { + fileStore.clear(); + Assert.assertEquals(0, fileStore.getSize()); + } + + + @Test + public void keys() throws Exception { + String[] keys = fileStore.keys(); + Arrays.sort(keys); + Assert.assertArrayEquals(new String[]{"tmp1", "tmp2"}, keys); + fileStore.clear(); + Assert.assertArrayEquals(new String[]{}, fileStore.keys()); + } + + + @Test + public void removeTest() throws Exception { + fileStore.remove("tmp1"); + Assert.assertEquals(1, fileStore.getSize()); + } + + @Test + public void pathTraversalSessionId() throws Exception { + File storageDir = dir.getAbsoluteFile(); + File outsideFile = new File(storageDir.getParentFile(), "conf" + File.separator + "test.session"); + File outsideDir = outsideFile.getParentFile(); + boolean createdOutsideDir = false; + if (!outsideDir.exists()) { + Assert.assertTrue(outsideDir.mkdirs()); + createdOutsideDir = true; + } + Assert.assertTrue(outsideFile.createNewFile()); + + try { + Session session = fileStore.load("./../conf/test"); + Assert.assertNull(session); + + fileStore.remove("./../conf/test"); + Assert.assertTrue(outsideFile.exists()); + } finally { + Assert.assertTrue(outsideFile.delete()); + if (createdOutsideDir) { + Assert.assertTrue(outsideDir.delete()); + } + } + } +} \ No newline at end of file diff -Nru tomcat11-11.0.15/test/org/apache/catalina/startup/LoggingBaseTest.java tomcat11-11.0.22/test/org/apache/catalina/startup/LoggingBaseTest.java --- tomcat11-11.0.15/test/org/apache/catalina/startup/LoggingBaseTest.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/startup/LoggingBaseTest.java 2026-05-01 18:56:05.000000000 +0000 @@ -89,7 +89,7 @@ * that have to be deleted on cleanup, register them with * {@link #addDeleteOnTearDown(File)}. */ - public File getTemporaryDirectory() { + public static File getTemporaryDirectory() { return tempDir; } diff -Nru tomcat11-11.0.15/test/org/apache/catalina/startup/TomcatBaseTest.java tomcat11-11.0.22/test/org/apache/catalina/startup/TomcatBaseTest.java --- tomcat11-11.0.15/test/org/apache/catalina/startup/TomcatBaseTest.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/startup/TomcatBaseTest.java 2026-05-01 18:56:05.000000000 +0000 @@ -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; @@ -466,6 +467,7 @@ */ public static final class SnoopServlet extends HttpServlet { + @Serial private static final long serialVersionUID = 1L; @Override @@ -481,7 +483,7 @@ 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()); @@ -558,7 +560,7 @@ 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]); @@ -597,7 +599,7 @@ } 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]; diff -Nru tomcat11-11.0.15/test/org/apache/catalina/storeconfig/TestStoreConfig.java tomcat11-11.0.22/test/org/apache/catalina/storeconfig/TestStoreConfig.java --- tomcat11-11.0.15/test/org/apache/catalina/storeconfig/TestStoreConfig.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/storeconfig/TestStoreConfig.java 2026-05-01 18:56:05.000000000 +0000 @@ -26,7 +26,9 @@ import org.junit.Assert; import org.junit.Test; +import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Catalina; +import org.apache.catalina.startup.CatalinaBaseConfigurationSource; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; import org.apache.catalina.util.IOTools; @@ -129,7 +131,7 @@ File serverXml = new File(tomcat.getServer().getCatalinaBase(), Catalina.SERVER_XML); Assert.assertTrue(serverXml.canRead()); addDeleteOnTearDown(serverXml); - String serverXmlDump = ""; + String serverXmlDump; try (FileReader reader = new FileReader(serverXml); StringWriter writer = new StringWriter()) { IOTools.flow(reader, writer); @@ -143,6 +145,20 @@ SAXParserFactory.newInstance().newSAXParser().getXMLReader().parse(new InputSource(new StringReader(serverXmlDump))); tomcat.stop(); + + tomcat.init(new CatalinaBaseConfigurationSource(getTemporaryDirectory(), Catalina.SERVER_XML)); + // If the persisted server.xml is malformed, start() will fail + tomcat.start(); + Connector[] connectors = tomcat.getService().findConnectors(); + boolean foundSsl = false; + for (Connector c : connectors) { + if (c.getSecure()) { + foundSsl = true; + break; + } + } + Assert.assertTrue("SSL connector not found after round-trip", foundSsl); + tomcat.stop(); } } diff -Nru tomcat11-11.0.15/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java tomcat11-11.0.22/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java --- tomcat11-11.0.15/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java 2026-05-01 18:56:05.000000000 +0000 @@ -161,64 +161,6 @@ } @Test - public void testOFB() throws Exception { - src.setEncryptionAlgorithm("AES/OFB/PKCS5Padding"); - src.start(Channel.SND_TX_SEQ); - dest.setEncryptionAlgorithm("AES/OFB/PKCS5Padding"); - dest.start(Channel.SND_TX_SEQ); - - String testInput = "The quick brown fox jumps over the lazy dog."; - - Assert.assertEquals("Failed in OFB mode", - testInput, - roundTrip(testInput, src, dest)); - } - - @Test - public void testCFB() throws Exception { - src.setEncryptionAlgorithm("AES/CFB/PKCS5Padding"); - src.start(Channel.SND_TX_SEQ); - dest.setEncryptionAlgorithm("AES/CFB/PKCS5Padding"); - dest.start(Channel.SND_TX_SEQ); - - String testInput = "The quick brown fox jumps over the lazy dog."; - - Assert.assertEquals("Failed in CFB mode", - testInput, - roundTrip(testInput, src, dest)); - } - - @Test - public void testGCM() throws Exception { - src.setEncryptionAlgorithm("AES/GCM/NoPadding"); - src.start(Channel.SND_TX_SEQ); - dest.setEncryptionAlgorithm("AES/GCM/NoPadding"); - dest.start(Channel.SND_TX_SEQ); - - String testInput = "The quick brown fox jumps over the lazy dog."; - - Assert.assertEquals("Failed in GCM mode", - testInput, - roundTrip(testInput, src, dest)); - } - - /* - * ECB mode isn't supported because it's insecure. - */ - @Test - public void testECB() throws Exception { - try { - src.setEncryptionAlgorithm("AES/ECB/PKCS5Padding"); - src.start(Channel.SND_TX_SEQ); - - // start() should trigger IllegalArgumentException - Assert.fail("ECB mode is not being refused"); - } catch (IllegalArgumentException iae) { - // Expected - } - } - - @Test public void testViaFile() throws Exception { src.start(Channel.SND_TX_SEQ); src.setNext(new ValueCaptureInterceptor()); diff -Nru tomcat11-11.0.15/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorAlgorithms.java tomcat11-11.0.22/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorAlgorithms.java --- tomcat11-11.0.15/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorAlgorithms.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorAlgorithms.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,162 @@ +/* + * 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.tribes.group.interceptors; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.tribes.Channel; + +@RunWith(Parameterized.class) +public class TestEncryptInterceptorAlgorithms extends EncryptionInterceptorBaseTest { + + @Parameters(name = "{index} {0}/{1}/{2}") + public static Collection inputs() { + + List result = new ArrayList<>(); + // Covers all cipher algorithm modes currently listed in Java Standard Names + + // Not supported - Insecure + result.add(new Object[] { "AES", "NONE", "NoPadding", Boolean.FALSE}); + // Not supported - Insecure - Padding makes no sense if there is no encryption + result.add(new Object[] { "AES", "NONE", "PKCS5Padding", Boolean.FALSE}); + + // Not supported - NoPadding requires fixed block size and cluster messages are variable length + result.add(new Object[] { "AES", "CBC", "NoPadding", Boolean.FALSE}); + // Supported but not recommended - possible security issues in some configurations - backwards compatibility + result.add(new Object[] { "AES", "CBC", "PKCS5Padding", Boolean.TRUE}); + + // Not supported - JCA provider doesn't included it + result.add(new Object[] { "AES", "CCM", "NoPadding", Boolean.FALSE}); + // Not supported - JCA provider doesn't included it - CCM doesn't need (support?) padding + result.add(new Object[] { "AES", "CCM", "PKCS5Padding", Boolean.FALSE}); + + // Not supported - NoPadding requires fixed block size and cluster messages are variable length + result.add(new Object[] { "AES", "CFB", "NoPadding", Boolean.FALSE}); + // Supported but not recommended - possible security issues in some configurations - backwards compatibility + result.add(new Object[] { "AES", "CFB", "PKCS5Padding", Boolean.TRUE}); + + // Not supported - Insecure and/or slow + result.add(new Object[] { "AES", "CFB8", "NoPadding", Boolean.FALSE}); + result.add(new Object[] { "AES", "CFB8", "PKCS5Padding", Boolean.FALSE}); + result.add(new Object[] { "AES", "CFB16", "NoPadding", Boolean.FALSE}); + result.add(new Object[] { "AES", "CFB16", "PKCS5Padding", Boolean.FALSE}); + // large block sizes not tested but will be rejected as well + + // Not supported - Insecure + result.add(new Object[] { "AES", "CTR", "NoPadding", Boolean.FALSE}); + // Not supported - Configuration not recommended + result.add(new Object[] { "AES", "CTR", "PKCS5Padding", Boolean.FALSE}); + + // Not supported - has minimum length + result.add(new Object[] { "AES", "CTS", "NoPadding", Boolean.FALSE}); + result.add(new Object[] { "AES", "CTS", "PKCS5Padding", Boolean.FALSE}); + + // Not supported - Insecure + result.add(new Object[] { "AES", "ECB", "NoPadding", Boolean.FALSE}); + result.add(new Object[] { "AES", "ECB", "PKCS5Padding", Boolean.FALSE}); + + // Default for Tomcat 12 onwards + result.add(new Object[] { "AES", "GCM", "NoPadding", Boolean.TRUE}); + // Not supported - GCM doesn't need (support?) padding + result.add(new Object[] { "AES", "GCM", "PKCS5Padding", Boolean.FALSE}); + + // Not supported - KW not appropriate for encrypting cluster messages + result.add(new Object[] { "AES", "KW", "NoPadding", Boolean.FALSE}); + result.add(new Object[] { "AES", "KW", "PKCS5Padding", Boolean.FALSE}); + + // Not supported - KWP not appropriate for encrypting cluster messages + result.add(new Object[] { "AES", "KWP", "NoPadding", Boolean.FALSE}); + result.add(new Object[] { "AES", "KWP", "PKCS5Padding", Boolean.FALSE}); + + // Not supported - NoPadding requires fixed block size and cluster messages are variable length + result.add(new Object[] { "AES", "OFB", "NoPadding", Boolean.FALSE}); + + // Supported but not recommended - possible security issues in some configurations - backwards compatibility + result.add(new Object[] { "AES", "OFB", "PKCS5Padding", Boolean.TRUE}); + + // Not supported - Insecure and/or slow + result.add(new Object[] { "AES", "OFB8", "NoPadding", Boolean.FALSE}); + result.add(new Object[] { "AES", "OFB8", "PKCS5Padding", Boolean.FALSE}); + result.add(new Object[] { "AES", "OFB16", "NoPadding", Boolean.FALSE}); + result.add(new Object[] { "AES", "OFB16", "PKCS5Padding", Boolean.FALSE}); + // large block sizes not tested but will be rejected as well + + // Not supported - Insecure + result.add(new Object[] { "AES", "PCBC", "NoPadding", Boolean.FALSE}); + result.add(new Object[] { "AES", "PCBC", "PKCS5Padding", Boolean.FALSE}); + + return result; + } + + @Parameter(0) + public String algorithm; + + @Parameter(1) + public String mode; + + @Parameter(2) + public String padding; + + @Parameter(3) + public boolean shouldSucceed; + + @Test + public void testAlgorithm() throws Exception { + if (shouldSucceed) { + doTestShouldSucceed(); + } else { + doTestShouldNotSucceed(); + } + } + + private void doTestShouldSucceed() throws Exception { + String transformation = String.format("%s/%s/%s", algorithm, mode, padding); + + src.setEncryptionAlgorithm(transformation); + src.start(Channel.SND_TX_SEQ); + dest.setEncryptionAlgorithm(transformation); + dest.start(Channel.SND_TX_SEQ); + + String testInput = "The quick brown fox jumps over the lazy dog."; + + Assert.assertEquals("Failed in " + transformation + " mode", + testInput, + roundTrip(testInput, src, dest)); + } + + private void doTestShouldNotSucceed() throws Exception { + try { + String transformation = String.format("%s/%s/%s", algorithm, mode, padding); + src.setEncryptionAlgorithm(transformation); + src.start(Channel.SND_TX_SEQ); + + // start() should trigger IllegalArgumentException + Assert.fail(transformation + " mode is not being refused"); + } catch (IllegalArgumentException iae) { + // Expected + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/util/TestServerInfo.java tomcat11-11.0.22/test/org/apache/catalina/util/TestServerInfo.java --- tomcat11-11.0.15/test/org/apache/catalina/util/TestServerInfo.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/util/TestServerInfo.java 2026-05-01 18:56:05.000000000 +0000 @@ -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,485 @@ public void testServerInfo() { ServerInfo.main(new String[0]); } + + /** + * Test that ServerInfo.main() outputs expected basic information. + * + * @throws Exception if the test experiences an unexpected error + */ + @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). + * + * @throws Exception if the test experiences an unexpected error + */ + @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). + * + * @throws Exception if the test experiences an unexpected error + */ + @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). + * + * @throws Exception if the test experiences an unexpected error + */ + @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). + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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. + * + * @throws Exception if the test experiences an unexpected error + */ + @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+.*")); + } + + /** + * Test parseVersionFromFilename() with various filename patterns. + * + * @throws Exception if the test experiences an unexpected error + */ + @Test + public void testParseVersionFromFilename() throws Exception { + // Test cases: [filename, expected version, description] + Object[][] testCases = { + {"commons-logging-1.2.jar", "1.2", "dash separator"}, + {"library_2.3.4.jar", "2.3.4", "underscore separator"}, + {"mylib-1.0.0-SNAPSHOT.jar", "1.0.0-SNAPSHOT", "SNAPSHOT version"}, + {"spring-core-5.3.20.RELEASE.jar", "5.3.20.RELEASE", "RELEASE suffix"}, + {"jackson-databind-2.15.0.jar", "2.15.0", "standard Maven version"}, + {"library.jar", null, "no version in filename"}, + {"library-core.jar", null, "non-numeric suffix"}, + {"test-1.jar", "1", "minimal version"} + }; + + for (Object[] testCase : testCases) { + String filename = (String) testCase[0]; + String expectedVersion = (String) testCase[1]; + String description = (String) testCase[2]; + + String actualVersion = invokeParseVersionFromFilename(filename); + Assert.assertEquals("Failed for: " + description + " (" + filename + ")", + expectedVersion, actualVersion); + } + } + + /** + * Test getJarVersion() fallback to filename parsing when manifest has no version. + * + * @throws Exception if the test experiences an unexpected error + */ + @Test + public void testGetJarVersionFallbackToFilename() throws Exception { + withTestJar("test-library-3.2.1.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + // No version attributes in manifest + }, jar -> Assert.assertEquals("Should fallback to filename parsing", "3.2.1", + invokeGetJarVersion(jar))); + } + + /** + * Test getJarVersion() prefers manifest version over filename. + * + * @throws Exception if the test experiences an unexpected error + */ + @Test + public void testGetJarVersionPrefersManifest() throws Exception { + withTestJar("library-1.0.0.jar", manifest -> { + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue("Bundle-Version", "2.0.0"); + }, jar -> Assert.assertEquals("Should prefer manifest version over filename", "2.0.0", + invokeGetJarVersion(jar))); + } + + /** + * 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)).booleanValue(); + } + + /** + * 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 invoke the private parseVersionFromFilename() method via reflection. + */ + private String invokeParseVersionFromFilename(String filename) throws Exception { + Method method = ServerInfo.class.getDeclaredMethod("parseVersionFromFilename", String.class); + method.setAccessible(true); + return (String) method.invoke(null, filename); + } + + /** + * 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); + @SuppressWarnings("unused") + JarOutputStream jos = new JarOutputStream(fos, manifest)) { + // Empty JAR with just manifest + } + + return jarFile; + } } diff -Nru tomcat11-11.0.15/test/org/apache/catalina/util/TestURLEncoder.java tomcat11-11.0.22/test/org/apache/catalina/util/TestURLEncoder.java --- tomcat11-11.0.15/test/org/apache/catalina/util/TestURLEncoder.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/util/TestURLEncoder.java 2026-05-01 18:56:05.000000000 +0000 @@ -16,11 +16,14 @@ */ package org.apache.catalina.util; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import org.junit.Assert; import org.junit.Test; +import org.apache.tomcat.util.buf.UDecoder; + public class TestURLEncoder { private static final String SPACE = " "; @@ -53,4 +56,29 @@ xml.removeSafeCharacter('&'); Assert.assertEquals(AMPERSAND_ENCODED, xml.encode(AMPERSAND, StandardCharsets.UTF_8)); } + + + @Test(expected = IllegalArgumentException.class) + public void testOssFuzz01() { + /* + * Round-trip URL encoding with ASCII only works for valid ASCII characters. + */ + testRoundTrip("\uFFFD", StandardCharsets.US_ASCII); + } + + + @Test + public void testOssFuzz02() { + testRoundTrip("\uFFFD", StandardCharsets.UTF_8); + } + + + private void testRoundTrip(String input, Charset charset) { + URLEncoder encoder = new URLEncoder(); + + String encoded = encoder.encode(input, charset); + String decoded = UDecoder.URLDecode(encoded, charset); + + Assert.assertEquals(input, decoded); + } } diff -Nru tomcat11-11.0.15/test/org/apache/catalina/valves/TestAccessLogValve.java tomcat11-11.0.22/test/org/apache/catalina/valves/TestAccessLogValve.java --- tomcat11-11.0.15/test/org/apache/catalina/valves/TestAccessLogValve.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestAccessLogValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -114,10 +114,18 @@ parameterSets.add(new Object[] {"pct-m", JSON_TYPE, "/", "%m", "\\{\"method\":\"GET\"\\}"}); parameterSets.add(new Object[] {"pct-p", TEXT_TYPE, "/", "%p", "\\d+"}); parameterSets.add(new Object[] {"pct-p", JSON_TYPE, "/", "%p", "\\{\"port\":\"\\d+\"\\}"}); + parameterSets.add(new Object[] {"pct-q", TEXT_TYPE, "/", "%q", "-"}); + parameterSets.add(new Object[] {"pct-q", JSON_TYPE, "/", "%q", "\\{\"query\":\"-\"\\}"}); + parameterSets.add(new Object[] {"pct-q", TEXT_TYPE, "/?", "%q", "\\?"}); + parameterSets.add(new Object[] {"pct-q", JSON_TYPE, "/?", "%q", "\\{\"query\":\"\\?\"\\}"}); parameterSets.add(new Object[] {"pct-q", TEXT_TYPE, "/?data=123", "%q", "\\?data=123"}); parameterSets.add(new Object[] {"pct-q", JSON_TYPE, "/?data=123", "%q", "\\{\"query\":\"\\?data=123\"\\}"}); parameterSets.add(new Object[] {"pct-r", TEXT_TYPE, "/", "%r", "GET / HTTP/1.1"}); parameterSets.add(new Object[] {"pct-r", JSON_TYPE, "/", "%r", "\\{\"request\":\"GET / HTTP/1.1\"\\}"}); + parameterSets.add(new Object[] {"pct-r", TEXT_TYPE, "/?", "%r", "GET /\\? HTTP/1.1"}); + parameterSets.add(new Object[] {"pct-r", JSON_TYPE, "/?", "%r", "\\{\"request\":\"GET /\\? HTTP/1.1\"\\}"}); + parameterSets.add(new Object[] {"pct-r", TEXT_TYPE, "/?data=123", "%r", "GET /\\?data=123 HTTP/1.1"}); + parameterSets.add(new Object[] {"pct-r", JSON_TYPE, "/?data=123", "%r", "\\{\"request\":\"GET /\\?data=123 HTTP/1.1\"\\}"}); parameterSets.add(new Object[] {"pct-s", TEXT_TYPE, "/", "%s", "200"}); parameterSets.add(new Object[] {"pct-s", JSON_TYPE, "/", "%s", "\\{\"statusCode\":\"200\"\\}"}); parameterSets.add(new Object[] {"pct-S", TEXT_TYPE, "/", "%S", "[A-F0-9]{32}"}); @@ -312,9 +320,9 @@ Assert.assertFalse("Access log line empty after " + (System.currentTimeMillis() - startWait) + " milliseconds", "".equals(result)); boolean matches = Pattern.matches(resultMatch, result); if (!matches) { - log.error("Resulting log line '" + result + "' does not match '" + resultMatch + "'"); + log.error("Resulting log line '" + result + "' does not match pattern '" + resultMatch + "'"); } - Assert.assertTrue("Resulting log line '" + result + "' does not match '" + resultMatch + "'", matches); + Assert.assertTrue("Resulting log line '" + result + "' does not match pattern '" + resultMatch + "'", matches); if (JSON_TYPE.equals(type)) { JSONParser parser = new JSONParser(result); diff -Nru tomcat11-11.0.15/test/org/apache/catalina/valves/TestAccessLogValveFile.java tomcat11-11.0.22/test/org/apache/catalina/valves/TestAccessLogValveFile.java --- tomcat11-11.0.15/test/org/apache/catalina/valves/TestAccessLogValveFile.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestAccessLogValveFile.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,273 @@ +/* + * 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.valves; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.compat.JrePlatform; + +/** + * Tests for {@link AccessLogValve} file I/O, rotation, encoding, and cleanup + * operations. Pattern-based access log format tests are covered by + * {@link TestAccessLogValve}. + */ +public class TestAccessLogValveFile extends TomcatBaseTest { + + private File logDir; + + + @Test + public void testLogWritesToFile() throws Exception { + createValve("access", Constants.AccessLog.COMBINED_ALIAS); + getTomcatInstance().start(); + + getUrl("http://localhost:" + getPort()); + + File logFile = new File(logDir, "access.log"); + awaitFile(logFile); + + String content = Files.readString(logFile.toPath()); + Assert.assertTrue(content.contains("200")); + Assert.assertTrue(content.contains("GET")); + } + + + @Test + public void testRotateWithNewFileName() throws Exception { + AccessLogValve valve = createValve("access", "%s"); + getTomcatInstance().start(); + + getUrl("http://localhost:" + getPort()); + File logFile = new File(logDir, "access.log"); + awaitFile(logFile); + + File rotatedFile = new File(logDir, "access_rotated.log"); + Assert.assertTrue(valve.rotate(rotatedFile.getAbsolutePath())); + Assert.assertTrue(rotatedFile.exists()); + + getUrl("http://localhost:" + getPort()); + awaitFile(logFile); + + File[] logFiles = logDir.listFiles((dir, name) -> name.startsWith("access") && name.endsWith(".log")); + Assert.assertNotNull(logFiles); + Assert.assertEquals(2, logFiles.length); + } + + + @Test + public void testRotateReturnsFalseWhenNoLogFile() { + AccessLogValve valve = new AccessLogValve(); + // currentLogFile will be null because the valve was never started + boolean result = valve.rotate("nonexistent.log"); + Assert.assertFalse("rotate() should return false when no log file", result); + } + + + @Test + public void testRenameOnRotate() throws Exception { + AccessLogValve valve = createValve("access", "%s"); + valve.setRotatable(true); + valve.setRenameOnRotate(true); + getTomcatInstance().start(); + + getUrl("http://localhost:" + getPort()); + + // With renameOnRotate, the active log has no date stamp + File logFile = new File(logDir, "access.log"); + awaitFile(logFile); + Assert.assertTrue(logFile.exists()); + + File[] datedFiles = logDir.listFiles( + (dir, name) -> name.startsWith("access.") && name.endsWith(".log") + && name.length() > "access.log".length()); + Assert.assertTrue("No dated log file should exist before rotation", + datedFiles == null || datedFiles.length == 0); + } + + + @Test + public void testMaxDaysCleanup() throws Exception { + AccessLogValve valve = createValve("access", "%s"); + valve.setRotatable(true); + valve.setMaxDays(1); + + File oldLog = new File(logDir, "access.old.log"); + Assert.assertTrue(oldLog.createNewFile()); + Assert.assertTrue(oldLog.setLastModified(System.currentTimeMillis() - 10L * 24 * 60 * 60 * 1000)); + + getTomcatInstance().start(); + + valve.backgroundProcess(); + + Assert.assertFalse(oldLog.exists()); + } + + + @Test + public void testBufferedFlush() throws Exception { + AccessLogValve valve = createValve("access_buffered", "%s"); + valve.setBuffered(true); + getTomcatInstance().start(); + + getUrl("http://localhost:" + getPort()); + + // Flush via backgroundProcess since buffered=true + valve.backgroundProcess(); + + File logFile = new File(logDir, "access_buffered.log"); + awaitFile(logFile); + String content = Files.readString(logFile.toPath()); + Assert.assertTrue(content.contains("200")); + } + + + @Test + public void testCheckExists() throws Exception { + Assume.assumeFalse("This test cannot pass on Windows due to locking of open files.", JrePlatform.IS_WINDOWS); + + AccessLogValve valve = createValve("access_check", "%s"); + valve.setCheckExists(true); + getTomcatInstance().start(); + + getUrl("http://localhost:" + getPort()); + File logFile = new File(logDir, "access_check.log"); + awaitFile(logFile); + + Assert.assertTrue(logFile.delete()); + Assert.assertFalse(logFile.exists()); + + getUrl("http://localhost:" + getPort()); + awaitFile(logFile); + Assert.assertTrue(logFile.exists()); + } + + + @Test + public void testCustomEncoding() throws Exception { + AccessLogValve valve = createValve("access_iso", "%s"); + valve.setEncoding("ISO-8859-1"); + getTomcatInstance().start(); + + getUrl("http://localhost:" + getPort()); + + File logFile = new File(logDir, "access_iso.log"); + awaitFile(logFile); + + String content = Files.readString(logFile.toPath(), StandardCharsets.ISO_8859_1); + Assert.assertTrue(content.contains("200")); + } + + @Test + public void testGetSetProperties() { + AccessLogValve valve = new AccessLogValve(); + + Assert.assertEquals("logs", valve.getDirectory()); + valve.setDirectory("custom-dir"); + Assert.assertEquals("custom-dir", valve.getDirectory()); + + Assert.assertEquals("access_log", valve.getPrefix()); + valve.setPrefix("myapp"); + Assert.assertEquals("myapp", valve.getPrefix()); + + Assert.assertEquals("", valve.getSuffix()); + valve.setSuffix(".txt"); + Assert.assertEquals(".txt", valve.getSuffix()); + + Assert.assertTrue(valve.isRotatable()); + valve.setRotatable(false); + Assert.assertFalse(valve.isRotatable()); + + Assert.assertFalse(valve.isRenameOnRotate()); + valve.setRenameOnRotate(true); + Assert.assertTrue(valve.isRenameOnRotate()); + + Assert.assertTrue(valve.isBuffered()); + valve.setBuffered(false); + Assert.assertFalse(valve.isBuffered()); + + Assert.assertFalse(valve.isCheckExists()); + valve.setCheckExists(true); + Assert.assertTrue(valve.isCheckExists()); + + Assert.assertEquals(".yyyy-MM-dd", valve.getFileDateFormat()); + valve.setFileDateFormat(".yyyyMMdd"); + Assert.assertEquals(".yyyyMMdd", valve.getFileDateFormat()); + + Assert.assertEquals(-1, valve.getMaxDays()); + valve.setMaxDays(30); + Assert.assertEquals(30, valve.getMaxDays()); + + Assert.assertNull(valve.getEncoding()); + valve.setEncoding("UTF-16"); + Assert.assertEquals("UTF-16", valve.getEncoding()); + } + + + /** + * Creates an {@link AccessLogValve} with common defaults (non-rotatable, + * unbuffered) attached to a Tomcat instance ready to start. + */ + private AccessLogValve createValve(String prefix, String pattern) + throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context ctx = getProgrammaticRootContext(); + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + logDir = getLogDir(); + AccessLogValve valve = new AccessLogValve(); + valve.setDirectory(logDir.getAbsolutePath()); + valve.setPrefix(prefix); + valve.setSuffix(".log"); + valve.setRotatable(false); + valve.setBuffered(false); + valve.setPattern(pattern); + tomcat.getHost().getPipeline().addValve(valve); + return valve; + } + + + private File getLogDir() throws IOException { + File dir = new File(getTemporaryDirectory(), "access-log-test"); + if (!dir.mkdirs() && !dir.isDirectory()) { + throw new IOException("Failed to create log directory: " + dir); + } + addDeleteOnTearDown(dir); + return dir; + } + + @SuppressWarnings("BusyWait") + private static void awaitFile(File file) throws InterruptedException { + long deadline = System.currentTimeMillis() + 5000; + while (!file.exists() || file.length() == 0) { + if (System.currentTimeMillis() > deadline) { + Assert.fail("Timed out waiting for " + file.getName()); + } + Thread.sleep(50); + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/valves/TestFilterValve.java tomcat11-11.0.22/test/org/apache/catalina/valves/TestFilterValve.java --- tomcat11-11.0.15/test/org/apache/catalina/valves/TestFilterValve.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestFilterValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,229 @@ +/* + * 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.valves; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestFilterValve extends TomcatBaseTest { + + + @Test + public void testFilterPassthrough() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + FilterValve valve = new FilterValve(); + valve.setFilterClass(PassthroughFilter.class.getName()); + ctx.getPipeline().addValve(valve); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(HelloWorldServlet.RESPONSE_TEXT, res.toString()); + } + + + @Test + public void testFilterBlocks() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + FilterValve valve = new FilterValve(); + valve.setFilterClass(BlockingFilter.class.getName()); + ctx.getPipeline().addValve(valve); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, rc); + } + + @Test + public void testFilterWrappingRequestThrows() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + FilterValve valve = new FilterValve(); + valve.setFilterClass(WrappingFilter.class.getName()); + ctx.getPipeline().addValve(valve); + + tomcat.start(); + + int rc = getUrl("http://localhost:" + getPort(), new ByteChunk(), null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + + + @Test(expected = LifecycleException.class) + public void testNullFilterClassThrowsOnStart() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + FilterValve valve = new FilterValve(); + // Do NOT set filterClassName + ctx.getPipeline().addValve(valve); + + tomcat.start(); + } + + + @Test(expected = LifecycleException.class) + public void testInvalidFilterClassThrowsOnStart() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + FilterValve valve = new FilterValve(); + valve.setFilterClass("com.nonexistent.FakeFilter"); + ctx.getPipeline().addValve(valve); + + tomcat.start(); + } + + + @Test + public void testGetFilterNameReturnsNull() { + FilterValve valve = new FilterValve(); + Assert.assertNull(valve.getFilterName()); + } + + + @Test + public void testInitParams() { + FilterValve valve = new FilterValve(); + + valve.addInitParam("key1", "value1"); + valve.addInitParam("key2", "value2"); + + Assert.assertEquals("value1", valve.getInitParameter("key1")); + Assert.assertEquals("value2", valve.getInitParameter("key2")); + Assert.assertNull(valve.getInitParameter("nonexistent")); + + List names = Collections.list(valve.getInitParameterNames()); + Assert.assertEquals(2, names.size()); + Assert.assertTrue(names.contains("key1")); + Assert.assertTrue(names.contains("key2")); + } + + + @Test + public void testInitParamsEmpty() { + FilterValve valve = new FilterValve(); + + Assert.assertNull(valve.getInitParameter("anything")); + Assert.assertFalse(valve.getInitParameterNames().hasMoreElements()); + } + + + @Test + public void testGetSetFilterClassName() { + FilterValve valve = new FilterValve(); + + Assert.assertNull(valve.getFilterClassName()); + + valve.setFilterClassName("com.example.MyFilter"); + Assert.assertEquals("com.example.MyFilter", valve.getFilterClassName()); + + valve.setFilterClass("com.example.OtherFilter"); + Assert.assertEquals("com.example.OtherFilter", valve.getFilterClassName()); + } + + @Test(expected = IllegalStateException.class) + public void testGetServletContextThrowsBeforeStart() { + FilterValve valve = new FilterValve(); + valve.getServletContext(); + } + + + /** + * A Filter that passes the request through to the next element in the chain. + */ + public static final class PassthroughFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + chain.doFilter(request, response); + } + } + + + /** + * A Filter that blocks the request by sending a 403 response without calling chain.doFilter(). + */ + public static final class BlockingFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + ((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN); + } + } + + /** + * A Filter that wraps the request before calling chain.doFilter(), which FilterValve explicitly forbids. + */ + public static final class WrappingFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + HttpServletRequestWrapper wrapped = new HttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(wrapped, response); + } + } + +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/valves/TestJsonErrorReportValve.java tomcat11-11.0.22/test/org/apache/catalina/valves/TestJsonErrorReportValve.java --- tomcat11-11.0.15/test/org/apache/catalina/valves/TestJsonErrorReportValve.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestJsonErrorReportValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,412 @@ +/* + * 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.valves; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.json.JSONParser; + +public class TestJsonErrorReportValve extends TomcatBaseTest { + + private static final String JSON_VALVE = "org.apache.catalina.valves.JsonErrorReportValve"; + + + @Test + public void testJsonErrorResponse500() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(JSON_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "sendError", new SendErrorServlet( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Server broke")); + ctx.addServletMappingDecoded("/", "sendError"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + Map> resHead = new HashMap<>(); + int rc = getUrl("http://localhost:" + getPort(), res, resHead); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + // Verify Content-Type + List contentType = resHead.get("Content-Type"); + Assert.assertNotNull("Content-Type header should be present", contentType); + Assert.assertTrue("Content-Type should be application/json", + contentType.get(0).contains("application/json")); + + // Parse and verify JSON + String body = res.toString(); + JSONParser parser = new JSONParser(body); + LinkedHashMap json = parser.parseObject(); + + Assert.assertEquals("Status Report", json.get("type")); + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ((Number) json.get("status")).intValue()); + Assert.assertEquals("Server broke", json.get("message")); + Assert.assertNotNull(json.get("description")); + } + + + @Test + public void testJsonErrorWithThrowable() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(JSON_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "exception", + new ExceptionServlet("Something went wrong")); + ctx.addServletMappingDecoded("/", "exception"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + // Parse and verify JSON + String body = res.toString(); + JSONParser parser = new JSONParser(body); + LinkedHashMap json = parser.parseObject(); + + Assert.assertEquals("Exception Report", json.get("type")); + Assert.assertEquals(500, + ((Number) json.get("status")).intValue()); + Assert.assertNotNull(json.get("throwable")); + + // throwable should be a list containing exception strings + @SuppressWarnings("unchecked") + ArrayList throwableList = (ArrayList) json.get("throwable"); + Assert.assertFalse("throwable array should not be empty", + throwableList.isEmpty()); + + String throwableStr = throwableList.toString(); + Assert.assertTrue("Response should contain exception class name", + throwableStr.contains("RuntimeException")); + Assert.assertTrue("Response should contain exception message", + throwableStr.contains("Something went wrong")); + } + + + @Test + public void testJsonErrorWithSpecialChars() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(JSON_VALVE); + + Context ctx = getProgrammaticRootContext(); + + // Characters that require JSON escaping: quotes and backslashes + String specialMessage = "Error with \"quotes\" and \\backslash\\"; + Tomcat.addServlet(ctx, "specialChars", new SendErrorServlet( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, specialMessage)); + ctx.addServletMappingDecoded("/", "specialChars"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + // Parse JSON - if escaping is broken, the parser will throw + String body = res.toString(); + JSONParser parser = new JSONParser(body); + LinkedHashMap json = parser.parseObject(); + + Assert.assertEquals("Status Report", json.get("type")); + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ((Number) json.get("status")).intValue()); + + // Verify the message field is present and contains the + // expected substrings (the parser returns raw escaped values) + String message = (String) json.get("message"); + Assert.assertNotNull("message should be present", message); + Assert.assertTrue("message should contain quotes", message.contains("quotes")); + Assert.assertTrue("message should contain backslash", message.contains("backslash")); + } + + + @Test + public void testJsonCustomStatusCode() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(JSON_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "customError", + new SendErrorServlet(999, "The sky is falling")); + ctx.addServletMappingDecoded("/", "customError"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(999, rc); + + // Parse and verify JSON + String body = res.toString(); + JSONParser parser = new JSONParser(body); + LinkedHashMap json = parser.parseObject(); + + Assert.assertEquals(999, ((Number) json.get("status")).intValue()); + Assert.assertEquals("The sky is falling", json.get("message")); + Assert.assertNotNull(json.get("description")); + } + + + @Test + public void testJsonError404() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(JSON_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "notFound", new SendErrorServlet( + HttpServletResponse.SC_NOT_FOUND, "Resource not found")); + ctx.addServletMappingDecoded("/", "notFound"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + res.setCharset(StandardCharsets.UTF_8); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + // Parse and verify JSON + String body = res.toString(); + JSONParser parser = new JSONParser(body); + LinkedHashMap json = parser.parseObject(); + + Assert.assertEquals("Status Report", json.get("type")); + Assert.assertEquals(404, + ((Number) json.get("status")).intValue()); + Assert.assertEquals("Resource not found", json.get("message")); + Assert.assertNotNull(json.get("description")); + } + + + @Test + public void testJsonErrorWithChainedExceptions() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(JSON_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "chained", new ChainedExceptionServlet()); + ctx.addServletMappingDecoded("/", "chained"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + res.setCharset(StandardCharsets.UTF_8); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + // Parse and verify JSON + String body = res.toString(); + JSONParser parser = new JSONParser(body); + LinkedHashMap json = parser.parseObject(); + + Assert.assertEquals("Exception Report", json.get("type")); + Assert.assertNotNull(json.get("throwable")); + + // The throwable array should contain both the outer and inner exceptions + @SuppressWarnings("unchecked") + ArrayList throwableList = (ArrayList) json.get("throwable"); + String throwableStr = throwableList.toString(); + Assert.assertTrue("Response should contain outer exception", + throwableStr.contains("RuntimeException")); + Assert.assertTrue("Response should contain root cause", + throwableStr.contains("IllegalStateException")); + Assert.assertFalse("Catalina core classes should be filtered", + throwableStr.contains("org.apache.catalina.core.")); + } + + @Test + public void testJsonErrorWithoutMessage() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(JSON_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "noMessage", new SendErrorServlet( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null)); + ctx.addServletMappingDecoded("/", "noMessage"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + String body = res.toString(); + JSONParser parser = new JSONParser(body); + LinkedHashMap json = parser.parseObject(); + + Assert.assertEquals("Status Report", json.get("type")); + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ((Number) json.get("status")).intValue()); + Assert.assertEquals("", json.get("message")); + Assert.assertNotNull(json.get("description")); + } + + @Test + public void testNoJsonBodyForNonErrorStatus() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(JSON_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + Map> resHead = new HashMap<>(); + int rc = getUrl("http://localhost:" + getPort(), res, resHead); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String body = res.toString(); + Assert.assertEquals(HelloWorldServlet.RESPONSE_TEXT, body); + } + + @Test + public void testJsonErrorWithUnicodeMessage() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(JSON_VALVE); + + Context ctx = getProgrammaticRootContext(); + + String unicodeMessage = "Error: \u00e9\u00e8\u00ea \u4e2d\u6587 \u00f1"; + Tomcat.addServlet(ctx, "unicode", new SendErrorServlet( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, unicodeMessage)); + ctx.addServletMappingDecoded("/", "unicode"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + res.setCharset(StandardCharsets.UTF_8); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + String body = res.toString(); + JSONParser parser = new JSONParser(body); + LinkedHashMap json = parser.parseObject(); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ((Number) json.get("status")).intValue()); + Assert.assertEquals(unicodeMessage, json.get("message")); + } + + @Test + public void testNoJsonForUnknownStatusWithoutMessage() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(JSON_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "unknownNoMessage", new SendErrorServlet(999, null)); + ctx.addServletMappingDecoded("/", "unknownNoMessage"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(999, rc); + + String body = res.toString(); + Assert.assertTrue(body == null || body.isEmpty()); + } + + + private static final class SendErrorServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private final int statusCode; + private final String message; + + private SendErrorServlet(int statusCode, String message) { + this.statusCode = statusCode; + this.message = message; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + if (message != null) { + resp.sendError(statusCode, message); + } else { + resp.sendError(statusCode); + } + } + } + + + private static final class ExceptionServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private final String message; + + private ExceptionServlet(String message) { + this.message = message; + } + + @Override + public void service(ServletRequest request, ServletResponse response) { + throw new RuntimeException(message); + } + } + + + private static final class ChainedExceptionServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void service(ServletRequest request, ServletResponse response) { + throw new RuntimeException("Outer exception", + new IllegalStateException("Root cause")); + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java tomcat11-11.0.22/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java --- tomcat11-11.0.15/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -45,11 +45,12 @@ public class TestLoadBalancerDrainingValve { @Parameters(name = "{index}: activation[{0}], validSessionID[{1}], expectInvokeNext[{2}], enableIgnore[{3}], " + - "queryString[{4}]") + "leadingSlashes[{4}], queryString[{5}]]") public static Collection parameters() { String[] jkActivations = new String[] { "ACT", "DIS" }; Boolean[] booleans = new Boolean[] { Boolean.TRUE, Boolean.FALSE }; + Integer[] integers = new Integer[] { Integer.valueOf(0), Integer.valueOf(1), Integer.valueOf(2) }; String[] queryStrings = new String[] { null, "foo=bar" }; List parameterSets = new ArrayList<>(); @@ -58,11 +59,14 @@ for (Boolean enableIgnore : booleans) { Boolean expectInvokeNext = Boolean.valueOf( "ACT".equals(jkActivation) || enableIgnore.booleanValue() || validSessionId.booleanValue()); - for (String queryString : queryStrings) { - for (Boolean secureRequest : booleans) { - for (Boolean secureSessionConfig : booleans) { - parameterSets.add(new Object[] { jkActivation, validSessionId, expectInvokeNext, - enableIgnore, queryString, secureRequest, secureSessionConfig }); + for (Integer leadingSlashes : integers) { + for (String queryString : queryStrings) { + for (Boolean secureRequest : booleans) { + for (Boolean secureSessionConfig : booleans) { + parameterSets.add(new Object[] { jkActivation, validSessionId, expectInvokeNext, + enableIgnore, leadingSlashes, queryString, secureRequest, + secureSessionConfig }); + } } } } @@ -86,12 +90,15 @@ public boolean enableIgnore; @Parameter(4) - public String queryString; + public Integer leadingSlashes; @Parameter(5) - public Boolean secureRequest; + public String queryString; @Parameter(6) + public Boolean secureRequest; + + @Parameter(7) public boolean secureSessionConfig; @@ -140,7 +147,7 @@ cookies.add(cookie); EasyMock.expect(request.getRequestedSessionId()).andStubReturn(sessionId); - EasyMock.expect(request.getRequestURI()).andStubReturn(requestURI); + EasyMock.expect(request.getRequestURI()).andStubReturn("/".repeat(leadingSlashes.intValue()) + requestURI); EasyMock.expect(request.getCookies()).andStubReturn(cookies.toArray(new Cookie[0])); EasyMock.expect(request.getContext()).andStubReturn(ctx); EasyMock.expect(ctx.getSessionCookieName()).andStubReturn(sessionCookieName); @@ -166,8 +173,7 @@ if (null != queryString) { expectedRequestURI = expectedRequestURI + '?' + queryString; } - response.setHeader("Location", expectedRequestURI); - response.setStatus(307); + response.sendRedirect(expectedRequestURI, 307); } } diff -Nru tomcat11-11.0.15/test/org/apache/catalina/valves/TestPersistentValve.java tomcat11-11.0.22/test/org/apache/catalina/valves/TestPersistentValve.java --- tomcat11-11.0.15/test/org/apache/catalina/valves/TestPersistentValve.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestPersistentValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -1,4 +1,5 @@ -/* Licensed to the Apache Software Foundation (ASF) under one or more +/* + * 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 @@ -25,6 +26,7 @@ import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; +import org.apache.catalina.session.StandardSession; import org.apache.tomcat.unittest.TesterRequest; import org.apache.tomcat.unittest.TesterResponse; @@ -61,27 +63,145 @@ }); } + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + Assert.assertEquals(1, testerValve.getMaximumConcurrency()); + } + + + @Test + public void testFilterMatchBypassesSession() throws Exception { + PersistentValve pv = new PersistentValve(); + Request request = new TesterRequest(); + Response response = new TesterResponse(); + TesterValve testerValve = new TesterValve(); + + pv.setFilter(".*\\.html"); + request.setRequestedSessionId("1234"); + + pv.setContainer(request.getContext()); + pv.setNext(testerValve); + pv.init(); + + Thread[] threads = new Thread[2]; + for (int i = 0; i < threads.length; i++) { - threads[i].start(); + threads[i] = new Thread(() -> { + try { + pv.invoke(request, response); + } catch (IOException | ServletException e) { + throw new RuntimeException(e); + } + }); } + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + Assert.assertEquals(2, testerValve.getMaximumConcurrency()); + } + + + @Test + public void testFilterNoMatchProcessesSession() throws Exception { + PersistentValve pv = new PersistentValve(); + Request request = new TesterRequest(); + Response response = new TesterResponse(); + TesterValve testerValve = new TesterValve(); + + // The filter defines which requests don't need sessions (e.g. static resources). + pv.setFilter(".*\\css"); + request.setRequestedSessionId("1234"); + + pv.setContainer(request.getContext()); + pv.setNext(testerValve); + pv.init(); + + Thread[] threads = new Thread[2]; + for (int i = 0; i < threads.length; i++) { - threads[i].join(); + threads[i] = new Thread(() -> { + try { + pv.invoke(request, response); + } catch (IOException | ServletException e) { + throw new RuntimeException(e); + } + }); + } + + for (Thread thread : threads) { + thread.start(); + } + + for (Thread thread : threads) { + thread.join(); } Assert.assertEquals(1, testerValve.getMaximumConcurrency()); } + @Test + public void testIsSessionStale() { + PersistentValve pv = new PersistentValve(); + StandardSession session = new StandardSession(null); + session.setId("test-stale", false); + session.setValid(true); + + session.setMaxInactiveInterval(1); + session.setCreationTime(System.currentTimeMillis() - 60 * 1000); + Assert.assertTrue(pv.isSessionStale(session, System.currentTimeMillis())); + + session.setMaxInactiveInterval(3600); + session.setCreationTime(System.currentTimeMillis()); + Assert.assertFalse(pv.isSessionStale(session, System.currentTimeMillis())); + + session.setMaxInactiveInterval(-1); + Assert.assertFalse(pv.isSessionStale(session, System.currentTimeMillis())); + + Assert.assertFalse(pv.isSessionStale(null, System.currentTimeMillis())); + } + + @Test + public void testRequestWithoutSessionNoFilter() { + PersistentValve pv = new PersistentValve(); + Assert.assertFalse(pv.isRequestWithoutSession("/index.html")); + } + + + @Test + public void testRequestWithoutSessionWithFilter() { + PersistentValve pv = new PersistentValve(); + pv.setContainer(new TesterRequest().getContext()); + + pv.setFilter(".*\\.(css|js|png)"); + + Assert.assertTrue(pv.isRequestWithoutSession("/style.css")); + Assert.assertTrue(pv.isRequestWithoutSession("/app.js")); + Assert.assertTrue(pv.isRequestWithoutSession("/logo.png")); + Assert.assertFalse(pv.isRequestWithoutSession("/index.html")); + Assert.assertFalse(pv.isRequestWithoutSession("/api/data")); + } private static class TesterValve extends ValveBase { - private static AtomicInteger maximumConcurrency = new AtomicInteger(); - private static AtomicInteger concurrency = new AtomicInteger(); + private static final AtomicInteger maximumConcurrency = new AtomicInteger(); + private static final AtomicInteger concurrency = new AtomicInteger(); @Override - public void invoke(Request request, Response response) throws IOException, ServletException { + public void invoke(Request request, Response response) { int c = concurrency.incrementAndGet(); - maximumConcurrency.getAndUpdate((v) -> c > v ? c : v); + maximumConcurrency.getAndUpdate(v -> Math.max(c, v)); try { Thread.sleep(1000); } catch (InterruptedException e) { diff -Nru tomcat11-11.0.15/test/org/apache/catalina/valves/TestProxyErrorReportValve.java tomcat11-11.0.22/test/org/apache/catalina/valves/TestProxyErrorReportValve.java --- tomcat11-11.0.15/test/org/apache/catalina/valves/TestProxyErrorReportValve.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestProxyErrorReportValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,277 @@ +/* + * 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.valves; + +import java.io.IOException; +import java.io.Serial; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestProxyErrorReportValve extends TomcatBaseTest { + + private static final String PROXY_VALVE = + "org.apache.catalina.valves.ProxyErrorReportValve"; + + + @Test + public void testRedirectMode() throws Exception { + Tomcat tomcat = getTomcatInstance(); + StandardHost host = (StandardHost) tomcat.getHost(); + host.setErrorReportValveClass(PROXY_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "error", new SendErrorServlet( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Server broke")); + ctx.addServletMappingDecoded("/", "error"); + + // Register an error page at the Host's error report valve level + // so findErrorPage() returns a URL for the redirect + Tomcat.addServlet(ctx, "errorPage", new ErrorPageServlet()); + ctx.addServletMappingDecoded("/error-page", "errorPage"); + + tomcat.start(); + + ProxyErrorReportValve valve = (ProxyErrorReportValve) host.getPipeline().getFirst(); + valve.setProperty("errorCode." + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "http://localhost:" + getPort() + "/error-page"); + + int rc = getUrl("http://localhost:" + getPort(), new ByteChunk(), false); + + Assert.assertEquals(HttpServletResponse.SC_FOUND, rc); + } + + @Test + public void testProxyMode() throws Exception { + Tomcat tomcat = getTomcatInstance(); + StandardHost host = (StandardHost) tomcat.getHost(); + host.setErrorReportValveClass(PROXY_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "error", new SendErrorServlet( + HttpServletResponse.SC_NOT_FOUND, "Not found")); + ctx.addServletMappingDecoded("/", "error"); + + Tomcat.addServlet(ctx, "errorPage", new ErrorPageServlet()); + ctx.addServletMappingDecoded("/error-page", "errorPage"); + + tomcat.start(); + + ProxyErrorReportValve valve = (ProxyErrorReportValve) host.getPipeline().getFirst(); + valve.setUseRedirect(false); + valve.setProperty("errorCode." + HttpServletResponse.SC_NOT_FOUND, + "http://localhost:" + getPort() + "/error-page"); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + Assert.assertTrue(res.toString().contains("ERROR_PAGE_OK")); + } + + + @Test + public void testNoErrorPageFallsBackToSuper() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(PROXY_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "error", new SendErrorServlet( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "No page configured")); + ctx.addServletMappingDecoded("/", "error"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + String body = res.toString(); + Assert.assertNotNull(body); + Assert.assertTrue("Should contain HTML error report", + body.contains("html") && + body.contains(String.valueOf(HttpServletResponse.SC_INTERNAL_SERVER_ERROR))); + } + + + @Test + public void testStatusBelow400Ignored() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(PROXY_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(HelloWorldServlet.RESPONSE_TEXT, res.toString()); + } + + + @Test + public void testStatusNotFound() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(PROXY_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "notFound", new SendErrorServlet( + HttpServletResponse.SC_NOT_FOUND, "Resource not found")); + ctx.addServletMappingDecoded("/", "notFound"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + + String body = res.toString(); + Assert.assertNotNull(body); + Assert.assertTrue("Should contain error report", + body.contains(String.valueOf(HttpServletResponse.SC_NOT_FOUND))); + } + + + @Test + public void testGetSetProperties() { + ProxyErrorReportValve valve = new ProxyErrorReportValve(); + + Assert.assertTrue(valve.getUseRedirect()); + Assert.assertFalse(valve.getUsePropertiesFile()); + + valve.setUseRedirect(false); + Assert.assertFalse(valve.getUseRedirect()); + + valve.setUsePropertiesFile(true); + Assert.assertTrue(valve.getUsePropertiesFile()); + } + + + @Test + public void testMessageInErrorReport() throws Exception { + final String customErrorMessage = "Custom error message"; + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(PROXY_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "error", new SendErrorServlet( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, customErrorMessage)); + ctx.addServletMappingDecoded("/", "error"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + String body = res.toString(); + Assert.assertNotNull(body); + // Falls back to super.report() which includes the message + Assert.assertTrue(body.contains(customErrorMessage)); + } + + + @Test + public void testExceptionErrorReport() throws Exception { + Tomcat tomcat = getTomcatInstance(); + ((StandardHost) tomcat.getHost()).setErrorReportValveClass(PROXY_VALVE); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "exception", new ExceptionServlet()); + ctx.addServletMappingDecoded("/", "exception"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + + String body = res.toString(); + Assert.assertNotNull(body); + Assert.assertTrue(body.contains("RuntimeException")); + } + + + private static final class SendErrorServlet extends HttpServlet { + + @Serial + private static final long serialVersionUID = 1L; + + private final int statusCode; + private final String message; + + private SendErrorServlet(int statusCode, String message) { + this.statusCode = statusCode; + this.message = message; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + resp.sendError(statusCode, message); + } + } + + private static final class ErrorPageServlet extends HttpServlet { + + @Serial + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + resp.getWriter().print("ERROR_PAGE_OK"); + } + } + + + private static final class ExceptionServlet extends HttpServlet { + + @Serial + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + throw new RuntimeException("Test exception"); + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/valves/TestSemaphoreValve.java tomcat11-11.0.22/test/org/apache/catalina/valves/TestSemaphoreValve.java --- tomcat11-11.0.15/test/org/apache/catalina/valves/TestSemaphoreValve.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/valves/TestSemaphoreValve.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,440 @@ +/* + * 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.valves; + +import java.io.IOException; +import java.io.Serial; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestSemaphoreValve extends TomcatBaseTest { + + + @Test + public void testBasicConcurrency() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + SemaphoreValve valve = new SemaphoreValve(); + valve.setConcurrency(10); + ctx.getPipeline().addValve(valve); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(HelloWorldServlet.RESPONSE_TEXT, res.toString()); + } + + @Test + public void testInterruptedConcurrency() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + SemaphoreValve valve = new SemaphoreValve(); + valve.setConcurrency(10); + valve.setInterruptible(true); + ctx.getPipeline().addValve(valve); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(HelloWorldServlet.RESPONSE_TEXT, res.toString()); + } + + + @Test + public void testNonBlockingDenied() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + CountDownLatch insideServlet = new CountDownLatch(1); + CountDownLatch canReturn = new CountDownLatch(1); + Tomcat.addServlet(ctx, "slow", new SlowServlet(insideServlet, canReturn)); + ctx.addServletMappingDecoded("/", "slow"); + + SemaphoreValve valve = new SemaphoreValve(); + valve.setConcurrency(1); + valve.setBlock(false); + valve.setHighConcurrencyStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + ctx.getPipeline().addValve(valve); + + tomcat.start(); + + // First request — should acquire the permit and block inside the servlet + AtomicInteger firstRc = new AtomicInteger(); + Thread firstThread = new Thread(() -> { + try { + firstRc.set(getUrl("http://localhost:" + getPort(), new ByteChunk(), null)); + } catch (IOException e) { + // Ignore + } + }); + firstThread.start(); + + // Wait until the first request is inside the servlet + Assert.assertTrue("First request should reach servlet", + insideServlet.await(10, TimeUnit.SECONDS)); + + // Second request — should be denied because concurrency=1 and block=false + int rc2 = getUrl("http://localhost:" + getPort(), new ByteChunk(), null); + + Assert.assertEquals(HttpServletResponse.SC_SERVICE_UNAVAILABLE, rc2); + + // Release the first request + canReturn.countDown(); + firstThread.join(10000); + Assert.assertFalse(firstThread.isAlive()); + Assert.assertEquals(HttpServletResponse.SC_OK, firstRc.get()); + } + + + @Test + public void testHighConcurrencyStatusNotSet() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + CountDownLatch insideServlet = new CountDownLatch(1); + CountDownLatch canReturn = new CountDownLatch(1); + Tomcat.addServlet(ctx, "slow", new SlowServlet(insideServlet, canReturn)); + ctx.addServletMappingDecoded("/", "slow"); + + SemaphoreValve valve = new SemaphoreValve(); + valve.setConcurrency(1); + valve.setBlock(false); + // highConcurrencyStatus is -1 by default (no error sent) + ctx.getPipeline().addValve(valve); + + tomcat.start(); + + // First request holds the permit + Thread firstThread = new Thread(() -> { + try { + getUrl("http://localhost:" + getPort(), new ByteChunk(), null); + } catch (IOException e) { + // Ignore + } + }); + firstThread.start(); + + Assert.assertTrue("First request should reach servlet", + insideServlet.await(10, TimeUnit.SECONDS)); + + // Second request — denied but no error status is sent + int rc2 = getUrl("http://localhost:" + getPort(), new ByteChunk(), null); + + // With no highConcurrencyStatus, response is 200 without body + Assert.assertEquals(HttpServletResponse.SC_OK, rc2); + + canReturn.countDown(); + firstThread.join(10000); + } + + + @Test + public void testGetSetProperties() { + SemaphoreValve valve = new SemaphoreValve(); + + // Defaults + Assert.assertEquals(10, valve.getConcurrency()); + Assert.assertFalse(valve.getFairness()); + Assert.assertTrue(valve.getBlock()); + Assert.assertFalse(valve.getInterruptible()); + Assert.assertEquals(-1, valve.getHighConcurrencyStatus()); + + // Setters + valve.setConcurrency(5); + Assert.assertEquals(5, valve.getConcurrency()); + + valve.setFairness(true); + Assert.assertTrue(valve.getFairness()); + + valve.setBlock(false); + Assert.assertFalse(valve.getBlock()); + + valve.setInterruptible(true); + Assert.assertTrue(valve.getInterruptible()); + + valve.setHighConcurrencyStatus(429); + Assert.assertEquals(429, valve.getHighConcurrencyStatus()); + } + + + @Test + public void testFairSemaphore() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + SemaphoreValve valve = new SemaphoreValve(); + valve.setConcurrency(5); + valve.setFairness(true); + ctx.getPipeline().addValve(valve); + + tomcat.start(); + + Assert.assertNotNull(valve.semaphore); + Assert.assertTrue(valve.semaphore.isFair()); + Assert.assertEquals(5, valve.semaphore.availablePermits()); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort(), res, null); + + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertEquals(HelloWorldServlet.RESPONSE_TEXT, res.toString()); + } + + @Test + public void testBlockingWaitsForPermit() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + CountDownLatch insideServlet = new CountDownLatch(1); + CountDownLatch canReturn = new CountDownLatch(1); + Tomcat.addServlet(ctx, "slow", new SlowServlet(insideServlet, canReturn)); + ctx.addServletMappingDecoded("/", "slow"); + + SemaphoreValve valve = new SemaphoreValve(); + valve.setConcurrency(1); + valve.setBlock(true); + ctx.getPipeline().addValve(valve); + + tomcat.start(); + + AtomicReference firstError = new AtomicReference<>(); + Thread firstThread = new Thread(() -> { + try { + getUrl("http://localhost:" + getPort(), new ByteChunk(), null); + } catch (IOException e) { + firstError.set(e); + } + }); + firstThread.start(); + + Assert.assertTrue("First request should reach servlet", + insideServlet.await(10, TimeUnit.SECONDS)); + + AtomicInteger secondRc = new AtomicInteger(); + AtomicReference secondError = new AtomicReference<>(); + Thread secondThread = new Thread(() -> { + try { + secondRc.set(getUrl("http://localhost:" + getPort(), new ByteChunk(), null)); + } catch (IOException e) { + secondError.set(e); + } + }); + secondThread.start(); + + // Give the second request time to arrive and block on the semaphore + Thread.sleep(500); + + Assert.assertTrue("Second request should be blocked waiting for permit", secondThread.isAlive()); + + canReturn.countDown(); + firstThread.join(10000); + Assert.assertNull(firstError.get()); + + secondThread.join(10000); + Assert.assertFalse(secondThread.isAlive()); + Assert.assertNull(secondError.get()); + Assert.assertEquals(HttpServletResponse.SC_OK, secondRc.get()); + } + + @Test + public void testControlConcurrencyBypass() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + + CountDownLatch insideServlet = new CountDownLatch(1); + CountDownLatch canReturn = new CountDownLatch(1); + Tomcat.addServlet(ctx, "slow", new SlowServlet(insideServlet, canReturn)); + ctx.addServletMappingDecoded("/slow", "slow"); + + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/bypass", "hello"); + + SemaphoreValve valve = new SemaphoreValve() { + @Override + public boolean controlConcurrency(Request request, Response response) { + return !request.getDecodedRequestURI().equals("/bypass"); + } + }; + valve.setConcurrency(1); + valve.setBlock(false); + valve.setHighConcurrencyStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + ctx.getPipeline().addValve(valve); + + tomcat.start(); + + Thread firstThread = new Thread(() -> { + try { + getUrl("http://localhost:" + getPort() + "/slow", new ByteChunk(), null); + } catch (IOException e) { + // Ignored + } + }); + firstThread.start(); + + Assert.assertTrue("First request should reach servlet", + insideServlet.await(10, TimeUnit.SECONDS)); + + // Request to /bypass should succeed despite all permits being held, + // because controlConcurrency() returns false for this path + int bypassRc = getUrl("http://localhost:" + getPort() + "/bypass", new ByteChunk(), null); + Assert.assertEquals(HttpServletResponse.SC_OK, bypassRc); + + int deniedRc = getUrl("http://localhost:" + getPort() + "/slow", new ByteChunk(), null); + Assert.assertEquals(HttpServletResponse.SC_SERVICE_UNAVAILABLE, deniedRc); + + canReturn.countDown(); + firstThread.join(10000); + } + + @Test + public void testInterruptibleDenied() throws Exception { + SemaphoreValve semaphoreValve = new SemaphoreValve(); + semaphoreValve.setConcurrency(1); + semaphoreValve.setBlock(true); + semaphoreValve.setInterruptible(true); + semaphoreValve.setHighConcurrencyStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + + semaphoreValve.semaphore = new Semaphore(1, false); + + AtomicBoolean nextInvoked = new AtomicBoolean(false); + semaphoreValve.setNext(new ValveBase() { + @Override + public void invoke(Request request, Response response) { + nextInvoked.set(true); + } + }); + + MockResponse response = new MockResponse(); + + semaphoreValve.semaphore.acquire(); + + // On a new thread, valve will block on semaphore.acquire() because the permit is already held. + CountDownLatch invokeStarted = new CountDownLatch(1); + Thread blocked = new Thread(() -> { + invokeStarted.countDown(); + try { + semaphoreValve.invoke(null, response); + } catch (Throwable t) { + // Ignored + } + }); + blocked.start(); + + Assert.assertTrue(invokeStarted.await(10, TimeUnit.SECONDS)); + Thread.sleep(200); + + blocked.interrupt(); + blocked.join(10000); + Assert.assertFalse(blocked.isAlive()); + + Assert.assertEquals(HttpServletResponse.SC_SERVICE_UNAVAILABLE, response.getStatus()); + + Assert.assertFalse("Next valve should not be invoked when permit denied", nextInvoked.get()); + + Assert.assertEquals(0, semaphoreValve.semaphore.availablePermits()); + + semaphoreValve.semaphore.release(); + } + + private static final class SlowServlet extends HttpServlet { + + @Serial + private static final long serialVersionUID = 1L; + private final CountDownLatch insideServlet; + private final CountDownLatch canReturn; + + private SlowServlet(CountDownLatch insideServlet, CountDownLatch canReturn) { + this.insideServlet = insideServlet; + this.canReturn = canReturn; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + insideServlet.countDown(); + try { + Assert.assertTrue(canReturn.await(30, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + // Ignore + } + resp.setContentType("text/plain"); + resp.getWriter().print("OK"); + } + } + + public static class MockResponse extends Response { + + public MockResponse() { + super(null); + } + + private int status = HttpServletResponse.SC_OK; + + @Override + public void sendError(int status) throws IOException { + this.status = status; + } + + @Override + public int getStatus() { + return status; + } + } + +} diff -Nru tomcat11-11.0.15/test/org/apache/catalina/webresources/TestCachedResource.java tomcat11-11.0.22/test/org/apache/catalina/webresources/TestCachedResource.java --- tomcat11-11.0.15/test/org/apache/catalina/webresources/TestCachedResource.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/catalina/webresources/TestCachedResource.java 2026-05-01 18:56:05.000000000 +0000 @@ -16,6 +16,7 @@ */ package org.apache.catalina.webresources; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.InputStream; import java.net.JarURLConnection; @@ -131,4 +132,38 @@ Assert.assertNotNull(is); } } + + + @Test + public void testGetContentWebInfClasses() throws Exception { + Tomcat tomcat = getTomcatInstance(); + File docBase = new File("test/webapp"); + Context ctx = tomcat.addWebapp("/test", docBase.getAbsolutePath()); + tomcat.start(); + + URL url = ctx.getLoader().getClassLoader().getResource("bug69623-a.mdd"); + Object o = url.getContent(); + /* + * Could test the actual content but a non-null return without an exception is enough to demonstrate the bug has + * not occurred. + */ + Assert.assertTrue(o instanceof ByteArrayInputStream); + } + + + @Test + public void testGetContentWebInfLib() throws Exception { + Tomcat tomcat = getTomcatInstance(); + File docBase = new File("test/webapp"); + Context ctx = tomcat.addWebapp("/test", docBase.getAbsolutePath()); + tomcat.start(); + + URL url = ctx.getLoader().getClassLoader().getResource("bug69623-b.mdd"); + Object o = url.getContent(); + /* + * Could test the actual content but a non-null return without an exception is enough to demonstrate the bug has + * not occurred. + */ + Assert.assertTrue(o instanceof ByteArrayInputStream); + } } diff -Nru tomcat11-11.0.15/test/org/apache/coyote/TestCompressionConfig.java tomcat11-11.0.22/test/org/apache/coyote/TestCompressionConfig.java --- tomcat11-11.0.15/test/org/apache/coyote/TestCompressionConfig.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/TestCompressionConfig.java 2026-05-01 18:56:05.000000000 +0000 @@ -17,6 +17,7 @@ package org.apache.coyote; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -33,29 +34,30 @@ public static Collection parameters() { List parameterSets = new ArrayList<>(); - parameterSets.add(new Object[] { new String[] { }, null, Boolean.FALSE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); - - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.FALSE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.FALSE }); - - parameterSets.add(new Object[] { new String[] { }, null, Boolean.FALSE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); - - parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] {}, null, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); + + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.FALSE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.FALSE, Boolean.FALSE }); + + parameterSets.add(new Object[] { new String[] {}, null, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "xgzip" }, null, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "<>gzip" }, null, Boolean.FALSE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "foo", "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "<>", "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + + parameterSets.add(new Object[] { new String[] { "gzip" }, null, Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "W/", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "gzip" }, "XX", Boolean.TRUE, Boolean.TRUE }); - parameterSets.add(new Object[] { new String[] { "foobar;foo=bar, gzip;bla=\"quoted\"" }, "XX", Boolean.TRUE, Boolean.TRUE }); + parameterSets.add(new Object[] { new String[] { "foobar;foo=bar, gzip;bla=\"quoted\"" }, "XX", Boolean.TRUE, + Boolean.TRUE }); return parameterSets; } @@ -110,4 +112,18 @@ } } } + + + @Test + public void testNoCompressionEncodings() { + CompressionConfig config = new CompressionConfig(); + String encodings = config.getNoCompressionEncodings(); + Assert.assertTrue(Arrays.asList("br", "compress", "dcb", "dcz", "deflate", "gzip", "pack200-gzip", "zstd") + .stream().anyMatch(encodings::contains)); + + config.setNoCompressionEncodings("br"); + String newEncodings = config.getNoCompressionEncodings(); + Assert.assertTrue(newEncodings.contains("br")); + Assert.assertFalse(newEncodings.contains("gzip")); + } } diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http11/TestHttp11Processor.java tomcat11-11.0.22/test/org/apache/coyote/http11/TestHttp11Processor.java --- tomcat11-11.0.15/test/org/apache/coyote/http11/TestHttp11Processor.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http11/TestHttp11Processor.java 2026-05-01 18:56:05.000000000 +0000 @@ -32,6 +32,7 @@ import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -421,7 +422,7 @@ tomcat.start(); ByteChunk responseBody = new ByteChunk(); - Map> responseHeaders = new HashMap<>(); + Map> responseHeaders = new HashMap<>(); int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders); Assert.assertEquals(HttpServletResponse.SC_OK, rc); @@ -445,7 +446,7 @@ tomcat.start(); ByteChunk responseBody = new ByteChunk(); - Map> responseHeaders = new HashMap<>(); + Map> responseHeaders = new HashMap<>(); int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders); Assert.assertEquals(HttpServletResponse.SC_OK, rc); @@ -854,11 +855,11 @@ tomcat.start(); ByteChunk getBody = new ByteChunk(); - Map> getHeaders = new HashMap<>(); + Map> getHeaders = new HashMap<>(); int getStatus = getUrl("http://localhost:" + getPort() + "/test", getBody, getHeaders); ByteChunk headBody = new ByteChunk(); - Map> headHeaders = new HashMap<>(); + Map> headHeaders = new HashMap<>(); int headStatus = getUrl("http://localhost:" + getPort() + "/test", headBody, headHeaders); Assert.assertEquals(HttpServletResponse.SC_OK, getStatus); @@ -997,7 +998,7 @@ tomcat.start(); ByteChunk responseBody = new ByteChunk(); - Map> responseHeaders = new HashMap<>(); + Map> responseHeaders = new HashMap<>(); int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, responseHeaders); Assert.assertEquals(HttpServletResponse.SC_RESET_CONTENT, rc); @@ -2149,7 +2150,6 @@ } - private static class EarlyHintsServlet extends HttpServlet { private static final long serialVersionUID = 1L; @@ -2165,6 +2165,7 @@ this.useSendError = useSendError; this.errorString = errorString; } + @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.addHeader("Link", "; rel=preload; as=style"); @@ -2185,4 +2186,19 @@ resp.getWriter().write("OK"); } } -} + + + @Test + public void testNoCompressionEncodings() { + Http11NioProtocol protocol = new Http11NioProtocol(); + String encodings = protocol.getNoCompressionEncodings(); + Assert.assertTrue(Arrays.asList("br", "compress", "dcb", "dcz", "deflate", "gzip", "pack200-gzip", "zstd") + .stream().anyMatch(encodings::contains)); + + protocol.setNoCompressionEncodings("br"); + + String newEncodings = protocol.getNoCompressionEncodings(); + Assert.assertTrue(newEncodings.contains("br")); + Assert.assertFalse(newEncodings.contains("gzip")); + } +} \ No newline at end of file diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java tomcat11-11.0.22/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java --- tomcat11-11.0.15/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java 2026-05-01 18:56:05.000000000 +0000 @@ -326,9 +326,9 @@ client.processRequest(); if (ok) { - Assert.assertTrue(client.isResponse200()); + Assert.assertTrue(client.getResponseLine(), client.isResponse200()); } else { - Assert.assertTrue(client.isResponse500()); + Assert.assertTrue(client.getResponseLine(), client.isResponse500()); } } @@ -1021,4 +1021,95 @@ */ Assert.assertEquals("5,4", client.getResponseBody()); } + + + @Test + public void testExtension01() throws Exception { + doTestExtension("abc", true); + } + + + @Test + public void testExtension02() throws Exception { + doTestExtension("abc=def", true); + } + + + @Test + public void testExtension03() throws Exception { + doTestExtension(" a = b ", true); + } + + + @Test + public void testExtension04() throws Exception { + doTestExtension(" a = \"b\" ", true); + } + + + @Test + public void testExtension05() throws Exception { + doTestExtension("a=b=c", false); + } + + + @Test + public void testExtension06() throws Exception { + doTestExtension("a=b;", false); + } + + + @Test + public void testExtension07() throws Exception { + doTestExtension("a=\"aa\r\n\"", false); + } + + + private void doTestExtension(String extension, boolean ok) throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + Assert.assertTrue(tomcat.getConnector().setProperty( + "maxExtensionSize", Integer.toString(EXT_SIZE_LIMIT))); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(ok)); + ctx.addServletMappingDecoded("/", "servlet"); + + tomcat.start(); + + // @formatter:off + String[] request = new String[] { + "POST /echo-params.jsp HTTP/1.1" + CRLF + + "Host: any" + CRLF + + "Transfer-encoding: chunked" + CRLF + + SimpleHttpClient.HTTP_HEADER_CONTENT_TYPE_FORM_URL_ENCODING + + "Connection: close" + CRLF + + CRLF + + "3;" + extension + CRLF + + "a=0" + CRLF + + "4" + CRLF + + "&b=1" + CRLF + + "0" + CRLF + + CRLF + }; + // @formatter:on + + TrailerClient client = + new TrailerClient(tomcat.getConnector().getLocalPort()); + client.setRequest(request); + + client.connect(); + client.processRequest(); + + if (ok) { + Assert.assertTrue(client.isResponse200()); + } else { + Assert.assertTrue(client.isResponse500()); + } + } + + } diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http11/filters/TestChunkedOutputFilter.java tomcat11-11.0.22/test/org/apache/coyote/http11/filters/TestChunkedOutputFilter.java --- tomcat11-11.0.15/test/org/apache/coyote/http11/filters/TestChunkedOutputFilter.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http11/filters/TestChunkedOutputFilter.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,123 @@ +/* + * 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.coyote.http11.filters; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +@RunWith(Parameterized.class) +public class TestChunkedOutputFilter { + + private final String VALID_STRING = "aaa"; + + @Parameterized.Parameters(name = "{index}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + + for (int i = 0; i < 500; i++) { + Boolean valid; + if (i < 32 && i != 9 || i == 127 || i > 255) { + valid = Boolean.FALSE; + } else { + valid = Boolean.TRUE; + } + parameterSets.add(new Object[] { Character.valueOf((char) i), valid}); + } + return parameterSets; + } + + + @Parameter(0) + public Character charUnderTest; + @Parameter(1) + public boolean valid; + + + @Test + public void testAtStart() { + StringBuilder sb = new StringBuilder(4); + sb.append(charUnderTest); + sb.append(VALID_STRING); + + String result = ChunkedOutputFilter.filterForHeaders(sb.toString()); + + String expected; + if (valid) { + expected = sb.toString(); + } else { + StringBuilder esb = new StringBuilder(4); + esb.append(' '); + esb.append(VALID_STRING); + expected = esb.toString(); + } + + Assert.assertEquals(expected, result); + } + + + @Test + public void testInMiddle() { + StringBuilder sb = new StringBuilder(4); + sb.append(VALID_STRING); + sb.append(charUnderTest); + sb.append(VALID_STRING); + + String result = ChunkedOutputFilter.filterForHeaders(sb.toString()); + + String expected; + if (valid) { + expected = sb.toString(); + } else { + StringBuilder esb = new StringBuilder(4); + esb.append(VALID_STRING); + esb.append(' '); + esb.append(VALID_STRING); + expected = esb.toString(); + } + + Assert.assertEquals(expected, result); + } + + + @Test + public void testAtEnd() { + StringBuilder sb = new StringBuilder(4); + sb.append(VALID_STRING); + sb.append(charUnderTest); + + String result = ChunkedOutputFilter.filterForHeaders(sb.toString()); + + String expected; + if (valid) { + expected = sb.toString(); + } else { + StringBuilder esb = new StringBuilder(4); + esb.append(VALID_STRING); + esb.append(' '); + expected = esb.toString(); + } + + Assert.assertEquals(expected, result); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http2/Http2TestBase.java tomcat11-11.0.22/test/org/apache/coyote/http2/Http2TestBase.java --- tomcat11-11.0.15/test/org/apache/coyote/http2/Http2TestBase.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http2/Http2TestBase.java 2026-05-01 18:56:05.000000000 +0000 @@ -226,7 +226,11 @@ String url) { List
    headers = new ArrayList<>(4); headers.add(new Header(":method", Method.GET)); - headers.add(new Header(":scheme", "http")); + if (getTomcatInstance().getConnector().getSecure()) { + headers.add(new Header(":scheme", "https")); + } else { + headers.add(new Header(":scheme", "http")); + } headers.add(new Header(":path", url)); headers.add(new Header(":authority", "localhost:" + getPort())); @@ -243,7 +247,8 @@ for (Header header : headers) { mimeHeaders.addValue(header.getName()).setString(header.getValue()); } - hpackEncoder.encode(mimeHeaders, headersPayload); + // Don't force lower case to allow testing with upper case field names + hpackEncoder.encode(mimeHeaders, headersPayload, false); if (padding != null) { headersPayload.put(padding); } @@ -264,7 +269,11 @@ protected void buildSimpleGetRequestPart1(byte[] frameHeader, ByteBuffer headersPayload, int streamId) { List
    headers = new ArrayList<>(3); headers.add(new Header(":method", Method.GET)); - headers.add(new Header(":scheme", "http")); + if (getTomcatInstance().getConnector().getSecure()) { + headers.add(new Header(":scheme", "https")); + } else { + headers.add(new Header(":scheme", "http")); + } headers.add(new Header(":path", "/simple")); buildSimpleGetRequestPart1(frameHeader, headersPayload, headers, streamId); @@ -377,7 +386,11 @@ MimeHeaders headers = new MimeHeaders(); headers.addValue(":method").setString(Method.POST); - headers.addValue(":scheme").setString("http"); + if (getTomcatInstance().getConnector().getSecure()) { + headers.addValue(":scheme").setString("https"); + } else { + headers.addValue(":scheme").setString("http"); + } headers.addValue(":path").setString(path); headers.addValue(":authority").setString("localhost:" + getPort()); if (useExpectation) { @@ -453,7 +466,11 @@ protected void buildHeadRequest(byte[] headersFrameHeader, ByteBuffer headersPayload, int streamId, String path) { MimeHeaders headers = new MimeHeaders(); headers.addValue(":method").setString(Method.HEAD); - headers.addValue(":scheme").setString("http"); + if (getTomcatInstance().getConnector().getSecure()) { + headers.addValue(":scheme").setString("https"); + } else { + headers.addValue(":scheme").setString("http"); + } headers.addValue(":path").setString(path); headers.addValue(":authority").setString("localhost:" + getPort()); hpackEncoder.encode(headers, headersPayload); @@ -1184,7 +1201,7 @@ public void headersEnd(int streamId, boolean endOfStream) { trace.append(streamId + "-HeadersEnd\n"); if (endOfStream) { - receivedEndOfStream(streamId) ; + receivedEndOfStream(streamId); } } @@ -1448,7 +1465,7 @@ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - Map params = req.getParameterMap(); + Map params = req.getParameterMap(); resp.setContentType("text/plain"); resp.setCharacterEncoding("UTF-8"); diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http2/TestHPackHuffman.java tomcat11-11.0.22/test/org/apache/coyote/http2/TestHPackHuffman.java --- tomcat11-11.0.15/test/org/apache/coyote/http2/TestHPackHuffman.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http2/TestHPackHuffman.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,45 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + + +public class TestHPackHuffman { + + /* + * This specific String exposed an edge case that triggered an unexpected HPACK parsing failure. + */ + @Test + public void testValueLeftBrace() throws Exception { + ByteBuffer buf = ByteBuffer.allocate(10); + String data = "x-value{"; + HPackHuffman.encode(buf, data); + + buf.flip(); + // Remove the header byte (in Tomcat this is parsed before the bytes are passed to the HPACK decoder) + buf.get(); + + StringBuilder target = new StringBuilder(); + HPackHuffman.decode(buf, buf.remaining(), target, false); + + Assert.assertEquals("Value changed after encode/decode roundtrip", data, target.toString()); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2RequestParameters.java tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2RequestParameters.java --- tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2RequestParameters.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2RequestParameters.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,52 @@ +/* + * 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.coyote.http2; + +import org.junit.Assert; +import org.junit.Test; + +public class TestHttp2RequestParameters extends Http2TestBase { + /** + * Test case for https://bz.apache.org/bugzilla/show_bug.cgi?id=69918 POST parameters are not returned from a call + * to any of the {@link org.apache.catalina.connector.Request} getParameterXXX() methods if the request is HTTP/2 + * and the content-length header is not set. + * + * @throws Exception If the test encounters an unexpected error + */ + @Test + public void testBug69918() throws Exception { + http2Connect(); + + sendParameterPostRequest(3, null, "a=1&b=2", -1, false); + output.setTraceBody(true); + + boolean foundBody = false; + while (parser.readFrame()) { + String trace = output.getTrace(); + if (trace.contains("3-Body-2")) { + foundBody = true; + } else if (trace.contains("3-Body-0")) { + Assert.fail("Parameter count was 0. Trace: " + trace); + } + if (trace.contains("3-EndOfStream")) { + break; + } + } + Assert.assertTrue("Parameter count should be 2, trace: " + output.getTrace(), foundBody); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2Section_6_2.java tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_6_2.java --- tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2Section_6_2.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_6_2.java 2026-05-01 18:56:05.000000000 +0000 @@ -92,6 +92,42 @@ @Test + public void testHeaderFrameTooMuchPaddingWithPriority() throws Exception { + // Tests the case where both PADDED and PRIORITY flags are set and the + // padding length is too large relative to the payload after accounting + // for the optional bytes (1 byte pad length + 5 bytes priority = 6 bytes). + // With payloadSize=8 and padLength=3, the actual available payload + // after optional bytes is only 2, so padLength >= available triggers + // a PROTOCOL_ERROR and a GOAWAY frame must be sent. + http2Connect(); + + // 9 bytes frame header + 8 bytes payload + byte[] headerFrame = new byte[17]; + + // Header + // length = 8 + ByteUtil.setThreeBytes(headerFrame, 0, 8); + headerFrame[3] = FrameType.HEADERS.getIdByte(); + // flags: PADDED (0x08) | PRIORITY (0x20) + headerFrame[4] = 0x28; + // stream 3 + ByteUtil.set31Bits(headerFrame, 5, 3); + // payload: + // pad length = 3 (too large: only 2 bytes remain after 6 optional bytes) + headerFrame[9] = 3; + // priority: 5 bytes (bytes 10-14, all zero) + // remaining 2 bytes: bytes 15-16 (all zero) + + os.write(headerFrame); + os.flush(); + + // 1 is the last stream processed before the connection error (stream 1 + // from the initial HTTP/1.1 upgrade) + handleGoAwayResponse(1); + } + + + @Test public void testHeaderFrameWithZeroLengthPadding() throws Exception { http2Connect(); diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2Section_8_1.java tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_1.java --- tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2Section_8_1.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_1.java 2026-05-01 18:56:05.000000000 +0000 @@ -281,6 +281,54 @@ @Test + public void testHostHeaderInvalid() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", Method.GET)); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/simple")); + headers.add(new Header("host", "local!host:" + getPort())); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + String trace = output.getTrace(); + Assert.assertTrue(trace, trace.contains("3-Header-[:status]-[400]")); + } + + + @Test + public void testAuthorityHeaderInvalid() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", Method.GET)); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/simple")); + headers.add(new Header(":authority", "local!host:" + getPort())); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + String trace = output.getTrace(); + Assert.assertTrue(trace, trace.contains("3-Header-[:status]-[400]")); + } + + + @Test public void testHostHeaderDuplicate() throws Exception { http2Connect(); @@ -301,7 +349,7 @@ parser.readFrame(); String trace = output.getTrace(); - Assert.assertTrue(trace, trace.contains("0-Goaway-[1]-[9]")); + Assert.assertTrue(trace, trace.contains("3-RST-[1]")); } @@ -413,7 +461,7 @@ parser.readFrame(); String trace = output.getTrace(); - Assert.assertTrue(trace, trace.contains("0-Goaway-[1]-[9]")); + Assert.assertTrue(trace, trace.contains("3-RST-[1]")); } @@ -506,6 +554,6 @@ headers.add(new Header(":path", "/simple")); headers.add(new Header("host", "localhost:" + getPort())); - doInvalidPseudoHeaderTest(headers, "0-Goaway-[3]-[1]-"); + doInvalidPseudoHeaderTest(headers, "3-RST-[1]\n"); } } diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2Section_8_2.java tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_2.java --- tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2Section_8_2.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_2.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,121 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.tomcat.util.http.Method; +import org.apache.tomcat.util.http.parser.HttpParser; + + +/** + * Unit tests for Section 8.2 of RFC 7540. + */ +@RunWith(Parameterized.class) +public class TestHttp2Section_8_2 extends Http2TestBase { + + @Parameterized.Parameters(name = "{index}: {0} {1} {2} {3} {4}") + public static Collection parameters() { + Collection baseData = data(); + + List parameterSets = new ArrayList<>(); + for (Object[] base : baseData) { + parameterSets.add(new Object[] { base[0], base[1], "x-test", "x-value", Boolean.TRUE }); + // Strings longer than 5 characters will be huffman encoded + for (char c = 0; c < 256; c++) { + // HTTP/2 field names must be tokens and must be lower case + boolean valid = HttpParser.isToken(c) && !Character.isUpperCase(c); + // Non-Huffman field names + parameterSets.add(new Object[] { base[0], base[1], "x-" + c + "t", "x-value", Boolean.valueOf(valid) }); + parameterSets.add(new Object[] { base[0], base[1], c + "x-t", "x-value", Boolean.valueOf(valid) }); + parameterSets.add(new Object[] { base[0], base[1], "x-t" + c, "x-value", Boolean.valueOf(valid) }); + // Huffman field names + parameterSets + .add(new Object[] { base[0], base[1], "x-te" + c + "st", "x-value", Boolean.valueOf(valid) }); + parameterSets.add(new Object[] { base[0], base[1], c + "x-test", "x-value", Boolean.valueOf(valid) }); + parameterSets.add(new Object[] { base[0], base[1], "x-test" + c, "x-value", Boolean.valueOf(valid) }); + + // HTTP/2 field values have same criteria as HTTP/1.1 + // Non-Huffman field values + parameterSets.add(new Object[] { base[0], base[1], "x-test", "x-" + c + "v", + Boolean.valueOf(HttpParser.isFieldContent(c)) }); + parameterSets.add(new Object[] { base[0], base[1], "x-test", c + "x-v", + Boolean.valueOf(HttpParser.isFieldVChar(c)) }); + parameterSets.add(new Object[] { base[0], base[1], "x-test", "x-v" + c, + Boolean.valueOf(HttpParser.isFieldVChar(c)) }); + parameterSets.add(new Object[] { base[0], base[1], "x-test", "" + c, + Boolean.valueOf(HttpParser.isFieldVChar(c)) }); + // Huffman field values + parameterSets.add(new Object[] { base[0], base[1], "x-test", "x-va" + c + "lue", + Boolean.valueOf(HttpParser.isFieldContent(c)) }); + parameterSets.add(new Object[] { base[0], base[1], "x-test", c + "x-value", + Boolean.valueOf(HttpParser.isFieldVChar(c)) }); + parameterSets.add(new Object[] { base[0], base[1], "x-test", "x-value" + c, + Boolean.valueOf(HttpParser.isFieldVChar(c)) }); + } + } + return parameterSets; + } + + + @Parameter(2) + public String fieldName; + + @Parameter(3) + public String fieldValue; + + @Parameter(4) + public boolean valid; + + + @Test + public void testFieldNameAndValue() throws Exception { + http2Connect(); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", Method.GET)); + headers.add(new Header(":scheme", "http")); + headers.add(new Header(":path", "/simple")); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header(fieldName, fieldValue)); + + byte[] headersFrameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3); + + writeFrame(headersFrameHeader, headersPayload); + + parser.readFrame(); + + String trace = output.getTrace(); + if (valid) { + Assert.assertTrue(trace, trace.contains("3-Header-[:status]-[200]")); + } else { + Assert.assertTrue(trace, trace.contains("3-RST-[1]")); + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2Section_8_3.java tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_3.java --- tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2Section_8_3.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_3.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,74 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.http.Method; + +/** + * Unit tests for Section 8.3 of RFC 9113. + */ +public class TestHttp2Section_8_3 extends Http2TestBase { + + /* + * Not explicitly specified in section 8.3 but closely aligned to it. + */ + + @Test + public void testSchemeInconsistencyNonTLS() throws Exception { + testSchemeInconsistency(false); + } + + + @Test + public void testSchemeInconsistencyTLS() throws Exception { + testSchemeInconsistency(true); + } + + + private void testSchemeInconsistency(boolean connectionUsesTls) throws Exception { + // Start HTTP/2 over non-TLS connection + http2Connect(connectionUsesTls); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", Method.GET)); + if (connectionUsesTls) { + headers.add(new Header(":scheme", "http")); + } else { + headers.add(new Header(":scheme", "https")); + } + headers.add(new Header(":path", "/simple")); + headers.add(new Header(":authority", "localhost:" + getPort())); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + writeFrame(frameHeader, headersPayload); + + parser.readFrame(); + + Assert.assertEquals("3-RST-[1]\n", output.getTrace()); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2Section_8_5.java tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_5.java --- tomcat11-11.0.15/test/org/apache/coyote/http2/TestHttp2Section_8_5.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http2/TestHttp2Section_8_5.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,84 @@ +/* + * 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.coyote.http2; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.http.Method; + +/** + * Unit tests for Section 8.5 of RFC 9113. + */ +public class TestHttp2Section_8_5 extends Http2TestBase { + + @Test + public void testConnectWithScheme() throws Exception { + testConnectWithHeader(":scheme", "http"); + } + + + @Test + public void testConnectWithPath() throws Exception { + testConnectWithHeader(":path", "/should/not/be/present"); + } + + + private void testConnectWithHeader(String headerName, String headerValue) throws Exception { + http2Connect(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", Method.CONNECT)); + headers.add(new Header(":authority", "localhost:" + getPort())); + headers.add(new Header(headerName, headerValue)); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + writeFrame(frameHeader, headersPayload); + + parser.readFrame(); + + Assert.assertEquals("3-RST-[1]\n", output.getTrace()); + } + + + @Test + public void testConnectWithoutAuthority() throws Exception { + http2Connect(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + + List
    headers = new ArrayList<>(4); + headers.add(new Header(":method", Method.CONNECT)); + + buildGetRequest(frameHeader, headersPayload, null, headers, 3); + + writeFrame(frameHeader, headersPayload); + + parser.readFrame(); + + Assert.assertEquals("3-RST-[1]\n", output.getTrace()); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http2/TestLargeUpload.java tomcat11-11.0.22/test/org/apache/coyote/http2/TestLargeUpload.java --- tomcat11-11.0.15/test/org/apache/coyote/http2/TestLargeUpload.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http2/TestLargeUpload.java 2026-05-01 18:56:05.000000000 +0000 @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -109,7 +110,11 @@ // Trailers writeFrame(trailerFrameHeader, trailerPayload); - done.await(); + /* + * Should complete very quickly (sub-second). Use timeout in case something fails. Long time out as some CI + * systems occasionally have long pauses. + */ + done.await(30, TimeUnit.SECONDS); Assert.assertEquals(Integer.valueOf(bodySize * bodyCount), Integer.valueOf(read)); } diff -Nru tomcat11-11.0.15/test/org/apache/coyote/http2/TestStreamProcessor.java tomcat11-11.0.22/test/org/apache/coyote/http2/TestStreamProcessor.java --- tomcat11-11.0.15/test/org/apache/coyote/http2/TestStreamProcessor.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/coyote/http2/TestStreamProcessor.java 2026-05-01 18:56:05.000000000 +0000 @@ -296,10 +296,7 @@ parser.readFrame(); StringBuilder expected = new StringBuilder(); - expected.append("3-HeadersStart\n"); - expected.append("3-Header-[:status]-[400]\n"); - expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n"); - expected.append("3-HeadersEnd\n"); + expected.append("3-RST-[1]\n"); Assert.assertEquals(expected.toString(), output.getTrace()); } @@ -574,7 +571,6 @@ List
    headers = new ArrayList<>(4); headers.add(new Header(":method", Method.CONNECT)); - headers.add(new Header(":scheme", "http")); headers.add(new Header(":authority", "example.local")); byte[] headersFrameHeader = new byte[9]; diff -Nru tomcat11-11.0.15/test/org/apache/el/TestValueExpressionImpl.java tomcat11-11.0.22/test/org/apache/el/TestValueExpressionImpl.java --- tomcat11-11.0.15/test/org/apache/el/TestValueExpressionImpl.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/el/TestValueExpressionImpl.java 2026-05-01 18:56:05.000000000 +0000 @@ -26,6 +26,7 @@ import jakarta.el.ELBaseTest; import jakarta.el.ELContext; import jakarta.el.ExpressionFactory; +import jakarta.el.PropertyNotFoundException; import jakarta.el.ValueExpression; import jakarta.el.ValueReference; @@ -384,4 +385,32 @@ Integer result = (Integer) ve.getValue(context); Assert.assertNull(result); } + + @Test + public void testBug69948() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(); + + TesterBeanEmptyMap beanEmptyMap = new TesterBeanEmptyMap(); + TesterBeanA beanA = new TesterBeanA(); + beanA.setName(null); + + ValueExpression var = factory.createValueExpression(beanEmptyMap, TesterBeanEmptyMap.class); + context.getVariableMapper().setVariable("beanEmptyMap", var); + var = factory.createValueExpression(beanA, TesterBeanA.class); + context.getVariableMapper().setVariable("beanA", var); + + + ValueExpression ve = factory.createValueExpression(context, "${beanEmptyMap[beanA.name][beanA.name]}", Object.class); + + Assert.assertThrows(PropertyNotFoundException.class, () -> ve.getValueReference(context)); + } + + public static class TesterBeanEmptyMap extends HashMap> { + private static final long serialVersionUID = 1L; + @Override + public Map get(Object key) { + return Collections.emptyMap(); + } + } } diff -Nru tomcat11-11.0.15/test/org/apache/jasper/compiler/TestGenerator.java tomcat11-11.0.22/test/org/apache/jasper/compiler/TestGenerator.java --- tomcat11-11.0.15/test/org/apache/jasper/compiler/TestGenerator.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/jasper/compiler/TestGenerator.java 2026-05-01 18:56:05.000000000 +0000 @@ -22,8 +22,12 @@ import java.beans.PropertyEditorSupport; import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.nio.charset.CodingErrorAction; +import java.util.ArrayList; import java.util.Date; +import java.util.List; +import java.util.Map; import java.util.Scanner; import jakarta.servlet.http.HttpServletResponse; @@ -48,6 +52,7 @@ import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; import org.apache.jasper.servlet.JasperInitializer; +import org.apache.jasper.servlet.JspServlet; import org.apache.tomcat.util.buf.ByteChunk; public class TestGenerator extends TomcatBaseTest { @@ -828,6 +833,44 @@ doTestJsp("include-01.jsp"); } + /* + * Verify that _jspx_dependants entries appear in the same order as the + * <%@ include file="..." %> directives in the source JSP, ensuring + * reproducible builds (LinkedHashMap preserves insertion order). + */ + @Test + public void testDependantsOrder() throws Exception { + Tomcat tomcat = getTomcatInstanceTestWebapp(false, true); + + ByteChunk body = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + + "/test/jsp/generator/dependants-order.jsp", body, null); + Assert.assertEquals(body.toString(), HttpServletResponse.SC_OK, rc); + + // JSP classes are loaded by a per-JSP JasperLoader child classloader, + // not by the webapp classloader. Retrieve the dependants map through + // the JspServletWrapper, which calls getDependants() on the live + // servlet instance via the JspSourceDependent interface. + Context ctx = (Context) tomcat.getHost().findChild("/test"); + Wrapper jspWrapper = (Wrapper) ctx.findChild("jsp"); + JspServlet jspServlet = (JspServlet) jspWrapper.getServlet(); + Field rctxtField = JspServlet.class.getDeclaredField("rctxt"); + rctxtField.setAccessible(true); + JspRuntimeContext rctxt = (JspRuntimeContext) rctxtField.get(jspServlet); + Map dependants = rctxt.getWrapper( + "/jsp/generator/dependants-order.jsp").getDependants(); + + // Expect exactly the three fragments, in directive order: a, b, c. + List keys = new ArrayList<>(dependants.keySet()); + Assert.assertEquals(3, keys.size()); + Assert.assertTrue("a.jspf should precede b.jspf in _jspx_dependants", + keys.indexOf("/jsp/generator/dependants-order-a.jspf") < + keys.indexOf("/jsp/generator/dependants-order-b.jspf")); + Assert.assertTrue("b.jspf should precede c.jspf in _jspx_dependants", + keys.indexOf("/jsp/generator/dependants-order-b.jspf") < + keys.indexOf("/jsp/generator/dependants-order-c.jspf")); + } + @Test public void testSetProperty01() throws Exception { doTestJsp("setproperty-01.jsp"); diff -Nru tomcat11-11.0.15/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java tomcat11-11.0.22/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java --- tomcat11-11.0.15/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java 2026-05-01 18:56:05.000000000 +0000 @@ -16,13 +16,29 @@ */ package org.apache.jasper.compiler; +import java.io.File; +import java.io.FileOutputStream; +import java.lang.reflect.Field; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + import jakarta.servlet.http.HttpServletResponse; import org.junit.Assert; import org.junit.Test; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.jasper.servlet.JspServlet; import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.scan.StandardJarScanFilter; +import org.apache.tomcat.util.scan.StandardJarScanner; /** * Test case for {@link TagLibraryInfoImpl}. @@ -39,7 +55,6 @@ Assert.assertEquals(HttpServletResponse.SC_OK, rc); } - /* * https://bz.apache.org/bugzilla/show_bug.cgi?id=64373 */ @@ -53,4 +68,107 @@ Assert.assertEquals(HttpServletResponse.SC_OK, rc); } + /* + * https://bz.apache.org/bugzilla/show_bug.cgi?id=70001 + * + * Verify that taglib directives referencing a TLD in a JAR that is outside + * the web application (i.e. on the classpath but not in WEB-INF/lib) produce + * a stable, environment-independent key in the generated servlet's + * {@code _jspx_dependants} map. + * + * Before the fix, the key was an absolute {@code jar:file:/...} URL that + * encoded the build-environment-specific JAR location, making JSP compilation + * non-deterministic. After the fix the key must use the {@code "uri:"} prefix + * followed by the taglib URI from the JSP directive. + */ + @Test + public void testExternalTaglibDependantUsesUri() throws Exception { + Tomcat tomcat = getTomcatInstance(); + File appDir = new File("test/webapp"); + Context ctx = tomcat.addWebapp(null, "/test", appDir.getAbsolutePath()); + + StandardJarScanner scanner = (StandardJarScanner) ctx.getJarScanner(); + StandardJarScanFilter filter = (StandardJarScanFilter) scanner.getJarScanFilter(); + filter.setTldSkip(filter.getTldSkip() + ",testclasses"); + filter.setPluggabilitySkip(filter.getPluggabilitySkip() + ",testclasses"); + + // Add a JAR containing the test TLD to the *parent* classloader rather + // than to WEB-INF/lib. The TLD scanner then sees it as an external JAR + // (TldResourcePath.getWebappPath() == null), which is the code path that + // the fix for non-deterministic _jspx_dependants addresses. + File jar = createExternalTaglibJar(); + ClassLoader parent = Thread.currentThread().getContextClassLoader(); + ctx.setParentClassLoader(new URLClassLoader(new URL[] { jar.toURI().toURL() }, parent)); + + tomcat.start(); + + ByteChunk body = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + + "/test/jsp/generator/external-taglib.jsp", body, null); + Assert.assertEquals(body.toString(), HttpServletResponse.SC_OK, rc); + + // Retrieve the _jspx_dependants map from the compiled servlet via the + // JspServletWrapper. + Context webCtx = (Context) tomcat.getHost().findChild("/test"); + Wrapper jspWrapper = (Wrapper) webCtx.findChild("jsp"); + JspServlet jspServlet = (JspServlet) jspWrapper.getServlet(); + Field rctxtField = JspServlet.class.getDeclaredField("rctxt"); + rctxtField.setAccessible(true); + JspRuntimeContext rctxt = (JspRuntimeContext) rctxtField.get(jspServlet); + Map dependants = rctxt.getWrapper( + "/jsp/generator/external-taglib.jsp").getDependants(); + + Assert.assertNotNull("Expected non-null _jspx_dependants map", dependants); + + // No key in _jspx_dependants should be an absolute file/jar URL. + // Such URLs embed environment-specific paths and make JSP compilation + // non-deterministic. + for (String key : dependants.keySet()) { + Assert.assertFalse( + "_jspx_dependants must not contain absolute paths for external taglib JARs, got: " + key, + key.startsWith("jar:file:") || key.startsWith("file:")); + } + + // The external taglib JAR and its TLD entry must each be recorded with + // a stable "uri:" key rather than an absolute path. + Assert.assertTrue( + "Expected 'uri:http://tomcat.apache.org/test/external-taglib' key in _jspx_dependants", + dependants.containsKey("uri:http://tomcat.apache.org/test/external-taglib")); + Assert.assertTrue( + "Expected 'uri:http://tomcat.apache.org/test/external-taglib!/META-INF/external-taglib-test.tld'" + + " key in _jspx_dependants", + dependants.containsKey( + "uri:http://tomcat.apache.org/test/external-taglib!/META-INF/external-taglib-test.tld")); + } + + /** + * Creates a temporary JAR containing a minimal TLD with URI + * {@code http://tomcat.apache.org/test/external-taglib}. The TLD has no + * validator and no tag-handler classes so the JAR itself is the only + * dependency required to compile a JSP that references it. + */ + private static File createExternalTaglibJar() throws Exception { + String tld = + "\n" + + "\n" + + " 1.0\n" + + " ext\n" + + " http://tomcat.apache.org/test/external-taglib\n" + + "\n"; + + File jar = File.createTempFile("external-taglib-test", ".jar"); + jar.deleteOnExit(); + + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar))) { + jos.putNextEntry(new JarEntry("META-INF/external-taglib-test.tld")); + jos.write(tld.getBytes(StandardCharsets.UTF_8)); + jos.closeEntry(); + } + + return jar; + } } diff -Nru tomcat11-11.0.15/test/org/apache/juli/TestFileHandlerNonRotatable.java tomcat11-11.0.22/test/org/apache/juli/TestFileHandlerNonRotatable.java --- tomcat11-11.0.15/test/org/apache/juli/TestFileHandlerNonRotatable.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/juli/TestFileHandlerNonRotatable.java 2026-05-01 18:56:05.000000000 +0000 @@ -53,31 +53,31 @@ @Test public void testBug61232() throws Exception { - testHandler = new FileHandler(this.getTemporaryDirectory().toString(), + testHandler = new FileHandler(getTemporaryDirectory().toString(), "juli.", ".log"); testHandler.open(); - File logFile = new File(this.getTemporaryDirectory(), "juli.log"); + File logFile = new File(getTemporaryDirectory(), "juli.log"); Assert.assertTrue(logFile.exists()); } @Test public void testCustomSuffixWithoutSeparator() throws Exception { - testHandler = new FileHandler(this.getTemporaryDirectory().toString(), + testHandler = new FileHandler(getTemporaryDirectory().toString(), "juli.", "log"); testHandler.open(); - File logFile = new File(this.getTemporaryDirectory(), "juli.log"); + File logFile = new File(getTemporaryDirectory(), "juli.log"); Assert.assertTrue(logFile.exists()); } @Test public void testCustomPrefixWithoutSeparator() throws Exception { - testHandler = new FileHandler(this.getTemporaryDirectory().toString(), + testHandler = new FileHandler(getTemporaryDirectory().toString(), "juli", ".log"); testHandler.open(); - File logFile = new File(this.getTemporaryDirectory(), "juli.log"); + File logFile = new File(getTemporaryDirectory(), "juli.log"); Assert.assertTrue(logFile.exists()); } } \ No newline at end of file diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java --- tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java 2026-05-01 18:56:05.000000000 +0000 @@ -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 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(), 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; + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java --- tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,73 @@ +/* + * 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.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Valve; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestBasicProxy extends HttpdIntegrationBaseTest { + + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + ProxyRequests Off + ProxyPreserveHost On + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + RequestHeader set X-Forwarded-For 140.211.11.130 + RequestHeader set X-Forwarded-Proto "https" + """; + + @Override + protected List getValveConfig() { + return new ArrayList<>(); + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void testBasicProxying() throws Exception { + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getHttpdPort() + "/endpoint", res, false); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + Assert.assertNotNull(requestDesc.getRequestInfo()); + Assert.assertEquals("127.0.0.1", requestDesc.getRequestInfo("REQUEST-REMOTE-ADDR")); + Assert.assertEquals(getHttpdPort(), Integer.valueOf(requestDesc.getRequestInfo("REQUEST-SERVER-PORT")).intValue()); + Assert.assertEquals(getPort(), Integer.valueOf(requestDesc.getRequestInfo("REQUEST-LOCAL-PORT")).intValue()); + // httpd sets X-Forwarded-Proto: https, but without RemoteIpValve Tomcat does not process it. + Assert.assertEquals("http", requestDesc.getRequestInfo("REQUEST-SCHEME")); + Assert.assertEquals("false", requestDesc.getRequestInfo("REQUEST-IS-SECURE")); + Assert.assertNotNull(requestDesc.getHeaders()); + Assert.assertNotNull(requestDesc.getHeader("X-Forwarded-For")); + Assert.assertEquals("https", requestDesc.getHeader("X-Forwarded-Proto")); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java --- tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,96 @@ +/* + * 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.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Valve; +import org.apache.catalina.startup.BytesStreamer; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestChunkedTransferEncodingWithProxy extends HttpdIntegrationBaseTest { + + private static final int PAYLOAD_SIZE = 10 * 1024 * 1024 * 100; + + private static final String HTTPD_CONFIG = """ + LoadModule env_module modules/mod_env.so \s + SetEnv proxy-sendchunked 1 + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + """; + + @Override + protected List getValveConfig() { + return new ArrayList<>(); + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + /* + * Verify that chunked transfer encoding works correctly through the httpd reverse proxy + * which sets proxy-sendchunked to minimize resource usage by using chunked encoding. + */ + @Test + public void testChunkedTransferEncoding() throws Exception { + byte[] payload = new byte[PAYLOAD_SIZE]; + Arrays.fill(payload, (byte) 'A'); + + BytesStreamer streamer = new BytesStreamer() { + private boolean sent = false; + + @Override + public int getLength() { + return -1; + } + + @Override + public int available() { + return sent ? 0 : payload.length; + } + + @Override + public byte[] next() { + sent = true; + return payload; + } + }; + + ByteChunk res = new ByteChunk(); + Map> reqHead = new HashMap<>(); + reqHead.put("Content-Type", List.of("application/octet-stream")); + int rc = postUrl(true, streamer, "http://localhost:" + getHttpdPort() + "/endpoint", res, reqHead, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + Assert.assertEquals(String.valueOf(PAYLOAD_SIZE), requestDesc.getRequestInfo("REQUEST-BODY-SIZE")); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java --- tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,81 @@ +/* + * 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.Serial; +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Valve; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestErrorHandling extends HttpdIntegrationBaseTest { + + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + ProxyRequests Off + ProxyPreserveHost On + ProxyPass / http://localhost:%{TOMCAT_PORT}/ + ProxyPassReverse / http://localhost:%{TOMCAT_PORT}/ + """; + + @Override + protected List getValveConfig() { + return new ArrayList<>(); + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void test404NotFound() throws Exception { + int rc = getUrl("http://localhost:" + getHttpdPort() + "/nonexistent", new ByteChunk(), false); + Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc); + } + + @Test + public void test500InternalError() throws Exception { + Context ctx = (Context) getTomcatInstance().getHost().findChildren()[0]; + Tomcat.addServlet(ctx, "error", new HttpServlet() { + @Serial + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException { + throw new ServletException("Expected error"); + } + }); + ctx.addServletMappingDecoded("/error", "error"); + int rc = getUrl("http://localhost:" + getHttpdPort() + "/error", new ByteChunk(), false); + Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc); + } + +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java --- tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,111 @@ +/* + * 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.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Globals; +import org.apache.catalina.Valve; +import org.apache.catalina.valves.RemoteIpValve; +import org.apache.catalina.valves.SSLValve; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.TesterSupport; + +public class TestFullReverseProxy extends HttpdIntegrationBaseTest { + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + LoadModule ssl_module modules/mod_ssl.so + SSLSessionCache none + Listen %{HTTPD_SSL_PORT} https + + ServerName localhost:%{HTTPD_SSL_PORT} + SSLEngine on + SSLCertificateFile "%{SSL_CERT_FILE}" + SSLCertificateKeyFile "%{SSL_KEY_FILE}" + ProxyRequests Off + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + RequestHeader set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s" + RequestHeader set SSL_CIPHER "%{SSL_CIPHER}s" + RequestHeader set SSL_SESSION_ID "%{SSL_SESSION_ID}s" + RequestHeader set SSL_CIPHER_USEKEYSIZE "%{SSL_CIPHER_USEKEYSIZE}s" + SSLVerifyClient optional + SSLCACertificateFile "%{SSL_CA_CERT_FILE}" + SSLOptions +ExportCertData + ProxyAddHeaders Off + RequestHeader set X-Forwarded-For 140.211.11.130 + RequestHeader set X-Forwarded-Proto https + RequestHeader set X-Forwarded-Host whoamI.tomcat + + """; + + @Override + protected List getValveConfig() { + List valves = new ArrayList<>(); + + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setHostHeader("X-Forwarded-Host"); + valves.add(remoteIpValve); + + SSLValve sslValve = new SSLValve(); + valves.add(sslValve); + + return valves; + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void testFullReverseProxying() throws Exception { + TesterSupport.configureClientSsl(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("https://localhost:" + getHttpdSslPort() + "/endpoint", res, false); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + Assert.assertNotNull(requestDesc.getRequestInfo()); + Assert.assertEquals("140.211.11.130", requestDesc.getRequestInfo("REQUEST-REMOTE-ADDR")); + Assert.assertEquals(443, Integer.valueOf(requestDesc.getRequestInfo("REQUEST-SERVER-PORT")).intValue()); + Assert.assertEquals("https", requestDesc.getRequestInfo("REQUEST-SCHEME")); + Assert.assertEquals("true", requestDesc.getRequestInfo("REQUEST-IS-SECURE")); + Assert.assertEquals("whoamI.tomcat", requestDesc.getRequestInfo("REQUEST-SERVER-NAME")); + + Assert.assertNotNull(requestDesc.getHeaders()); + Assert.assertNull(requestDesc.getHeader("X-Forwarded-For")); + Assert.assertEquals("https", requestDesc.getHeader("X-Forwarded-Proto")); + + + Assert.assertNotNull(requestDesc.getAttributes()); + Assert.assertNotNull(requestDesc.getAttribute(Globals.CIPHER_SUITE_ATTR)); + Assert.assertNotNull(requestDesc.getAttribute(Globals.SSL_SESSION_ID_ATTR)); + Assert.assertNotNull(requestDesc.getAttribute(Globals.KEY_SIZE_ATTR)); + Assert.assertNotNull(requestDesc.getAttribute(Globals.CERTIFICATES_ATTR)); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java --- tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,111 @@ +/* + * 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.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Valve; +import org.apache.catalina.startup.BytesStreamer; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestLargePayloadWithProxy extends HttpdIntegrationBaseTest { + + private static final int PAYLOAD_SIZE = 10 * 1024 * 1024; + + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + """; + + @Override + protected List getValveConfig() { + return new ArrayList<>(); + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + /* + * Verify that a large POST body passes through the httpd reverse proxy + */ + @Test + public void testLargePostBody() throws Exception { + byte[] payload = new byte[PAYLOAD_SIZE]; + Arrays.fill(payload, (byte) 'A'); + + ByteChunk res = new ByteChunk(); + Map> reqHead = new HashMap<>(); + reqHead.put("Content-Type", List.of("application/octet-stream")); + int rc = postUrl(payload, "http://localhost:" + getHttpdPort() + "/endpoint", res, reqHead, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + Assert.assertEquals(String.valueOf(PAYLOAD_SIZE), requestDesc.getRequestInfo("REQUEST-BODY-SIZE")); + } + + /* + * Verify that chunked transfer encoding works correctly through the httpd reverse proxy. + */ + @Test + public void testChunkedTransferEncoding() throws Exception { + byte[] payload = new byte[PAYLOAD_SIZE]; + Arrays.fill(payload, (byte) 'A'); + + BytesStreamer streamer = new BytesStreamer() { + private boolean sent = false; + + @Override + public int getLength() { + return -1; + } + + @Override + public int available() { + return sent ? 0 : payload.length; + } + + @Override + public byte[] next() { + sent = true; + return payload; + } + }; + + ByteChunk res = new ByteChunk(); + Map> reqHead = new HashMap<>(); + reqHead.put("Content-Type", List.of("application/octet-stream")); + int rc = postUrl(true, streamer, "http://localhost:" + getHttpdPort() + "/endpoint", res, reqHead, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + Assert.assertEquals(String.valueOf(PAYLOAD_SIZE), requestDesc.getRequestInfo("REQUEST-BODY-SIZE")); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java --- tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,81 @@ +/* + * 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.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Valve; +import org.apache.catalina.valves.RemoteIpValve; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestRemoteIpValveWithProxy extends HttpdIntegrationBaseTest { + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + ProxyRequests Off + ProxyPreserveHost On + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyAddHeaders Off + RequestHeader set X-Forwarded-For 140.211.11.130 + RequestHeader set X-Forwarded-Proto https + RequestHeader set X-Forwarded-Host whoamI.tomcat + """; + + @Override + protected List getValveConfig() { + List valves = new ArrayList<>(); + + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setHostHeader("X-Forwarded-Host"); + valves.add(remoteIpValve); + + return valves; + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void testRemoteIpValveProxying() throws Exception { + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getHttpdPort() + "/endpoint", res, false); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + Assert.assertNotNull(requestDesc.getRequestInfo()); + Assert.assertEquals("140.211.11.130", requestDesc.getRequestInfo("REQUEST-REMOTE-ADDR")); + Assert.assertEquals(443, Integer.valueOf(requestDesc.getRequestInfo("REQUEST-SERVER-PORT")).intValue()); + Assert.assertEquals(getPort(), Integer.valueOf(requestDesc.getRequestInfo("REQUEST-LOCAL-PORT")).intValue()); + Assert.assertEquals("https", requestDesc.getRequestInfo("REQUEST-SCHEME")); + Assert.assertEquals("true", requestDesc.getRequestInfo("REQUEST-IS-SECURE")); + Assert.assertEquals("whoamI.tomcat", requestDesc.getRequestInfo("REQUEST-SERVER-NAME")); + Assert.assertNotNull(requestDesc.getHeaders()); + Assert.assertNull(requestDesc.getHeader("X-Forwarded-For")); + Assert.assertEquals("https", requestDesc.getHeader("X-Forwarded-Proto")); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java --- tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,88 @@ +/* + * 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.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Globals; +import org.apache.catalina.Valve; +import org.apache.catalina.valves.SSLValve; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.TesterSupport; + +public class TestSSLValveWithProxy01 extends HttpdIntegrationBaseTest { + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + LoadModule ssl_module modules/mod_ssl.so + SSLSessionCache none + Listen %{HTTPD_SSL_PORT} https + + ServerName localhost:%{HTTPD_SSL_PORT} + SSLEngine on + SSLCertificateFile "%{SSL_CERT_FILE}" + SSLCertificateKeyFile "%{SSL_KEY_FILE}" + ProxyRequests Off + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + RequestHeader set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s" + RequestHeader set SSL_CIPHER "%{SSL_CIPHER}s" + RequestHeader set SSL_SESSION_ID "%{SSL_SESSION_ID}s" + RequestHeader set SSL_CIPHER_USEKEYSIZE "%{SSL_CIPHER_USEKEYSIZE}s" + + """; + + @Override + protected List getValveConfig() { + List valves = new ArrayList<>(); + + SSLValve sslValve = new SSLValve(); + valves.add(sslValve); + + return valves; + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void testSSLValveProxying() throws Exception { + TesterSupport.configureClientSsl(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("https://localhost:" + getHttpdSslPort() + "/endpoint", res, false); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + Assert.assertNotNull(requestDesc.getAttributes()); + Assert.assertNotNull(requestDesc.getAttribute(Globals.CIPHER_SUITE_ATTR)); + Assert.assertNotNull(requestDesc.getAttribute(Globals.SSL_SESSION_ID_ATTR)); + Assert.assertNotNull(requestDesc.getAttribute(Globals.KEY_SIZE_ATTR)); + // No client certificate in this test, mod_ssl sends null which SSLValve correctly treats as absent. + Assert.assertNull(requestDesc.getAttribute(Globals.CERTIFICATES_ATTR)); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java --- tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,86 @@ +/* + * 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.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Globals; +import org.apache.catalina.Valve; +import org.apache.catalina.valves.SSLValve; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.TesterSupport; + +public class TestSSLValveWithProxy02 extends HttpdIntegrationBaseTest { + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + LoadModule ssl_module modules/mod_ssl.so + SSLSessionCache none + Listen %{HTTPD_SSL_PORT} https + + ServerName localhost:%{HTTPD_SSL_PORT} + SSLEngine on + SSLCertificateFile "%{SSL_CERT_FILE}" + SSLCertificateKeyFile "%{SSL_KEY_FILE}" + ProxyRequests Off + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + RequestHeader set SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s" + RequestHeader set SSL_CIPHER "%{SSL_CIPHER}s" + RequestHeader set SSL_SESSION_ID "%{SSL_SESSION_ID}s" + RequestHeader set SSL_CIPHER_USEKEYSIZE "%{SSL_CIPHER_USEKEYSIZE}s" + SSLVerifyClient optional + SSLCACertificateFile "%{SSL_CA_CERT_FILE}" + SSLOptions +ExportCertData + + """; + + @Override + protected List getValveConfig() { + List valves = new ArrayList<>(); + + SSLValve sslValve = new SSLValve(); + valves.add(sslValve); + + return valves; + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + @Test + public void testSSLValveProxying() throws Exception { + TesterSupport.configureClientSsl(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("https://localhost:" + getHttpdSslPort() + "/endpoint", res, false); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + Assert.assertNotNull(requestDesc.getAttribute(Globals.CERTIFICATES_ATTR)); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java --- tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,117 @@ +/* + * 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.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Valve; +import org.apache.catalina.valves.RemoteIpValve; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.TesterSupport; + +public class TestSessionWithProxy extends HttpdIntegrationBaseTest { + private static final String HTTPD_CONFIG = """ + LoadModule proxy_module modules/mod_proxy.so + LoadModule proxy_http_module modules/mod_proxy_http.so + LoadModule headers_module modules/mod_headers.so + LoadModule ssl_module modules/mod_ssl.so + SSLSessionCache none + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + Listen %{HTTPD_SSL_PORT} https + + ServerName localhost:%{HTTPD_SSL_PORT} + SSLEngine on + SSLCertificateFile "%{SSL_CERT_FILE}" + SSLCertificateKeyFile "%{SSL_KEY_FILE}" + ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME} + RequestHeader set X-Forwarded-Proto https + + """; + + @Override + protected List getValveConfig() { + List valves = new ArrayList<>(); + + RemoteIpValve remoteIpValve = new RemoteIpValve(); + valves.add(remoteIpValve); + + return valves; + } + + @Override + protected String getHttpdConfig() { + return HTTPD_CONFIG; + } + + /* + * Verify that a session created through httpd can be retrieved + * on a subsequent request using the session cookie. + */ + @Test + public void testSessionCookieSetAndRetrieved() throws Exception { + // Create a session + ByteChunk res = new ByteChunk(); + Map> resHead = new HashMap<>(); + int rc = getUrl("http://localhost:" + getHttpdPort() + "/endpoint?createSession=true", res, null, resHead); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + Assert.assertNotNull(requestDesc.getRequestInfo()); + String sessionId = requestDesc.getRequestInfo("SESSION-ID"); + Assert.assertNotNull(sessionId); + Assert.assertEquals("true", requestDesc.getRequestInfo("SESSION-IS-NEW")); + + String setCookie = resHead.get("Set-Cookie").get(0); + Assert.assertTrue(setCookie.contains("JSESSIONID")); + + // Send the session cookie back + Map> reqHead = new HashMap<>(); + reqHead.put("Cookie", List.of("JSESSIONID=" + sessionId)); + rc = getUrl("http://localhost:" + getHttpdPort() + "/endpoint", res, reqHead, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + requestDesc = SnoopResult.parse(res.toString()); + Assert.assertNotNull(requestDesc.getRequestInfo()); + Assert.assertEquals(sessionId, requestDesc.getRequestInfo("SESSION-ID")); + Assert.assertEquals("false", requestDesc.getRequestInfo("SESSION-IS-NEW")); + } + + /* + * Verify that when SSL is used at httpd, but not Tomcat, and RemoteIpValve + * sets the scheme to https, session cookies have the Secure flag. + */ + @Test + public void testSecureCookieWithSslTermination() throws Exception { + TesterSupport.configureClientSsl(); + ByteChunk res = new ByteChunk(); + Map> resHead = new HashMap<>(); + getUrl("https://localhost:" + getHttpdSslPort() + "/endpoint?createSession=true", res, null, resHead); + Assert.assertTrue("Session cookie should have Secure flag", resHead.get("Set-Cookie").get(0).contains("Secure")); + } + +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TesterHttpd.java tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TesterHttpd.java --- tomcat11-11.0.15/test/org/apache/tomcat/integration/httpd/TesterHttpd.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/integration/httpd/TesterHttpd.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,149 @@ +/* + * 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.IOException; +import java.io.PrintStream; +import java.io.Reader; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; + + +public class TesterHttpd { + + private final File httpdConfDir; + private final int httpdPort; + + private static final String HTTPD_PATH = "tomcat.test.httpd.path"; + + private Process p; + + public TesterHttpd(File httpdConfDir, int httpdPort) { + this.httpdConfDir = httpdConfDir; + this.httpdPort = httpdPort; + } + + public void start() throws IOException, InterruptedException { + start(false); + } + + public void start(boolean swallowOutput) throws IOException, InterruptedException { + if (p != null) { + throw new IllegalStateException("Already started"); + } + + String httpdPath = System.getProperty(HTTPD_PATH); + if (httpdPath == null || httpdPath.isEmpty()) { + httpdPath = "httpd"; + } + + File httpdConfFile = new File(httpdConfDir, "httpd.conf"); + validateHttpdConfig(httpdPath, httpdConfFile.getAbsolutePath()); + + List cmd = new ArrayList<>(4); + cmd.add(httpdPath); + cmd.add("-f"); + cmd.add(httpdConfFile.getAbsolutePath()); + cmd.add("-X"); + + ProcessBuilder pb = new ProcessBuilder(cmd.toArray(new String[0])); + + p = pb.start(); + + redirect(p.inputReader(), System.out, swallowOutput); + redirect(p.errorReader(), System.err, swallowOutput); + + Assert.assertTrue(p.isAlive() && isHttpdReady()); + } + + public void stop() { + if (p == null) { + throw new IllegalStateException("Not started"); + } + p.destroy(); + + try { + if (!p.waitFor(30, TimeUnit.SECONDS)) { + throw new IllegalStateException("Failed to stop"); + } + } catch (InterruptedException e) { + throw new IllegalStateException("Interrupted while waiting to stop", e); + } + } + + private void redirect(final Reader r, final PrintStream os, final boolean swallow) { + /* + * InputStream will close when process ends. Thread will exit once stream closes. + */ + new Thread( () -> { + char[] cbuf = new char[1024]; + try { + int read; + while ((read = r.read(cbuf)) > 0) { + if (!swallow) { + os.print(new String(cbuf, 0, read)); + } + } + } catch (IOException ignore) { + // Ignore + } + + }).start(); + } + + private static void validateHttpdConfig(final String httpdPath, final String httpdConfPath) throws IOException, InterruptedException { + List cmd = new ArrayList<>(4); + + cmd.add(httpdPath); + cmd.add("-t"); + cmd.add("-f"); + cmd.add(httpdConfPath); + + ProcessBuilder pb = new ProcessBuilder(cmd.toArray(new String[0])); + pb.redirectErrorStream(true); + + Process p = pb.start(); + + String output = new String(p.getInputStream().readAllBytes()); + int exitCode = p.waitFor(); + + if (exitCode != 0) { + throw new IllegalStateException("Httpd configuration invalid. Output: " + output); + } + } + + @SuppressWarnings("BusyWait") + private boolean isHttpdReady() throws InterruptedException { + long deadline = System.currentTimeMillis() + 1000; + while (System.currentTimeMillis() < deadline) { + try (@SuppressWarnings("unused") Socket ignored = new Socket("localhost", this.httpdPort)) { + return true; + } catch (IOException e) { + Thread.sleep(100); + } + } + throw new IllegalStateException("Httpd has not been started."); + } + + +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/jni/TesterLibraryLoad.java tomcat11-11.0.22/test/org/apache/tomcat/jni/TesterLibraryLoad.java --- tomcat11-11.0.15/test/org/apache/tomcat/jni/TesterLibraryLoad.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/jni/TesterLibraryLoad.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,37 @@ +/* + * 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.jni; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Simple test to verify tomcat-native library can be loaded. + */ +public class TesterLibraryLoad { + + @Test + public void testLibraryLoads() throws Exception { + try { + Library.initialize(null); + Library.terminate(); + } catch (LibraryNotFoundError e) { + // Library not available - fail test to set property + Assert.fail("Library not found"); + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/security/TestSecurity2017.java tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2017.java --- tomcat11-11.0.15/test/org/apache/tomcat/security/TestSecurity2017.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2017.java 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -/* - * 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.security; - -import javax.net.ssl.SSLHandshakeException; - -import org.junit.Assert; -import org.junit.Test; - -import org.apache.catalina.startup.TomcatBaseTest; -import org.apache.tomcat.util.net.ocsp.TestOcspIntegration; - -public class TestSecurity2017 extends TomcatBaseTest { - /* - * https://www.cve.org/CVERecord?id=CVE-2017-15698 - * - * Fixed in Tomcat Native - * 1.2.16 https://github.com/apache/tomcat-native/commit/4582e6d9223da618b42db6e992bb2d55d9cd4c42 - */ - @Test - public void testCVE_2017_15698() throws Exception { - try { - TestOcspIntegration.testLongUrlForOcspViaAIAWithTomcatNative(getTomcatInstance()); - } catch (SSLHandshakeException sslHandshakeException) { - Assert.assertTrue(sslHandshakeException.toString().contains("certificate_revoked")); - } - } -} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java --- tomcat11-11.0.15/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,113 @@ +/* + * 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.security; + +import java.net.SocketException; + +import javax.net.ssl.SSLHandshakeException; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.TesterSupport; +import org.apache.tomcat.util.net.TesterSupport.SimpleServlet; +import org.apache.tomcat.util.net.ocsp.OcspBaseTest; +import org.apache.tomcat.util.net.ocsp.TesterOcspResponder; +import org.apache.tomcat.util.net.openssl.OpenSSLStatus; + +@RunWith(Parameterized.class) +public class TestSecurity2017Ocsp extends OcspBaseTest { + + private static TesterOcspResponder ocspResponder; + + @BeforeClass + public static void startOcspResponder() { + ocspResponder = new TesterOcspResponder(); + try { + ocspResponder.start(); + } catch (Exception e) { + ocspResponder = null; + } + } + + + @AfterClass + public static void stopOcspResponder() { + if (ocspResponder != null) { + ocspResponder.stop(); + ocspResponder = null; + } + } + + + /* + * In addition to testing Tomcat Native (where the CVE occurred), this also tests JSSE and OpenSSl via FFM. + */ + @Test(expected=SSLHandshakeException.class) + public void testCVE_2017_15698() throws Exception { + if ("OpenSSL-FFM".equals(connectorName)) { + Assume.assumeFalse(OpenSSLStatus.isBoringSSL() || OpenSSLStatus.isLibreSSLPre35()); + } + Assume.assumeNotNull(ocspResponder); + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("", null); + + Tomcat.addServlet(ctx, "simple", new SimpleServlet()); + ctx.addServletMappingDecoded("/simple", "simple"); + + // Use the default (valid, non-revoked) server certificate + TesterSupport.initSsl(tomcat, useOpenSSLTrust); + + // Require client certificates and enable verification + SSLHostConfig sslHostConfig = tomcat.getConnector().findSslHostConfigs()[0]; + sslHostConfig.setOcspEnabled(true); + sslHostConfig.setCertificateVerification("required"); + + // Configure a revoked client certificate with a long AIA + // Don't verify the server certificate + TesterSupport.configureClientSsl(false, TesterSupport.CLIENT_CRL_LONG_JKS); + + // Disable soft-fail + sslHostConfig.setOcspSoftFail(false); + + tomcat.start(); + + int rc; + try { + rc = getUrl("https://localhost:" + getPort() + "/simple", new ByteChunk(), false); + } catch (SocketException se) { + throw new SSLHandshakeException(se.getMessage()); + } + + // If the TLS handshake fails, the test won't get this far. + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/security/TestSecurity2018.java tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2018.java --- tomcat11-11.0.15/test/org/apache/tomcat/security/TestSecurity2018.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2018.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,79 @@ +/* + * 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.security; + +import java.io.File; +import java.net.URI; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +import jakarta.websocket.ClientEndpointConfig; +import jakarta.websocket.ContainerProvider; +import jakarta.websocket.DeploymentException; +import jakarta.websocket.WebSocketContainer; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.net.TesterKeystoreGenerator; +import org.apache.tomcat.util.net.TesterSupport; +import org.apache.tomcat.websocket.TesterEchoServer; +import org.apache.tomcat.websocket.TesterMessageCountClient; +import org.apache.tomcat.websocket.WebSocketBaseTest; + +public class TestSecurity2018 extends WebSocketBaseTest { + + // https://www.cve.org/CVERecord?id=CVE-2018-8034 + @Test(expected = DeploymentException.class) + public void testCVE_2018_8034() throws Exception { + File keystoreFile = TesterKeystoreGenerator.generateKeystore( + "localhost", "tomcat", + new String[]{"localhost"}, null); + + Tomcat tomcat = getTomcatInstance(); + + TesterSupport.initSsl(tomcat, keystoreFile.getAbsolutePath(), false); + + Context ctx = getProgrammaticRootContext(); + ctx.addApplicationListener(TesterEchoServer.Config.class.getName()); + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + + tomcat.start(); + + WebSocketContainer wsContainer = ContainerProvider.getWebSocketContainer(); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[]{new TesterSupport.TrustAllCerts()}, null); + + ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().sslContext(sslContext) + .build(); + + wsContainer.connectToServer( + TesterMessageCountClient.TesterProgrammaticEndpoint.class, + clientEndpointConfig, + new URI("wss://127.0.0.1:" + getPort() + + TesterEchoServer.Config.PATH_ASYNC)); + Assert.fail( + "Hostname verification should have failed for 127.0.0.1 with a certificate issued for localhost only."); + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/security/TestSecurity2019.java tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2019.java --- tomcat11-11.0.15/test/org/apache/tomcat/security/TestSecurity2019.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2019.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,185 @@ +/* + * 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.security; + +import java.io.File; +import java.io.FileWriter; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.servlets.CGIServlet; +import org.apache.catalina.servlets.DefaultServlet; +import org.apache.catalina.ssi.SSIFilter; +import org.apache.catalina.ssi.SSIServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.compat.JrePlatform; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; + +public class TestSecurity2019 extends TomcatBaseTest { + + // https://www.cve.org/CVERecord?id=CVE-2019-0221 + @Test + public void testCVE_2019_0221_01() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getTemporaryDirectory(), "ssitest"); + Assert.assertTrue(appDir.mkdirs()); + addDeleteOnTearDown(appDir); + + File shtml = new File(appDir, "printenv.shtml"); + try (FileWriter fw = new FileWriter(shtml)) { + fw.write(""); + } + + Context ctx = tomcat.addContext("", appDir.getAbsolutePath()); + + Tomcat.addServlet(ctx, "ssi", new SSIServlet()); + ctx.addServletMappingDecoded("*.shtml", "ssi"); + + ctx.setPrivileged(true); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/printenv.shtml?%3Ch1%3EXSS%3C/h1%3E", res, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertFalse("SSI printenv should not render unescaped HTML ", res.toString().contains("

    ")); + + } + + @Test + public void testCVE_2019_0221_02() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getTemporaryDirectory(), "ssitest"); + Assert.assertTrue(appDir.mkdirs()); + addDeleteOnTearDown(appDir); + + File shtml = new File(appDir, "printenv.shtml"); + try (FileWriter fw = new FileWriter(shtml)) { + fw.write(""); + } + + Context ctx = tomcat.addContext("", appDir.getAbsolutePath()); + + FilterDef filterDef = new FilterDef(); + filterDef.setFilterClass(SSIFilter.class.getName()); + filterDef.setFilterName("ssi"); + ctx.addFilterDef(filterDef); + + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName("ssi"); + filterMap.addURLPatternDecoded("*.shtml"); + ctx.addFilterMap(filterMap); + + Tomcat.addServlet(ctx, "default", new DefaultServlet()); + ctx.addServletMappingDecoded("/", "default"); + ctx.addMimeMapping("shtml", "text/x-server-parsed-html"); + + ctx.setPrivileged(true); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/printenv.shtml?%3Ch1%3EXSS%3C/h1%3E", res, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertFalse("SSI printenv should not render unescaped HTML ", res.toString().contains("

    ")); + } + + // https://www.cve.org/CVERecord?id=CVE-2019-0232 + @Test + public void testCVE_2019_0232() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getTemporaryDirectory(), "cgitest"); + Assert.assertTrue(appDir.mkdirs()); + addDeleteOnTearDown(appDir); + + File cgiDir = new File(appDir, "WEB-INF/cgi"); + Assert.assertTrue(cgiDir.mkdirs()); + + File testScript; + File maliciousScript; + + if (JrePlatform.IS_WINDOWS) { + testScript = new File(cgiDir, "test.bat"); + try (FileWriter fw = new FileWriter(testScript)) { + fw.write("@echo off\r\n"); + fw.write("echo Content-Type: text/plain\r\n"); + fw.write("echo.\r\n"); + fw.write("echo Query string: %QUERY_STRING%\r\n"); + } + + maliciousScript = new File(cgiDir, "malicious.bat"); + try (FileWriter fw = new FileWriter(maliciousScript)) { + fw.write("@echo off\r\n"); + fw.write("echo vulnerable > \"" + new File(appDir, "vulnerable").getAbsolutePath() + "\"\r\n"); + } + } else { + testScript = new File(cgiDir, "test.sh"); + try (FileWriter fw = new FileWriter(testScript)) { + fw.write("#!/bin/sh\n"); + fw.write("echo \"Content-Type: text/plain\"\n"); + fw.write("echo\n"); + fw.write("echo \"Query string: $QUERY_STRING\"\n"); + } + + maliciousScript = new File(cgiDir, "malicious.sh"); + try (FileWriter fw = new FileWriter(maliciousScript)) { + fw.write("#!/bin/sh\n"); + fw.write("touch " + new File(appDir, "vulnerable").getAbsolutePath() + "\n"); + } + } + + Assert.assertTrue(testScript.setExecutable(true)); + Assert.assertTrue(maliciousScript.setExecutable(true)); + + Context ctx = tomcat.addContext("", appDir.getAbsolutePath()); + ctx.setPrivileged(true); + + Wrapper cgi = Tomcat.addServlet(ctx, "cgi", new CGIServlet()); + cgi.addInitParameter("cgiPathPrefix", "WEB-INF/cgi"); + cgi.addInitParameter("executable", ""); + cgi.addInitParameter("enableCmdLineArguments", "true"); + ctx.addServletMappingDecoded("/cgi/*", "cgi"); + + tomcat.start(); + + String scriptName = JrePlatform.IS_WINDOWS ? "test.bat" : "test.sh"; + String maliciousName = JrePlatform.IS_WINDOWS ? "malicious.bat" : "malicious.sh"; + + ByteChunk res = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + "/cgi/" + scriptName + "?firstName=Dimitris", + res, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + Assert.assertTrue(res.toString().contains("Query string:")); + + res.recycle(); + getUrl("http://localhost:" + getPort() + "/cgi/" + scriptName + "?&" + maliciousName, res, null); + Assert.assertFalse("CGI command injection succeeded", new File(appDir, "vulnerable").exists()); + } + +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/security/TestSecurity2023.java tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2023.java --- tomcat11-11.0.15/test/org/apache/tomcat/security/TestSecurity2023.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/security/TestSecurity2023.java 2026-05-01 18:56:05.000000000 +0000 @@ -24,6 +24,10 @@ import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import jakarta.servlet.MultipartConfigElement; import jakarta.servlet.ServletException; @@ -37,8 +41,14 @@ import static org.apache.catalina.startup.SimpleHttpClient.CRLF; import org.apache.catalina.Context; import org.apache.catalina.Wrapper; +import org.apache.catalina.authenticator.FormAuthenticator; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.http.Method; public class TestSecurity2023 extends TomcatBaseTest { /* @@ -81,6 +91,90 @@ Assert.assertEquals(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, status); } + /* + * https://www.cve.org/CVERecord?id=CVE-2023-41080 + * + * Fixed in + * 11.0.0-M11 https://github.com/apache/tomcat/commit/e3703c9abb8fe0d5602f6ba8a8f11d4b6940815a + * 10.1.13 https://github.com/apache/tomcat/commit/bb4624a9f3e69d495182ebfa68d7983076407a27 + * 9.0.80 https://github.com/apache/tomcat/commit/77c0ce2d169efa248b64b992e547aad549ec906b + */ + @Test + public void testCVE_2023_41080() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Context context = tomcat.addContext("", null); + + LoginConfig loginConfig = new LoginConfig(); + loginConfig.setAuthMethod("FORM"); + loginConfig.setLoginPage("/login"); + context.setLoginConfig(loginConfig); + context.getPipeline().addValve(new FormAuthenticator()); + + SecurityConstraint securityConstraint = new SecurityConstraint(); + securityConstraint.addAuthRole("admin"); + SecurityCollection securityCollection = new SecurityCollection(); + securityCollection.addPattern("/secret.html"); + securityCollection.addPattern("/example.com"); + securityConstraint.addCollection(securityCollection); + context.addConstraint(securityConstraint); + + tomcat.addUser("admin", "admin"); + tomcat.addRole("admin", "admin"); + + Tomcat.addServlet(context, "login", new TestServlet()); + context.addServletMappingDecoded("/login", "login"); + Tomcat.addServlet(context, "secret", new TestServlet()); + context.addServletMappingDecoded("/secret.html", "secret"); + Tomcat.addServlet(context, "example", new TestServlet()); + context.addServletMappingDecoded("/example.com", "example"); + + tomcat.start(); + + String location = doFormLoginAndGetRedirectLocation("http://localhost:" + getPort(), "/secret.html;@example.com"); + Assert.assertNotNull(location); + Assert.assertFalse(location.startsWith("//")); + URI locationUri = new URI(location); + Assert.assertNull(locationUri.getHost()); + Assert.assertTrue(locationUri.getPath().contains("secret.html")); + + location = doFormLoginAndGetRedirectLocation("http://localhost:" + getPort(), "//example.com"); + Assert.assertNotNull(location); + Assert.assertFalse(location.startsWith("//")); + } + + private static String doFormLoginAndGetRedirectLocation(String basePath, String targetPath) throws Exception { + String url = basePath + targetPath; + Map> resHead = new HashMap<>(); + int rc = getUrl(url, new ByteChunk(), resHead); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + String sessionCookie = null; + List cookies = resHead.get("Set-Cookie"); + if (cookies != null) { + for (String cookie : cookies) { + if (cookie.startsWith("JSESSIONID=")) { + sessionCookie = cookie.split(";")[0]; + break; + } + } + } + Assert.assertNotNull("JSESSIONID not found in response", sessionCookie); + String loginUrl = basePath + "/j_security_check"; + HttpURLConnection conn = (HttpURLConnection) new URI(loginUrl).toURL().openConnection(); + conn.setRequestMethod(Method.POST); + conn.setDoOutput(true); + conn.setInstanceFollowRedirects(false); // want to check the redirect location manually + conn.setRequestProperty("Cookie", sessionCookie); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + String params = "j_username=admin&j_password=admin"; + try (OutputStream os = conn.getOutputStream()) { + os.write(params.getBytes(StandardCharsets.UTF_8)); + } + int loginRc = conn.getResponseCode(); + Assert.assertEquals(HttpServletResponse.SC_SEE_OTHER, loginRc); + return conn.getHeaderField("Location"); + } + private static int postMultipart(String path, String queryStringParams, int parts) throws IOException, URISyntaxException { String urlStr = path + (queryStringParams == null || queryStringParams.isEmpty() ? "" : "?" + queryStringParams); String boundary = "--simpleboundary"; @@ -104,6 +198,7 @@ inputStream = conn.getErrorStream(); } if (inputStream != null) { + //noinspection StatementWithEmptyBody while (inputStream.read() != -1) {} inputStream.close(); } @@ -138,5 +233,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { req.getParameterMap(); } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + } } } diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/unittest/TesterRequest.java tomcat11-11.0.22/test/org/apache/tomcat/unittest/TesterRequest.java --- tomcat11-11.0.15/test/org/apache/tomcat/unittest/TesterRequest.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/unittest/TesterRequest.java 2026-05-01 18:56:05.000000000 +0000 @@ -31,6 +31,7 @@ import org.apache.catalina.Context; import org.apache.catalina.connector.Request; import org.apache.catalina.session.StandardSession; +import org.apache.tomcat.util.buf.MessageBytes; public class TesterRequest extends Request { @@ -81,6 +82,15 @@ return "/level1/level2/foo.html"; } + + @Override + public MessageBytes getRequestPathMB() { + MessageBytes result = MessageBytes.newInstance(); + result.setString(getRequestURI()); + return result; + } + + @Override public String getDecodedRequestURI() { // Decoding not required diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/unittest/TesterResponseWithStatus.java tomcat11-11.0.22/test/org/apache/tomcat/unittest/TesterResponseWithStatus.java --- tomcat11-11.0.15/test/org/apache/tomcat/unittest/TesterResponseWithStatus.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/unittest/TesterResponseWithStatus.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,36 @@ +/* + * 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.unittest; + +import java.io.IOException; + +public class TesterResponseWithStatus extends TesterResponse { + + int status = 200; + String message = "OK"; + + @Override + public void sendError(int status, String message) throws IOException { + this.status = status; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } +} \ No newline at end of file diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java tomcat11-11.0.22/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java 2026-05-01 18:56:05.000000000 +0000 @@ -59,6 +59,9 @@ parameterSets.add(new String[] { "/a/b/.", "/a/b" }); parameterSets.add(new String[] { "/a/b/../", "/a/" }); parameterSets.add(new String[] { "/a/b/./", "/a/b/" }); + parameterSets.add(new String[] { "/a\u0000/b/./", null }); + parameterSets.add(new String[] { "\u0000/a/b/./", null }); + parameterSets.add(new String[] { "/a/b/./\u0000", null }); return parameterSets; } diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/http/parser/TestChunkExtension.java tomcat11-11.0.22/test/org/apache/tomcat/util/http/parser/TestChunkExtension.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/http/parser/TestChunkExtension.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/http/parser/TestChunkExtension.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,186 @@ +/* + * 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.util.http.parser; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.http.parser.ChunkExtension.State; + +public class TestChunkExtension { + + @Test + public void testEmpty() { + doTest("\r\n", true); + } + + @Test + public void testInvalid() { + doTest("x\r\n", false); + } + + @Test + public void testNoToken01() { + doTest(";\r\n", false); + } + + @Test + public void testNoToken02() { + doTest(" ;\r\n", false); + } + + @Test + public void testNoToken03() { + doTest("; \r\n", false); + } + + @Test + public void testNoToken04() { + doTest(";\t\r\n", false); + } + + @Test + public void testInvalidToken01() { + doTest("; =\r\n", false); + } + + @Test + public void testTokenOnly01() { + doTest("; abc\r\n", true); + } + + @Test + public void testTokenOnly02() { + doTest("; abc \r\n", true); + } + + @Test + public void testTokenOnly03() { + doTest("; abc \r\n", true); + } + + @Test + public void testTokenOnlyTokenOnly01() { + doTest(";abc;abc\r\n", true); + } + + @Test + public void testTokenOnlyTokenOnly02() { + doTest("; abc ; abc \r\n", true); + } + + @Test + public void testTokenToken01() { + doTest(";abc=abc\r\n", true); + } + + @Test + public void testTokenToken02() { + doTest("; abc = abc \r\n", true); + } + + @Test + public void testTokenQs01() { + doTest("; abc =\"\"\r\n", true); + } + + @Test + public void testTokenQs02() { + doTest("; abc =\"abc\"\r\n", true); + } + + @Test + public void testTokenQs03() { + doTest("; abc =\"a\tbc\"\r\n", true); + } + + @Test + public void testTokenInvalidQs01() { + doTest("; abc =\"a\rbc\"\r\n", false); + } + + @Test + public void testTokenInvalidQs02() { + doTest("; abc =\"a\\bc\"\r\n", false); + } + + @Test + public void testTokenInvalidQs03() { + doTest("; abc =\"a\u007f\"\r\n", false); + } + + @Test + public void testTokenInvalid01() { + doTest("; abc =\r\n", false); + } + + @Test + public void testTokenInvalid02() { + doTest("; abc ==\r\n", false); + } + + @Test + public void testTokenInvalid03() { + doTest(";a=b=c\r\n", false); + } + + @Test + public void testTokenInvalid04() { + doTest(";a\"r\n", false); + } + + @Test + public void testTokenInvalid05() { + doTest(";a \"r\n", false); + } + + @Test + public void testValidValid() { + doTest(";abc=def;ghi=jkl\r\n", true); + } + + @Test + public void testValidInvalid() { + doTest(";abc=def;=\r\n", false); + } + + private void doTest(String input, boolean valid) { + byte[] bytes = input.getBytes(StandardCharsets.ISO_8859_1); + + try { + // This state assumes either ';' or CRLF will follow, preceded by optional white space. + State state = State.POST_VALUE; + for (byte b : bytes) { + state = ChunkExtension.parse(b, state); + /* + * The test values all end in \r\n but ChunkExtension only looks for \r. In real usage the + * ChunkedInputFilter then parses the CRLF. + */ + if (state == State.CR) { + break; + } + } + Assert.assertTrue("The input was invalid but no exception was thrown", valid); + Assert.assertEquals("Parsing ended at state other than CR", State.CR, state); + } catch (IOException ioe) { + Assert.assertFalse("The input was valid but an exception was thrown", valid); + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/http/parser/TestMediaType.java tomcat11-11.0.22/test/org/apache/tomcat/util/http/parser/TestMediaType.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/http/parser/TestMediaType.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/http/parser/TestMediaType.java 2026-05-01 18:56:05.000000000 +0000 @@ -60,9 +60,7 @@ new Parameter("charset", CHARSET_QUOTED); - private static final String[] LWS_VALUES = new String[] { - "", " ", "\t", "\r", "\n", "\r\n", " \r", " \n", " \r\n", - "\r ", "\n ", "\r\n ", " \r ", " \n ", " \r\n " }; + private static final String[] WHITESPACE = new String[] { "", " ", "\t", " \t", "\t " }; @Test @@ -236,7 +234,7 @@ private void doTest(Parameter... parameters) throws IOException { - for (String lws : LWS_VALUES) { + for (String lws : WHITESPACE) { doTest(lws, parameters); } } diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestAlpnFallback.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestAlpnFallback.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestAlpnFallback.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestAlpnFallback.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,52 @@ +/* + * 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.util.net; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.http2.Http2TestBase; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestAlpnFallback extends Http2TestBase { + + @Test + public void testAlpnFallbackToHttp11() throws Exception { + TesterSupport.configureClientSsl(); + //There's no ALPN negotiation without TLS + enableHttp2(true); + + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + Tomcat.addServlet(ctx, "snoop", new SnoopServlet()); + ctx.addServletMappingDecoded("/", "snoop"); + + tomcat.start(); + + // HttpURLConnection does not support ALPN, so this request will connect over TLS without negotiating h2. + // The connector must fall back to HTTP/1.1 rather than dropping the connection. + ByteChunk res = new ByteChunk(); + getUrl("https://localhost:" + getPort() + "/", res, null); + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + Assert.assertEquals("HTTP/1.1", requestDesc.getRequestInfo("REQUEST-PROTOCOL")); + } + +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestLargeClientHello.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestLargeClientHello.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestLargeClientHello.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestLargeClientHello.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,91 @@ +/* + * 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.util.net; + +import java.io.File; +import java.util.logging.Level; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERUTF8String; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; + +public class TestLargeClientHello extends TomcatBaseTest { + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=67938 + @Test + public void testLargeClientHelloWithSessionResumption() throws Exception { + File keystoreFile = TesterKeystoreGenerator.generateKeystore("localhost", "tomcat", + new String[]{"localhost", "*.localhost"}, + (keyPair, certBuilder) -> { + JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, + extUtils.createSubjectKeyIdentifier(keyPair.getPublic())); + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, + extUtils.createAuthorityKeyIdentifier(keyPair.getPublic())); + certBuilder.addExtension(Extension.basicConstraints, true, + new BasicConstraints(true)); + certBuilder.addExtension(Extension.keyUsage, false, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment)); + certBuilder.addExtension(new ASN1ObjectIdentifier("2.999"), false, + new DERUTF8String("x".repeat(16922))); + }); + + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + TesterSupport.initSsl(tomcat, keystoreFile.getAbsolutePath(), false); + + try (LogCapture nioCapture = attachLogCapture(Level.FINE, + "org.apache.tomcat.util.net.SecureNioChannel"); + LogCapture nio2Capture = attachLogCapture(Level.FINE, + "org.apache.tomcat.util.net.SecureNio2Channel")) { + + tomcat.start(); + + SSLContext sc = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_3); + sc.init(null, new TrustManager[]{new TesterSupport.TrustAllCerts()}, null); + javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + + String url = "https://localhost:" + getPort() + "/"; + Assert.assertTrue(getUrl(url).toString().contains("Hello World")); + Assert.assertTrue(getUrl(url).toString().contains("Hello World")); + + Assert.assertTrue(nioCapture.containsText( + TomcatBaseTest.getKeyFromPropertiesFile("org.apache.tomcat.util.net", + "channel.nio.ssl.handshakeUnwrapBufferUnderflow")) || nio2Capture.containsText( + TomcatBaseTest.getKeyFromPropertiesFile("org.apache.tomcat.util.net", + "channel.nio.ssl.handshakeUnwrapBufferUnderflow"))); + } + + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestSSLHostConfig.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSSLHostConfig.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestSSLHostConfig.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSSLHostConfig.java 2026-05-01 18:56:05.000000000 +0000 @@ -40,6 +40,9 @@ // Single JSSE name hc.setCiphers(c.getJsseNames().iterator().next()); Assert.assertEquals(c.getOpenSSLAlias(), hc.getCiphers()); + + // TLS 1.3 should be using defaults + Assert.assertEquals(SSLHostConfig.DEFAULT_TLS_CIPHERS_13, hc.getCipherSuites()); } @@ -53,6 +56,9 @@ hc.setCiphers(c1.getJsseNames().iterator().next() + "," + c2.getJsseNames().iterator().next()); Assert.assertEquals(c1.getOpenSSLAlias() + ":" + c2.getOpenSSLAlias(), hc.getCiphers()); + + // TLS 1.3 should be using defaults + Assert.assertEquals(SSLHostConfig.DEFAULT_TLS_CIPHERS_13, hc.getCipherSuites()); } @@ -62,6 +68,9 @@ // Single OpenSSL alias hc.setCiphers("ALL"); Assert.assertEquals("ALL", hc.getCiphers()); + + // TLS 1.3 should be using defaults + Assert.assertEquals(SSLHostConfig.DEFAULT_TLS_CIPHERS_13, hc.getCipherSuites()); } @@ -73,6 +82,88 @@ // Single OpenSSLName name hc.setCiphers(c.getOpenSSLAlias()); Assert.assertEquals(c.getOpenSSLAlias(), hc.getCiphers()); + + // TLS 1.3 should be using defaults + Assert.assertEquals(SSLHostConfig.DEFAULT_TLS_CIPHERS_13, hc.getCipherSuites()); + } + + + @Test + public void testCipher05() { + SSLHostConfig hc = new SSLHostConfig(); + Cipher c = Cipher.TLS_AES_128_CCM_SHA256; + + // Single TLSv1.3 name - should be filtered out ... + hc.setCiphers(c.getOpenSSLAlias()); + Assert.assertEquals("", hc.getCiphers()); + + // ... and added to cipher suite list + Assert.assertEquals(SSLHostConfig.DEFAULT_TLS_CIPHERS_13 + ":" + c.getOpenSSLAlias(), hc.getCipherSuites()); + } + + + @Test + public void testCipher06() { + SSLHostConfig hc = new SSLHostConfig(); + Cipher c1 = Cipher.TLS_AES_128_CCM_SHA256; + Cipher c2 = Cipher.TLS_RSA_WITH_NULL_MD5; + + // TLSv1.3 then TLSv1.2 - TLSv1.3 name should be filtered out ... + hc.setCiphers(c1.getOpenSSLAlias() + ":" + c2.getOpenSSLAlias()); + Assert.assertEquals(c2.getOpenSSLAlias(), hc.getCiphers()); + + // ... and added to cipher suite list + Assert.assertEquals(SSLHostConfig.DEFAULT_TLS_CIPHERS_13 + ":" + c1.getOpenSSLAlias(), hc.getCipherSuites()); + } + + + @Test + public void testCipher07() { + SSLHostConfig hc = new SSLHostConfig(); + Cipher c1 = Cipher.TLS_AES_128_CCM_SHA256; + Cipher c2 = Cipher.TLS_RSA_WITH_NULL_MD5; + + // TLSv1.2 then TLSv1.3 - TLSv1.3 name should be filtered out ... + hc.setCiphers(c2.getOpenSSLAlias() + ":" + c1.getOpenSSLAlias()); + Assert.assertEquals(c2.getOpenSSLAlias(), hc.getCiphers()); + + // ... and added to cipher suite list + Assert.assertEquals(SSLHostConfig.DEFAULT_TLS_CIPHERS_13 + ":" + c1.getOpenSSLAlias(), hc.getCipherSuites()); + } + + + @Test + public void testCiphersuite01() { + SSLHostConfig hc = new SSLHostConfig(); + Cipher c = Cipher.TLS_AES_128_CCM_SHA256; + + // Single TLSv1.3 cipher suite name + hc.setCipherSuites(c.getOpenSSLAlias()); + Assert.assertEquals(c.getOpenSSLAlias(), hc.getCipherSuites()); + } + + + @Test + public void testCiphersuite02() { + SSLHostConfig hc = new SSLHostConfig(); + Cipher c1 = Cipher.TLS_AES_128_CCM_SHA256; + Cipher c2 = Cipher.TLS_RSA_WITH_NULL_MD5; + + // TLSv1.3 then TLSv1.2 - TLSv1.2 name should be filtered out + hc.setCipherSuites(c1.getOpenSSLAlias() + ":" + c2.getOpenSSLAlias()); + Assert.assertEquals(c1.getOpenSSLAlias(), hc.getCipherSuites()); + } + + + @Test + public void testCiphersuite03() { + SSLHostConfig hc = new SSLHostConfig(); + Cipher c1 = Cipher.TLS_AES_128_CCM_SHA256; + Cipher c2 = Cipher.TLS_RSA_WITH_NULL_MD5; + + // TLSv1.2 then TLSv1.3 - TLSv1.2 name should be filtered out + hc.setCipherSuites(c2.getOpenSSLAlias() + ":" + c1.getOpenSSLAlias()); + Assert.assertEquals(c1.getOpenSSLAlias(), hc.getCipherSuites()); } diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestSSLHostConfigCipher.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSSLHostConfigCipher.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestSSLHostConfigCipher.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSSLHostConfigCipher.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,161 @@ +/* + * 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.util.net; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.SSLHandshakeException; + +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.openssl.OpenSSLStatus; + +@RunWith(Parameterized.class) +public class TestSSLHostConfigCipher extends TomcatBaseTest { + + private static final String CIPHER_12_AVAILABLE = "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"; + private static final String CIPHER_12_NOT_AVAILABLE = "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384"; + private static final String CIPHER_13_AVAILABLE = "TLS_AES_128_GCM_SHA256"; + private static final String CIPHER_13_NOT_AVAILABLE = "TLS_AES_256_GCM_SHA384"; + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { + "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL-FFM", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation"}); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean useOpenSSL; + + @Parameter(2) + public String sslImplementationName; + + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + // Server-side TLS configuration + TesterSupport.initSsl(tomcat); + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, useOpenSSL); + + // Test specific, server-side cipher & protocol configuration + SSLHostConfig sslHostConfig = getSSLHostConfig(); + sslHostConfig.setProtocols("+TLSv1.2+TLSv1.3"); + sslHostConfig.setCiphers(CIPHER_12_AVAILABLE); + sslHostConfig.setCipherSuites(CIPHER_13_AVAILABLE); + + // Simple webapp + Context ctxt = getProgrammaticRootContext(); + Tomcat.addServlet(ctxt, "TesterServlet", new TesterServlet()); + ctxt.addServletMappingDecoded("/*", "TesterServlet"); + } + + + @Test + public void testTls12CipherAvailable() throws Exception { + if ("OpenSSL-FFM".equals(connectorName)) { + // The functionality works, but the two ciphers used are not available + Assume.assumeFalse(OpenSSLStatus.isBoringSSL()); + } + // Client-side TLS configuration + TesterSupport.configureClientSsl(true, new String[] { CIPHER_12_AVAILABLE } ); + + doTest(); + } + + + @Test(expected=SSLHandshakeException.class) + public void testTls12CipherNotAvailable() throws Exception { + if ("OpenSSL-FFM".equals(connectorName)) { + Assume.assumeFalse(OpenSSLStatus.isBoringSSL()); + } + // Client-side TLS configuration + TesterSupport.configureClientSsl(true, new String[] { CIPHER_12_NOT_AVAILABLE } ); + + doTest(); + } + + + @Test + public void testTls13CipherAvailable() throws Exception { + if ("OpenSSL-FFM".equals(connectorName)) { + Assume.assumeFalse(OpenSSLStatus.isBoringSSL()); + } + // Client-side TLS configuration + TesterSupport.configureClientSsl(new String[] { CIPHER_13_AVAILABLE } ); + + doTest(); + } + + + @Test(expected=SSLHandshakeException.class) + public void testTls13CipherNotAvailable() throws Exception { + if ("OpenSSL-FFM".equals(connectorName)) { + // The TLS 1.3 call might not be present + Assume.assumeFalse(OpenSSLStatus.isLibreSSLPre35()); + Assume.assumeFalse(OpenSSLStatus.isBoringSSL()); + } + // Client-side TLS configuration + TesterSupport.configureClientSsl(new String[] { CIPHER_13_NOT_AVAILABLE } ); + + doTest(); + } + + + private void doTest() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + tomcat.start(); + + // Check a request can be made + ByteChunk res = getUrl("https://localhost:" + getPort() + "/"); + Assert.assertEquals("OK", res.toString()); + } + + + private SSLHostConfig getSSLHostConfig() { + Tomcat tomcat = getTomcatInstance(); + Connector connector = tomcat.getConnector(); + return connector.findSslHostConfigs()[0]; + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestSSLHostConfigProtocol.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSSLHostConfigProtocol.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestSSLHostConfigProtocol.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSSLHostConfigProtocol.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,153 @@ +/* + * 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.util.net; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManager; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +@RunWith(Parameterized.class) +public class TestSSLHostConfigProtocol extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { + "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL-FFM", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation"}); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean useOpenSSL; + + @Parameter(2) + public String sslImplementationName; + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tomcat tomcat = getTomcatInstance(); + + // Server-side TLS configuration + TesterSupport.initSsl(tomcat); + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, useOpenSSL); + } + + + @Test + public void testSSLv2() throws Exception { + doTestIgnoreProtocol("SSLv2"); + } + + + @Test + public void testUnknown() throws Exception { + doTestIgnoreProtocol("Unknown"); + } + + + private void doTestIgnoreProtocol(String protocol) throws Exception { + SSLHostConfig sslHostConfig = getSSLHostConfig(); + + sslHostConfig.setProtocols("+" + protocol + "+TLSv1.2"); + + Tomcat tomcat = getTomcatInstance(); + tomcat.start(); + + // Expect only TLSv1.2 as unrecognised protocol should always be disabled + String[] enabledProtocols = sslHostConfig.getEnabledProtocols(); + + Assert.assertNotNull(enabledProtocols); + Assert.assertEquals(1, enabledProtocols.length); + Assert.assertEquals("TLSv1.2", enabledProtocols[0]); + } + + @Test(expected = SSLHandshakeException.class) + public void testTlsVersionMismatchServerTls13ClientTls12() throws Exception { + SSLHostConfig sslHostConfig = getSSLHostConfig(); + sslHostConfig.setProtocols(Constants.SSL_PROTO_TLSv1_3); + + Context ctx = getProgrammaticRootContext(); + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + Tomcat tomcat = getTomcatInstance(); + tomcat.start(); + + SSLContext sc = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_2); + sc.init(null, new TrustManager[] { new TesterSupport.TrustAllCerts() }, null); + TesterSupport.ClientSSLSocketFactory clientSSLSocketFactory = new TesterSupport.ClientSSLSocketFactory(sc.getSocketFactory()); + clientSSLSocketFactory.setProtocols(new String[] { Constants.SSL_PROTO_TLSv1_2 }); + HttpsURLConnection.setDefaultSSLSocketFactory(clientSSLSocketFactory); + + getUrl("https://localhost:" + getPort() + "/"); + } + + @Test(expected = SSLHandshakeException.class) + public void testTlsVersionMismatchServerTls12ClientTls13() throws Exception { + SSLHostConfig sslHostConfig = getSSLHostConfig(); + sslHostConfig.setProtocols(Constants.SSL_PROTO_TLSv1_2); + + Context ctx = getProgrammaticRootContext(); + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + Tomcat tomcat = getTomcatInstance(); + tomcat.start(); + + SSLContext sc = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_3); + sc.init(null, new TrustManager[] { new TesterSupport.TrustAllCerts() }, null); + TesterSupport.ClientSSLSocketFactory clientSSLSocketFactory = new TesterSupport.ClientSSLSocketFactory(sc.getSocketFactory()); + clientSSLSocketFactory.setProtocols(new String[] { Constants.SSL_PROTO_TLSv1_3 }); + HttpsURLConnection.setDefaultSSLSocketFactory(clientSSLSocketFactory); + + getUrl("https://localhost:" + getPort() + "/"); + } + + + private SSLHostConfig getSSLHostConfig() { + Tomcat tomcat = getTomcatInstance(); + Connector connector = tomcat.getConnector(); + return connector.findSslHostConfigs()[0]; + } + +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestSsl.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSsl.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestSsl.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSsl.java 2026-05-01 18:56:05.000000000 +0000 @@ -259,7 +259,7 @@ Context ctxt = tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); ctxt.addApplicationListener(WsContextListener.class.getName()); - TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS, + TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS, false, TesterSupport.JKS_PASS, null, TesterSupport.JKS_KEY_PASS, null); TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, useOpenSSL); @@ -282,7 +282,7 @@ Context ctxt = tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); ctxt.addApplicationListener(WsContextListener.class.getName()); - TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS, + TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_KEYPASS_JKS, false, null, TesterSupport.JKS_PASS_FILE, null, TesterSupport.JKS_KEY_PASS_FILE); TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, useOpenSSL); diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestSslHandshakeFailure.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSslHandshakeFailure.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/TestSslHandshakeFailure.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/TestSslHandshakeFailure.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,83 @@ +/* + * 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.util.net; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; + +import org.junit.Test; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestSslHandshakeFailure extends TomcatBaseTest { + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { + "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation"}); + parameterSets.add(new Object[] { + "OpenSSL-FFM", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation"}); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean useOpenSSL; + + @Parameter(2) + public String sslImplementationName; + + @Test(expected = SSLHandshakeException.class) + public void testMissingClientCertificate() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + TesterSupport.initSsl(tomcat); + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, useOpenSSL); + + tomcat.getConnector().findSslHostConfigs()[0].setCertificateVerification("required"); + + tomcat.start(); + + SSLContext sc = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_2); + sc.init(null, TesterSupport.getTrustManagers(), null); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + + getUrl("https://localhost:" + getPort() + "/"); + + } + +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,103 @@ +/* + * 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.util.net; + +import java.io.File; +import java.io.FileOutputStream; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Date; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +public final class TesterKeystoreGenerator { + + private TesterKeystoreGenerator() {} + + @FunctionalInterface + public interface CertificateExtensionsCustomizer { + void customize(KeyPair keyPair, X509v3CertificateBuilder certBuilder) + throws Exception; + } + + /** + * Generate a temporary JKS keystore containing a self-signed RSA certificate. + * + * @param cn the Common Name for the certificate subject + * @param alias the keystore alias for the key entry + * @param sanNames DNS Subject Alternative Names to include, or {@code null} for none + * @param customizer callback to add extensions to the certificate, or {@code null} for none. + * + * @return a temporary keystore file with password {@link TesterSupport#JKS_PASS} + * + * @throws Exception if certificate generation or keystore creation fails + */ + public static File generateKeystore(String cn, String alias, String[] sanNames, + CertificateExtensionsCustomizer customizer) throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(4096); + KeyPair keyPair = kpg.generateKeyPair(); + + X500Name subject = new X500Name("CN=" + cn); + BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); + long oneDay = 86400000L; + Date notBefore = new Date(System.currentTimeMillis() - oneDay); + Date notAfter = new Date(System.currentTimeMillis() + 365L * oneDay); + + X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(subject, serial, notBefore, notAfter, + subject, keyPair.getPublic()); + + if (sanNames != null && sanNames.length > 0) { + GeneralName[] generalNames = new GeneralName[sanNames.length]; + for (int i = 0; i < sanNames.length; i++) { + generalNames[i] = new GeneralName(GeneralName.dNSName, sanNames[i]); + } + certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(generalNames)); + } + + if (customizer != null) { + customizer.customize(keyPair, certBuilder); + } + + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); + X509Certificate certificate = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)); + + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + ks.setKeyEntry(alias, keyPair.getPrivate(), TesterSupport.JKS_PASS.toCharArray(), new X509Certificate[] { certificate }); + + File keystoreFile = File.createTempFile("test-cert-", ".jks"); + keystoreFile.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(keystoreFile)) { + ks.store(fos, TesterSupport.JKS_PASS.toCharArray()); + } + + return keystoreFile; + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/TesterSupport.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/TesterSupport.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/TesterSupport.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/TesterSupport.java 2026-05-01 18:56:05.000000000 +0000 @@ -24,12 +24,24 @@ import java.net.Socket; import java.net.UnknownHostException; import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.PrivateKey; +import java.security.cert.CertPathValidator; +import java.security.cert.Certificate; import java.security.cert.CertificateException; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.PKIXRevocationChecker; +import java.security.cert.TrustAnchor; +import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import javax.net.ssl.CertPathTrustManagerParameters; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -87,6 +99,12 @@ public static final String LOCALHOST_EC_KEY_PEM = SSL_DIR + "localhost-ec-key.pem"; public static final String LOCALHOST_RSA_CERT_PEM = SSL_DIR + "localhost-rsa-cert.pem"; public static final String LOCALHOST_RSA_KEY_PEM = SSL_DIR + "localhost-rsa-key.pem"; + public static final String DB_INDEX = SSL_DIR + "index.db"; + public static final String OCSP_RESPONDER_RSA_CERT = SSL_DIR + "ocsp-responder-rsa-cert.pem"; + public static final String OCSP_RESPONDER_RSA_KEY = SSL_DIR + "ocsp-responder-rsa-key.pem"; + public static final String LOCALHOST_CRL_RSA_JKS = SSL_DIR + "localhost-crl-rsa.jks"; + public static final String CLIENT_CRL_JKS = SSL_DIR + "user2-crl.jks"; + public static final String CLIENT_CRL_LONG_JKS = SSL_DIR + "user3-crl-long.jks"; public static final boolean TLSV13_AVAILABLE; public static final String ROLE = "testrole"; @@ -110,10 +128,21 @@ } public static void initSsl(Tomcat tomcat) { - initSsl(tomcat, LOCALHOST_RSA_JKS, null, null, null, null); + // By default, use JSSE trust + initSsl(tomcat, false); } - protected static void initSsl(Tomcat tomcat, String keystore, + public static void initSsl(Tomcat tomcat, boolean opensslTrust) { + // By default, use valid JSSE configuration + initSsl(tomcat, LOCALHOST_RSA_JKS, opensslTrust); + } + + public static void initSsl(Tomcat tomcat, String keystore, boolean opensslTrust) { + // TLS material for tests uses default password + initSsl(tomcat, keystore, opensslTrust, null, null, null, null); + } + + protected static void initSsl(Tomcat tomcat, String keystore, boolean opensslTrust, String keystorePass, String keystorePassFile, String keyPass, String keyPassFile) { Connector connector = tomcat.getConnector(); @@ -135,7 +164,11 @@ } sslHostConfig.setSslProtocol("tls"); certificate.setCertificateKeystoreFile(new File(keystore).getAbsolutePath()); - sslHostConfig.setTruststoreFile(new File(CA_JKS).getAbsolutePath()); + if (opensslTrust) { + sslHostConfig.setCaCertificateFile(new File(CA_CERT_PEM).getAbsolutePath()); + } else { + sslHostConfig.setTruststoreFile(new File(CA_JKS).getAbsolutePath()); + } if (keystorePassFile != null) { certificate.setCertificateKeystorePasswordFile(new File(keystorePassFile).getAbsolutePath()); } @@ -150,10 +183,10 @@ } } - protected static KeyManager[] getUser1KeyManagers() throws Exception { + protected static KeyManager[] getUserKeyManagers(String keyStore) throws Exception { KeyManagerFactory kmf = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(getKeyStore(CLIENT_JKS), JKS_PASS.toCharArray()); + kmf.init(getKeyStore(keyStore), JKS_PASS.toCharArray()); KeyManager[] managers = kmf.getKeyManagers(); KeyManager manager; for (int i=0; i < managers.length; i++) { @@ -168,17 +201,61 @@ } protected static TrustManager[] getTrustManagers() throws Exception { - TrustManagerFactory tmf = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(getKeyStore(CA_JKS)); + return getTrustManagers(false); + } + + protected static TrustManager[] getTrustManagers(boolean enableOcsp) throws Exception { + KeyStore trustStore = getKeyStore(CA_JKS); + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + if (enableOcsp) { + Set trustAnchors = getTrustAnchorsFromKeystore(trustStore); + PKIXBuilderParameters pkix = new PKIXBuilderParameters(trustAnchors, new X509CertSelector()); + PKIXRevocationChecker revocationChecker = + (PKIXRevocationChecker) CertPathValidator.getInstance("PKIX").getRevocationChecker(); + revocationChecker.setOptions(EnumSet.of(PKIXRevocationChecker.Option.NO_FALLBACK)); + pkix.addCertPathChecker(revocationChecker); + tmf.init(new CertPathTrustManagerParameters(pkix)); + } else { + tmf.init(trustStore); + } return tmf.getTrustManagers(); } + private static Set getTrustAnchorsFromKeystore(KeyStore keyStore) throws KeyStoreException { + Set trustAnchors = new HashSet<>(); + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + Certificate certificate = keyStore.getCertificate(alias); + if (certificate instanceof X509Certificate) { + trustAnchors.add(new TrustAnchor((X509Certificate) certificate, null)); + } + } + return trustAnchors; + } + public static ClientSSLSocketFactory configureClientSsl() { - return configureClientSsl(false); + return configureClientSsl(false, null, false, CLIENT_JKS); } public static ClientSSLSocketFactory configureClientSsl(boolean forceTls12) { + return configureClientSsl(forceTls12, null, false, CLIENT_JKS); + } + + public static ClientSSLSocketFactory configureClientSsl(String[] ciphers) { + return configureClientSsl(false, ciphers, false, CLIENT_JKS); + } + + public static ClientSSLSocketFactory configureClientSsl(boolean forceTls12, String[] ciphers) { + return configureClientSsl(forceTls12, ciphers, false, CLIENT_JKS); + } + + public static ClientSSLSocketFactory configureClientSsl(boolean enableOcsp, String keyStore) { + return configureClientSsl(false, null, enableOcsp, keyStore); + } + + public static ClientSSLSocketFactory configureClientSsl(boolean forceTls12, String[] ciphers, boolean enableOcsp, + String keyStore) { ClientSSLSocketFactory clientSSLSocketFactory = null; try { SSLContext sc; @@ -187,8 +264,11 @@ } else { sc = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_2); } - sc.init(getUser1KeyManagers(), getTrustManagers(), null); + sc.init(getUserKeyManagers(keyStore), getTrustManagers(enableOcsp), null); clientSSLSocketFactory = new ClientSSLSocketFactory(sc.getSocketFactory()); + if (ciphers != null) { + clientSSLSocketFactory.setCipher(ciphers); + } javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(clientSSLSocketFactory); } catch (Exception e) { e.printStackTrace(); @@ -576,6 +656,7 @@ private final SSLSocketFactory delegate; private String[] ciphers = null; + private String[] protocols = null; public ClientSSLSocketFactory(SSLSocketFactory delegate) { @@ -591,6 +672,15 @@ this.ciphers = ciphers; } + /** + * Forces the use of the specified protocols. + * + * @param protocols Array of standard protocols to use + */ + public void setProtocols(String[] protocols) { + this.protocols = protocols; + } + @Override public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { Socket result = delegate.createSocket(s, host, port, autoClose); @@ -642,6 +732,9 @@ if (ciphers != null) { ((SSLSocket) socket).setEnabledCipherSuites(ciphers); } + if (protocols != null) { + ((SSLSocket) socket).setEnabledProtocols(protocols); + } return socket; } } diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ca-cert.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/ca-cert.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ca-cert.pem 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ca-cert.pem 2026-05-01 18:56:05.000000000 +0000 @@ -1,39 +1,39 @@ -----BEGIN CERTIFICATE----- -MIIGvzCCBKegAwIBAgIUd8TEpsZJz7k3Ddw/oRfCrdlmRlcwDQYJKoZIhvcNAQEL -BQAwgZMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJNQTESMBAGA1UEBxMJV2FrZWZp -ZWxkMScwJQYDVQQKEx5UaGUgQXBhY2hlIFNvZnR3YXJlIEZvdW5kYXRpb24xGjAY -BgNVBAsTEUFwYWNoZSBUb21jYXQgUE1DMR4wHAYDVQQDExVBcGFjaGUgVG9tY2F0 -IFRlc3QgQ0EwHhcNMjUwODE3MDczNDAwWhcNMzUwODE1MDczNDAwWjCBkzELMAkG -A1UEBhMCVVMxCzAJBgNVBAgTAk1BMRIwEAYDVQQHEwlXYWtlZmllbGQxJzAlBgNV -BAoTHlRoZSBBcGFjaGUgU29mdHdhcmUgRm91bmRhdGlvbjEaMBgGA1UECxMRQXBh -Y2hlIFRvbWNhdCBQTUMxHjAcBgNVBAMTFUFwYWNoZSBUb21jYXQgVGVzdCBDQTCC -AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK6iRe7N1DwdOhFVwvTsB3Ed -b1aZoylw9Zv+/JB1y6Fr4lcn1a753t+nGml67GeMOYMJk2jSSKYjudggQMx3dZV6 -fvlqgxmdDv6838ZlBfHNI/Pa/NNnIQRMCqzsOwbA7kIrbLmxYqXnv5ALP837EzsT -A7I2Sa37vEjV5D7PaxQ0tEwrtpuibyeb/BV+0YBciKuJuZOq6SjGOvx2kQODvtUL -L16p7qRyKXoW+euZismqavDMaV1SXDiOoeXUgmcnnPbzW/PNDRTlQt6K4GkMZdw4 -ehofzNjGKUFWk+5ENEE5xfXt7k2i5m0NMV6aQmrmezc8O35aMm+5g1IQssFuc/5L -VXqa34xT0+wXpN2VVlnHEUP2E9ioDPKbAkbNMpsTyO5mu5CzAviJ+3IQuYs+Mix6 -1W1eJ+xQ1rLoJ8Ovmw3qv4ouuElZSqGKLaYfWPHhwuaV/fTxM5zzROV2+8j1x+tv -n3r3nhwQT7t2VpS+dFsEB5U0c0eXl7rx+/CkqC29CG2DWtnbDV+hKQDLI1GRitRk -p7jO1TH0TbD2oibc6RnIxw9MpiQFVqFHorVsKjr25pSvFjg1piU98p0mYZ+Kj8nZ -YZ2our2driIdHJilvnMq9iYf1w2maflXjbKGZfu2MwnNTJbDwrFekT+oRUHbCXXX -A2C4QvwJpnL7e7SJoKptAgMBAAGjggEHMIIBAzAdBgNVHQ4EFgQUAPKYTSEsADxA -m4T03irwJu4yDp8wgdMGA1UdIwSByzCByIAUAPKYTSEsADxAm4T03irwJu4yDp+h -gZmkgZYwgZMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJNQTESMBAGA1UEBxMJV2Fr -ZWZpZWxkMScwJQYDVQQKEx5UaGUgQXBhY2hlIFNvZnR3YXJlIEZvdW5kYXRpb24x -GjAYBgNVBAsTEUFwYWNoZSBUb21jYXQgUE1DMR4wHAYDVQQDExVBcGFjaGUgVG9t -Y2F0IFRlc3QgQ0GCFHfExKbGSc+5Nw3cP6EXwq3ZZkZXMAwGA1UdEwQFMAMBAf8w -DQYJKoZIhvcNAQELBQADggIBAD839dBKpesTMCsiSrx1GXP6hbjXfNtKcskzlce3 -zGYDdIUFYit1gnuftvcl4u2hDCqodCn30LUE6wRQd3E2hiItOzlWufp4Rb6KZNie -xp/A5ifu90qbJgEBlZScLDydcvkts1JTF4WuzQdKGY5MEh1OgNPj1M3cwg92UmGJ -fGU57jnvpaJxS9IoJXIeIpmCr5dM1nk5NMY3Vt2Fg+vssHLS9t7glKQBlSa4+Ts6 -26xWINXQvJOomWWEDrs96pzZ2AHAJ+L/eExofAa6FyCXB1Oxd5M+U8A9DgehHVqi -9r5/FMi3nS+v8UR0/4KpAUPjKAMCyZZbLFza0o0sHYWKehG/NdAUio4jKJKFCh/Z -bQxGtPNW92N0FOQaWv3TUCbfv5bDtX6RC8b9D8u/bvUlYDYnT+c/2Z1RQHwxuoyF -+/514VUZztpr/DBidlUlHBpqsAbRAHsoRVtn1xjTXraDRHewnvdA/AV1f+PuIKJf -4DAnUCn7OKExUAFe87EZizdoaKu5lr0kNUhXdKKVgqGrSolHD67etEviHjAqEJdn -mPO1aIeG3ukFGh3vp8/SFZk9J34qZ5jEl+QldIhAEFWHEX5wmZ74GatAUnOSmiHq -xSQMB2ULGTOC1HMBUQMgelY4Qiwsa92pO1qySsZIL5npmBM9ok+o/jshCpoTCCin -nzrk +MIIGwjCCBKqgAwIBAgIUb23jgHGhJDQJaz0iHUfKwRV5z5kwDQYJKoZIhvcNAQEL +BQAwgZQxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJERTETMBEGA1UEBxMKV2lsbWlu +Z3RvbjEnMCUGA1UEChMeVGhlIEFwYWNoZSBTb2Z0d2FyZSBGb3VuZGF0aW9uMRow +GAYDVQQLExFBcGFjaGUgVG9tY2F0IFBNQzEeMBwGA1UEAxMVQXBhY2hlIFRvbWNh +dCBUZXN0IENBMB4XDTI1MTIwNTE5MTIzOFoXDTM1MTIwMzE5MTIzOFowgZQxCzAJ +BgNVBAYTAlVTMQswCQYDVQQIEwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUG +A1UEChMeVGhlIEFwYWNoZSBTb2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFB +cGFjaGUgVG9tY2F0IFBNQzEeMBwGA1UEAxMVQXBhY2hlIFRvbWNhdCBUZXN0IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqeIoIFXsR468dJCUYnNX +E4DLI9J2jLIz/T1Jiq/tA9XCzA+XFqf3tlZdXLsfQhDLg0lf+wHqQfSvbQ0Gcatn +Iw8BIvA7yq6PDKsO4cjQ2EdrkTn2JD7hJW5uUB9Je95TUUbKuNxOatihwS4DNU2J +aoI2R2iUGasXsawVR+4qj1UJByawlsGHxFCqhVoaUKM/fKXK7LllbyHsRxuZTgab +Spmvj2jcMxyeDN2ZjlBdsrSg+8FpYvnPKzHpdFiA6d4pxU6QJbeDeWPQ6B64yWi1 +eImFvilqZQoQ3RR/1gDjoFuLCdWK7mAvfaSXFzax1YK3D/MiCowTZ8M0iWmGqMg1 +QF6luhPL+5p8Q2/KooE3CZol0IpCUGr3cQs3FC1deL5ySNMYniXQMgxGqVE3x0i1 +Wlf5bItGY0Ha9vc+s4vmnRZxW4O+fXiuG9OXS5XjfWGAjzkJ6wXgFRJKbWDsSPkR +Sw9j9SAsLE6cycY66eMl94QdOl6KRXVyeaLHJbRMBux7WvDXYS/eWKFHjmPYNKq6 +btIB7ELTqoMuwTAWj83bdkQgMac5fsTozKduM+C63sSg3mhmyy6yVgCX1eECqPNk +N4K4oWdhQvKYvCp1n1EySAupZqrxudBLp/LDHRfGh7gfAZgP8hxRaoBqh2u0H8Fh +RwSAML8zEeRH/si3G7LfXzECAwEAAaOCAQgwggEEMB0GA1UdDgQWBBSsh9yLPYrb +iUh+bzscxnCSCRdv+TCB1AYDVR0jBIHMMIHJgBSsh9yLPYrbiUh+bzscxnCSCRdv ++aGBmqSBlzCBlDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkRFMRMwEQYDVQQHEwpX +aWxtaW5ndG9uMScwJQYDVQQKEx5UaGUgQXBhY2hlIFNvZnR3YXJlIEZvdW5kYXRp +b24xGjAYBgNVBAsTEUFwYWNoZSBUb21jYXQgUE1DMR4wHAYDVQQDExVBcGFjaGUg +VG9tY2F0IFRlc3QgQ0GCFG9t44BxoSQ0CWs9Ih1HysEVec+ZMAwGA1UdEwQFMAMB +Af8wDQYJKoZIhvcNAQELBQADggIBAA/DyqPhxRAluY1e1S9L2SkkrcYePjb033Wl +ZmMVdXvf6LZtpjquNz81ThNoX5yfgV6CawDRw7COI4dAU3HOLVtOeKybm11Jq1BH +AWmlZVQIOplgnitoEoZi8brxEpSgeyRGLKHYaW1YxshPRC5tVDKsbyYBtvmTaBtK +dMirc36cEDkSfEe2dq+MkHgzHyyvySqTSlI/MH7CKQYBjGg2D+Gcle/BNrK02wef +Rol9M3R3skDfV/P0NFmrfFi4B/cXt/EFxm1thMsQ6ofd0GUMGgv6fJKN1AVDihx6 +sQvbn712+54ilkcWMa3gW5L/BIxcDS+ZZiO/7T2tZ2d3tcz86gYbFkiQbWelnTmn +FBSHno9PDgxRWZzqauKqlsczAsz5qLf8zF+GDubEcxvOgEf0wQ/vPwtSb2R4egb+ +3Vn+InqDy9/78ug497KV842jxQy2G6wZLND0q81H04t8NYQopbX4DI0hS5BJnKuV +TM3rh4C/hNXs8kUpjusqnnV8nbBpab2R1YzEOefhgtTFdk/dHI7a5+6BKNSK55yn +hDqgemD5p+5A6FLu3M0RDgbTZ1WdrtYDvQZrAMB10mrV0YDoDgnEzGYEc/PQwkYF +gOeqb97mOdjf9YUJli4S4XLkZ5zn/IitEQRbnKen2VE60qIgW/UMYKTiwuSZtRaW +gxBAeoi3 -----END CERTIFICATE----- Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/ca.jks and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ca.jks differ diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/index.db tomcat11-11.0.22/test/org/apache/tomcat/util/net/index.db --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/index.db 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/index.db 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,7 @@ +V 271205191323Z 1000 unknown /C=US/ST=DE/L=Wilmington/O=The Apache Software Foundation/OU=Apache Tomcat PMC/CN=localhost +R 271205191327Z 251205192921Z 1001 unknown /C=US/ST=DE/L=Wilmington/O=The Apache Software Foundation/OU=Apache Tomcat PMC/CN=localhost +V 271205191331Z 1002 unknown /C=US/ST=DE/L=Wilmington/O=The Apache Software Foundation/OU=Apache Tomcat PMC/CN=user1 +R 271205191335Z 251205192924Z 1003 unknown /C=US/ST=DE/L=Wilmington/O=The Apache Software Foundation/OU=Apache Tomcat PMC/CN=user2 +V 271205191540Z 1004 unknown /C=US/ST=DE/L=Wilmington/O=The Apache Software Foundation/OU=Apache Tomcat PMC/CN=localhost +V 271205193355Z 1005 unknown /C=US/ST=DE/L=Wilmington/O=The Apache Software Foundation/OU=Apache Tomcat PMC/CN=OCSP Responder +R 271210104820Z 251210105017Z 1006 unknown /C=US/ST=DE/L=Wilmington/O=The Apache Software Foundation/OU=Apache Tomcat PMC/CN=user3 diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-crl-rsa-cert.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-crl-rsa-cert.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-crl-rsa-cert.pem 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-crl-rsa-cert.pem 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,108 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 4097 (0x1001) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=DE, L=Wilmington, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=Apache Tomcat Test CA + Validity + Not Before: Dec 5 19:13:27 2025 GMT + Not After : Dec 5 19:13:27 2027 GMT + Subject: C=US, ST=DE, L=Wilmington, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:aa:f2:ee:01:dd:21:fd:4b:f6:1e:9f:a1:a6:65: + 42:71:1c:db:cf:d7:47:6d:20:81:63:01:c7:6c:a2: + 5a:2e:17:0d:5d:d9:60:78:e5:d9:2a:9a:c0:2d:1c: + 2d:24:9d:b3:f6:26:a9:93:80:86:ad:0e:b0:06:4e: + 64:dc:11:da:70:a0:eb:47:aa:9d:39:3e:10:1c:b5: + fe:bc:ca:b2:5b:b8:3f:0c:a8:d4:b3:cb:9e:dd:f5: + 19:0e:dc:83:34:54:1d:17:b9:0d:2e:5d:4c:31:d7: + 9b:64:e9:40:89:de:77:75:ad:6e:86:3e:46:15:88: + ed:48:88:bb:a2:77:a5:05:4a:33:33:3e:bb:cb:7b: + f7:00:9b:eb:4e:2b:f5:4a:59:07:ff:18:5a:a1:49: + c2:25:48:21:03:55:64:da:d7:75:0f:e8:55:a8:61: + 39:ca:af:09:64:c2:c1:67:4b:ec:95:df:66:50:2d: + 39:45:d6:4c:8d:07:a8:3d:d1:fc:3c:46:76:92:2f: + 1a:b0:27:f0:0c:e8:19:2b:c6:b4:ad:62:c4:c6:21: + 7c:23:17:c4:13:a4:0e:7d:d3:05:3b:0b:43:ab:43: + 32:88:12:67:91:1b:01:e4:98:90:fc:67:88:d7:8b: + 8e:6b:f0:2a:59:62:d1:dc:24:38:28:8c:b7:e9:1b: + 7b:27 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + CB:A6:D2:18:07:CE:1A:E1:A7:F1:DC:FB:4C:A0:2C:85:AE:A5:40:A2 + X509v3 Authority Key Identifier: + AC:87:DC:8B:3D:8A:DB:89:48:7E:6F:3B:1C:C6:70:92:09:17:6F:F9 + Authority Information Access: + OCSP - URI:http://127.0.0.1:8888 + X509v3 Subject Alternative Name: + DNS:localhost, IP Address:127.0.0.1 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 11:8b:03:aa:50:38:53:88:71:66:01:2f:d5:ab:82:49:ba:4d: + 45:5a:20:b6:f7:6e:1f:ca:77:ce:f7:93:85:ec:4d:da:ba:17: + 71:dc:30:7b:7f:55:35:07:88:a2:4a:47:06:9c:8c:1b:a0:33: + 5f:46:dd:c9:c6:f5:0e:27:48:6e:1f:0b:a0:e8:17:e4:16:3b: + d0:e3:0b:56:4f:5c:5f:b7:ae:fb:da:46:13:0b:fc:e8:3d:72: + 7e:ab:2d:19:4d:6c:09:8b:32:51:9a:7a:b8:49:70:7e:ce:ce: + 78:7f:95:9c:3a:8e:05:cc:29:af:0b:de:42:b8:cd:5e:56:a8: + 2f:78:ea:19:67:ed:a7:59:c0:ce:0a:4c:50:46:67:9a:6e:72: + 6b:fd:ae:00:a6:2f:9a:93:97:39:58:06:b4:05:a3:78:a9:b6: + df:66:22:0d:ae:5b:fd:e8:37:78:25:ac:2e:5a:a0:65:56:3e: + 83:04:bf:98:96:30:e1:11:f9:da:3b:6a:97:c4:77:28:d6:d7: + 46:27:a4:05:67:2e:f5:b8:d4:2e:c7:00:39:49:08:9b:a7:58: + 21:48:4c:fb:8f:dd:99:69:ea:59:bc:41:96:fd:1a:29:1a:fe: + 53:16:6a:2b:ac:92:51:18:ac:85:d9:7d:d5:77:ad:60:63:98: + 47:2c:52:ec:85:ef:ad:ff:b0:58:d7:12:7b:6f:70:2b:93:2a: + 92:f8:99:3c:15:7a:26:d4:79:16:05:93:9a:f4:01:bc:e6:9b: + a0:31:c3:d7:12:23:47:12:81:30:02:06:0c:13:ca:38:4c:63: + 96:2d:17:41:53:06:8b:c5:a3:8c:c3:96:b7:e2:b5:85:49:f7: + cd:46:01:21:ff:4f:d1:65:be:37:1b:84:7c:79:a6:e1:a8:a8: + d7:8c:63:34:35:45:d5:42:10:03:0f:60:fa:52:c5:39:03:26: + b1:41:6c:b9:53:3c:cf:8d:5d:fa:02:ac:0a:7a:04:27:28:df: + 33:b2:01:69:d8:3e:72:38:57:60:d3:a8:47:5f:98:55:8c:55: + 15:36:58:6e:c7:fc:65:a5:f8:c7:7f:1a:8c:de:b7:79:21:50: + 98:76:69:8a:b3:48:b0:1d:b2:91:fd:25:8f:bd:59:17:81:22: + bc:67:32:d5:fb:e6:c3:cd:3c:b3:79:6f:cd:1c:4a:b0:04:13: + 3c:af:80:53:7c:f0:c6:28:f3:37:3e:36:4d:65:ef:38:dc:00: + 5f:88:7e:04:34:75:66:fa:ba:6c:1f:76:b2:55:bb:21:60:fe: + b8:48:f8:ac:cb:6e:16:2f:75:87:44:d0:c3:6d:d5:6a:e1:e6: + b9:32:7e:39:a6:e6:d0:7f +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT +MQswCQYDVQQIEwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUGA1UEChMeVGhl +IEFwYWNoZSBTb2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFBcGFjaGUgVG9t +Y2F0IFBNQzEeMBwGA1UEAxMVQXBhY2hlIFRvbWNhdCBUZXN0IENBMB4XDTI1MTIw +NTE5MTMyN1oXDTI3MTIwNTE5MTMyN1owgYgxCzAJBgNVBAYTAlVTMQswCQYDVQQI +EwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUGA1UEChMeVGhlIEFwYWNoZSBT +b2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFBcGFjaGUgVG9tY2F0IFBNQzES +MBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAqvLuAd0h/Uv2Hp+hpmVCcRzbz9dHbSCBYwHHbKJaLhcNXdlgeOXZKprALRwt +JJ2z9iapk4CGrQ6wBk5k3BHacKDrR6qdOT4QHLX+vMqyW7g/DKjUs8ue3fUZDtyD +NFQdF7kNLl1MMdebZOlAid53da1uhj5GFYjtSIi7onelBUozMz67y3v3AJvrTiv1 +SlkH/xhaoUnCJUghA1Vk2td1D+hVqGE5yq8JZMLBZ0vsld9mUC05RdZMjQeoPdH8 +PEZ2ki8asCfwDOgZK8a0rWLExiF8IxfEE6QOfdMFOwtDq0MyiBJnkRsB5JiQ/GeI +14uOa/AqWWLR3CQ4KIy36Rt7JwIDAQABo4HLMIHIMAkGA1UdEwQCMAAwLAYJYIZI +AYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQW +BBTLptIYB84a4afx3PtMoCyFrqVAojAfBgNVHSMEGDAWgBSsh9yLPYrbiUh+bzsc +xnCSCRdv+TAxBggrBgEFBQcBAQQlMCMwIQYIKwYBBQUHMAGGFWh0dHA6Ly8xMjcu +MC4wLjE6ODg4ODAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcN +AQELBQADggIBABGLA6pQOFOIcWYBL9Wrgkm6TUVaILb3bh/Kd873k4XsTdq6F3Hc +MHt/VTUHiKJKRwacjBugM19G3cnG9Q4nSG4fC6DoF+QWO9DjC1ZPXF+3rvvaRhML +/Og9cn6rLRlNbAmLMlGaerhJcH7Oznh/lZw6jgXMKa8L3kK4zV5WqC946hln7adZ +wM4KTFBGZ5pucmv9rgCmL5qTlzlYBrQFo3iptt9mIg2uW/3oN3glrC5aoGVWPoME +v5iWMOER+do7apfEdyjW10YnpAVnLvW41C7HADlJCJunWCFITPuP3Zlp6lm8QZb9 +Gika/lMWaiusklEYrIXZfdV3rWBjmEcsUuyF763/sFjXEntvcCuTKpL4mTwVeibU +eRYFk5r0Abzmm6Axw9cSI0cSgTACBgwTyjhMY5YtF0FTBovFo4zDlrfitYVJ981G +ASH/T9FlvjcbhHx5puGoqNeMYzQ1RdVCEAMPYPpSxTkDJrFBbLlTPM+NXfoCrAp6 +BCco3zOyAWnYPnI4V2DTqEdfmFWMVRU2WG7H/GWl+Md/Gozet3khUJh2aYqzSLAd +spH9JY+9WReBIrxnMtX75sPNPLN5b80cSrAEEzyvgFN88MYo8zc+Nk1l7zjcAF+I +fgQ0dWb6umwfdrJVuyFg/rhI+KzLbhYvdYdE0MNt1Wrh5rkyfjmm5tB/ +-----END CERTIFICATE----- diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-crl-rsa-key.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-crl-rsa-key.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-crl-rsa-key.pem 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-crl-rsa-key.pem 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCq8u4B3SH9S/Ye +n6GmZUJxHNvP10dtIIFjAcdsolouFw1d2WB45dkqmsAtHC0knbP2JqmTgIatDrAG +TmTcEdpwoOtHqp05PhActf68yrJbuD8MqNSzy57d9RkO3IM0VB0XuQ0uXUwx15tk +6UCJ3nd1rW6GPkYViO1IiLuid6UFSjMzPrvLe/cAm+tOK/VKWQf/GFqhScIlSCED +VWTa13UP6FWoYTnKrwlkwsFnS+yV32ZQLTlF1kyNB6g90fw8RnaSLxqwJ/AM6Bkr +xrStYsTGIXwjF8QTpA590wU7C0OrQzKIEmeRGwHkmJD8Z4jXi45r8CpZYtHcJDgo +jLfpG3snAgMBAAECggEAGwmoPlA454SjrU1HNneujhBn7dQZXne1LzEhVxvIkqCf +wxe2C/kio3vtaFUcCL4OsCCGUqeR5X48tgqhJjOGrqis/d1M17hquPfKDhcrJZmb +E0gCuFs8ydNRvsl+0QB6x31DyfEEs59r2waPaB7xGwIuyHnCAPbyvLWvo68zwQWs +q3YF8Z03wPoSVxx9IrIsi01vaZGYREXmsNEX5krvd+Ku2JsQJ9NreduMg/gtegbB +snN9MpjbydRWnwql4YzOCPaaGlQltrUFixKw6InZJRkKaSri49gEseaiSqwx31mY +RvnWtyZfq1qRkcQm+NC9WO3cyCH38Um1egQmVpmguQKBgQDUlfAuAotMDN9aFnQ5 +DUviMyVy+ea1SDvJ3TnoaplZ4vp9pmwX5B2RPz8MzHKVLhll9h6GjAIXZuLT2Fed +OyGUhM0Cd//+5esp0dHMKQFdzVMrhn8eGPPPBReEZtILbi3ulRJI6nsagAmKnLLN +bik5fbcsc1KhtIUzGFbrw9ThWQKBgQDN3DTFrxP50XLc1hfs3OjP3OqZ7mqMyVXq +JFJ4JNiFCFk/Jhqz83IuxrzoWGVmtnDjj9sdN/TsClG337xD9L3OgpS1yjjbzhi5 +wYG4cCjNxpqis097LFHqZDBBSZ9d3SWsdENqSWPbWOVLZQKrwR239m3mjCipSewI +JRnX+1cwfwKBgQCrySoYFAg6gWBvrRtoCv2aIZiOi7DKJz/hRPsJfDLFtyySIszQ +lY7rE3/AuOmS8XimszdBpJwACy0a4YUTUng1Swdbilr8wRDCb4Ioh65J/aTK1Fme +ma1TShsR7ACqKfPGCwKGl6y44mRTdYLrjKyVf6horBxG/dhxTKbYyBwbqQKBgQDE +vQPuPXFg2ivlI/L/muwg46eN704DONOUDpyGV+hZ022/rUHt4uaoD6UwhHJ8ZDWl +NbGZzgWTpBUPHpMFexv/BcrrpdULNH9q36WCyCYm6vyUK7v1Ipky4gdADgVxpk0/ +8GkRZgw58E5K7MFNtiUZ0DieEis2BwC9k/+L65gbLwKBgG87Ag/C0un6LYMHI8gT +DyqrsjARdlwVeyL864p3GHQbfGeQ2VTxSriEPHwSc2H1FtRsFo06P98DBjVW1xDD +MPY8PD9MZ347rnNi1lcG42moxY6ywYesRwKaDPC98PgvaXzmm81wSyjOerwDQubi +UQ6bYhVLe3BdVfFI8N8kSRzu +-----END PRIVATE KEY----- Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-crl-rsa.jks and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-crl-rsa.jks differ diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-ec-cert.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-ec-cert.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-ec-cert.pem 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-ec-cert.pem 2026-05-01 18:56:05.000000000 +0000 @@ -1,22 +1,22 @@ Certificate: Data: Version: 3 (0x2) - Serial Number: 4113 (0x1011) + Serial Number: 4100 (0x1004) Signature Algorithm: sha256WithRSAEncryption - Issuer: C=US, ST=MA, L=Wakefield, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=Apache Tomcat Test CA + Issuer: C=US, ST=DE, L=Wilmington, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=Apache Tomcat Test CA Validity - Not Before: Feb 16 17:59:33 2025 GMT - Not After : Feb 16 17:59:33 2027 GMT + Not Before: Dec 5 19:15:40 2025 GMT + Not After : Dec 5 19:15:40 2027 GMT Subject: C=US, ST=DE, L=Wilmington, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=localhost Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (256 bit) pub: - 04:0a:0a:c9:36:e4:18:27:f0:ef:ac:f3:8a:8b:a8: - 9c:15:f5:71:95:94:10:bc:74:4a:c5:1e:e7:c5:fd: - b0:b3:35:c0:5e:fe:11:2b:ae:ba:a7:a4:27:09:bd: - 37:c5:de:bc:e7:6d:f7:de:dd:ab:ed:9d:cf:02:59: - 1e:ba:b2:6a:b0 + 04:4a:67:a8:52:9a:a4:a8:54:e0:0a:26:01:7b:c8: + 20:fc:99:99:09:b9:ce:a4:11:bd:99:0f:9d:73:b3: + 82:ef:9f:a9:73:31:bf:e5:f2:10:b2:b4:91:82:e1: + d5:9e:91:4b:e4:f2:ce:87:e1:a6:ee:6b:b5:68:39: + c0:c0:9a:e7:5d ASN1 OID: prime256v1 NIST CURVE: P-256 X509v3 extensions: @@ -25,62 +25,66 @@ Netscape Comment: OpenSSL Generated Certificate X509v3 Subject Key Identifier: - B2:76:F0:C0:C9:97:89:B7:00:A2:9C:AA:5B:49:D8:62:C7:2B:81:AD + AD:35:5C:21:09:CA:97:51:8F:CC:F2:33:B0:86:80:DF:73:4F:FD:73 X509v3 Authority Key Identifier: - 00:F2:98:4D:21:2C:00:3C:40:9B:84:F4:DE:2A:F0:26:EE:32:0E:9F + AC:87:DC:8B:3D:8A:DB:89:48:7E:6F:3B:1C:C6:70:92:09:17:6F:F9 + Authority Information Access: + OCSP - URI:http://127.0.0.1:8888 Signature Algorithm: sha256WithRSAEncryption Signature Value: - 55:68:78:de:91:87:35:36:63:ca:bf:b1:bb:17:89:ef:01:7d: - be:40:d6:e3:cb:45:ae:3b:7f:97:0f:5f:f4:7c:eb:2b:5d:0f: - 2f:23:17:2d:2d:25:76:b1:1b:b8:59:24:4c:8b:0a:c7:cc:07: - 37:f5:55:b2:20:b4:51:c9:ee:3b:0b:2f:b0:b2:d0:7f:76:30: - 0e:46:65:63:45:67:cd:bd:42:ba:55:72:16:cd:6d:37:07:9b: - 9c:9b:ee:cc:63:e1:32:15:c2:3c:76:05:a6:25:51:c2:a7:99: - dd:03:36:d4:d4:75:47:50:a9:bc:e6:d6:a7:95:49:6a:54:12: - 26:27:94:a2:67:48:8a:42:38:fd:e4:58:8b:3c:c1:67:a2:dc: - ba:4e:72:72:f7:60:d8:ce:18:c7:56:2d:61:bf:b5:5b:f7:d8: - b9:d5:f0:3e:62:20:2f:f1:b2:6d:0e:b0:e2:9a:60:a0:3f:8c: - 7c:4a:ec:4f:fa:90:29:e5:b5:01:6e:20:f5:e5:d8:cd:ed:29: - 59:e3:93:29:08:a9:a3:6b:1f:9f:6b:91:48:78:ad:03:1f:64: - 0d:36:c7:c4:d3:40:a0:fc:cf:72:09:f5:8e:71:7b:cb:6c:fd: - 1a:d3:02:5e:37:96:d0:41:ef:b2:2b:a9:28:18:fc:de:68:7e: - 53:80:c4:8c:2b:90:42:de:61:ff:56:88:ea:f7:80:42:3f:cd: - 13:46:9d:6d:70:b3:83:b9:69:35:94:a0:56:d9:3e:39:fc:b1: - bb:f5:6d:d4:3c:22:80:86:b3:32:2a:eb:2a:c9:5d:a8:4b:b9: - 48:70:68:d1:2c:2b:b7:bd:9c:2c:79:70:b4:e7:69:ba:32:bc: - 97:ff:07:2a:49:78:d7:a1:6f:d0:24:96:73:1e:8c:3f:20:a2: - 3c:fb:8f:11:e0:88:d3:86:06:6c:02:b8:a5:25:f2:10:1a:b3: - bf:9a:ec:57:81:d4:d4:91:e1:6d:70:21:0b:6f:c0:a2:5d:49: - 29:9c:fa:bd:78:7f:a7:3b:45:65:96:79:68:33:71:f8:a9:ea: - 5c:69:69:b4:bf:8a:45:66:49:0d:e1:13:65:8b:27:72:cc:e6: - 99:42:80:6c:b8:cc:6b:d8:f0:d2:71:bb:a6:f1:fa:5a:70:8a: - 7b:df:65:4c:2f:bf:18:2c:b9:de:db:7f:e1:73:96:66:5d:73: - aa:ef:f3:87:9e:e6:79:14:50:99:ae:bd:38:04:f2:78:54:db: - ef:03:8a:57:9c:99:a8:57:87:c7:e7:84:19:d1:d9:4d:cf:a2: - 8d:66:29:1a:78:a8:28:85:3e:0b:44:9e:60:d6:f0:14:92:11: - 80:48:b5:5c:1b:81:8f:ba + 9a:74:81:f5:9d:87:bc:47:ea:33:a9:ef:d4:6b:6a:de:77:4e: + fc:41:c4:44:e7:ca:02:63:8b:a6:4f:b3:ed:14:44:8d:3f:29: + 3b:ee:2f:30:2b:31:bc:2f:88:64:ce:e7:4e:77:d7:d2:94:c6: + 31:98:ea:b8:c3:0c:23:e9:cd:00:99:2f:bd:41:bb:49:5d:ae: + f0:65:c7:b8:1b:c3:07:58:ea:ed:0e:ff:22:be:7b:0a:77:15: + 62:49:e0:7c:39:6a:9d:02:7d:b3:6d:94:eb:14:e6:75:1f:8a: + 40:cd:88:13:bd:42:03:49:18:ae:11:43:d5:f1:98:c5:a0:87: + 18:33:ae:84:c6:65:8f:61:89:be:54:d9:52:02:80:59:30:42: + df:02:d0:e5:44:9c:f5:36:28:3b:26:5c:a3:28:89:d2:b9:a3: + c7:2a:99:d3:86:b5:fd:13:6d:d7:f9:5d:7e:88:35:af:19:9e: + 4e:9c:8b:48:92:15:62:47:c7:c4:1e:25:4e:d5:8b:96:1f:7e: + cc:55:10:5f:82:1e:3f:b7:c7:ba:00:38:59:55:53:59:3c:f9: + 0b:2a:ab:d4:bf:0e:84:2b:71:61:be:14:4d:12:c1:8d:ab:8b: + 14:d7:af:b2:b4:6a:7e:b6:23:64:a2:7b:11:6b:ec:6b:1a:64: + ea:49:f0:a7:9f:d0:c9:47:71:fa:01:8a:3b:be:b9:6d:4a:d9: + 58:25:1a:a8:f6:88:35:e5:d4:98:53:48:62:f9:24:db:87:21: + bb:67:5f:b7:d1:60:0f:34:f5:63:f9:52:fd:f1:33:cb:5f:79: + 16:c8:e5:e4:c1:79:0c:d9:76:b4:44:56:ff:e7:86:a5:9f:dd: + 72:c6:15:4f:ed:6e:2d:1a:54:61:b3:5c:c7:26:10:98:9f:7e: + 77:0b:80:4c:a9:d7:bd:28:4c:3c:3b:05:67:e1:af:6d:1b:26: + d5:24:96:5f:01:69:84:37:1e:f2:30:3e:9d:e9:8a:fd:0f:28: + 56:87:d1:28:8f:32:5d:c9:96:f0:ec:14:b1:34:b9:0d:63:16: + 1b:26:e5:76:33:12:fe:52:f5:49:17:2d:14:06:fc:3f:4e:81: + b2:02:7d:e7:16:ae:62:d2:0f:1c:df:a1:50:a3:ff:3e:69:d5: + 7f:e7:c4:e7:6a:b8:93:b2:d8:99:9b:72:30:e9:e0:62:d6:c3: + c1:39:a0:1b:30:37:63:6e:b0:0f:47:20:71:8a:20:56:2f:2d: + a6:54:35:ee:72:38:fc:e2:bb:67:60:4f:f7:b8:79:cb:57:6e: + f3:7c:b3:55:03:01:41:29:48:ca:f6:a9:3a:4a:6a:26:cb:f7: + 21:54:fa:70:31:3d:95:d0 -----BEGIN CERTIFICATE----- -MIIESTCCAjGgAwIBAgICEBEwDQYJKoZIhvcNAQELBQAwgZMxCzAJBgNVBAYTAlVT -MQswCQYDVQQIEwJNQTESMBAGA1UEBxMJV2FrZWZpZWxkMScwJQYDVQQKEx5UaGUg -QXBhY2hlIFNvZnR3YXJlIEZvdW5kYXRpb24xGjAYBgNVBAsTEUFwYWNoZSBUb21j -YXQgUE1DMR4wHAYDVQQDExVBcGFjaGUgVG9tY2F0IFRlc3QgQ0EwHhcNMjUwMjE2 -MTc1OTMzWhcNMjcwMjE2MTc1OTMzWjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgM -AkRFMRMwEQYDVQQHDApXaWxtaW5ndG9uMScwJQYDVQQKDB5UaGUgQXBhY2hlIFNv -ZnR3YXJlIEZvdW5kYXRpb24xGjAYBgNVBAsMEUFwYWNoZSBUb21jYXQgUE1DMRIw -EAYDVQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQKCsk2 -5Bgn8O+s84qLqJwV9XGVlBC8dErFHufF/bCzNcBe/hErrrqnpCcJvTfF3rznbffe -3avtnc8CWR66smqwo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVu -U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUsnbwwMmXibcAopyq -W0nYYscrga0wHwYDVR0jBBgwFoAUAPKYTSEsADxAm4T03irwJu4yDp8wDQYJKoZI -hvcNAQELBQADggIBAFVoeN6RhzU2Y8q/sbsXie8Bfb5A1uPLRa47f5cPX/R86ytd -Dy8jFy0tJXaxG7hZJEyLCsfMBzf1VbIgtFHJ7jsLL7Cy0H92MA5GZWNFZ829QrpV -chbNbTcHm5yb7sxj4TIVwjx2BaYlUcKnmd0DNtTUdUdQqbzm1qeVSWpUEiYnlKJn -SIpCOP3kWIs8wWei3LpOcnL3YNjOGMdWLWG/tVv32LnV8D5iIC/xsm0OsOKaYKA/ -jHxK7E/6kCnltQFuIPXl2M3tKVnjkykIqaNrH59rkUh4rQMfZA02x8TTQKD8z3IJ -9Y5xe8ts/RrTAl43ltBB77IrqSgY/N5oflOAxIwrkELeYf9WiOr3gEI/zRNGnW1w -s4O5aTWUoFbZPjn8sbv1bdQ8IoCGszIq6yrJXahLuUhwaNEsK7e9nCx5cLTnaboy -vJf/BypJeNehb9AklnMejD8gojz7jxHgiNOGBmwCuKUl8hAas7+a7FeB1NSR4W1w -IQtvwKJdSSmc+r14f6c7RWWWeWgzcfip6lxpabS/ikVmSQ3hE2WLJ3LM5plCgGy4 -zGvY8NJxu6bx+lpwinvfZUwvvxgsud7bf+FzlmZdc6rv84ee5nkUUJmuvTgE8nhU -2+8DilecmahXh8fnhBnR2U3Poo1mKRp4qCiFPgtEnmDW8BSSEYBItVwbgY+6 +MIIEfzCCAmegAwIBAgICEAQwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT +MQswCQYDVQQIEwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUGA1UEChMeVGhl +IEFwYWNoZSBTb2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFBcGFjaGUgVG9t +Y2F0IFBNQzEeMBwGA1UEAxMVQXBhY2hlIFRvbWNhdCBUZXN0IENBMB4XDTI1MTIw +NTE5MTU0MFoXDTI3MTIwNTE5MTU0MFowgYgxCzAJBgNVBAYTAlVTMQswCQYDVQQI +DAJERTETMBEGA1UEBwwKV2lsbWluZ3RvbjEnMCUGA1UECgweVGhlIEFwYWNoZSBT +b2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLDBFBcGFjaGUgVG9tY2F0IFBNQzES +MBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESmeo +UpqkqFTgCiYBe8gg/JmZCbnOpBG9mQ+dc7OC75+pczG/5fIQsrSRguHVnpFL5PLO +h+Gm7mu1aDnAwJrnXaOBrzCBrDAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1P +cGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUrTVcIQnKl1GP +zPIzsIaA33NP/XMwHwYDVR0jBBgwFoAUrIfciz2K24lIfm87HMZwkgkXb/kwMQYI +KwYBBQUHAQEEJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjg4ODgw +DQYJKoZIhvcNAQELBQADggIBAJp0gfWdh7xH6jOp79Rrat53TvxBxETnygJji6ZP +s+0URI0/KTvuLzArMbwviGTO505319KUxjGY6rjDDCPpzQCZL71Bu0ldrvBlx7gb +wwdY6u0O/yK+ewp3FWJJ4Hw5ap0CfbNtlOsU5nUfikDNiBO9QgNJGK4RQ9XxmMWg +hxgzroTGZY9hib5U2VICgFkwQt8C0OVEnPU2KDsmXKMoidK5o8cqmdOGtf0Tbdf5 +XX6INa8Znk6ci0iSFWJHx8QeJU7Vi5YffsxVEF+CHj+3x7oAOFlVU1k8+Qsqq9S/ +DoQrcWG+FE0SwY2rixTXr7K0an62I2SiexFr7GsaZOpJ8Kef0MlHcfoBiju+uW1K +2VglGqj2iDXl1JhTSGL5JNuHIbtnX7fRYA809WP5Uv3xM8tfeRbI5eTBeQzZdrRE +Vv/nhqWf3XLGFU/tbi0aVGGzXMcmEJiffncLgEyp170oTDw7BWfhr20bJtUkll8B +aYQ3HvIwPp3piv0PKFaH0SiPMl3JlvDsFLE0uQ1jFhsm5XYzEv5S9UkXLRQG/D9O +gbICfecWrmLSDxzfoVCj/z5p1X/nxOdquJOy2JmbcjDp4GLWw8E5oBswN2NusA9H +IHGKIFYvLaZUNe5yOPziu2dgT/e4ectXbvN8s1UDAUEpSMr2qTpKaibL9yFU+nAx +PZXQ -----END CERTIFICATE----- diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-ec-key.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-ec-key.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-ec-key.pem 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-ec-key.pem 2026-05-01 18:56:05.000000000 +0000 @@ -1,5 +1,8 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgIGiUWTenuT85M7WD -zpFQvWMq5xsQgZoNqmgg1BfGuyahRANCAAQKCsk25Bgn8O+s84qLqJwV9XGVlBC8 -dErFHufF/bCzNcBe/hErrrqnpCcJvTfF3rznbffe3avtnc8CWR66smqw ------END PRIVATE KEY----- +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIF9PqIbcIMlMOKkaAID2L1ULbJApzzOj3WlBYuyZQqaaoAoGCCqGSM49 +AwEHoUQDQgAESmeoUpqkqFTgCiYBe8gg/JmZCbnOpBG9mQ+dc7OC75+pczG/5fIQ +srSRguHVnpFL5PLOh+Gm7mu1aDnAwJrnXQ== +-----END EC PRIVATE KEY----- Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-ec.jks and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-ec.jks differ diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-rsa-cert.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-rsa-cert.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-rsa-cert.pem 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-rsa-cert.pem 2026-05-01 18:56:05.000000000 +0000 @@ -1,35 +1,35 @@ Certificate: Data: Version: 3 (0x2) - Serial Number: 4112 (0x1010) + Serial Number: 4096 (0x1000) Signature Algorithm: sha256WithRSAEncryption - Issuer: C=US, ST=MA, L=Wakefield, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=Apache Tomcat Test CA + Issuer: C=US, ST=DE, L=Wilmington, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=Apache Tomcat Test CA Validity - Not Before: Feb 16 17:53:36 2025 GMT - Not After : Feb 16 17:53:36 2027 GMT + Not Before: Dec 5 19:13:23 2025 GMT + Not After : Dec 5 19:13:23 2027 GMT Subject: C=US, ST=DE, L=Wilmington, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=localhost Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: - 00:c0:40:4c:7d:58:4b:93:4a:0c:4d:ba:59:cc:50: - a1:99:c1:ce:e3:3e:c8:a2:41:b0:e8:24:92:92:d4: - f6:c1:31:d9:d6:41:c0:fa:de:d0:2e:59:09:75:a9: - c3:3b:d4:56:d3:4f:79:a3:30:df:71:15:cd:d8:a7: - 03:d6:be:fb:49:b0:cc:69:75:c4:b9:fb:a2:de:2e: - da:30:db:72:27:1c:f9:96:e1:9f:bd:13:39:8f:da: - a3:12:95:c7:a1:67:3e:4c:14:8c:98:03:6a:86:18: - 8b:3a:d5:4e:09:77:7d:c1:be:6a:53:75:cc:c8:0f: - 35:fc:4d:fd:40:63:a3:e4:10:0b:77:d9:b3:82:1b: - 30:4c:35:02:9e:41:dc:8f:b5:70:a6:d7:be:75:1e: - 57:a7:26:14:33:5a:e7:17:68:98:27:10:a6:9c:0c: - 17:81:6b:d1:15:36:44:01:95:f8:91:38:9c:6d:d0: - 97:45:0d:02:3c:c3:18:20:8d:a6:2a:e7:e0:df:8d: - c1:81:81:71:bb:89:b2:b6:02:87:51:2c:7a:ca:9f: - c9:ec:ab:e0:01:26:0c:dc:38:bd:17:be:2d:fe:48: - 4c:7f:9c:42:1a:38:a0:39:60:5c:38:65:54:d4:71: - ab:d3:a6:d0:c9:a2:a3:3c:0f:59:09:8d:8b:57:dc: - 2b:05 + 00:b1:da:2e:50:5c:5b:22:db:84:61:5e:e4:9d:e1: + a4:24:6c:92:d3:96:5a:77:52:c8:f7:cf:16:b5:60: + 80:a7:15:0e:12:75:1a:bc:3f:0b:54:38:96:f6:2f: + 0a:38:6c:21:68:da:13:49:a2:7b:7c:80:c7:aa:69: + fe:e3:8c:b1:ce:73:e6:5b:80:e2:36:89:49:5b:03: + ce:76:47:bf:35:20:ee:c6:78:df:62:aa:d1:d7:bf: + 91:b0:fc:aa:fd:7f:59:3f:8a:51:9f:7d:59:0f:e1: + d6:dc:7b:16:80:62:3f:6c:c2:2c:1b:a5:05:1e:40: + 90:60:20:e1:c3:6d:3f:ff:1c:73:84:92:86:59:03: + e5:4a:f6:d6:dc:e6:6e:68:1f:05:5f:f7:21:a9:fc: + 5e:e6:fc:8b:c7:36:34:18:fc:d5:70:21:54:5a:f1: + 10:c9:24:c8:08:92:2c:91:13:54:b1:d9:59:70:80: + 39:9f:8c:90:ad:c9:d2:b8:bd:a3:28:20:3f:67:35: + a2:d9:20:e2:3f:2e:be:3d:88:d2:3b:7d:3b:47:44: + 79:53:e2:52:98:72:13:ae:a2:9d:fe:68:bd:36:8c: + 2e:43:b1:3b:cd:50:35:3d:b5:f7:2f:43:ea:1e:c9: + d1:10:ac:e8:24:c1:8c:ee:e6:d0:c2:06:de:14:2d: + 1a:89 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: @@ -37,69 +37,72 @@ Netscape Comment: OpenSSL Generated Certificate X509v3 Subject Key Identifier: - BE:46:EA:26:EA:2A:17:93:67:DD:17:0B:57:8D:ED:A0:30:14:00:B2 + 70:34:99:AD:29:E3:27:9D:BD:97:72:50:20:98:D2:97:B3:6C:AD:A1 X509v3 Authority Key Identifier: - 00:F2:98:4D:21:2C:00:3C:40:9B:84:F4:DE:2A:F0:26:EE:32:0E:9F + AC:87:DC:8B:3D:8A:DB:89:48:7E:6F:3B:1C:C6:70:92:09:17:6F:F9 + Authority Information Access: + OCSP - URI:http://127.0.0.1:8888 X509v3 Subject Alternative Name: DNS:localhost, IP Address:127.0.0.1 Signature Algorithm: sha256WithRSAEncryption Signature Value: - 90:9b:b3:66:a4:42:07:c2:68:05:aa:f3:a2:c9:66:35:45:42: - b4:be:40:c7:90:34:71:b4:cf:79:fd:74:f7:4b:46:3f:f3:da: - 81:3b:55:c3:62:7f:79:12:12:7f:b2:8d:72:55:d8:85:97:cf: - 54:44:0e:9f:22:02:57:98:e9:02:a8:c7:6d:45:3e:b7:f6:7c: - 56:58:a1:1b:0b:fa:a7:4c:57:66:92:3a:92:bd:f4:27:c6:5c: - af:06:3b:d8:be:e9:b7:9a:d3:85:ae:9f:da:5f:59:05:bf:12: - c9:13:49:75:25:1e:76:2d:c9:b7:c1:55:76:25:30:db:48:f4: - c9:c4:cb:a4:dc:b6:df:13:1c:c1:44:2b:67:14:15:da:53:56: - 18:e8:86:5a:75:89:96:a5:88:72:47:36:b5:0b:9c:51:13:50: - c4:93:1c:a1:2b:d0:d8:0c:c7:f6:c5:30:88:93:6c:02:ff:6e: - fe:92:ce:60:4b:17:27:e7:67:9a:d0:27:ae:4e:94:86:2a:ad: - 99:3b:2e:6f:99:a5:d0:a1:a8:e8:3f:d1:c2:b6:1c:1a:f8:dc: - 96:49:3b:79:be:c0:f8:3b:6a:93:d9:08:1e:3a:a9:36:0b:a1: - 64:df:eb:fa:14:34:9b:73:25:49:29:25:e0:39:aa:49:0f:2d: - 4b:ec:40:19:f7:44:00:47:7b:41:72:e8:5f:06:31:31:74:ca: - 7b:98:e7:63:f8:19:ac:7e:5d:1f:5a:b4:22:d8:73:88:27:70: - f4:18:5d:10:a9:52:6c:e3:e1:35:b0:4e:e3:91:79:63:49:86: - 86:19:98:c4:9a:46:a8:bd:dd:5f:6a:8e:14:75:9e:e1:35:6b: - ad:ab:83:8c:31:b6:cb:c2:cc:a5:86:4f:d9:e7:9a:cb:d6:a6: - f5:ee:a9:8e:85:a1:85:54:3a:f6:f3:0e:b9:9f:e2:bc:5d:e5: - eb:42:75:f1:3f:e1:a6:50:09:cc:a6:d3:c5:34:40:55:84:66: - 61:60:99:f0:ea:f7:56:0b:fe:c7:b2:d2:ed:68:dc:0b:35:2f: - 79:bf:f6:0a:af:2f:eb:25:8a:f2:9a:c7:72:00:04:ad:99:21: - 08:14:b6:e4:12:f8:d0:c4:a3:55:c8:c7:ec:4f:e4:fb:4a:3e: - 14:e4:56:cf:f8:a5:e1:b2:b2:50:8b:83:ce:fc:13:68:45:55: - f6:1c:bc:e5:fe:f2:f2:51:0d:b7:9e:49:f2:19:3e:10:59:a3: - dd:e0:72:ab:75:25:a0:0e:3c:b1:b9:17:ad:7e:46:9c:37:f1: - 2a:19:be:91:54:2d:ab:3e:13:54:4c:aa:d6:05:56:b9:f6:3e: - 19:61:cb:b3:c8:95:25:ad + 77:af:4a:b3:09:d8:9b:43:5a:73:78:10:ba:31:ad:a5:06:50: + 49:5d:14:f7:7b:1c:38:f0:3a:9b:67:b3:cb:71:52:7a:df:67: + fa:e1:c1:8b:18:dc:4e:bf:21:6d:58:b1:5f:df:2d:f1:ee:62: + a1:b6:78:dd:e2:c5:f2:4f:0b:9d:3d:cc:88:c7:26:8d:f0:41: + 02:ad:bd:e0:29:21:f9:0c:0a:07:cf:38:a4:f1:db:87:f7:e5: + ed:55:59:b5:34:7d:b4:39:70:ca:81:7a:bb:20:fa:2e:85:47: + 72:9c:35:f8:f2:e1:57:f9:9e:5c:9c:76:92:50:6a:c9:52:e9: + a0:d3:4d:41:03:f7:4f:bb:fc:5c:b8:aa:2c:7d:10:ff:3f:6c: + 82:e0:60:b8:d5:58:66:de:e0:db:30:b7:bf:3c:09:b9:3c:5c: + 36:cc:c6:d3:aa:58:9f:90:fe:e7:eb:5b:62:e2:66:a5:88:ec: + dd:2b:64:d1:25:08:e6:0a:49:91:5f:ac:1f:35:8d:2c:a2:71: + de:01:18:b0:0e:13:3d:cd:83:92:67:62:63:8a:63:bb:eb:8d: + f9:b7:95:b0:b6:a8:9c:14:8c:0b:00:34:8b:65:af:b7:6c:b9: + 1b:94:e2:54:3f:ee:87:0f:81:a9:3f:61:10:5f:ee:4d:42:b5: + 67:77:30:2e:04:4f:ad:5d:4e:de:e3:20:b3:8f:ca:46:98:7d: + 97:91:1d:4c:04:d9:bb:ae:02:3d:6f:55:dd:39:64:f3:53:86: + b6:4a:6a:eb:4a:7a:46:4e:0f:3b:44:8e:c2:46:c6:4b:07:1e: + b1:4e:8a:1c:fd:6a:06:55:2e:88:c6:85:8c:9b:c2:ea:90:e1: + 0b:d9:b1:af:2d:80:7f:b6:a2:c7:7a:63:7d:0a:cd:60:f6:46: + ea:40:3d:7d:4e:89:3e:0b:5d:22:8f:b7:3a:3a:38:18:26:a3: + 76:e9:cf:18:bc:c1:3c:8b:06:74:b6:f2:a8:a9:b4:cf:e1:96: + 54:bd:87:21:1d:20:b6:25:d1:d9:c6:fa:94:25:c6:6e:2b:1e: + a1:51:e3:31:ff:43:77:bd:e0:4c:6a:c2:24:97:43:99:f6:cb: + dd:3f:63:31:88:af:07:ef:e0:2b:3b:8e:3b:80:26:27:a6:c3: + 58:95:53:fa:39:14:59:42:3d:3a:e5:02:ff:0d:5b:e0:63:ad: + 93:72:8e:1d:55:c2:f9:49:7d:5c:cb:35:d2:11:b4:86:2d:26: + 61:74:1e:4d:e6:c7:97:e7:99:8b:fc:91:41:c8:9f:b8:ec:3e: + a3:f9:18:b5:2a:2e:1d:ee:2d:b6:cd:8f:14:be:b8:1c:6d:fd: + c3:59:61:66:87:e8:31:82 -----BEGIN CERTIFICATE----- -MIIFMjCCAxqgAwIBAgICEBAwDQYJKoZIhvcNAQELBQAwgZMxCzAJBgNVBAYTAlVT -MQswCQYDVQQIEwJNQTESMBAGA1UEBxMJV2FrZWZpZWxkMScwJQYDVQQKEx5UaGUg -QXBhY2hlIFNvZnR3YXJlIEZvdW5kYXRpb24xGjAYBgNVBAsTEUFwYWNoZSBUb21j -YXQgUE1DMR4wHAYDVQQDExVBcGFjaGUgVG9tY2F0IFRlc3QgQ0EwHhcNMjUwMjE2 -MTc1MzM2WhcNMjcwMjE2MTc1MzM2WjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgT -AkRFMRMwEQYDVQQHEwpXaWxtaW5ndG9uMScwJQYDVQQKEx5UaGUgQXBhY2hlIFNv -ZnR3YXJlIEZvdW5kYXRpb24xGjAYBgNVBAsTEUFwYWNoZSBUb21jYXQgUE1DMRIw -EAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQDAQEx9WEuTSgxNulnMUKGZwc7jPsiiQbDoJJKS1PbBMdnWQcD63tAuWQl1qcM7 -1FbTT3mjMN9xFc3YpwPWvvtJsMxpdcS5+6LeLtow23InHPmW4Z+9EzmP2qMSlceh -Zz5MFIyYA2qGGIs61U4Jd33BvmpTdczIDzX8Tf1AY6PkEAt32bOCGzBMNQKeQdyP -tXCm1751HlenJhQzWucXaJgnEKacDBeBa9EVNkQBlfiROJxt0JdFDQI8wxggjaYq -5+DfjcGBgXG7ibK2AodRLHrKn8nsq+ABJgzcOL0Xvi3+SEx/nEIaOKA5YFw4ZVTU -cavTptDJoqM8D1kJjYtX3CsFAgMBAAGjgZgwgZUwCQYDVR0TBAIwADAsBglghkgB -hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE -FL5G6ibqKheTZ90XC1eN7aAwFACyMB8GA1UdIwQYMBaAFADymE0hLAA8QJuE9N4q -8CbuMg6fMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsF -AAOCAgEAkJuzZqRCB8JoBarzoslmNUVCtL5Ax5A0cbTPef1090tGP/PagTtVw2J/ -eRISf7KNclXYhZfPVEQOnyICV5jpAqjHbUU+t/Z8VlihGwv6p0xXZpI6kr30J8Zc -rwY72L7pt5rTha6f2l9ZBb8SyRNJdSUedi3Jt8FVdiUw20j0ycTLpNy23xMcwUQr -ZxQV2lNWGOiGWnWJlqWIckc2tQucURNQxJMcoSvQ2AzH9sUwiJNsAv9u/pLOYEsX -J+dnmtAnrk6UhiqtmTsub5ml0KGo6D/RwrYcGvjclkk7eb7A+Dtqk9kIHjqpNguh -ZN/r+hQ0m3MlSSkl4DmqSQ8tS+xAGfdEAEd7QXLoXwYxMXTKe5jnY/gZrH5dH1q0 -IthziCdw9BhdEKlSbOPhNbBO45F5Y0mGhhmYxJpGqL3dX2qOFHWe4TVrrauDjDG2 -y8LMpYZP2eeay9am9e6pjoWhhVQ69vMOuZ/ivF3l60J18T/hplAJzKbTxTRAVYRm -YWCZ8Or3Vgv+x7LS7WjcCzUveb/2Cq8v6yWK8prHcgAErZkhCBS25BL40MSjVcjH -7E/k+0o+FORWz/il4bKyUIuDzvwTaEVV9hy85f7y8lENt55J8hk+EFmj3eByq3Ul -oA48sbkXrX5GnDfxKhm+kVQtqz4TVEyq1gVWufY+GWHLs8iVJa0= +MIIFZjCCA06gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT +MQswCQYDVQQIEwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUGA1UEChMeVGhl +IEFwYWNoZSBTb2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFBcGFjaGUgVG9t +Y2F0IFBNQzEeMBwGA1UEAxMVQXBhY2hlIFRvbWNhdCBUZXN0IENBMB4XDTI1MTIw +NTE5MTMyM1oXDTI3MTIwNTE5MTMyM1owgYgxCzAJBgNVBAYTAlVTMQswCQYDVQQI +EwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUGA1UEChMeVGhlIEFwYWNoZSBT +b2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFBcGFjaGUgVG9tY2F0IFBNQzES +MBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAsdouUFxbItuEYV7kneGkJGyS05Zad1LI988WtWCApxUOEnUavD8LVDiW9i8K +OGwhaNoTSaJ7fIDHqmn+44yxznPmW4DiNolJWwPOdke/NSDuxnjfYqrR17+RsPyq +/X9ZP4pRn31ZD+HW3HsWgGI/bMIsG6UFHkCQYCDhw20//xxzhJKGWQPlSvbW3OZu +aB8FX/chqfxe5vyLxzY0GPzVcCFUWvEQySTICJIskRNUsdlZcIA5n4yQrcnSuL2j +KCA/ZzWi2SDiPy6+PYjSO307R0R5U+JSmHITrqKd/mi9NowuQ7E7zVA1PbX3L0Pq +HsnREKzoJMGM7ubQwgbeFC0aiQIDAQABo4HLMIHIMAkGA1UdEwQCMAAwLAYJYIZI +AYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQW +BBRwNJmtKeMnnb2XclAgmNKXs2ytoTAfBgNVHSMEGDAWgBSsh9yLPYrbiUh+bzsc +xnCSCRdv+TAxBggrBgEFBQcBAQQlMCMwIQYIKwYBBQUHMAGGFWh0dHA6Ly8xMjcu +MC4wLjE6ODg4ODAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcN +AQELBQADggIBAHevSrMJ2JtDWnN4ELoxraUGUEldFPd7HDjwOptns8txUnrfZ/rh +wYsY3E6/IW1YsV/fLfHuYqG2eN3ixfJPC509zIjHJo3wQQKtveApIfkMCgfPOKTx +24f35e1VWbU0fbQ5cMqBersg+i6FR3KcNfjy4Vf5nlycdpJQaslS6aDTTUED90+7 +/Fy4qix9EP8/bILgYLjVWGbe4Nswt788Cbk8XDbMxtOqWJ+Q/ufrW2LiZqWI7N0r +ZNElCOYKSZFfrB81jSyicd4BGLAOEz3Ng5JnYmOKY7vrjfm3lbC2qJwUjAsANItl +r7dsuRuU4lQ/7ocPgak/YRBf7k1CtWd3MC4ET61dTt7jILOPykaYfZeRHUwE2buu +Aj1vVd05ZPNThrZKautKekZODztEjsJGxksHHrFOihz9agZVLojGhYybwuqQ4QvZ +sa8tgH+2osd6Y30KzWD2RupAPX1OiT4LXSKPtzo6OBgmo3bpzxi8wTyLBnS28qip +tM/hllS9hyEdILYl0dnG+pQlxm4rHqFR4zH/Q3e94ExqwiSXQ5n2y90/YzGIrwfv +4Cs7jjuAJiemw1iVU/o5FFlCPTrlAv8NW+BjrZNyjh1VwvlJfVzLNdIRtIYtJmF0 +Hk3mx5fnmYv8kUHIn7jsPqP5GLUqLh3uLbbNjxS+uBxt/cNZYWaH6DGC -----END CERTIFICATE----- Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-rsa-copy1.jks and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-rsa-copy1.jks differ diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-rsa-key.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-rsa-key.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-rsa-key.pem 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-rsa-key.pem 2026-05-01 18:56:05.000000000 +0000 @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDAQEx9WEuTSgxN -ulnMUKGZwc7jPsiiQbDoJJKS1PbBMdnWQcD63tAuWQl1qcM71FbTT3mjMN9xFc3Y -pwPWvvtJsMxpdcS5+6LeLtow23InHPmW4Z+9EzmP2qMSlcehZz5MFIyYA2qGGIs6 -1U4Jd33BvmpTdczIDzX8Tf1AY6PkEAt32bOCGzBMNQKeQdyPtXCm1751HlenJhQz -WucXaJgnEKacDBeBa9EVNkQBlfiROJxt0JdFDQI8wxggjaYq5+DfjcGBgXG7ibK2 -AodRLHrKn8nsq+ABJgzcOL0Xvi3+SEx/nEIaOKA5YFw4ZVTUcavTptDJoqM8D1kJ -jYtX3CsFAgMBAAECggEAR6xxF9YALwRPO/c6nTp+VOV1bTEt+ZgGDTX9UzKEYBhm -v0M6YA0Ljgvxw+UrfTW3vQYHMjj5RJ69ZIU3oCsJYjrAqpyWYEQZPz42aDqX/08F -GiQ5unLdZe08GKSwjVMBXbnXhKDZaO7jkWaOtmbPApkr6LulQfyxwsOMpCHBqzdN -peFcartqRkqhSYyXsMNekp9kZfZqzOkLkpFH0Qo0Y0BkK/1Bo+96iiD72FT3Cmgm -X5JQhjap6H6v2iy0A2ERAsYwNItnauMDGQbZoYk8pyKNfQBYpHKziA/AomEDXHLE -N42hjqTjlWfVvJ70TcQ06OcBPnpxdNrFrhYq2LSbbQKBgQD4YQE9/+hh7JrHElBq -bmF5sR2PyYp6+G1C74b8fSXS69Aijtbtvm2KNII2lCalP0iK1UPBlBAyhrqZs+Pw -B0fc/FGgZq1O/aM9JYUxGn+LpRfD1a73UrmKakgQj5Ph+6R2VrWSNlDYe/2hhUAB -WUnyFrPY56bQuhy+uu1K7amatwKBgQDGJmpLSGJ4BRZS2Nb6EDJJOva+KF49cKTC -GO8UtXXllOP2Y9e86wQ+f+/ZPC53C36hNItd6n9PG2CIpEBeDYbiv6E6a+tNWe27 -GqNCR/Rbs5MPiOm3tgbrK+eX0PT/+tKElopozp7ijIjYbjd5YbBuPDC7fr4pgBHc -x1nSWUAcIwKBgEYsggFWT/16r7AWyImJbhjHpaZ+NpXPom8K5YRoh0s0PLXGzYqW -BrTJaRCqgtClNIWlA2OpxXnA/u92F6w1ImSZFSbQW565omkgNKVO60I0/qXhtXFC -+hDVPk8PgaSTSD/29427stO3FE2SLCc1ZuLy7xUVPdgy/PQWM/y7cPO5AoGBALR6 -UN5485c1qvuSnVvuXJ7uocbZpdx4ONjBNSG7lcKiNnA4yEs+FAOJK8DFW9z6D0Bt -R3MnHwXCfc53LFl+IJluyT8ZBJzTiACfZov8VckgapX/skCt/uc8ehiBLmXo4s34 -/+AuMfFO52WaHdTk8Xm2QPzFmBbcm2hn+pvoxsfZAoGBAOxLL1iMDwMSAsECzy3s -B337lOWxb5pE1vIDxuUHScy1GMNpQ9YFyXAwDb0RkVfbQAutu4Uj0Eto58YjtpIP -y7Qw2vm19vVvyAjtdSTGHOhUB9v77MO8abrVBzlAgOVCGnvk3mUaCwL4uHKDMFLJ -WePSjVhSZRNhjwootd8hbPtr +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCx2i5QXFsi24Rh +XuSd4aQkbJLTllp3Usj3zxa1YICnFQ4SdRq8PwtUOJb2Lwo4bCFo2hNJont8gMeq +af7jjLHOc+ZbgOI2iUlbA852R781IO7GeN9iqtHXv5Gw/Kr9f1k/ilGffVkP4dbc +exaAYj9swiwbpQUeQJBgIOHDbT//HHOEkoZZA+VK9tbc5m5oHwVf9yGp/F7m/IvH +NjQY/NVwIVRa8RDJJMgIkiyRE1Sx2VlwgDmfjJCtydK4vaMoID9nNaLZIOI/Lr49 +iNI7fTtHRHlT4lKYchOuop3+aL02jC5DsTvNUDU9tfcvQ+oeydEQrOgkwYzu5tDC +Bt4ULRqJAgMBAAECggEAAqX0dvsIodFhzimzbSpMuCdZnObktgPd60UpnYpJLz5k +fTjQc/X3lVt9zffN2KC7yILhOiTTiZ7eSHiT+AUNN/YTdIJL7cuu7Hj+SZ1QYbMr +lpYlV2PNRH+vAShpjp4KzWsMpyZJehMmEKStHjzGgEZxWIdBqOSFgA3Tgzf5Q/NS +OiWNgarUkwVGLDRFH99WWbVmAJEnJeB9pxEQllqb/5AkgeiDD3Mx+BwNgO5GRu4u +z4BwB6InA4e6T+vBhnPra3qxaS3bPfna1P2Gz7jsH8xOHFt51hIVMCzeVj0/wOWL +Ie7YinLbCAhIa5YD8F1LTvrzCGuJn+a8Rlt6AXPYDQKBgQD1O0plltNO/3y8WN1i +c3Ox7sQfTsTj5ZFoW4U6kbAIPF5ByKTBAXdWHWxUwBxoZA+4lgYPEvdwXWPG+VXy +JXAFbR2/JQO/yROKnodB4O73WZitt81PIHdFP7myEeN5QSjnYSvd6lIiaxe178BM +qyO/85GgcBf/o3MAQib0UCwXHQKBgQC5qXZjspZd8nC33NP6A6HYawjkrZeVYG8S +qPgT0ctmsb1EpihK+wU1iAJzECZuTSgoZtcRPL/N17/xgSuwk6nTUJyOdQwnmVP+ ++Cfz7lcvx8Dg9FdrmjiEjFHnAYzaZ4bJP5Tub0tsUJRE/7YbhPlvlzaD95USLMKB +IHzAQWx5XQKBgHgsmgS2qM6pvQK/uZ2pXiTwEQQWob3cnik50EwnYNBoZPhvzu0W +Ptjgilnt2v39KwcV3do9PSy/V0oGneuQFRlTo6QsC25Mp1ri3P2XsQNd0Mgwrlf8 +XPZ+iA2PXp3pJJZetBSH48AiIvhxiRcJNve18MNiqyAHhS+3O0e2kiSxAoGAL3uY +rKzK6iIME+nlSMbPCKNvNdTztJ9iKNqP/7mjFJOWfU0ldu+2CFfNkJHr0j/nalXK +4TyxLTrleyV3AATz5Phz4bcrsaD0K3xZ83fcUnr66E11Yi6iD7w3YiYyWNUrUqLx +Ov25w2zkTrU7ZNRgWtrIdX3HYUuTPyUI4r6YuH0CgYA+Qs6ZexX1Xys6qBbO2OiR +6+eRItL12doGDDCO7tqtscf8S3VGSLk29lfwipyz6YhO46yRP2FypqVHYYhwvYuW +ubXGUUZhzI3JBvqI0Lrtwt0KJ2QUXzfqZqO1cvKe3+gooywmemvzg5s1jZ+/Ys3b +rQR0KWEn04xhcDMyTr/A9Q== -----END PRIVATE KEY----- Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/localhost-rsa.jks and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/localhost-rsa.jks differ diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/OcspBaseTest.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/OcspBaseTest.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/OcspBaseTest.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/OcspBaseTest.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,186 @@ +/* + * 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.util.net.ocsp; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileLock; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.net.SSLHostConfig; +import org.apache.tomcat.util.net.TesterSupport; +import org.apache.tomcat.util.net.TesterSupport.SimpleServlet; +import org.apache.tomcat.util.net.openssl.OpenSSLStatus; + +public class OcspBaseTest extends TomcatBaseTest { + + private static final File lockFile = new File("test/org/apache/tomcat/util/net/ocsp/ocsp-responder.lock"); + private static FileLock lock = null; + + @BeforeClass + public static void obtainOcspResponderLock() throws IOException { + @SuppressWarnings("resource") + FileOutputStream fos = new FileOutputStream(lockFile); + lock = fos.getChannel().lock(); + } + + @AfterClass + public static void releaseOcspResponderLock() throws IOException { + // Should not be null be in case obtaining the lock fails, avoid a second error. + if (lock != null) { + lock.release(); + } + } + + + @Parameters(name = "{0} with OpenSSL trust {2}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { "JSSE", Boolean.FALSE, Boolean.FALSE, + "org.apache.tomcat.util.net.jsse.JSSEImplementation"}); + parameterSets.add(new Object[] { "OpenSSL", Boolean.TRUE, Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" }); + parameterSets.add(new Object[] { "OpenSSL", Boolean.TRUE, Boolean.FALSE, + "org.apache.tomcat.util.net.openssl.OpenSSLImplementation" }); + parameterSets.add(new Object[] { "OpenSSL-FFM", Boolean.TRUE, Boolean.TRUE, + "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" }); + parameterSets.add(new Object[] { "OpenSSL-FFM", Boolean.TRUE, Boolean.FALSE, + "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation" }); + + return parameterSets; + } + + @Parameter(0) + public String connectorName; + + @Parameter(1) + public boolean useOpenSSL; + + @Parameter(2) + public boolean useOpenSSLTrust; + + @Parameter(3) + public String sslImplementationName; + + @Override + public void setUp() throws Exception { + super.setUp(); + Tomcat tomcat = getTomcatInstance(); + TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, useOpenSSL); + } + + + protected void doTest(boolean clientCertValid, boolean serverCertValid, ClientCertificateVerification verifyClientCert, + boolean verifyServerCert) throws Exception { + doTest(clientCertValid, serverCertValid, verifyClientCert, verifyServerCert, null); + } + + + protected void doTest(boolean clientCertValid, boolean serverCertValid, ClientCertificateVerification verifyClientCert, + boolean verifyServerCert, Boolean softFail) throws Exception { + + if ("OpenSSL-FFM".equals(connectorName)) { + Assume.assumeFalse(OpenSSLStatus.isBoringSSL() || OpenSSLStatus.isLibreSSLPre35()); + } + Assume.assumeFalse(!useOpenSSLTrust && verifyClientCert == ClientCertificateVerification.OPTIONAL_NO_CA); + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = tomcat.addContext("", null); + + Tomcat.addServlet(ctx, "simple", new SimpleServlet()); + ctx.addServletMappingDecoded("/simple", "simple"); + + if (serverCertValid) { + TesterSupport.initSsl(tomcat, useOpenSSLTrust); + } else { + TesterSupport.initSsl(tomcat, TesterSupport.LOCALHOST_CRL_RSA_JKS, useOpenSSLTrust); + } + SSLHostConfig sslHostConfig = tomcat.getConnector().findSslHostConfigs()[0]; + switch (verifyClientCert) { + case DEFAULT: + sslHostConfig.setCertificateVerification("required"); + break; + case DISABLED: + sslHostConfig.setOcspEnabled(false); + sslHostConfig.setCertificateVerification("required"); + break; + case ENABLED: + sslHostConfig.setOcspEnabled(true); + sslHostConfig.setCertificateVerification("required"); + break; + case OPTIONAL_NO_CA: + sslHostConfig.setOcspEnabled(true); + sslHostConfig.setCertificateVerification("optionalNoCA"); + break; + + } + + if (clientCertValid) { + TesterSupport.configureClientSsl(verifyServerCert, TesterSupport.CLIENT_JKS); + } else { + TesterSupport.configureClientSsl(verifyServerCert, TesterSupport.CLIENT_CRL_JKS); + } + + if (softFail != null) { + sslHostConfig.setOcspSoftFail(softFail.booleanValue()); + } + + /* + * Use shorter timeout to speed up test. + * + * Note: JSSE timeout set earlier as it requires setting a system property that is read once in a static + * initializer. + */ + if (useOpenSSLTrust) { + sslHostConfig.setOcspTimeout(1000); + } + + tomcat.start(); + + int rc = getUrl("https://localhost:" + getPort() + "/simple", new ByteChunk(), false); + + // If the TLS handshake fails, the test won't get this far. + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + } + + + protected enum ClientCertificateVerification { + DEFAULT, + ENABLED, + OPTIONAL_NO_CA, + DISABLED + } + +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,121 @@ +/* + * 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.util.net.ocsp; + +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.SSLHandshakeException; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class TestOcspEnabled extends OcspBaseTest { + + private static TesterOcspResponder ocspResponder; + + @BeforeClass + public static void startOcspResponder() { + ocspResponder = new TesterOcspResponder(); + try { + ocspResponder.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + @AfterClass + public static void stopOcspResponder() { + if (ocspResponder != null) { + ocspResponder.stop(); + ocspResponder = null; + } + } + + + @Parameters(name = "{0} with OpenSSL trust {2}: clientOk {4}, serverOk {5}, verifyClient {6}, verifyServer {7}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + Collection baseData = OcspBaseTest.parameters(); + + for (Object[] base : baseData) { + for (Boolean clientCertValid : booleans) { + for (Boolean serverCertValid : booleans) { + for (ClientCertificateVerification verifyClientCert : ClientCertificateVerification.values()) { + boolean useOpenSSLTrust = ((Boolean) base[2]).booleanValue(); + if (verifyClientCert == ClientCertificateVerification.OPTIONAL_NO_CA && !useOpenSSLTrust || + verifyClientCert == ClientCertificateVerification.ENABLED && !clientCertValid.booleanValue()) { + continue; + } + for (Boolean verifyServerCert : booleans) { + Boolean handshakeFailureExpected; + if (!serverCertValid.booleanValue() && verifyServerCert.booleanValue()) { + handshakeFailureExpected = Boolean.TRUE; + } else { + handshakeFailureExpected = Boolean.FALSE; + } + parameterSets.add(new Object[] { base[0], base[1], base[2], base[3], clientCertValid, + serverCertValid, verifyClientCert, verifyServerCert, handshakeFailureExpected}); + } + } + } + } + } + return parameterSets; + } + + @Parameter(4) + public boolean clientCertValid; + + @Parameter(5) + public boolean serverCertValid; + + @Parameter(6) + public ClientCertificateVerification verifyClientCert; + + @Parameter(7) + public boolean verifyServerCert; + + @Parameter(8) + public boolean handshakeFailureExpected; + + @Test + public void test() throws Exception { + Assume.assumeNotNull(ocspResponder); + try { + doTest(clientCertValid, serverCertValid, verifyClientCert, verifyServerCert); + if (handshakeFailureExpected) { + Assert.fail("Handshake did not fail when expected to do so."); + } + } catch (SSLHandshakeException | SocketException e) { + if (!handshakeFailureExpected) { + Assert.fail("Handshake failed when not expected to do so."); + } + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspIntegration.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspIntegration.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspIntegration.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspIntegration.java 1970-01-01 00:00:00.000000000 +0000 @@ -1,432 +0,0 @@ -/* - * 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.util.net.ocsp; - -import java.io.Closeable; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.URI; -import java.net.URL; -import java.nio.file.Files; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.cert.CRLReason; -import java.security.cert.CertPathValidator; -import java.security.cert.CertPathValidatorException; -import java.security.cert.Certificate; -import java.security.cert.CertificateRevokedException; -import java.security.cert.PKIXBuilderParameters; -import java.security.cert.PKIXRevocationChecker; -import java.security.cert.TrustAnchor; -import java.security.cert.X509CertSelector; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.EnumSet; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import javax.net.ssl.CertPathTrustManagerParameters; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManagerFactory; -import javax.security.auth.x500.X500Principal; - -import jakarta.servlet.http.HttpServletResponse; - -import org.junit.Assert; -import org.junit.Assume; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; - -import org.apache.catalina.Context; -import org.apache.catalina.connector.Connector; -import org.apache.catalina.startup.Tomcat; -import org.apache.catalina.startup.TomcatBaseTest; -import org.apache.tomcat.util.net.Constants; -import org.apache.tomcat.util.net.SSLHostConfig; -import org.apache.tomcat.util.net.SSLHostConfigCertificate; -import org.apache.tomcat.util.net.TesterSupport; -import org.apache.tomcat.util.net.openssl.OpenSSLStatus; - -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpServer; - - -@RunWith(Parameterized.class) -public class TestOcspIntegration extends TomcatBaseTest { - - private static final String CA_CERTIFICATE_PATH = "ca-cert.pem"; - private static final String SERVER_CERTIFICATE_PATH = "server-cert.pem"; - private static final String SERVER_CERTIFICATE_KEY_PATH = "server-key.pem"; - private static final String TRUSTSTORE_PATH = "trustStore.p12"; - private static final String TRUSTSTORE_PASS = "trust-password"; - private static final String KEYSTORE_TYPE = "PKCS12"; - private static final String OCSP_SERVER_CERT_GOOD_RESPONSE = "ocsp-good.der"; - private static final String OCSP_SERVER_CERT_REVOKED_RESPONSE = "ocsp-revoked.der"; - private static final String CLIENT_KEYSTORE_PATH = "client-keystore.p12"; - private static final String CLIENT_KEYSTORE_PASS = "client-password"; - private static final String OCSP_CLIENT_CERT_GOOD_RESPONSE = "ocsp-client-good.der"; - private static final String OCSP_CLIENT_CERT_REVOKED_RESPONSE = "ocsp-client-revoked.der"; - - @Parameterized.Parameters(name = "{0}") - public static Collection parameters() { - List parameterSets = new ArrayList<>(); - /* - * Future JSSE testing - parameterSets.add(new Object[] { - "JSSE", Boolean.FALSE, "org.apache.tomcat.util.net.jsse.JSSEImplementation"}); - */ - /* - * Disabled until a Tomcat Native release (2.0.10 onwards) is available with the OCSP fix - parameterSets.add(new Object[] { - "OpenSSL", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.OpenSSLImplementation"}); - */ - parameterSets.add(new Object[] { - "OpenSSL-FFM", Boolean.TRUE, "org.apache.tomcat.util.net.openssl.panama.OpenSSLImplementation"}); - - return parameterSets; - } - - @Parameter(0) - public String connectorName; - - @Parameter(1) - public boolean useOpenSSL; - - @Parameter(2) - public String sslImplementationName; - - - @Test - public void testOcspGood_ClientVerifiesServerCertificateOnly() throws Exception { - Assert.assertEquals(HttpServletResponse.SC_OK, testOCSP(OCSP_SERVER_CERT_GOOD_RESPONSE, false, true)); - } - - @Test - public void testOcspGood_Mutual() throws Exception { - testOCSPWithClientResponder(OCSP_CLIENT_CERT_GOOD_RESPONSE, () -> Assert.assertEquals(HttpServletResponse.SC_OK, - testOCSP(OCSP_SERVER_CERT_GOOD_RESPONSE, true, true))); - } - - @Test - public void testOcspGood_ServerVerifiesClientCertificateOnly() throws Exception { - testOCSPWithClientResponder(OCSP_CLIENT_CERT_GOOD_RESPONSE, () -> Assert.assertEquals(HttpServletResponse.SC_OK, - testOCSP(OCSP_SERVER_CERT_REVOKED_RESPONSE, true, false))); - } - - @Test(expected = CertificateRevokedException.class) - public void testOcspRevoked_ClientVerifiesServerCertificateOnly() throws Exception { - try { - testOCSP(OCSP_SERVER_CERT_REVOKED_RESPONSE, false, true); - } catch (SSLHandshakeException sslHandshakeException) { - handleExceptionWhenRevoked(sslHandshakeException); - } - } - - @Test(expected = CertificateRevokedException.class) - public void testOcspRevoked_Mutual() throws Exception { - try { - // The exception is thrown before server side verification, while client does OCSP verification. - testOCSP(OCSP_SERVER_CERT_REVOKED_RESPONSE, true, true); - } catch (SSLHandshakeException sslHandshakeException) { - handleExceptionWhenRevoked(sslHandshakeException); - } - } - - @Test(expected = SSLHandshakeException.class) - public void testOcspRevoked_ServerVerifiesClientCertificateOnly() throws Exception { - Assume.assumeFalse("BoringSSL does not support OCSP in a compatible way", - TesterSupport.isOpenSSLVariant(sslImplementationName, OpenSSLStatus.Name.BORINGSSL)); - testOCSPWithClientResponder(OCSP_CLIENT_CERT_REVOKED_RESPONSE, - () -> testOCSP(OCSP_SERVER_CERT_GOOD_RESPONSE, true, false)); - } - - @Test - public void testOcsp_NoVerification() throws Exception { - testOCSPWithClientResponder(OCSP_CLIENT_CERT_REVOKED_RESPONSE, () -> Assert - .assertEquals(HttpServletResponse.SC_OK, testOCSP(OCSP_SERVER_CERT_REVOKED_RESPONSE, false, false))); - } - - @Test - public void testOcspResponderUrlDiscoveryViaCertificateAIA() throws Exception { - final int ocspPort = 8888; - Assume.assumeTrue("Port " + ocspPort + " is not available.", isPortAvailable(ocspPort)); - Assert.assertEquals(HttpServletResponse.SC_OK, - testOCSP(OCSP_SERVER_CERT_GOOD_RESPONSE, false, true, true, ocspPort)); - } - - public static void testLongUrlForOcspViaAIAWithTomcatNative(Tomcat tomcat) throws Exception { - final int ocspResponderPortForClient = 8889; - Assume.assumeTrue("Port " + ocspResponderPortForClient + " is not available.", isPortAvailable(ocspResponderPortForClient)); - try (FakeOcspResponder fakeOcspResponder = new FakeOcspResponder(true, "/ocsp/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - Files.readAllBytes(new File(getPath(OCSP_CLIENT_CERT_REVOKED_RESPONSE)).toPath()), ocspResponderPortForClient)) { - fakeOcspResponder.start(); - testOCSP(tomcat, OCSP_SERVER_CERT_GOOD_RESPONSE, true, false, false, 0, - "org.apache.tomcat.util.net.openssl.OpenSSLImplementation", true); - } - } - - @FunctionalInterface - private interface TestOCSPAction { - void execute() throws Exception; - } - - private void testOCSPWithClientResponder(String clientResponsePath, TestOCSPAction testOCSPAction) - throws Exception { - final int ocspResponderPortForClient = 8889; - Assume.assumeTrue("Port " + ocspResponderPortForClient + " is not available.", - isPortAvailable(ocspResponderPortForClient)); - try (FakeOcspResponder fakeOcspResponder = new FakeOcspResponder( - Files.readAllBytes(new File(getPath(clientResponsePath)).toPath()), ocspResponderPortForClient)) { - fakeOcspResponder.start(); - testOCSPAction.execute(); - } - } - - private int testOCSP(String pathToOcspResponse, boolean serverSideVerificationEnabled, - boolean clientSideOcspVerificationEnabled) throws Exception { - return testOCSP(pathToOcspResponse, serverSideVerificationEnabled, clientSideOcspVerificationEnabled, false, 0); - } - - private int testOCSP(String pathToOcspResponse, boolean serverSideVerificationEnabled, - boolean clientSideOcspVerificationEnabled, boolean clientDiscoversResponderFromAIA, int ocspResponderPort) - throws Exception { - return testOCSP(getTomcatInstance(), pathToOcspResponse, serverSideVerificationEnabled, - clientSideOcspVerificationEnabled, clientDiscoversResponderFromAIA, ocspResponderPort, - sslImplementationName, useOpenSSL); - } - - private static int testOCSP(Tomcat tomcat, String pathToOcspResponse, boolean serverSideVerificationEnabled, - boolean clientSideOcspVerificationEnabled, boolean clientDiscoversResponderFromAIA, int ocspResponderPort, - String sslImplementationName, boolean useOpenSSL) - throws Exception { - - File certificateFile = new File(getPath(SERVER_CERTIFICATE_PATH)); - File certificateKeyFile = new File(getPath(SERVER_CERTIFICATE_KEY_PATH)); - File certificateChainFile = new File(getPath(CA_CERTIFICATE_PATH)); - initSsl(tomcat, serverSideVerificationEnabled, certificateFile, certificateKeyFile, certificateChainFile); - - TesterSupport.configureSSLImplementation(tomcat, sslImplementationName, useOpenSSL); - - Context context = tomcat.addContext("", null); - Tomcat.addServlet(context, "simple", new TesterSupport.SimpleServlet()); - context.addServletMappingDecoded("/", "simple"); - - KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE); - String trustStorePass = Files.readString(new File(getPath(TRUSTSTORE_PASS)).toPath()).trim(); - trustStore.load(new FileInputStream(new File(getPath(TRUSTSTORE_PATH)).getAbsolutePath()), - trustStorePass.toCharArray()); - KeyStore clientKeystore = KeyStore.getInstance(KEYSTORE_TYPE); - String clientKeystorePass = Files.readString(new File(getPath(CLIENT_KEYSTORE_PASS)).toPath()).trim(); - clientKeystore.load(new FileInputStream(new File(getPath(CLIENT_KEYSTORE_PATH)).getAbsolutePath()), - clientKeystorePass.toCharArray()); - byte[] ocspResponse = Files.readAllBytes(new File(getPath(pathToOcspResponse)).toPath()); - try (FakeOcspResponder fakeOcspResponder = new FakeOcspResponder(ocspResponse, ocspResponderPort)) { - fakeOcspResponder.start(); - tomcat.start(); - - URL url = new URI("https://127.0.0.1:" + tomcat.getConnector().getLocalPort() + "/").toURL(); - HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); - SSLSocketFactory sslSocketFactory; - if (clientSideOcspVerificationEnabled) { - sslSocketFactory = buildClientSslSocketFactoryWithOcsp( - clientDiscoversResponderFromAIA ? null : fakeOcspResponder.url(), trustStore, clientKeystore, - clientKeystorePass); - } else { - sslSocketFactory = buildClientSslSocketFactoryNoOcsp(trustStore, clientKeystore, clientKeystorePass); - } - connection.setSSLSocketFactory(sslSocketFactory); - connection.connect(); - return connection.getResponseCode(); - } finally { - tomcat.stop(); - } - } - - private static void initSsl(Tomcat tomcat, boolean serverSideVerificationEnabled, File certificateFile, - File certificateKeyFile, File certificateChainFile) { - Connector connector = tomcat.getConnector(); - connector.setSecure(true); - connector.setProperty("SSLEnabled", "true"); - - SSLHostConfig sslHostConfig = new SSLHostConfig(); - SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.Type.UNDEFINED); - sslHostConfig.addCertificate(certificate); - certificate.setCertificateFile(certificateFile.getAbsolutePath()); - certificate.setCertificateKeyFile(certificateKeyFile.getAbsolutePath()); - certificate.setCertificateChainFile(certificateChainFile.getAbsolutePath()); - if (serverSideVerificationEnabled) { - sslHostConfig.setCertificateVerification("required"); - } else { - sslHostConfig.setCertificateVerification("optionalNoCA"); - } - sslHostConfig.setCaCertificateFile(certificateChainFile.getAbsolutePath()); - connector.addSslHostConfig(sslHostConfig); - } - - private static SSLSocketFactory buildClientSslSocketFactoryWithOcsp(String ocspUrl, KeyStore trustStore, - KeyStore clientKeystore, String clientKeystorePass) throws Exception { - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(clientKeystore, clientKeystorePass.toCharArray()); - Set trustAnchors = getTrustAnchorsFromKeystore(trustStore); - PKIXRevocationChecker revocationChecker =(PKIXRevocationChecker) CertPathValidator.getInstance("PKIX").getRevocationChecker(); - if (ocspUrl != null) { - revocationChecker.setOcspResponder(new URI(ocspUrl)); - } - revocationChecker.setOptions(EnumSet.of(PKIXRevocationChecker.Option.NO_FALLBACK)); - - PKIXBuilderParameters pkix = new PKIXBuilderParameters(trustAnchors, new X509CertSelector()); - pkix.addCertPathChecker(revocationChecker); - - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX"); - trustManagerFactory.init(new CertPathTrustManagerParameters(pkix)); - return initSSLContext(kmf, trustManagerFactory).getSocketFactory(); - } - - private static SSLSocketFactory buildClientSslSocketFactoryNoOcsp(KeyStore trustStore, KeyStore clientKeystore, - String clientKeystorePass) throws Exception { - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(clientKeystore, clientKeystorePass.toCharArray()); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(trustStore); - return initSSLContext(kmf, trustManagerFactory).getSocketFactory(); - } - - private static SSLContext initSSLContext(KeyManagerFactory keyManagerFactory, - TrustManagerFactory trustManagerFactory) throws Exception { - SSLContext sslContext; - if (TesterSupport.isTlsv13Available()) { - sslContext = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_3); - } else { - sslContext = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_2); - } - sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); - return sslContext; - } - - private static Set getTrustAnchorsFromKeystore(KeyStore keyStore) throws KeyStoreException { - Set trustAnchors = new HashSet<>(); - Enumeration aliases = keyStore.aliases(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - Certificate certificate = keyStore.getCertificate(alias); - if (certificate instanceof X509Certificate) { - trustAnchors.add(new TrustAnchor((X509Certificate)certificate, null)); - } - } - return trustAnchors; - } - - private static void handleExceptionWhenRevoked(Exception exception) throws Exception { - if (exception.getCause().getCause() instanceof CertPathValidatorException cpe) { - Assert.assertEquals("REVOKED", cpe.getReason().toString()); - Assert.assertTrue(cpe.toString().contains("reason: KEY_COMPROMISE")); - // Some JDKs only expose CertPathValidatorException - if (cpe.getCause() instanceof CertificateRevokedException) { - throw (CertificateRevokedException) cpe.getCause(); - } else { - throw new CertificateRevokedException(new Date(), CRLReason.KEY_COMPROMISE, new X500Principal(""), new HashMap<>()); - } - } - } - - private static class FakeOcspResponder implements Closeable { - private final byte[] ocspResponse; - private HttpServer server; - private int port; - private boolean strictPath = false; - private String path = "/ocsp"; - - FakeOcspResponder(boolean strictPath, String path, byte[] ocspResponse, int port) { - this(ocspResponse, port); - this.strictPath = strictPath; - this.path = path; - } - - FakeOcspResponder(byte[] ocspResponse, int port) { - this.ocspResponse = ocspResponse; - this.port = port; - } - - void start() throws IOException { - server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0); - server.createContext(this.path, httpExchange -> { - if (strictPath) { - String path = httpExchange.getRequestURI().getPath(); - if (!this.path.equals(path)) { - httpExchange.sendResponseHeaders(404, -1); - httpExchange.close(); - return; - } - } - byte[] body = ocspResponse; - Headers headers = httpExchange.getResponseHeaders(); - headers.add("Content-Type", "application/ocsp-response"); - httpExchange.sendResponseHeaders(HttpServletResponse.SC_OK, body.length); - try (OutputStream os = httpExchange.getResponseBody()) { - os.write(body); - } - }); - server.start(); - port = server.getAddress().getPort(); - } - - String url() { - return "http://127.0.0.1:" + port + path; - } - @Override public void close() { - if (server != null) { - server.stop(0); - } - } - } - - private static String getPath(String file) throws IOException { - if (file == null) { - return null; - } - String packageName = TestOcspIntegration.class.getPackageName(); - String path = packageName.replace(".", File.separator); - File f = new File("test" + File.separator + path + File.separator + file); - - return f.getCanonicalPath(); - } - - @SuppressWarnings("unused") - private static boolean isPortAvailable(int port) { - try (ServerSocket serverSocket = new ServerSocket(port)) { - return true; - } catch (IOException e) { - return false; - } - } -} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFail.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFail.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFail.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFail.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,57 @@ +/* + * 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.util.net.ocsp; + +import java.net.SocketException; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/* + * Only looking to test the Tomcat (server) side configuration of soft fail. + */ +@RunWith(Parameterized.class) +public class TestOcspSoftFail extends OcspBaseTest { + + @Test + public void testNoResponderDefaultSoftFail() throws Exception { + // Default behaviour should be the same for all configurations and equivalent to enabled. + doTest(false, false, ClientCertificateVerification.ENABLED, false, null); + } + + + @Test + public void testNoResponderWithSoftFail() throws Exception { + doTest(false, false, ClientCertificateVerification.ENABLED, false, Boolean.TRUE); + } + + + @Test(expected = SSLHandshakeException.class) + public void testNoResponderWithoutSoftFail() throws Exception { + try { + doTest(false, false, ClientCertificateVerification.ENABLED, false, Boolean.FALSE); + } catch (SocketException | SSLException e) { + // APR or NIO2 may throw a SocketException rather than a SSLHandshakeException + // Different Java versions may throw an SSLException rather than a SSLHandshakeException + throw new SSLHandshakeException(e.getMessage()); + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailInternalError.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailInternalError.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailInternalError.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailInternalError.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,111 @@ +/* + * 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.util.net.ocsp; + +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.SSLHandshakeException; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.tomcat.util.net.ocsp.TesterOcspResponder.OcspResponse; + +@RunWith(Parameterized.class) +public class TestOcspSoftFailInternalError extends OcspBaseTest { + + private static TesterOcspResponder ocspResponder; + + @BeforeClass + public static void startOcspResponder() { + ocspResponder = new TesterOcspResponder(); + ocspResponder.setFixedResponse(OcspResponse.INTERNAL_ERROR); + try { + ocspResponder.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + @AfterClass + public static void stopOcspResponder() { + if (ocspResponder != null) { + ocspResponder.stop(); + ocspResponder = null; + } + } + + + @Parameters(name = "{0} with OpenSSL trust {2}: softFail {4}, clientOk {5}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + Collection baseData = OcspBaseTest.parameters(); + + for (Object[] base : baseData) { + for (Boolean softFail : booleans) { + for (Boolean clientCertValid : booleans) { + Boolean handshakeFailureExpected; + + if (softFail.booleanValue()) { + handshakeFailureExpected = Boolean.FALSE; + } else { + handshakeFailureExpected = Boolean.TRUE; + } + + parameterSets.add(new Object[] { base[0], base[1], base[2], base[3], softFail, clientCertValid, + handshakeFailureExpected}); + } + } + } + return parameterSets; + } + + @Parameter(4) + public Boolean softFail; + + @Parameter(5) + public boolean clientCertValid; + + @Parameter(6) + public boolean handshakeFailureExpected; + + @Test + public void test() throws Exception { + Assume.assumeNotNull(ocspResponder); + try { + doTest(clientCertValid, true, ClientCertificateVerification.ENABLED, false, softFail); + if (handshakeFailureExpected) { + Assert.fail("Handshake did not fail when expected to do so."); + } + } catch (SSLHandshakeException | SocketException e) { + if (!handshakeFailureExpected) { + Assert.fail("Handshake failed when not expected to do so."); + } + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailTryLater.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailTryLater.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailTryLater.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailTryLater.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,111 @@ +/* + * 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.util.net.ocsp; + +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.net.ssl.SSLHandshakeException; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import org.apache.tomcat.util.net.ocsp.TesterOcspResponder.OcspResponse; + +@RunWith(Parameterized.class) +public class TestOcspSoftFailTryLater extends OcspBaseTest { + + private static TesterOcspResponder ocspResponder; + + @BeforeClass + public static void startOcspResponder() { + ocspResponder = new TesterOcspResponder(); + ocspResponder.setFixedResponse(OcspResponse.TRY_LATER); + try { + ocspResponder.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + @AfterClass + public static void stopOcspResponder() { + if (ocspResponder != null) { + ocspResponder.stop(); + ocspResponder = null; + } + } + + + @Parameters(name = "{0} with OpenSSL trust {2}: softFail {4}, clientOk {5}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + Collection baseData = OcspBaseTest.parameters(); + + for (Object[] base : baseData) { + for (Boolean softFail : booleans) { + for (Boolean clientCertValid : booleans) { + Boolean handshakeFailureExpected; + + if (softFail.booleanValue()) { + handshakeFailureExpected = Boolean.FALSE; + } else { + handshakeFailureExpected = Boolean.TRUE; + } + + parameterSets.add(new Object[] { base[0], base[1], base[2], base[3], softFail, clientCertValid, + handshakeFailureExpected}); + } + } + } + return parameterSets; + } + + @Parameter(4) + public Boolean softFail; + + @Parameter(5) + public boolean clientCertValid; + + @Parameter(6) + public boolean handshakeFailureExpected; + + @Test + public void test() throws Exception { + Assume.assumeNotNull(ocspResponder); + try { + doTest(clientCertValid, true, ClientCertificateVerification.ENABLED, false, softFail); + if (handshakeFailureExpected) { + Assert.fail("Handshake did not fail when expected to do so."); + } + } catch (SSLHandshakeException | SocketException e) { + if (!handshakeFailureExpected) { + Assert.fail("Handshake failed when not expected to do so."); + } + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspTimeout.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspTimeout.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TestOcspTimeout.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TestOcspTimeout.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,73 @@ +/* + * 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.util.net.ocsp; + +import java.net.SocketException; +import java.net.SocketTimeoutException; + +import javax.net.ssl.SSLHandshakeException; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/* + * The timeout for reading an OCSP response is 15s by default for both JSSE and OpenSSL. + */ +@RunWith(Parameterized.class) +public class TestOcspTimeout extends OcspBaseTest { + + private static TesterOcspResponderNoResponse ocspResponder; + + @BeforeClass + public static void startOcspResponder() { + /* + * Use shorter timeout to speed up test. + * + * Note: OpenSSL timeout set later as it requires access to SSLHostConfig. + */ + System.setProperty("com.sun.security.ocsp.readtimeout", "1000ms"); + ocspResponder = new TesterOcspResponderNoResponse(); + ocspResponder.start(); + } + + + @AfterClass + public static void stopOcspResponder() { + ocspResponder.stop(); + ocspResponder = null; + } + + + @Test + public void testTimeoutWithSoftFail() throws Exception { + doTest(false, false, ClientCertificateVerification.ENABLED, false, Boolean.TRUE); + } + + + @Test(expected = SSLHandshakeException.class) + public void testTimeoutWithoutSoftFail() throws Exception { + try { + doTest(false, false, ClientCertificateVerification.ENABLED, false, Boolean.FALSE); + } catch (SocketTimeoutException | SocketException e) { + // May throw a SocketTimeoutException or SocketException rather than a SSLHandshakeException + throw new SSLHandshakeException(e.getMessage()); + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,112 @@ +/* + * 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.util.net.ocsp; + +import java.io.File; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.ExpandWar; +import org.apache.catalina.startup.Tomcat; + +public class TesterOcspResponder { + + private OcspResponse fixedResponse; + + private File catalinaBase; + private Tomcat ocspResponder; + + public void setFixedResponse(OcspResponse fixedResponse) { + this.fixedResponse = fixedResponse; + } + + public void start() throws Exception { + ocspResponder = new Tomcat(); + + Connector connector = new Connector("HTTP/1.1"); + connector.setPort(8888); + connector.setThrowOnFailure(true); + connector.setEncodedSolidusHandling("passthrough"); + ocspResponder.getService().addConnector(connector); + + // Create a temporary directory structure for the OCSP responder + File tempBase = new File(System.getProperty("tomcat.test.temp", "output/tmp")); + if (!tempBase.mkdirs() && !tempBase.isDirectory()) { + throw new IllegalStateException("Unable to create tempBase"); + } + + // Create and configure CATALINA_BASE + Path tempBasePath = FileSystems.getDefault().getPath(tempBase.getAbsolutePath()); + catalinaBase = Files.createTempDirectory(tempBasePath, "ocsp").toFile(); + if (!catalinaBase.isDirectory()) { + throw new IllegalStateException("Unable to create CATALINA_BASE for OCSP responder"); + } + ocspResponder.setBaseDir(catalinaBase.getAbsolutePath()); + + // Create and configure a web apps directory + File appBase = new File(catalinaBase, "webapps"); + if (!appBase.exists() && !appBase.mkdir()) { + throw new IllegalStateException("Unable to create appBase for OCSP responder"); + } + ocspResponder.getHost().setAppBase(appBase.getAbsolutePath()); + + // Configure the ROOT web application + // No file system docBase required + Context ctx = ocspResponder.addContext("", null); + Wrapper w = Tomcat.addServlet(ctx, "responder", new TesterOcspResponderServlet()); + ctx.addServletMappingDecoded("/", "responder"); + if (fixedResponse != null) { + w.addInitParameter(TesterOcspResponderServlet.INIT_FIXED_RESPONSE, fixedResponse.toString()); + } + + // Start the responder + ocspResponder.start(); + } + + public void stop() { + if (ocspResponder != null) { + try { + ocspResponder.stop(); + } catch (LifecycleException e) { + // Good enough for testing + e.printStackTrace(); + } + try { + ocspResponder.destroy(); + } catch (LifecycleException e) { + // Good enough for testing + e.printStackTrace(); + } + } + if (catalinaBase != null) { + ExpandWar.deleteDir(catalinaBase); + } + } + + public enum OcspResponse { + OK, + REVOKED, + UNKNOWN, + TRY_LATER, + INTERNAL_ERROR + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderNoResponse.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderNoResponse.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderNoResponse.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderNoResponse.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,113 @@ +/* + * 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.util.net.ocsp; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; + +import javax.net.ServerSocketFactory; + +import org.junit.Assert; + +/* + * An OCSP responder that swallows any input received and never responds. Use to test timeouts. + */ +public class TesterOcspResponderNoResponse { + + private ServerRunnable sr; + + public void start() { + if (sr != null) { + throw new IllegalStateException("Already started"); + } + + sr = new ServerRunnable(); + Thread t = new Thread(sr); + t.start(); + + Assert.assertTrue(sr.isAlive()); + } + + public void stop() { + if (sr == null) { + throw new IllegalStateException("Not started"); + } + sr.stop(); + } + + + private static class ServerRunnable implements Runnable { + + private volatile boolean alive = true; + private volatile ServerSocket serverSocket; + + @Override + public void run() { + try { + serverSocket = ServerSocketFactory.getDefault().createServerSocket(); + serverSocket.bind(new InetSocketAddress("localhost", 8888)); + + while (alive) { + Socket socket = serverSocket.accept(); + Thread t = new Thread(new SwallowRunnable(socket)); + t.start(); + } + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + public void stop() { + try { + serverSocket.close(); + alive = false; + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + public boolean isAlive() { + return alive; + } + } + + + private static class SwallowRunnable implements Runnable { + + private final Socket socket; + + SwallowRunnable(Socket socket) { + this.socket = socket; + } + + @Override + public void run() { + byte[] buf = new byte[1024]; + try (InputStream os = socket.getInputStream()) { + // Read until the client closes the socket + while (os.read(buf) > 0) { + // Ignore any data read + } + } catch (IOException ignore) { + // Ignore + } + } + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,271 @@ +/* + * 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.util.net.ocsp; + +import java.io.FileReader; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.Security; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Date; + +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.util.IOTools; +import org.apache.tomcat.util.http.fileupload.ByteArrayOutputStream; +import org.apache.tomcat.util.net.TesterSupport; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.BasicOCSPRespBuilder; +import org.bouncycastle.cert.ocsp.CertificateID; +import org.bouncycastle.cert.ocsp.CertificateStatus; +import org.bouncycastle.cert.ocsp.OCSPException; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.cert.ocsp.OCSPRespBuilder; +import org.bouncycastle.cert.ocsp.Req; +import org.bouncycastle.cert.ocsp.RespID; +import org.bouncycastle.cert.ocsp.RevokedStatus; +import org.bouncycastle.cert.ocsp.UnknownStatus; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DigestCalculatorProvider; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; + +/* + * Based on https://github.com/wdawson/revoker - ALv2 licensed + */ +public class TesterOcspResponderServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + // Config + public static final String INIT_FIXED_RESPONSE = "fixedResponse"; + private TesterOcspResponder.OcspResponse fixedResponse; + + // Cached OCSP processing components + private DigestCalculatorProvider digestCalculatorProvider; + private X509CertificateHolder[] responderCertificateChain; + private RespID responderID; + private ContentSigner contentSigner; + + + @Override + public void init(ServletConfig config) throws ServletException { + String value = config.getInitParameter(INIT_FIXED_RESPONSE); + if (value != null) { + fixedResponse = TesterOcspResponder.OcspResponse.valueOf(value); + } + + // Enable the Bouncy Castle Provider + Provider provider = new BouncyCastleProvider(); + Security.addProvider(provider); + + // Create the digest provider + try { + this.digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build(); + } catch (OperatorCreationException e) { + throw new ServletException(e); + } + + // Parse the OCSP responder cert + X509Certificate responderCert; + try (PEMParser pemParser = new PEMParser(new FileReader(TesterSupport.OCSP_RESPONDER_RSA_CERT))) { + JcaX509CertificateConverter x509Converter = new JcaX509CertificateConverter().setProvider(provider); + responderCert = x509Converter.getCertificate((X509CertificateHolder) pemParser.readObject()); + } catch (IOException | CertificateException e) { + throw new ServletException(e); + } + + // Parse the OCSP responder issuer certificate + X509Certificate issuerCert; + try (PEMParser pemParser = new PEMParser(new FileReader(TesterSupport.CA_CERT_PEM))) { + JcaX509CertificateConverter x509Converter = new JcaX509CertificateConverter().setProvider(provider); + issuerCert = x509Converter.getCertificate((X509CertificateHolder) pemParser.readObject()); + } catch (IOException | CertificateException e) { + throw new ServletException(e); + } + + // Create the responder certificate chain + try { + responderCertificateChain = new X509CertificateHolder[] { new JcaX509CertificateHolder(responderCert), + new JcaX509CertificateHolder(issuerCert) }; + } catch (CertificateEncodingException e) { + throw new ServletException(e); + } + + // Create the responder ID + SubjectPublicKeyInfo publicKeyInfo = + SubjectPublicKeyInfo.getInstance(responderCert.getPublicKey().getEncoded()); + + try { + // Only SHA-1 supported + responderID = new RespID(publicKeyInfo, + digestCalculatorProvider.get(new DefaultDigestAlgorithmIdentifierFinder().find("SHA-1"))); + } catch (OperatorCreationException | OCSPException e) { + throw new ServletException(e); + } + + // Parse the private key + PrivateKey responderKey; + try (PEMParser pemParser = new PEMParser(new FileReader(TesterSupport.OCSP_RESPONDER_RSA_KEY))) { + PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(pemParser.readObject()); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); + responderKey = converter.getPrivateKey(privateKeyInfo); + } catch (IOException e) { + throw new ServletException(e); + } + + // Create the content signer + try { + contentSigner = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(responderKey); + } catch (OperatorCreationException e) { + throw new ServletException(e); + } + } + + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + // The request is base64 encoded and passed as the path (less the leading '/') + String urlEncoded = req.getRequestURI().substring(1); + + // Handle longer URI used for TestSecurity2017Ocsp + if (urlEncoded.startsWith("xxxxxxxx")) { + urlEncoded = urlEncoded.substring(urlEncoded.indexOf("/") + 1); + } + String base64 = URLDecoder.decode(urlEncoded, StandardCharsets.US_ASCII); + byte[] derEncodeOCSPRequest = Base64.getDecoder().decode(base64); + + // Process the OCSP request + OCSPResp ocspResponse = processOscpRequest(derEncodeOCSPRequest); + + // Write the OCSP response + ServletOutputStream sos = resp.getOutputStream(); + sos.write(ocspResponse.getEncoded()); + } + + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // The request is passed in the request body + + // Determine request content length (or start with a reasonable default) + int contentLength = req.getContentLength(); + if (contentLength == -1) { + // OCSP requests are small. 1k should be plenty and it can expand if necessary. + contentLength = 1024; + } + + // Read the body into a byte array + ByteArrayOutputStream baos = new ByteArrayOutputStream(contentLength); + IOTools.flow(req.getInputStream(), baos); + + // Process the OCSP request + OCSPResp ocspResponse = processOscpRequest(baos.toByteArray()); + + // Write the OCSP response + ServletOutputStream sos = resp.getOutputStream(); + sos.write(ocspResponse.getEncoded()); + } + + + private OCSPResp processOscpRequest(byte[] derEncodeOCSPRequest) throws ServletException, IOException { + + OCSPReq ocspReq = new OCSPReq(derEncodeOCSPRequest); + + // For the tests as currently written it is safe to assume the request is valid + + // Set the responses for each certificate + BasicOCSPRespBuilder responseBuilder = new BasicOCSPRespBuilder(responderID); + Req[] requests = ocspReq.getRequestList(); + for (Req request : requests) { + CertificateID certificateID = request.getCertID(); + if (fixedResponse == null) { + switch (certificateID.getSerialNumber().intValue()) { + // TODO read index.db rather than hard-code certificate serial numbers + case 4096: + case 4098: + case 4100: + case 4101: + responseBuilder.addResponse(certificateID, CertificateStatus.GOOD); + break; + case 4097: + case 4099: + case 4102: + responseBuilder.addResponse(certificateID, new RevokedStatus(new Date(0))); + break; + default: + responseBuilder.addResponse(certificateID, new UnknownStatus()); + } + } else { + switch (fixedResponse) { + case OK: + responseBuilder.addResponse(certificateID, CertificateStatus.GOOD); + break; + case REVOKED: + responseBuilder.addResponse(certificateID, new RevokedStatus(new Date(0))); + break; + case TRY_LATER: + // NO-OP + break; + case UNKNOWN: + responseBuilder.addResponse(certificateID, new UnknownStatus()); + break; + case INTERNAL_ERROR: + throw new ServletException("Internal error"); + } + } + } + + // Build and sign the response + OCSPResp ocspResponse; + try { + BasicOCSPResp basicResponse = responseBuilder.build(contentSigner, responderCertificateChain, new Date()); + if (fixedResponse == TesterOcspResponder.OcspResponse.TRY_LATER) { + ocspResponse = new OCSPRespBuilder().build(OCSPRespBuilder.TRY_LATER, null); + } else { + ocspResponse = new OCSPRespBuilder().build(OCSPRespBuilder.SUCCESSFUL, basicResponse); + } + } catch (OCSPException e) { + throw new ServletException(e); + } + + return ocspResponse; + } +} diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/ca-cert.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/ca-cert.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/ca-cert.pem 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/ca-cert.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDFTCCAf2gAwIBAgIUAyOAYMldD+vXvTMjdb9wXBFMQhQwDQYJKoZIhvcNAQEL -BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNTEwMDkxNzUwMTlaFw0zNTEwMDcx -NzUwMTlaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQCR5uGEWWQ5x9WNSDB/38SAT30PR+DxhNL4v+sjx9u2yARgVI9q -ENLoXCrQUxMocRvwdb30owOe/vxJPaCrNQunUfSQpeKW9KYPj9tCWxrc/LPeciqO -m+XTgxowDbAY7gBGSUM75vsv0CBMo2fSGDEmQqB3+guCQBHmvV4iTnctN6jg6e1t -p+Xq4VwoTLaLMuI+G+pBvv+xVk+Mkw0L+wRChsqegaxq740V0FNfBeId4nYLAWrT -WRKmmkhrfRm2DQJ+gG46RGb1jA+3y3i+nTkEzn9ZmzAGpF3PIVhrPzYbC7AByE+T -2NqtHHp37jBvuChr20ReY+Gzx0zUdkQcF/vlAgMBAAGjYzBhMA8GA1UdEwEB/wQF -MAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTI+niWdI2AaIVg+4CTF2g6 -prb8qDAfBgNVHSMEGDAWgBTI+niWdI2AaIVg+4CTF2g6prb8qDANBgkqhkiG9w0B -AQsFAAOCAQEAKKXD005JJTCSuQckD856ZzVa6ffoGMSssSOlqvS7KrpiUID/twvG -/r8SEXbguRGauyh0FY9UlbNeWjN8u92v7zILmnBUdYm7sPEi6+bTFDrjJ22rFxei -Xpwb02WY9P+Kty96DTjByrKfodtGt9Ey+R/fsBE0dNEKgqOu9H+6nVcxjO2bONJH -QJsHEn3liLj+DpHn5Pe4laQHuPpXSZgvtjL5/Y8D6G4UCNQ7gVxq8GCDK1eQuyKM -oboDINCN8/6XUmCuLk51urHcsZXOJ7PX8VirdPWBqpjGa2hMNsIak6NAyK86rDFP -brZOqWk1hfVTbW9LlIxkU7dQniz/PKFxgw== ------END CERTIFICATE----- Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/client-keystore.p12 and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/client-keystore.p12 differ diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/client-password tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/client-password --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/client-password 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/client-password 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -changeit diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/generate-ocsp-test-artifacts.sh tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/generate-ocsp-test-artifacts.sh --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/generate-ocsp-test-artifacts.sh 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/generate-ocsp-test-artifacts.sh 1970-01-01 00:00:00.000000000 +0000 @@ -1,172 +0,0 @@ -#!/bin/sh - -# -# 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. -# - -# Generate OCSP-ready test material for Tomcat integration tests. -# -# Output: -# ca-cert.pem -# client-keystore.p12 -# client-password -# ocsp-client-good.der -# ocsp-client-revoked.der -# ocsp-good.der -# ocsp-revoked.der -# server-cert.pem -# server-key.pem -# trustStore.p12 -# trust-password -# -# Usage: generate-ocsp-test-artifacts.sh -# - -PASS="changeit" -WORK_DIR="ocsp-work" - -command -v openssl >/dev/null 2>&1 || (printf "OpenSSL not found. Please install it.\r\n" && exit) -command -v keytool >/dev/null 2>&1 || (printf "keytool not found. Please install it.\r\n" && exit) - -rm -rf "$WORK_DIR" -mkdir -p "$WORK_DIR"/private "$WORK_DIR"/newcerts "$WORK_DIR"/certs -touch "$WORK_DIR/index" -echo 1000 > "$WORK_DIR/serial" - -printf "Writing minimal OpenSSL config..." -cat > "$WORK_DIR/openssl.cnf" <<'EOF' -[ ca ] -default_ca = CA_default - -[ CA_default ] -dir = . -database = $dir/index -new_certs_dir = $dir/newcerts -serial = $dir/serial -default_md = sha256 -policy = policy_loose -copy_extensions = copy -private_key = $dir/private/ca.key.pem -certificate = $dir/certs/ca-cert.pem - -[ policy_loose ] -commonName = supplied - -[ v3_ca ] -basicConstraints = critical,CA:TRUE -keyUsage = critical,keyCertSign,cRLSign -subjectKeyIdentifier = hash -authorityKeyIdentifier = keyid:always,issuer - -[ v3_server ] -basicConstraints = critical,CA:FALSE -keyUsage = critical,digitalSignature,keyEncipherment -extendedKeyUsage = serverAuth -authorityInfoAccess = OCSP;URI:http://127.0.0.1:8888/ocsp -subjectAltName = @san -[ san ] -IP.1 = 127.0.0.1 -DNS.1 = localhost - -[ v3_client ] -basicConstraints = critical,CA:FALSE -keyUsage = critical,digitalSignature,keyEncipherment -extendedKeyUsage = clientAuth -# Make the AIA field >127 bytes to test CVE-2017-15698 -authorityInfoAccess = OCSP;URI:http://127.0.0.1:8889/ocsp/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - -[ v3_ocsp ] -basicConstraints = critical,CA:FALSE -keyUsage = critical,digitalSignature -extendedKeyUsage = OCSPSigning -EOF -printf "Done.\r\n" - -cd "$WORK_DIR" || (printf "Something went wrong.\r\n" && exit) - -printf "Generating CA key and certificate...\r\n" -openssl genrsa -out private/ca.key.pem 2048 -openssl req -x509 -new -nodes -key private/ca.key.pem -days 3650 -subj "/CN=Test CA" -config openssl.cnf -extensions v3_ca -out certs/ca-cert.pem -printf "Done.\r\n" - -printf "Generating server key and certificate...\r\n" -openssl genrsa -out private/server.key.pem 2048 -openssl req -new -key private/server.key.pem -out server.csr.pem -subj "/CN=localhost" -openssl ca -batch -config openssl.cnf -extensions v3_server -in server.csr.pem -out certs/server.cert.pem -days 365 -printf "Done.\r\n" - -printf "Generating OCSP responder key and certificate...\r\n" -openssl genrsa -out private/ocsp.key.pem 2048 -openssl req -new -key private/ocsp.key.pem -out ocsp.csr.pem -subj "/CN=Test OCSP Responder" -openssl ca -batch -config openssl.cnf -extensions v3_ocsp -in ocsp.csr.pem -out certs/ocsp.cert.pem -days 365 -printf "Done.\r\n" - -printf "Building OCSP request for the server certificate...\r\n" -openssl ocsp -issuer certs/ca-cert.pem -cert certs/server.cert.pem -no_nonce -reqout request.der -printf "Done.\r\n" - -printf "Answering request with good status (ocsp-good.der)...\r\n" -openssl ocsp -index index -CA certs/ca-cert.pem -rsigner certs/ocsp.cert.pem -rkey private/ocsp.key.pem -no_nonce -ndays 365 -reqin request.der -respout ../ocsp-good.der -printf "Done.\r\n" - -printf "Revoking the server certificate in the CA database...\r\n" -openssl ca -config openssl.cnf -revoke certs/server.cert.pem -crl_reason keyCompromise -printf "Done.\r\n" - -printf "Answering request with REVOKED status (ocsp-revoked.der)...\r\n" -openssl ocsp -index index -CA certs/ca-cert.pem -rsigner certs/ocsp.cert.pem -rkey private/ocsp.key.pem -no_nonce -ndays 365 -reqin request.der -respout ../ocsp-revoked.der -printf "Done.\r\n" - -cp certs/ca-cert.pem .. -cp private/server.key.pem ../server-key.pem -cp certs/server.cert.pem ../server-cert.pem - -printf "Creating PKCS12 client's truststore (trustStore.p12) with the CA...\r\n" -rm -f ../trustStore.p12 -echo "$PASS" > ../trust-password -keytool -importcert -alias ocsp-ca -file certs/ca-cert.pem -keystore ../trustStore.p12 -storetype PKCS12 -storepass "$PASS" -noprompt -printf "Done.\r\n" - -printf "Generating client key and certificate...\r\n" -openssl genrsa -out private/client.key.pem 2048 -openssl req -new -key private/client.key.pem -out client.csr.pem -subj "/CN=test-client" -openssl ca -batch -config openssl.cnf -extensions v3_client -in client.csr.pem -out certs/client.cert.pem -days 365 -printf "Done.\r\n" - -printf "Building OCSP request for the CLIENT certificate...\r\n" -openssl ocsp -issuer certs/ca-cert.pem -cert certs/client.cert.pem -no_nonce -reqout client-request.der -printf "Done.\r\n" - -printf "Answering request with good status for client (ocsp-client-good.der)...\r\n" -openssl ocsp -index index -CA certs/ca-cert.pem -rsigner certs/ocsp.cert.pem -rkey private/ocsp.key.pem -no_nonce -ndays 365 -reqin client-request.der -respout ../ocsp-client-good.der -printf "Done.\r\n" - -printf "Revoking the client certificate in the CA database...\r\n" -openssl ca -config openssl.cnf -revoke certs/client.cert.pem -crl_reason keyCompromise -printf "Done.\r\n" - -printf "Answering request with REVOKED status for client (ocsp-client-revoked.der)...\r\n" -openssl ocsp -index index -CA certs/ca-cert.pem -rsigner certs/ocsp.cert.pem -rkey private/ocsp.key.pem -no_nonce -ndays 365 -reqin client-request.der -respout ../ocsp-client-revoked.der -printf "Done.\r\n" - -printf "Creating PKCS12 client keystore for mutual TLS...\r\n" -echo "$PASS" > ../client-password -openssl pkcs12 -export -name ocsp-client -out ../client-keystore.p12 -inkey private/client.key.pem -in certs/client.cert.pem -certfile certs/ca-cert.pem -passout pass:"$PASS" -printf "Done.\r\n" - -printf "\r\nOptional verification:\r\n" -printf " openssl ocsp -respin ocsp-good.der -verify_other ocsp-work/certs/ocsp.cert.pem -CAfile ca-cert.pem\r\n" -printf " openssl ocsp -respin ocsp-revoked.der -verify_other ocsp-work/certs/ocsp.cert.pem -CAfile ca-cert.pem\r\n" \ No newline at end of file Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/ocsp-client-good.der and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/ocsp-client-good.der differ Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/ocsp-client-revoked.der and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/ocsp-client-revoked.der differ Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/ocsp-good.der and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/ocsp-good.der differ Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/ocsp-revoked.der and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/ocsp-revoked.der differ diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/server-cert.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/server-cert.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/server-cert.pem 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/server-cert.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,86 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 4096 (0x1000) - Signature Algorithm: sha256WithRSAEncryption - Issuer: CN=Test CA - Validity - Not Before: Oct 9 17:50:19 2025 GMT - Not After : Oct 9 17:50:19 2026 GMT - Subject: CN=localhost - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) - Modulus: - 00:b3:a6:2f:95:ec:a3:9f:18:df:92:02:2f:f6:d8: - e5:90:0d:45:b7:9b:fb:2c:63:62:a0:5d:06:a2:c6: - 5a:38:d0:02:7e:bf:ae:22:05:5a:c9:83:e1:5f:7b: - 74:00:b6:7e:58:e2:1e:40:29:b1:5f:2c:3c:5a:c2: - 5a:04:bb:4e:0f:1a:ae:f0:bc:cb:16:49:c4:6a:59: - 2e:56:e7:73:e0:c1:01:72:b0:d5:a0:86:b0:f5:77: - 16:24:57:9b:51:24:97:af:bc:3a:2c:9f:c2:89:b0: - cc:f2:d9:f9:1d:6d:25:90:2a:1a:36:3e:cb:8a:13: - 7c:e2:99:6d:7e:a1:ef:a8:36:b1:3b:75:36:14:88: - 3e:32:7b:c0:5a:55:ec:2c:9f:f8:32:f7:55:86:22: - 46:ea:ba:19:46:d0:e0:77:df:f3:95:d3:98:f8:62: - 1c:b7:fe:11:01:e7:bb:5d:82:d9:ee:32:44:9f:88: - e1:7d:f9:20:83:02:f8:8d:4d:76:26:69:c7:b1:fd: - 82:7f:a9:c9:0a:8d:26:a4:18:05:ee:04:e9:61:04: - 9f:e5:80:cc:f4:d5:3d:f7:d3:ce:1f:00:27:b5:d1: - 09:b6:cb:93:36:b4:4c:58:e3:65:f6:85:4d:51:55: - 5f:25:01:35:b5:a7:5a:44:7f:0b:69:70:5d:8e:eb: - ff:b9 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Basic Constraints: critical - CA:FALSE - X509v3 Key Usage: critical - Digital Signature, Key Encipherment - X509v3 Extended Key Usage: - TLS Web Server Authentication - Authority Information Access: - OCSP - URI:http://127.0.0.1:8888/ocsp - X509v3 Subject Alternative Name: - IP Address:127.0.0.1, DNS:localhost - X509v3 Subject Key Identifier: - F0:0D:29:83:76:05:84:3A:15:D1:D7:3D:7C:80:B1:A2:B9:B2:34:E9 - X509v3 Authority Key Identifier: - C8:FA:78:96:74:8D:80:68:85:60:FB:80:93:17:68:3A:A6:B6:FC:A8 - Signature Algorithm: sha256WithRSAEncryption - Signature Value: - 55:20:97:f2:f9:44:63:65:50:f5:a2:92:99:d2:ef:90:1b:97: - aa:0d:3a:bb:6f:a1:d1:dd:99:64:86:f0:ba:f2:12:61:b6:c4: - 22:20:f1:5b:b4:8c:19:68:57:18:a4:63:47:52:e9:2e:d1:68: - b2:de:da:02:a8:4f:8f:ec:c1:d4:f7:e4:69:09:25:de:d2:60: - 5f:bf:e9:fe:12:74:ae:f1:25:59:04:53:e1:a3:3c:b9:c2:99: - 91:78:3d:79:a3:29:f9:3a:5b:59:32:b1:2c:c3:f0:3f:c2:49: - 14:36:b9:3e:c5:3f:47:1c:14:bc:da:4f:39:2d:e1:16:f6:a6: - c7:fa:f4:b8:bb:95:d9:49:b7:0b:51:ae:9b:67:b6:01:c2:30: - aa:db:17:21:6f:64:cd:2d:ff:ea:9a:ce:4a:a7:44:f5:8f:a7: - b5:f8:87:48:bf:03:8e:3e:ab:8e:44:e7:a6:64:9a:f2:06:5a: - 33:21:e1:01:8c:bf:c7:61:36:90:e4:5f:b6:e7:26:55:9e:44: - 7c:d8:52:75:f9:1b:df:7a:ef:ea:b1:a3:08:19:ae:e1:39:42: - ec:d2:ad:dc:63:a8:f4:54:db:8d:9f:12:ea:fe:94:cf:af:eb: - 88:66:bb:3c:cf:a9:a3:5e:4a:c5:09:94:f8:4e:03:a2:01:a1: - e5:24:92:eb ------BEGIN CERTIFICATE----- -MIIDbTCCAlWgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHVGVz -dCBDQTAeFw0yNTEwMDkxNzUwMTlaFw0yNjEwMDkxNzUwMTlaMBQxEjAQBgNVBAMM -CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALOmL5Xs -o58Y35ICL/bY5ZANRbeb+yxjYqBdBqLGWjjQAn6/riIFWsmD4V97dAC2fljiHkAp -sV8sPFrCWgS7Tg8arvC8yxZJxGpZLlbnc+DBAXKw1aCGsPV3FiRXm1Ekl6+8Oiyf -womwzPLZ+R1tJZAqGjY+y4oTfOKZbX6h76g2sTt1NhSIPjJ7wFpV7Cyf+DL3VYYi -Ruq6GUbQ4Hff85XTmPhiHLf+EQHnu12C2e4yRJ+I4X35IIMC+I1NdiZpx7H9gn+p -yQqNJqQYBe4E6WEEn+WAzPTVPffTzh8AJ7XRCbbLkza0TFjjZfaFTVFVXyUBNbWn -WkR/C2lwXY7r/7kCAwEAAaOByjCBxzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQE -AwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATA2BggrBgEFBQcBAQQqMCgwJgYIKwYB -BQUHMAGGGmh0dHA6Ly8xMjcuMC4wLjE6ODg4OC9vY3NwMBoGA1UdEQQTMBGHBH8A -AAGCCWxvY2FsaG9zdDAdBgNVHQ4EFgQU8A0pg3YFhDoV0dc9fICxormyNOkwHwYD -VR0jBBgwFoAUyPp4lnSNgGiFYPuAkxdoOqa2/KgwDQYJKoZIhvcNAQELBQADggEB -AFUgl/L5RGNlUPWikpnS75Abl6oNOrtvodHdmWSG8LryEmG2xCIg8Vu0jBloVxik -Y0dS6S7RaLLe2gKoT4/swdT35GkJJd7SYF+/6f4SdK7xJVkEU+GjPLnCmZF4PXmj -Kfk6W1kysSzD8D/CSRQ2uT7FP0ccFLzaTzkt4Rb2psf69Li7ldlJtwtRrptntgHC -MKrbFyFvZM0t/+qazkqnRPWPp7X4h0i/A44+q45E56ZkmvIGWjMh4QGMv8dhNpDk -X7bnJlWeRHzYUnX5G9967+qxowgZruE5QuzSrdxjqPRU242fEur+lM+v64hmuzzP -qaNeSsUJlPhOA6IBoeUkkus= ------END CERTIFICATE----- diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/server-key.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/server-key.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/server-key.pem 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/server-key.pem 1970-01-01 00:00:00.000000000 +0000 @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCzpi+V7KOfGN+S -Ai/22OWQDUW3m/ssY2KgXQaixlo40AJ+v64iBVrJg+Ffe3QAtn5Y4h5AKbFfLDxa -wloEu04PGq7wvMsWScRqWS5W53PgwQFysNWghrD1dxYkV5tRJJevvDosn8KJsMzy -2fkdbSWQKho2PsuKE3zimW1+oe+oNrE7dTYUiD4ye8BaVewsn/gy91WGIkbquhlG -0OB33/OV05j4Yhy3/hEB57tdgtnuMkSfiOF9+SCDAviNTXYmacex/YJ/qckKjSak -GAXuBOlhBJ/lgMz01T33084fACe10Qm2y5M2tExY42X2hU1RVV8lATW1p1pEfwtp -cF2O6/+5AgMBAAECggEABdjR0apbBPGVTTY/A2S5y/9ylnBAM4ikjB2a+q9D/nde -rWtlqnvMIaTmL+pfsyo9YH0ziNkSictxx25t6ZuDBeDTcshiP7504xU1+eQclGMT -vfzdZxUbK1IN6W7kgjaTrUeOCSfF+B9F/F11yyxjPZbxNXTDwSGzPdjKhIWWvD3F -T0xUHKm+XKHWmEveporOmH1M6wxVfZ8NbAqWYRhFaUp+GZwjv2M+trXdyDmlNPWQ -k66FdZNM+Roa2nGVcgXDdue/+Xah5eNrNzIm7ilodBQzS7Z5ADYVFktyBTGfRCs3 -Uc2BUGZrVZSRu10DybZo+0XE7HLgdMzlTxTax65hBQKBgQD8ltIYCRBzbRn7EC9O -tbmyKCutxUCNe8t5cLm6iFTILOR2DZSsQH8cav0uGNMgXo0oHvY2H49xw5DqfSy2 -jcVhIKXoDKvHOrU7Ph/attIvyTTizprupAEi7dnZGf0sENQBR1BuSRMGv4B8hYKL -51PfkspTBtsaZZhDAjJlYjYZVwKBgQC2EzfFeNKF9IZ4Gfexsrvj84Y8S3kfkxzT -jZkT+JZFRDhNCA7tORZJcriQnitK1M2dA0L1xNFQH+5mVLnn2YwZMgWGYWWKNfD9 -r/oS7yVy9qr3yf5i+XHk/Dr4LvDsyYlGuRLVfLltcggj5g84JTYabvVzo6j5ss/3 -VVbzMIg1bwKBgQDjDTP31RYLm/Pxwf+0chhldESnJJu9Up16IYFikrAbbHdFQzn0 -iNl6ExZY/Im0HCoo+YP67O3FprU6g6DQzKmzgGSCLfmv0i8c/OAne5V+zRAUHQIa -KCS5YDMmO31fwhTAvXkoWdmXhEccJ+tMlXnIjCwA7DXCCbcP7QaeKeVVawKBgQCw -HPCiOpbvaTE0NHHo2OhyuhgKdDpJd0O3wUvjDF1VzIHkyLmfbcuH6cZqZAOeEy57 -BC+dh+2qYeh35NYZU8z2hfLgI49S25Ap4jCyZc1EYSHIIgLEe2FWSz9C1izF7L6y -wMtd4pF9MoJ7Lslj1mJ5uQAEBbapJ/OO2mYLPtNRUwKBgQCC4aV+boMhDfvYpAir -PTk+od7rBBcBZnV2dnqz4BHl7cfZCzD2GmJesgBLEBuf3r3sMGov3tItLkCteCqt -cc3vwC+tNCpz8INLUMCR8i64J4ZgrwUgUbmt+myoAIDx/46GVGsknvuzSabLRH3h -L9eCqiIiXCbiXjWw4vDfkXqdIA== ------END PRIVATE KEY----- diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/trust-password tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/trust-password --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/trust-password 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/trust-password 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -changeit Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp/trustStore.p12 and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp/trustStore.p12 differ diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp-responder-rsa-cert.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp-responder-rsa-cert.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp-responder-rsa-cert.pem 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp-responder-rsa-cert.pem 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,105 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 4101 (0x1005) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=DE, L=Wilmington, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=Apache Tomcat Test CA + Validity + Not Before: Dec 5 19:33:55 2025 GMT + Not After : Dec 5 19:33:55 2027 GMT + Subject: C=US, ST=DE, L=Wilmington, O=The Apache Software Foundation, OU=Apache Tomcat PMC, CN=OCSP Responder + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:9b:ae:b1:40:e5:e4:84:b6:c3:ec:93:cf:23:63: + 51:51:7f:bd:d3:64:1d:e0:23:bb:33:44:f1:47:c6: + f5:d2:4d:2a:13:2a:f4:4c:d2:44:13:56:03:73:16: + 30:98:be:66:b1:fb:3c:e9:9c:5a:61:25:b8:47:ef: + 72:04:ad:ab:6a:f8:83:fe:00:a6:c0:8b:a3:ef:34: + 09:4f:13:e6:fa:96:d3:2b:24:db:e3:d1:4a:40:50: + 5f:9f:8f:31:fa:de:42:0d:e6:a2:4a:7a:e6:a6:48: + 5a:97:04:b3:b9:bd:52:2e:2f:e9:02:27:1b:8f:bf: + 2b:19:28:e8:96:eb:29:d0:6e:39:0b:d9:bf:2a:d2: + 3e:65:a4:e0:87:30:ab:26:09:01:ea:4a:a7:a2:38: + 09:0e:f5:d3:b8:66:0a:cc:17:61:72:fb:a9:5e:e4: + 98:ff:30:e4:93:11:f7:6e:13:f3:76:f6:f4:44:b7: + a8:2a:79:b3:4f:59:d8:67:b7:c0:4c:cd:50:57:d2: + 09:65:f5:5e:2d:3b:e7:29:bf:e3:11:ff:37:1d:ad: + f2:cf:3b:ff:f7:49:d1:1a:05:1d:2b:0e:59:fe:fe: + e6:6a:5d:73:7f:0b:8c:b3:6d:c3:65:2a:93:f8:87: + b7:fd:c2:4a:e3:b8:d5:e6:55:8b:b3:e5:0e:4f:f4: + fa:a7 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Key Usage: critical + Digital Signature + X509v3 Extended Key Usage: + OCSP Signing + X509v3 Subject Key Identifier: + FA:23:45:E2:94:8E:6A:88:B8:72:B1:53:EA:2A:AE:B9:39:F8:6A:C8 + X509v3 Authority Key Identifier: + AC:87:DC:8B:3D:8A:DB:89:48:7E:6F:3B:1C:C6:70:92:09:17:6F:F9 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 3b:39:47:9e:28:f1:c0:46:eb:a8:de:0e:44:63:4d:3c:f4:59: + c1:a4:2a:eb:6a:e5:50:76:ea:76:f7:a9:ca:a2:cb:70:91:c9: + d3:c5:fd:d6:c9:86:3d:ad:f2:b8:a7:e4:b1:93:09:44:eb:27: + 30:01:85:13:db:0a:62:f0:0f:fa:59:de:af:93:1a:9e:df:07: + da:b8:ec:19:c1:c9:56:f9:a6:8e:4e:03:ae:e0:f9:d8:d2:b7: + d2:94:4a:70:95:b7:a8:7f:42:b2:1d:43:77:3a:9e:f8:76:93: + 4b:3a:48:09:5a:8b:3b:2c:38:97:ed:27:6b:d5:31:0b:f0:3d: + a9:dc:d4:0b:96:55:07:41:59:e1:1f:68:70:47:04:6a:11:a4: + 81:e7:7d:75:08:15:9f:ca:20:20:69:fe:ef:50:3f:cf:02:0b: + 96:4a:9e:e7:ce:08:07:8a:c0:93:a6:aa:a9:91:53:ac:5e:80: + 3b:bd:6f:cb:d2:7c:e3:9b:cd:ff:b0:12:ec:9a:71:48:e1:01: + 96:21:f4:ee:bb:f8:1d:99:77:00:68:ef:e3:bc:5d:1d:58:bc: + 91:88:51:39:93:b1:91:88:5c:d0:9d:0f:87:0b:0d:04:ca:be: + 79:05:b9:42:ae:b3:62:df:7a:02:d7:d5:4e:a8:27:8f:0b:b8: + 4c:aa:d5:07:a6:e4:65:b0:13:78:cd:3a:3b:10:58:49:13:d7: + 74:88:76:8a:77:a4:d4:24:01:61:fe:0b:46:fb:4e:15:3a:fa: + 2b:c3:ca:10:9c:5f:2b:f5:33:21:93:16:be:a4:c5:bd:a4:80: + 88:74:2a:1e:09:d4:2a:c6:af:ed:be:46:7f:b8:d7:ad:a6:e0: + 8f:92:ae:2b:8a:97:ca:9c:fb:21:29:48:f1:38:98:09:f9:2b: + 55:53:7c:99:a5:23:21:58:35:1a:36:15:67:79:80:3f:fc:94: + 60:94:2b:78:a5:f1:81:b4:51:5c:08:1a:50:24:21:da:0b:cc: + db:d7:3c:7d:d8:2c:b1:93:9d:f4:94:bb:fe:37:ad:8d:3f:06: + 9d:83:b0:e5:f2:ef:e4:88:75:e9:50:3a:f3:ef:aa:e4:00:54: + ad:1d:cd:a7:6b:ec:0e:b7:31:92:82:da:0d:4b:c3:27:ee:7a: + a8:f4:e0:a9:0a:f1:40:61:a0:09:a5:fb:24:f9:68:34:28:a9: + 1b:b6:5a:bd:aa:3a:c4:b8:89:92:ba:92:9c:81:f8:a8:ff:5a: + 0c:ef:af:97:b4:86:09:71:e8:13:28:1c:89:16:6f:43:de:1b: + 2f:1d:16:9f:37:7f:9c:5f:4e:4a:a8:22:4b:77:9e:f6:94:f8: + d9:05:4b:bc:a6:aa:f7:6e +-----BEGIN CERTIFICATE----- +MIIFFDCCAvygAwIBAgICEAUwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT +MQswCQYDVQQIEwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUGA1UEChMeVGhl +IEFwYWNoZSBTb2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFBcGFjaGUgVG9t +Y2F0IFBNQzEeMBwGA1UEAxMVQXBhY2hlIFRvbWNhdCBUZXN0IENBMB4XDTI1MTIw +NTE5MzM1NVoXDTI3MTIwNTE5MzM1NVowgY0xCzAJBgNVBAYTAlVTMQswCQYDVQQI +EwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUGA1UEChMeVGhlIEFwYWNoZSBT +b2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFBcGFjaGUgVG9tY2F0IFBNQzEX +MBUGA1UEAxMOT0NTUCBSZXNwb25kZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCbrrFA5eSEtsPsk88jY1FRf73TZB3gI7szRPFHxvXSTSoTKvRM0kQT +VgNzFjCYvmax+zzpnFphJbhH73IEratq+IP+AKbAi6PvNAlPE+b6ltMrJNvj0UpA +UF+fjzH63kIN5qJKeuamSFqXBLO5vVIuL+kCJxuPvysZKOiW6ynQbjkL2b8q0j5l +pOCHMKsmCQHqSqeiOAkO9dO4ZgrMF2Fy+6le5Jj/MOSTEfduE/N29vREt6gqebNP +Wdhnt8BMzVBX0gll9V4tO+cpv+MR/zcdrfLPO//3SdEaBR0rDln+/uZqXXN/C4yz +bcNlKpP4h7f9wkrjuNXmVYuz5Q5P9PqnAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAw +DgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMJMB0GA1UdDgQWBBT6 +I0XilI5qiLhysVPqKq65OfhqyDAfBgNVHSMEGDAWgBSsh9yLPYrbiUh+bzscxnCS +CRdv+TANBgkqhkiG9w0BAQsFAAOCAgEAOzlHnijxwEbrqN4ORGNNPPRZwaQq62rl +UHbqdvepyqLLcJHJ08X91smGPa3yuKfksZMJROsnMAGFE9sKYvAP+lner5Mant8H +2rjsGcHJVvmmjk4DruD52NK30pRKcJW3qH9Csh1Ddzqe+HaTSzpICVqLOyw4l+0n +a9UxC/A9qdzUC5ZVB0FZ4R9ocEcEahGkged9dQgVn8ogIGn+71A/zwILlkqe584I +B4rAk6aqqZFTrF6AO71vy9J845vN/7AS7JpxSOEBliH07rv4HZl3AGjv47xdHVi8 +kYhROZOxkYhc0J0PhwsNBMq+eQW5Qq6zYt96AtfVTqgnjwu4TKrVB6bkZbATeM06 +OxBYSRPXdIh2inek1CQBYf4LRvtOFTr6K8PKEJxfK/UzIZMWvqTFvaSAiHQqHgnU +Ksav7b5Gf7jXrabgj5KuK4qXypz7ISlI8TiYCfkrVVN8maUjIVg1GjYVZ3mAP/yU +YJQreKXxgbRRXAgaUCQh2gvM29c8fdgssZOd9JS7/jetjT8GnYOw5fLv5Ih16VA6 +8++q5ABUrR3Np2vsDrcxkoLaDUvDJ+56qPTgqQrxQGGgCaX7JPloNCipG7Zavao6 +xLiJkrqSnIH4qP9aDO+vl7SGCXHoEygciRZvQ94bLx0Wnzd/nF9OSqgiS3ee9pT4 +2QVLvKaq924= +-----END CERTIFICATE----- diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp-responder-rsa-key.pem tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp-responder-rsa-key.pem --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/ocsp-responder-rsa-key.pem 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/ocsp-responder-rsa-key.pem 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCbrrFA5eSEtsPs +k88jY1FRf73TZB3gI7szRPFHxvXSTSoTKvRM0kQTVgNzFjCYvmax+zzpnFphJbhH +73IEratq+IP+AKbAi6PvNAlPE+b6ltMrJNvj0UpAUF+fjzH63kIN5qJKeuamSFqX +BLO5vVIuL+kCJxuPvysZKOiW6ynQbjkL2b8q0j5lpOCHMKsmCQHqSqeiOAkO9dO4 +ZgrMF2Fy+6le5Jj/MOSTEfduE/N29vREt6gqebNPWdhnt8BMzVBX0gll9V4tO+cp +v+MR/zcdrfLPO//3SdEaBR0rDln+/uZqXXN/C4yzbcNlKpP4h7f9wkrjuNXmVYuz +5Q5P9PqnAgMBAAECggEAALCpC3sZZLc0rh9Hm0YM7boNutqmQUCXS1ZiZWmN3GVI +KvaVR6Pk9lr6v+9YvsyVQvO0pOpzAhw7MWJv1HJ6oIpCd4VEN/VGgEBwTB/v9atm +ezn6GYvrctaIXfoyBAUpMMuVa5QY4qLOd/3m2AttSlQfCcnnlt8Hj2B9i3G2aTya +EgwH9GKBoQ0FIMP3Znytx2YEj2dMk2wxApafNhxpZ302JZgvQG163UovQeGfJXvN +d0xwtA8Xyzuv+/l1Bx8KyD0dYjeA6++khJ/oDNkM1+JKfN6qRzIhcNYWZAx6Au78 +tJwhBuUVqpbZnPkMXwyFjnAMvzSRcJrGIHdrIyPsWQKBgQDIgbcgiECHldkdrIUF +3qKluX8g8mLTGts/wXG20iY730KThAnbxJ0ORPD5S3HZDSKZkG31ohoZHH7YT9iC +0+pnu4bEhqtfPttXIK9q3PQh6i5jhiENc2x0NvJ0nUg8DkVqVmnmC3eIVIHfnnUU +PDhe0GjHC02TegBFXfOFz3LDSQKBgQDGxRa4tKtB/0BeJCu4RD7pHuikt+NUKS7n +4F3QBpyIzchjImIlHbutFwIbmUQ6C5TzEcgnTDAlJadBmljZYemaX5RN1UEPulPT +NvSOdGdxyhJmKpFX+umUbypAcpKC6+EElaOuCUphMDOISIyooKUtJqWUXTVgO60x +2vh94BHebwKBgGpIL/0DnEkCikLrdtukpsx35kZdlTrXio5iCNfizzd/Ybf2Do+6 +yZGNw7oxXpiyGLwTzeWdVn4nF6mrsVWv5Rm1UnuL8v0awYOOpM1db98mVg6VQ6Hw +o/V6Rsy/rlF/MxZp1dqGC4dtXCZfxSnDvhGsIU4Y1LcuvUQHyBUO4INZAoGAPATV +GI7NS49Uk1iervREfsXrf5lbFlWdKT0RdrgYWiKxCGYgdo6k1d4lr8m21UQcBM1w +v5T80Kqu91swyusKy6dis6HaFHKxzwxACifR+IUIyzq9SnCkSULL4kv2O3wYJPc+ +RlXd1LzmQpeNiXmAhsKtqr/+VwGOCYjSEcgv/RcCgYBfUrsTG3dgWUVKviFzS63R +nb0T9yKZij79HkIM0bROpaU5atTgQa/iwvSktw8mHGuJkjQZ6BqVpKHbDOq/Y/zB +NihYIlf4WUPlQVr/sBLzfTtaxBKcnuVm1d8igKK3qQ0XuH5jk0gD6tzaU4ySTJ+P +ldxvbWraHRcPzn08xJ6+UA== +-----END PRIVATE KEY----- diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java 2026-05-01 18:56:05.000000000 +0000 @@ -362,8 +362,6 @@ "AES256-CCM8+TLSv1.2", "ARIA128-GCM-SHA256+TLSv1.2", "ARIA256-GCM-SHA384+TLSv1.2", - "DES-CBC-MD5+SSLv2", - "DES-CBC3-MD5+SSLv2", "DHE-DSS-ARIA128-GCM-SHA256+TLSv1.2", "DHE-DSS-ARIA256-GCM-SHA384+TLSv1.2", "DHE-PSK-AES128-CCM+TLSv1.2", @@ -403,9 +401,6 @@ "ECDHE-RSA-CAMELLIA128-SHA256+TLSv1.2", "ECDHE-RSA-CAMELLIA256-SHA384+TLSv1.2", "ECDHE-RSA-CHACHA20-POLY1305+TLSv1.2", - "EXP-RC2-CBC-MD5+SSLv2", - "EXP-RC4-MD5+SSLv2", - "IDEA-CBC-MD5+SSLv2", "PSK-AES128-CCM+TLSv1.2", "PSK-AES128-CCM8+TLSv1.2", "PSK-AES256-CCM+TLSv1.2", @@ -415,8 +410,6 @@ "PSK-CAMELLIA128-SHA256+TLSv1", "PSK-CAMELLIA256-SHA384+TLSv1", "PSK-CHACHA20-POLY1305+TLSv1.2", - "RC2-CBC-MD5+SSLv2", - "RC4-MD5+SSLv2", "RSA-PSK-ARIA128-GCM-SHA256+TLSv1.2", "RSA-PSK-ARIA256-GCM-SHA384+TLSv1.2", "RSA-PSK-CAMELLIA128-SHA256+TLSv1", @@ -576,8 +569,6 @@ "CAMELLIA256-SHA+SSLv3", "CAMELLIA128-SHA256+TLSv1.2", "CAMELLIA256-SHA256+TLSv1.2", - "DES-CBC-MD5+SSLv2", - "DES-CBC3-MD5+SSLv2", "DH-DSS-AES128-GCM-SHA256+TLSv1.2", "DH-DSS-AES256-GCM-SHA384+TLSv1.2", "DH-DSS-AES128-SHA+SSLv3", @@ -675,9 +666,6 @@ "ECDHE-RSA-CHACHA20-POLY1305+TLSv1.2", "EXP-DH-DSS-DES-CBC-SHA+SSLv3", "EXP-DH-RSA-DES-CBC-SHA+SSLv3", - "EXP-RC2-CBC-MD5+SSLv2", - "EXP-RC4-MD5+SSLv2", - "IDEA-CBC-MD5+SSLv2", "IDEA-CBC-SHA+SSLv3", "PSK-3DES-EDE-CBC-SHA+SSLv3", "PSK-AES128-CBC-SHA+SSLv3", @@ -699,8 +687,6 @@ "PSK-NULL-SHA256+TLSv1", "PSK-NULL-SHA384+TLSv1", "PSK-RC4-SHA+SSLv3", - "RC2-CBC-MD5+SSLv2", - "RC4-MD5+SSLv2", "RSA-PSK-3DES-EDE-CBC-SHA+SSLv3", "RSA-PSK-AES128-CBC-SHA+SSLv3", "RSA-PSK-AES128-CBC-SHA256+TLSv1", diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java 2026-05-01 18:56:05.000000000 +0000 @@ -89,18 +89,6 @@ @Test - public void testEXPORT40() throws Exception { - testSpecification("EXPORT40"); - } - - - @Test - public void testEXPORT() throws Exception { - testSpecification("EXPORT"); - } - - - @Test public void testRSA() throws Exception { testSpecification("RSA"); } @@ -245,30 +233,6 @@ @Test - public void testkFZA() throws Exception { - testSpecification("kFZA"); - } - - - @Test - public void testaFZA() throws Exception { - testSpecification("aFZA"); - } - - - @Test - public void testeFZA() throws Exception { - testSpecification("eFZA"); - } - - - @Test - public void testFZA() throws Exception { - testSpecification("FZA"); - } - - - @Test public void testTLSv1_2() throws Exception { testSpecification("TLSv1.2"); } @@ -282,17 +246,15 @@ @Test public void testSSLv3() throws Exception { + if (TesterOpenSSL.VERSION < 40000) { + return; + } + // As of OpenSSL 4.0.0, support for the SSLv3 alias has been removed testSpecification("SSLv3"); } @Test - public void testSSLv2() throws Exception { - testSpecification("SSLv2"); - } - - - @Test public void testDH() throws Exception { testSpecification("DH"); } @@ -431,12 +393,6 @@ @Test - public void testKRB5() throws Exception { - testSpecification("KRB5"); - } - - - @Test public void testaGOST() throws Exception { testSpecification("aGOST"); } @@ -515,6 +471,12 @@ @Test + public void testARIAGCM() throws Exception { + testSpecification("ARIAGCM"); + } + + + @Test public void testARIA128() throws Exception { testSpecification("ARIA128"); } @@ -526,6 +488,12 @@ } + @Test + public void testCBC() throws Exception { + testSpecification("CBC"); + } + + // TODO: Add tests for the individual operators @Test @@ -535,17 +503,17 @@ // a number of the reference browsers if (TesterOpenSSL.VERSION < 30200) { // OpenSSL 3.2.x moved the CCM8 ciphers from high to medium - testSpecification("HIGH:!AESCCM8:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5"); + testSpecification("HIGH:!AESCCM8:!aNULL:!eNULL:!DES:!RC4:!MD5"); } else { - testSpecification("HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5"); + testSpecification("HIGH:!aNULL:!eNULL:!DES:!RC4:!MD5"); } } @Test public void testSpecification02() throws Exception { - // Suggestion from dev list (s/ECDHE/kEECDH/, s/DHE/EDH/ - testSpecification("!aNULL:!eNULL:!EXPORT:!DSS:!DES:!SSLv2:kEECDH:ECDH:EDH:AES256-GCM-SHA384:AES128-GCM-SHA256:+RC4:HIGH:aRSA:kECDHr:MEDIUM"); + // Suggestion from dev list (s/ECDHE/kEECDH/, s/DHE/EDH/, s/\!SSLv2//, s/\!EXPORT//) + testSpecification("!aNULL:!eNULL:!DSS:!DES:kEECDH:ECDH:EDH:AES256-GCM-SHA384:AES128-GCM-SHA256:+RC4:HIGH:aRSA:kECDHr:MEDIUM"); } @@ -564,13 +532,80 @@ public void testSpecification04() throws Exception { if (TesterOpenSSL.VERSION < 30200) { // OpenSSL 3.2.x moved the CCM8 ciphers from high to medium - testSpecification("HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK:!DSS:!SHA1:!SHA256:!SHA384:!AESCCM8"); + testSpecification("HIGH:!aNULL:!eNULL:!DES:!RC4:!3DES:!MD5:!PSK:!DSS:!SHA1:!SHA256:!SHA384:!AESCCM8"); } else { - testSpecification("HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK:!DSS:!SHA1:!SHA256:!SHA384:"); + testSpecification("HIGH:!aNULL:!eNULL:!DES:!RC4:!3DES:!MD5:!PSK:!DSS:!SHA1:!SHA256:!SHA384:"); } } + @Test + public void testSpecificationIsEmptyNonsense() throws Exception { + testSpecificationIsEmpty("Nonsense"); + } + + + @Test + public void testSpecificationIsEmptySSLv2() throws Exception { + testSpecificationIsEmpty("SSLv2"); + } + + + @Test + public void testSpecificationIsEmptyEXPORT() throws Exception { + testSpecificationIsEmpty("EXPORT"); + } + + + @Test + public void testSpecificationIsEmptyEXPORT40() throws Exception { + testSpecificationIsEmpty("EXPORT40"); + } + + + @Test + public void testSpecificationIsEmptyEXPORT56() throws Exception { + testSpecificationIsEmpty("EXPORT56"); + } + + + @Test + public void testSpecificationIsEmptyKRB5() throws Exception { + testSpecificationIsEmpty("KRB5"); + } + + + @Test + public void testSpecificationIsEmptykFZA() throws Exception { + testSpecificationIsEmpty("kFZA"); + } + + + @Test + public void testSpecificationIsEmptyaFZA() throws Exception { + testSpecificationIsEmpty("aFZA"); + } + + + @Test + public void testSpecificationIsEmptyeFZA() throws Exception { + testSpecificationIsEmpty("eFZA"); + } + + + @Test + public void testSpecificationIsEmptyFZA() throws Exception { + testSpecificationIsEmpty("FZA"); + } + + + private void testSpecificationIsEmpty(String specification) throws Exception { + String openSSLCipherList = TesterOpenSSL.getOpenSSLCiphersAsExpression(specification); + Assert.assertEquals("Specification [" + specification + "] returned [" + openSSLCipherList + + "] rather than the expected empty list", "", openSSLCipherList); + } + + private void testSpecification(String specification) throws Exception { // Filter out cipher suites that OpenSSL does not implement String openSSLCipherList = TesterOpenSSL.getOpenSSLCiphersAsExpression(specification); diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java tomcat11-11.0.22/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java 2026-05-01 18:56:05.000000000 +0000 @@ -49,7 +49,11 @@ } catch (IOException ioe) { versionString = ""; } - if (versionString.startsWith("OpenSSL 3.6.")) { + if (versionString.startsWith("OpenSSL 4.1.")) { + VERSION = 40100; + } else if (versionString.startsWith("OpenSSL 4.0.")) { + VERSION = 40000; + } else if (versionString.startsWith("OpenSSL 3.6.")) { VERSION = 30600; } else if (versionString.startsWith("OpenSSL 3.5.")) { VERSION = 30500; @@ -58,12 +62,10 @@ VERSION = 30400; } else if (versionString.startsWith("OpenSSL 3.3.")) { VERSION = 30300; - } else if (versionString.startsWith("OpenSSL 3.2.")) { - VERSION = 30200; } else if (versionString.startsWith("OpenSSL 3.0.")) { VERSION = 30000; } else { - // Note: 3.1.x is no longer supported by OpenSSL + // Note: 3.2.x and 3.1.x are no longer supported by OpenSSL // Note: Release branches 1.1.1 and earlier are no longer supported by // the OpenSSL team so these tests don't support them either. VERSION = -1; @@ -244,13 +246,18 @@ public static Set getOpenSSLCiphersAsSet(String specification) throws Exception { - String[] ciphers = getOpenSSLCiphersAsExpression(specification).trim().split(":"); + String[] ciphers = getOpenSSLCiphersAsExpression(specification, true).trim().split(":"); Set result = new HashSet<>(Arrays.asList(ciphers)); return result; } public static String getOpenSSLCiphersAsExpression(String specification) throws Exception { + return getOpenSSLCiphersAsExpression(specification, false); + } + + + public static String getOpenSSLCiphersAsExpression(String specification, boolean withProtocol) throws Exception { List args = new ArrayList<>(); // Standard command to list the ciphers @@ -294,10 +301,12 @@ i++; } - // Protocol is the second - int j = cipher.indexOf(' ', i); - name.append('+'); - name.append(cipher.substring(i, j)); + if (withProtocol) { + // Optionally include the protocol, separated with a '+' + int j = cipher.indexOf(' ', i); + name.append('+'); + name.append(cipher.substring(i, j)); + } // More renames if (OPENSSL_RENAMED_CIPHERS.containsKey(name.toString())) { Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/user1.jks and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/user1.jks differ Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/user2-crl.jks and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/user2-crl.jks differ Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/org/apache/tomcat/util/net/user3-crl-long.jks and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/org/apache/tomcat/util/net/user3-crl-long.jks differ diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java tomcat11-11.0.22/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java --- tomcat11-11.0.15/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java 2026-05-01 18:56:05.000000000 +0000 @@ -17,14 +17,20 @@ package org.apache.tomcat.util.scan; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; +import java.util.Deque; import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import org.junit.Assert; import org.junit.Test; import org.apache.tomcat.Jar; @@ -57,6 +63,57 @@ scanner.scan(JarScanType.PLUGGABILITY, context, callback); } + @Test + public void testScanManifestDefault() throws Exception { + Assert.assertTrue("Referenced JAR from manifest Class-Path should be scanned", + doTestScanManifest(true)); + } + + @Test + public void testScanManifestDisabled() throws Exception { + Assert.assertFalse("Referenced JAR from manifest Class-Path should not be scanned", + doTestScanManifest(false)); + } + + private boolean doTestScanManifest(boolean scanManifest) throws Exception { + File referencedJar = new File(System.getProperty("java.io.tmpdir"), "referenced.jar"); + referencedJar.deleteOnExit(); + JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(referencedJar), new Manifest()); + jarOutputStream.close(); + + File testJar = new File(System.getProperty("java.io.tmpdir"), "manifest-test.jar"); + testJar.deleteOnExit(); + + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.CLASS_PATH, "referenced.jar"); + + jarOutputStream = new JarOutputStream(new FileOutputStream(testJar), manifest); + jarOutputStream.close(); + + StandardJarScanner scanner = new StandardJarScanner() { + @Override + protected void addClassPath(Deque classPathUrlsToProcess) { + super.addClassPath(classPathUrlsToProcess); + try { + classPathUrlsToProcess.add(testJar.toURI().toURL()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + }; + scanner.setScanManifest(scanManifest); + + LoggingCallback callback = new LoggingCallback(); + scanner.scan(JarScanType.PLUGGABILITY, new TesterServletContext(), callback); + + for (String cb : callback.callbacks) { + if (cb.contains("referenced.jar")) { + return true; + } + } + return false; + } private static class LoggingCallback implements JarScannerCallback { diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java tomcat11-11.0.22/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java --- tomcat11-11.0.15/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java 2026-05-01 18:56:05.000000000 +0000 @@ -58,11 +58,22 @@ * ProxyVia On * AllowCONNECT 0-65535 * - * Order deny,allow - * Allow from all * AuthType Basic * AuthName "Proxy Password Required" - * AuthUserFile password.file + * AuthUserFile "/etc/apache2/password.file" + * Require valid-user + * + * + * + * Listen 8890 + * + * ProxyRequests On + * ProxyVia On + * AllowCONNECT 0-65535 + * + * AuthType Digest + * AuthName "Proxy Password Required" + * AuthUserFile "/etc/apache2/password-digest.file" * Require valid-user * * @@ -71,16 +82,20 @@ * # htpasswd -c password.file proxy * New Password: proxy-pass * + * # htdigest -c password-digest.file proxy + * New Password: proxy-pass + * */ public class TesterWebSocketClientProxy extends WebSocketBaseTest { private static final String MESSAGE_STRING = "proxy-test-message"; - private static final String PROXY_ADDRESS = "192.168.0.200"; + private static final String PROXY_ADDRESS = "192.168.23.32"; private static final String PROXY_PORT_NO_AUTH = "8888"; - private static final String PROXY_PORT_AUTH = "8889"; + private static final String PROXY_PORT_BASIC_AUTH = "8889"; + private static final String PROXY_PORT_DIGEST_AUTH = "8890"; // The IP address of the test instance that is reachable from the proxy - private static final String TOMCAT_ADDRESS = "192.168.0.100"; + private static final String TOMCAT_ADDRESS = "192.168.23.12"; private static final String TOMCAT_USER = "tomcat"; private static final String TOMCAT_PASSWORD = "tomcat-pass"; @@ -91,35 +106,49 @@ @Test public void testConnectToServerViaProxyWithNoAuthentication() throws Exception { - doTestConnectToServerViaProxy(false, false); + doTestConnectToServerViaProxy(false, null); } @Test public void testConnectToServerViaProxyWithServerAuthentication() throws Exception { - doTestConnectToServerViaProxy(true, false); + doTestConnectToServerViaProxy(true, null); + } + + + @Test + public void testConnectToServerViaProxyWithProxyBasicAuthentication() throws Exception { + doTestConnectToServerViaProxy(false, "basic"); + } + + + @Test + public void testConnectToServerViaProxyWithServerAndProxyBasicAuthentication() throws Exception { + doTestConnectToServerViaProxy(true, "basic"); } @Test - public void testConnectToServerViaProxyWithProxyAuthentication() throws Exception { - doTestConnectToServerViaProxy(false, true); + public void testConnectToServerViaProxyWithProxyDigestAuthentication() throws Exception { + doTestConnectToServerViaProxy(false, "digest"); } @Test - public void testConnectToServerViaProxyWithServerAndProxyAuthentication() throws Exception { - doTestConnectToServerViaProxy(true, true); + public void testConnectToServerViaProxyWithServerAndProxyDigestAuthentication() throws Exception { + doTestConnectToServerViaProxy(true, "digest"); } - private void doTestConnectToServerViaProxy(boolean serverAuthentication, boolean proxyAuthentication) + private void doTestConnectToServerViaProxy(boolean serverAuthentication, String proxyAuthentication) throws Exception { // Configure the proxy System.setProperty("http.proxyHost", PROXY_ADDRESS); - if (proxyAuthentication) { - System.setProperty("http.proxyPort", PROXY_PORT_AUTH); + if ("basic".equalsIgnoreCase(proxyAuthentication)) { + System.setProperty("http.proxyPort", PROXY_PORT_BASIC_AUTH); + } else if ("digest".equalsIgnoreCase(proxyAuthentication)) { + System.setProperty("http.proxyPort", PROXY_PORT_DIGEST_AUTH); } else { System.setProperty("http.proxyPort", PROXY_PORT_NO_AUTH); } @@ -166,7 +195,7 @@ clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_USER_NAME, TOMCAT_USER); clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PASSWORD, TOMCAT_PASSWORD); } - if (proxyAuthentication) { + if (proxyAuthentication != null) { clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PROXY_USER_NAME, PROXY_USER); clientEndpointConfig.getUserProperties().put(Constants.WS_AUTHENTICATION_PROXY_PASSWORD, PROXY_PASSWORD); } diff -Nru tomcat11-11.0.15/test/org/apache/tomcat/websocket/server/TestClose.java tomcat11-11.0.22/test/org/apache/tomcat/websocket/server/TestClose.java --- tomcat11-11.0.15/test/org/apache/tomcat/websocket/server/TestClose.java 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/test/org/apache/tomcat/websocket/server/TestClose.java 2026-05-01 18:56:05.000000000 +0000 @@ -46,7 +46,7 @@ */ public class TestClose extends WebSocketBaseTest { - private static Log log = LogFactory.getLog(TestClose.class); + private static final Log log = LogFactory.getLog(TestClose.class); // These are static because it is simpler than trying to inject them into // the endpoint @@ -132,7 +132,9 @@ client.httpUpgrade(BaseEndpointConfig.PATH); client.forceCloseSocket(); - // TODO: I'm not entirely sure when onError should be called + // A TCP reset causes an IOException when the server reads from + // the connection. Per WSC-5.2.2-2, errors arising during the + // functioning of the endpoint must be reported via onError. awaitOnError(IOException.class); awaitOnClose(CloseCodes.CLOSED_ABNORMALLY); } @@ -186,6 +188,19 @@ awaitOnClose(CloseCodes.CLOSED_ABNORMALLY); } + @Test + public void testCustomCloseCode() throws Exception { + startServer(TestEndpointConfig.class); + + TesterWsClient client = new TesterWsClient("localhost", getPort()); + client.httpUpgrade(BaseEndpointConfig.PATH); + client.sendCloseFrame(CloseCodes.getCloseCode(3500)); + client.closeSocket(); + + awaitLatch(events.onCloseCalled, "onClose not called"); + Assert.assertEquals(3500, events.closeReason.getCloseCode().getCode()); + } + @Test public void testTcpResetInOnMessage() throws Exception { diff -Nru tomcat11-11.0.15/test/webapp/WEB-INF/classes/bug69623-a.mdd tomcat11-11.0.22/test/webapp/WEB-INF/classes/bug69623-a.mdd --- tomcat11-11.0.15/test/webapp/WEB-INF/classes/bug69623-a.mdd 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/webapp/WEB-INF/classes/bug69623-a.mdd 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,16 @@ + 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. + +This is a test file for https://bz.apache.org/bugzilla/show_bug.cgi?id=69623 \ No newline at end of file Binary files /srv/release.debian.org/tmp/joSIQdG7is/tomcat11-11.0.15/test/webapp/WEB-INF/lib/bug69623-lib.jar and /srv/release.debian.org/tmp/zsMpCzb1JJ/tomcat11-11.0.22/test/webapp/WEB-INF/lib/bug69623-lib.jar differ diff -Nru tomcat11-11.0.15/test/webapp/jsp/generator/dependants-order-a.jspf tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order-a.jspf --- tomcat11-11.0.15/test/webapp/jsp/generator/dependants-order-a.jspf 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order-a.jspf 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,17 @@ +<%-- + 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. + --%> +a \ No newline at end of file diff -Nru tomcat11-11.0.15/test/webapp/jsp/generator/dependants-order-b.jspf tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order-b.jspf --- tomcat11-11.0.15/test/webapp/jsp/generator/dependants-order-b.jspf 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order-b.jspf 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,17 @@ +<%-- + 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. + --%> +b \ No newline at end of file diff -Nru tomcat11-11.0.15/test/webapp/jsp/generator/dependants-order-c.jspf tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order-c.jspf --- tomcat11-11.0.15/test/webapp/jsp/generator/dependants-order-c.jspf 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order-c.jspf 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,17 @@ +<%-- + 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. + --%> +c \ No newline at end of file diff -Nru tomcat11-11.0.15/test/webapp/jsp/generator/dependants-order.jsp tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order.jsp --- tomcat11-11.0.15/test/webapp/jsp/generator/dependants-order.jsp 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/webapp/jsp/generator/dependants-order.jsp 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,19 @@ +<%-- + 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. + --%> +<%@ include file="dependants-order-a.jspf" %> +<%@ include file="dependants-order-b.jspf" %> +<%@ include file="dependants-order-c.jspf" %> diff -Nru tomcat11-11.0.15/test/webapp/jsp/generator/external-taglib.jsp tomcat11-11.0.22/test/webapp/jsp/generator/external-taglib.jsp --- tomcat11-11.0.15/test/webapp/jsp/generator/external-taglib.jsp 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test/webapp/jsp/generator/external-taglib.jsp 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,19 @@ +<%-- + 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. +--%> +<%@ page contentType="text/plain" %> +<%@ taglib prefix="ext" uri="http://tomcat.apache.org/test/external-taglib" %> +OK diff -Nru tomcat11-11.0.15/test-profiles.properties.default tomcat11-11.0.22/test-profiles.properties.default --- tomcat11-11.0.15/test-profiles.properties.default 1970-01-01 00:00:00.000000000 +0000 +++ tomcat11-11.0.22/test-profiles.properties.default 2026-05-01 18:56:05.000000000 +0000 @@ -0,0 +1,104 @@ +# ----------------------------------------------------------------------------- +# 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. +# ----------------------------------------------------------------------------- +# test-profiles.properties +# +# This file contains test profile pattern definitions for ant test targets. +# These patterns are used by the test profile system in build.xml. +# +# To add a new profile: +# 1. Add the property: test.profile.myprofile=pattern +# 2. Add a condition in build.xml's set-profile-pattern macro +# +# ----------------------------------------------------------------------------- + +# Smoke test profile +test.profile.smoke=\ +org/apache/catalina/authenticator/TestFormAuthenticatorA.java,\ +org/apache/catalina/authenticator/TestSSOnonLoginAndBasicAuthenticator.java,\ +org/apache/catalina/core/TestAsyncContextStateChanges.java,\ +org/apache/catalina/core/TestStandardContextResources.java,\ +org/apache/catalina/core/TestStandardWrapper.java,\ +org/apache/catalina/loader/TestVirtualContext.java,\ +org/apache/catalina/mapper/TestMapperWebapps.java,\ +org/apache/catalina/nonblocking/TestNonBlockingAPI.java,\ +org/apache/catalina/servlets/TestDefaultServletEncodingPassThroughBom.java,\ +org/apache/catalina/servlets/TestDefaultServletEncodingWithBom.java,\ +org/apache/catalina/servlets/TestDefaultServletEncodingWithoutBom.java,\ +org/apache/catalina/servlets/TestDefaultServletIfMatchRequests.java,\ +org/apache/catalina/servlets/TestDefaultServlet.java,\ +org/apache/catalina/servlets/TestDefaultServletOptions.java,\ +org/apache/catalina/servlets/TestWebdavServletOptionsFile.java,\ +org/apache/catalina/startup/TestContextConfig.java,\ +org/apache/catalina/startup/TestHostConfigAutomaticDeploymentA.java,\ +org/apache/catalina/startup/TestHostConfigAutomaticDeploymentB.java,\ +org/apache/catalina/startup/TestHostConfigAutomaticDeploymentC.java,\ +org/apache/catalina/valves/rewrite/TestRewriteValve.java,\ +org/apache/catalina/valves/TestStuckThreadDetectionValve.java,\ +org/apache/coyote/ajp/TestAbstractAjpProcessor.java,\ +org/apache/coyote/http11/filters/TestChunkedInputFilter.java,\ +org/apache/coyote/http11/TestHttp11InputBufferCRLF.java,\ +org/apache/coyote/http11/TestHttp11InputBuffer.java,\ +org/apache/coyote/http11/TestHttp11Processor.java,\ +org/apache/coyote/http2/TestAsync.java,\ +org/apache/coyote/http2/TestHttp2ConnectionTimeouts.java,\ +org/apache/coyote/http2/TestHttp2Limits.java,\ +org/apache/coyote/http2/TestHttp2Section_6_8.java,\ +org/apache/coyote/http2/TestStreamQueryString.java,\ +org/apache/el/TestELInJsp.java,\ +org/apache/jasper/compiler/TestCompiler.java,\ +org/apache/jasper/compiler/TestEncodingDetector.java,\ +org/apache/jasper/compiler/TestGenerator.java,\ +org/apache/jasper/compiler/TestJspConfig.java,\ +org/apache/jasper/compiler/TestJspDocumentParser.java,\ +org/apache/jasper/compiler/TestValidator.java,\ +org/apache/jasper/optimizations/TestELInterpreterTagSetters.java,\ +org/apache/jasper/optimizations/TestStringInterpreterTagSetters.java,\ +org/apache/jasper/runtime/TestCustomHttpJspPage.java,\ +org/apache/jasper/runtime/TestJspContextWrapper.java,\ +org/apache/jasper/runtime/TestJspRuntimeLibrary.java,\ +org/apache/jasper/runtime/TestPageContextImpl.java,\ +org/apache/jasper/servlet/TestTldScanner.java,\ +org/apache/naming/resources/TestWarDirContext.java,\ +org/apache/naming/TestEnvEntry.java,\ +org/apache/tomcat/util/net/TestClientCert.java,\ +org/apache/tomcat/util/net/TestCustomSslTrustManager.java,\ +org/apache/tomcat/util/net/TestSSLHostConfigCompat.java,\ +org/apache/tomcat/util/net/TestSsl.java,\ +org/apache/tomcat/websocket/server/TestSlowClient.java,\ +org/apache/tomcat/websocket/server/TestWsRemoteEndpointImplServerDeadlock.java,\ +org/apache/tomcat/websocket/TestWebSocketFrameClientSSL.java,\ +org/apache/tomcat/websocket/TestWsWebSocketContainer.java,\ +org/apache/tomcat/websocket/TestWsWebSocketContainerTimeoutServer.java,\ +org/apache/catalina/tribes/group/interceptors/TestTcpFailureDetector.java + +# Component test profiles +test.profile.catalina=**/catalina/**/*Test*.java +test.profile.coyote=**/coyote/**/*Test*.java + +# Performance test profile +test.profile.performance=**/*Performance.java + +# Tribes test profile +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 \ No newline at end of file diff -Nru tomcat11-11.0.15/webapps/docs/changelog.xml tomcat11-11.0.22/webapps/docs/changelog.xml --- tomcat11-11.0.15/webapps/docs/changelog.xml 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/webapps/docs/changelog.xml 2026-05-01 18:56:05.000000000 +0000 @@ -90,6 +90,9 @@ -
    +
    + + + + 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) + + + Refactor generation of the remote user element in the access log to + remove unnecessary code. (markt) + + + Fix a regression in the previous release that meant ?- + could appear in the access log rather than ? when the query + string was present but empty. (markt) + + + Failed precondition should make WebDAV DELETE fail. 982 + submitted by Mahmoud Alarby. (remm) + + + Align the escaping in ExtendedAccessLogValve with the other + AccessLogValve implementations. (markt) + + + 70000: fix duplication of special headers in the response + after commit, following fix for 69967. (remm) + + + Correct the handling of URIs mapped to a security constraint that only + specifies the special ** role for all authenticated users. + Requests without authentication were receiving 403 responses rather than + 401 responses. (markt) + + + Fix a race condition in StandardContext.getServletContext() + that could cause the jakarta.servlet.context.tempdir + attribute to be lost during a context reload. Make the + context field volatile and use locking to + ensure only one ApplicationContext instance is created. + (dsoumis) + + + Update the Windows authentication (kerberos) documentation to reflect + that both Java and Windows are removing / have removed support for + RC4-HMAC. The guide now uses AES256-SHA1. (markt) + + + Add a new initialisation parameter for WebDAV, + maxRequestBodySize which limits the size of a WebDAV + request body for LOCK and PROPFIND. The default value is 4096 bytes. + (markt) + + + Add a new caseSensitive attribute to the + LockOutRealm that controls the manner in which user names + are treated when making locking decisions. The default is + false, meaning user names are treated in a case insensitive + manner. (markt) + + + Correct the handling of invalid users with DIGEST authentication. (markt) + + + Ensure RealmBase finds all matching extension based + security constraints. (markt) + + + + + + + Avoid various edge cases if Content-Length is set via + setHeader(String,String) or + addHeader(String,String) with an invalid value by always + clearing the previous value whether the new value is valid or not and + ignoring any invalid new value. (markt) + + + Refactor the calculation of the real index in the HPACK dynamic header + table implementation to reduce code duplication. (markt) + + + Fix various minor issues with some HTTP/2 stream error messages for + HTTP/2. (markt) + + + Consistently reject URIs containing NULL bytes when + normalizing. (markt) + + + Fix a few minor memory leaks on error paths reading TLS keys and + certificates when using FFM. (markt) + + + Refactor clean-up after HTTP/2 headers have been processed to aid GC + after a stream reset. (markt) + + + Align HTTP/2 trailer fields with HTTP/1.1 and filter out any fields + not permitted in trailers. (markt) + + + Free private keys after use in FFM based connector configuration. + (markt) + + + Correct an unlikely edge-case parsing bug in the HTTP/2 HPACK header + decoding that could result in a valid header triggering an unexpected + connection close. (markt) + + + Refactor HTTP/2 HPACK encoding so header field names are only converted + to lower case once during the encoding process. (markt) + + + Refactor HTTP/2 header field validation so it occurs earlier. Extend + validation to check for disallowed characters as well as upper case + characters. (markt) + + + Add TLS 1.3 groups added in OpenSSL 4.0. (remm) + + + Add validation that the HTTP/2 :scheme pseudo-header is + consistent with the use (or not) of TLS. (markt) + + + Correct the validation of pseudo headers and CONNECT requests to align + Tomcat's behaviour with RFC 9113, section 8.5. (markt) + + + Fix a potential integer overflow when allocating capacity from a + connection level window update to individual HTTP/2 streams. Based on + 996 by Mike Tingey Jr. (markt) + + + Switch AJP secret comparison to a constant time algorithm. (markt) + + + + + + + Fix the initial connection to a WebSocket end point where the connection + is made via a proxy that requires DIGEST authentication. (markt) + + + + + + + 69993: Update the URL to the CDDL 1.0 license. (markt) + + + Add warning when OpenSSL binary is not found. (csutherl) + + + Add check for Tomcat Native library, and log warning when it's not found + to make it easier to see when it's not used by the suite. (csutherl) + + + Update Byte Buddy to 1.18.8. (markt) + + + Update Bouncy Castle to 1.84. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations provided by tak7iji. (markt) + + + +
    +
    + + + + Add escaping for URI and query string in the access log. (markt) + + + + + + + Fix an HTTP/2 header frame parsing bug that could result in a connection + being closed without a GOAWAY frame if an invalid + HEADERS frame was received. (markt) + + + 69982: Fix a bug in the non-blocking flushing code for + NIO+TLS that meant that a response may not be fully written until the + connection is closed. Pull request 966 provided by Phil Clay. + (markt) + + + Ensure the HTTP/2 request header read buffer is reset (including + restoration to default size) after a stream reset. (markt) + + + Provide trailer field filtering equivalent to that provided for + non-trailer fields. Control characters (excluding TAB), and characters + with code points above 255 will be replaced with a space. (markt) + + + Align OpenSSL FFM behaviour with Tomcat Native for various OCSP edge + cases. (markt) + + + + + + + 69995: Make dependent ordering predictable. Patch submitted + by Jan Luehe. (remm) + + + 70001: Ensure JSP compilation is reproducible when + dependencies exist for tag files and tag libraries located on the class + path but outside of the web application. 992 submitted by Jan + Luehe. (markt) + + + + + + + Reduce log verbosity of the Kubernetes connection attempts and failure. + (remm) + + + Better error handling for the EncryptInterceptor. (markt) + + + + + + + Update bnd to 7.2.3. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Japanese translations provided by tak7iji. (markt) + + + +
    +
    + + + + Fix case sensitive handling of the protocol host name. (remm) + + + +
    +
    + + + + 69967: Fix inconsistencies related to + Content-Length and Content-Type headers when + accessed using the getHeader method and similar. (remm) + + + 69940: Improve redirect handling in the + LoadBalancerDrainingValve. (schultz) + + + + + + + Add an HTTP configuration setting, noCompressionEncodings, + that can be used to control which content encodings will not be + compressed when compression is enabled. Based on pull request + 914 by Long9725. (markt) + + + Add size limit for OCSP responses. Based on code submitted by Chenjp. + (remm) + + + To maintain the documented alignment with the OpenSSL development + branch, the use of the aliases SSLv3, EXPORT, + EXPORT40, EXPORT56, KRB5, + kFZA, aFZA, eFZA and + FZA are no longer supported when setting the + ciphers attribute of an SSLHostConfig element. + (markt) + + + To maintain the documented alignment with the OpenSSL development + branch, add support for the aliases ARIAGCM and + CBCwhen setting the ciphers attribute of + an SSLHostConfig element. (markt) + + + 69870: Add a drainTimeout to the HTTP/2 + UpgradeProtocol element to allow configuration of an + time between the two final GOAWAY frames sent by Tomcat + when closing am HTTP/2 connection. Pull request 917 provided by + Kai Burjack. (markt) + + + Update the minimum recommended version of Tomcat Native so that users of + 1.3.x are recommended to update to 2.0.x. (markt) + + + Respect the value for the jdk.tls.namedGroups system + property as the default value for the configured group list on the + Connector. (remm) + + + 69964: Respect the configured cipher order, which was no + longer respected following the addition of TLS 1.3 specific cipher + configuration. TLS 1.3 ciphers will always be first in the list. (remm) + + + Free the x509 object in the FFM code when getting the peer certificate + if getting the bytes from the certificate somehow fails. + Pull request 951 provided by Chenjp. (remm) + + + Improve HPACK exception use, making sure HpackException + is thrown instead of unexpected types. (remm) + + + Update the parser for the HTTP Host header and + :authority pseudo header to convert the port, if any, to an + Integer rather than a Long to be consistent + with how port is exposed in the Servlet API. (markt) + + + To aid the migration from the single ciphers configuration + attribute to the use of ciphers and + cipherSuites, TLS 1.3 cipher suites listed in the + ciphers attribute will be removed from the + ciphers attribute and added to the end of the + cipherSuites attribute. This behaviour will be removed in + Tomcat 12.0.x onwards. (markt) + + + Replace the external OpenSSL based OCSP responder used during unit tests + with a Bouncy Castle based, in-process Java OCSP responder. (markt) + + + Relax HTTP/2 header validation and respond to invalid requests with a + stream reset or a 400 response as appropriate rather then with a + connection reset. (markt) + + + Add validation of chunk extensions for chunked transfer encoding. + (markt) + + + Update the recommended version for Tomcat Native 2.x to 2.0.14. (markt) + + + Align the FFM handling of OCSP TRY_LATER responses with + Tomcat Native. (remm) + + + Free CA certificate after calling SSL_CTX_add_client_CA + in the FFM code. Based on code from PR 44 from tomcat-native. (remm) + + + Free certificate chain if an error occurs, in the FFM code. (remm) + + + Report handshake issues as SSLException in the FFM + code, rather than IllegalStateException. (remm) + + + + + + + 69948: Avoid ArrayOutOfBoundsException instead of + PropertyNotFoundException when generating a properties not found + exception in AstValue. Based on 950 submitted by Jérôme + Besnard. (remm) + + + Add support for specifying Java 27 (with the value 27) as + the compiler source and/or compiler target for JSP compilation. If used + with an Eclipse JDT compiler version that does not support these values, + a warning will be logged and the default will be used. + (markt) + + + + + + + 69970: Support raw IPv6 in Kubernetes membership provider + for the service host. (remm) + + + Add support for new algorithms provided by JPA providers to the + EncryptInterceptor. (markt) + + + + + + + 69972: Remove unwanted space in DIGEST authorization header. + Patch submitted by Stefan Kalscheuer in 957. (remm) + + + + + + + 69931: Add <label> for fields in the HTML manager + application. Patch provided by yukitidev. (schultz) + + + + + + + Update the internal fork of Apache Commons BCEL to 6.12.0. (markt) + + + Update to the Eclipse JDT compiler 4.39. (markt) + + + Update Tomcat Native to 2.0.14. (markt) + + + Update Objenesis to 3.5. (markt) + + + Update Byte Buddy to 1.18.7. (markt) + + + Update BND to 7.2.1. (markt) + + + Improvements to French translations. (remm) + + + Improvements to Chinese translations provided by eaststrongox. (markt) + + + Improvements to Japanese translations provided by tak7iji. (markt) + + + +
    +
    + + + + 69936: Fix bug in previous fix for Tomcat Native crashes on + shutdown that triggered a significant memory leak. Patch provided by + Wes. (markt) + + + 69938: Avoid changing the closed state of TLS channel when + resetting it after close. (remm) + + + +
    +
    + + + + 69932: Fix request end access log pattern regression, + which would log the start time of the request instead. (remm) + + + + + + + Prevent concurrent release of OpenSSLEngine resources and + the termination of the Tomcat Native library as it can cause crashes + during Tomcat shutdown. (markt) + + + + + + + Add property "gpg.sign.files" to optionally disable release artefact + signing with GPG. (rjung) + + + +
    +
    + + + + 69623: Additional fix for the long standing regression that + meant that calls to ClassLoader.getResource().getContent() + failed when made from within a web application with resource caching + enabled if the target resource was packaged in a JAR file. (markt) + + + Pull request 923: Avoid adding multiple CSRF tokens to a URL in + the CsrfPreventionFilter. (schultz) + + + 69918: Ensure request parameters are correctly parsed for + HTTP/2 requests when the content-length header is not set. (dsoumis) + + + Enable minimum and recommended Tomcat Native versions to be set + separately for Tomcat Native 1.x and 2.x. Update the minimum and + recommended versions for Tomcat Native 1.x to 1.3.4. Update the minimum + and recommended versions for Tomcat Native 2.x to 2.0.12. (markt) + + + Add a new ssoReauthenticationMode to the Tomcat provided + Authenticators that provides a per Authenticator override of the SSO + Valve requireReauthentication attribute. (markt) + + + Ensure URL encoding errors in the Rewrite Valve trigger an exception + rather than silently using a replacement character. (markt) + + + + + + + Improve warnings when setting ciphers lists in the FFM code, mirroring + the tomcat-native changes. (remm) + + + 69910: Dereference TLS objects right after closing a socket + to improve memory efficiency. (remm) + + + Relax the JSSE vs OpenSSL configuration style checks on + SSLHostConfig to reflect the existing implementation that + allows one configuration style to be used for the trust attributes and a + different style for all the other attributes. (markt) + + + Better warning message when OpenSSLConf configuration + elements are used with a JSSE TLS implementation. (markt) + + + When using OpenSSL via FFM, don't log a warning about missing CA + certificates unless CA certificates were configured and the + configuration failed. (markt) + + + For configuration consistency between OpenSSL and JSSE TLS + implementations, TLSv1.3 cipher suites included in the + ciphers attribute of an SSLHostConfig are now + always ignored (previously they would be ignored with OpenSSL + implementations and used with JSSE implementations) and a warning is + logged that the cipher suite has been ignored. (markt) + + + Add the ciphersuite attribute to + SSLHostConfig to configure the TLSv1.3 cipher suites. + (markt) + + + Add OCSP support to JSSE based TLS connectors and make the use of OCSP + configurable per connector for both JSSE and OpenSSL based TLS + implementations. Align the checks performed by OpenSSL with those + performed by JSSE. (markt) + + + Add support for soft failure of OCSP checks with soft failure support + disabled by default. (markt) + + + Add support for configuring the verification flags passed to + OCSP_basic_verify when using an OpenSSL based TLS + implementation. (markt) + + + Fix OpenSSL FFM code compatibility with LibreSSL versions below 3.5. + (remm) + + + + + + + 69333: Correct a regression in the previous fix for + 69333 and ensure that reuse() or + release() is always called for a tag. (markt) + + + + + + + 62814: Document that human-readable names may be used for + mapSendOptions and align documentation with + channelSendOptions. Based on pull request 929 by + archan0621. (markt) + + + + + + + 69920: When attempting to write to a closed + Writer or OutputStream obtained from a + WebSocket session, throw an IOException rather than an + IllegalStateExcpetion as required by Writer + and strongly suggested by OutputStream. (markt) + + + + + + + Add test.silent property to suppress JUnit console output + during test execution. Useful for cleaner console output when running + tests with multiple threads. (csutherl) + + + Update the internal fork of Commons Pool to 2.13.1. (markt) + + + Update the internal fork of Commons DBCP to 2.14.0. (markt) + + + Update Commons Daemon to 1.5.1. (markt) + + + Update to the Eclipse JDT compiler 4.37. (markt) + + + Update ByteBuddy to 1.18.3. (markt) + + + Update UnboundID to 7.0.4. (markt) + + + Update Checkstyle to 12.3.1. (markt) + + + Improvements to French translations. (markt) + + + Improvements to Japanese translations provided by tak7iji. (markt) + + + Improvements to Chinese translations provided by Yang. vincent.h and + yong hu. (markt) + + + Update Tomcat Native to 2.0.12. (markt) + + + Update bnd to 7.2.0. (markt) + + + +
    +
    @@ -145,7 +818,7 @@ HTTP/0.9 only allows GET as the HTTP method. (remm) - Add strictSNI attribute on the Connector to + Add strictSni attribute on the Connector to allow matching the SSLHostConfig configuration associated with the SNI host name to the SSLHostConfig configuration matched from the HTTP protocol host name. Non matching configurations @@ -202,6 +875,12 @@ + + Add test profile system for selective test execution. Profiles can be + specified via -Dtest.profile=<name> to run specific + test subsets without using patterns directly. Profile patterns are + defined in test-profiles.properties. (csutherl) + Update file extension to media type mappings to align with the current list used by the Apache Web Server (httpd). (markt) diff -Nru tomcat11-11.0.15/webapps/docs/cluster-howto.xml tomcat11-11.0.22/webapps/docs/cluster-howto.xml --- tomcat11-11.0.15/webapps/docs/cluster-howto.xml 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/webapps/docs/cluster-howto.xml 2026-05-01 18:56:05.000000000 +0000 @@ -72,7 +72,7 @@ The following is the default cluster configuration:

    + channelSendOptions="async"> + channelSendOptions="use_ack,sync"> + mapSendOptions="use_ack,sync"/> sslImplementationName attribute on each.

    With Tomcat Native:

    ]]> -

    With Java 22 FFM API:

    +

    With Java FFM API:

    ]]>

    The SSLRandomSeed attribute of the listeners allows specifying a @@ -531,6 +531,39 @@

    +
    + +

    Use of PQC functionality requires OpenSSL 3.5 or newer.

    + +

    Setting up a hybrid PQC configuration involves generating two certificates. +A traditional RSA one will handle non PQC clients, while a MLDSA certificate +will be used for PQC. This kind of hybrid setup allows transitioning to PQC +while still maintaining support for existing clients.

    + +

    A pure PQC configuration needs only a MLDSA certificate.

    + +

    Example hybrid configuration, assuming two certificates generated with +OpenSSL and protected with a password:

    + + + + + + +]]> + + +

    PQC support can then be verified by connecting using curl:

    + + + +
    +

    Additional information may be obtained about TLS handshake failures by diff -Nru tomcat11-11.0.15/webapps/docs/windows-auth-howto.xml tomcat11-11.0.22/webapps/docs/windows-auth-howto.xml --- tomcat11-11.0.15/webapps/docs/windows-auth-howto.xml 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/webapps/docs/windows-auth-howto.xml 2026-05-01 18:56:05.000000000 +0000 @@ -98,6 +98,8 @@

  • Create a domain user that will be mapped to the service name used by the Tomcat server. In this how-to, this user is called tc01 and has a password of tc01pass.
  • +
  • Enable AES256 encryption for this user (via Active Directory Users and + Computers)
  • Map the service principal name (SPN) to the user account. SPNs take the form <service class>/<host>:<port>/<service name>. @@ -109,9 +111,12 @@ itself to the domain controller. This file contains the Tomcat private key for the service provider account and should be protected accordingly. To generate the file, run the following command (all on a single line): - ktpass /out c:\tomcat.keytab /mapuser tc01@DEV.LOCAL + ktpass /out c:\tomcat.keytab + /mapuser tc01@DEV.LOCAL /princ HTTP/win-tc01.dev.local@DEV.LOCAL - /pass tc01pass /kvno 0
  • + /pass tc01pass + /kvno 0 + /crypto AES256-SHA1
  • Create a domain user to be used on the client. In this how-to the domain user is test with a password of testpass.
  • @@ -135,8 +140,9 @@ contained:[libdefaults] default_realm = DEV.LOCAL default_keytab_name = FILE:c:\apache-tomcat-.x\conf\tomcat.keytab -default_tkt_enctypes = rc4-hmac,aes256-cts-hmac-sha1-96,aes128-cts-hmac-sha1-96 -default_tgs_enctypes = rc4-hmac,aes256-cts-hmac-sha1-96,aes128-cts-hmac-sha1-96 +default_tkt_enctypes = aes256-cts-hmac-sha1-96,aes128-cts-hmac-sha1-96 +default_tgs_enctypes = aes256-cts-hmac-sha1-96,aes128-cts-hmac-sha1-96 +permitted_enctypes = aes256-cts-hmac-sha1-96,aes128-cts-hmac-sha1-96 forwardable=true [realms] @@ -182,7 +188,8 @@ may be used that will simply return a Principal based on the authenticated user name that does not have any roles.

    The above steps have been tested on a Tomcat server running Windows Server - 2019 Standard with AdoptOpenJDK 8u232-b09 (64-bit).

    + 2019 Standard with Temurin Java versions 8u482-b08, 11.0.31+11, 17.0.18+8, + 21.0.11+10 and 25.0.3+9 (all 64-bit).

    diff -Nru tomcat11-11.0.15/webapps/examples/WEB-INF/classes/LocalStrings_zh_CN.properties tomcat11-11.0.22/webapps/examples/WEB-INF/classes/LocalStrings_zh_CN.properties --- tomcat11-11.0.15/webapps/examples/WEB-INF/classes/LocalStrings_zh_CN.properties 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/webapps/examples/WEB-INF/classes/LocalStrings_zh_CN.properties 2026-05-01 18:56:05.000000000 +0000 @@ -17,7 +17,7 @@ # To edit translations see: https://tomcat.apache.org/getinvolved.html#Translations cookies.cookies=你的浏览器正在发送下面的cookie: -cookies.make-cookie=创建一个发送到你的浏览器的cookie +cookies.make-cookie=创建一个cookie并发送到你的浏览器 cookies.name=名.称: cookies.no-cookies=你的浏览器未发送任何cookie cookies.set=你刚刚将以下cookie发送到你的浏览器: diff -Nru tomcat11-11.0.15/webapps/manager/WEB-INF/jsp/connectorCerts.jsp tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/connectorCerts.jsp --- tomcat11-11.0.15/webapps/manager/WEB-INF/jsp/connectorCerts.jsp 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/connectorCerts.jsp 2026-05-01 18:56:05.000000000 +0000 @@ -32,7 +32,7 @@ - + Configured certificate chains per Connector diff -Nru tomcat11-11.0.15/webapps/manager/WEB-INF/jsp/connectorCiphers.jsp tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/connectorCiphers.jsp --- tomcat11-11.0.15/webapps/manager/WEB-INF/jsp/connectorCiphers.jsp 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/connectorCiphers.jsp 2026-05-01 18:56:05.000000000 +0000 @@ -32,7 +32,7 @@ - + Configured ciphers per Connector diff -Nru tomcat11-11.0.15/webapps/manager/WEB-INF/jsp/connectorTrustedCerts.jsp tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/connectorTrustedCerts.jsp --- tomcat11-11.0.15/webapps/manager/WEB-INF/jsp/connectorTrustedCerts.jsp 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/connectorTrustedCerts.jsp 2026-05-01 18:56:05.000000000 +0000 @@ -32,7 +32,7 @@ - + Trusted certificates per Connector diff -Nru tomcat11-11.0.15/webapps/manager/WEB-INF/jsp/sessionDetail.jsp tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/sessionDetail.jsp --- tomcat11-11.0.15/webapps/manager/WEB-INF/jsp/sessionDetail.jsp 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/sessionDetail.jsp 2026-05-01 18:56:05.000000000 +0000 @@ -50,7 +50,7 @@ - + Sessions Administration: details for <%= currentSessionId %> diff -Nru tomcat11-11.0.15/webapps/manager/WEB-INF/jsp/sessionsList.jsp tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/sessionsList.jsp --- tomcat11-11.0.15/webapps/manager/WEB-INF/jsp/sessionsList.jsp 2025-12-02 16:54:08.000000000 +0000 +++ tomcat11-11.0.22/webapps/manager/WEB-INF/jsp/sessionsList.jsp 2026-05-01 18:56:05.000000000 +0000 @@ -41,7 +41,7 @@ - + Sessions Administration for <%= JspHelper.escapeXml(cn.getDisplayName()) %>

    \n" + - " {3}\n" + + " \n" + " \n" + - " \n" + + " \n" + "