Version in base suite: 10.1.52-1~deb13u1 Base version: tomcat10_10.1.52-1~deb13u1 Target version: tomcat10_10.1.55-1~deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/t/tomcat10/tomcat10_10.1.52-1~deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/t/tomcat10/tomcat10_10.1.55-1~deb13u1.dsc /srv/release.debian.org/tmp/ziwITPmTS3/tomcat10-10.1.55/res/install-win/Uninstall.exe.sig |binary /srv/release.debian.org/tmp/ziwITPmTS3/tomcat10-10.1.55/res/install-win/tomcat-installer.exe.sig |binary tomcat10-10.1.55/.gitignore | 1 tomcat10-10.1.55/BUILDING.txt | 93 + tomcat10-10.1.55/CONTRIBUTING.md | 61 + tomcat10-10.1.55/MERGE.txt | 2 tomcat10-10.1.55/README.md | 6 tomcat10-10.1.55/bin/catalina.bat | 2 tomcat10-10.1.55/bin/catalina.sh | 4 tomcat10-10.1.55/build.properties.default | 50 - tomcat10-10.1.55/build.properties.release | 8 tomcat10-10.1.55/build.xml | 111 ++ tomcat10-10.1.55/debian/ant.properties | 3 tomcat10-10.1.55/debian/changelog | 13 tomcat10-10.1.55/debian/control | 2 tomcat10-10.1.55/debian/patches/0004-split-deploy-webapps-target-from-deploy-target.patch | 6 tomcat10-10.1.55/debian/patches/0005-skip-test-failures.patch | 4 tomcat10-10.1.55/debian/patches/0009-Use-java.security.policy-file-in-catalina.sh.patch | 2 tomcat10-10.1.55/debian/patches/0010-debianize-build-xml.patch | 6 tomcat10-10.1.55/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch | 4 tomcat10-10.1.55/debian/patches/0018-fix-manager-webapp.patch | 12 tomcat10-10.1.55/debian/patches/0019-add-distribution-to-error-page.patch | 6 tomcat10-10.1.55/debian/patches/0021-dont-test-unsupported-ciphers.patch | 10 tomcat10-10.1.55/debian/patches/0023-disable-shutdown-by-socket.patch | 2 tomcat10-10.1.55/debian/patches/0024-systemd-log-formatter.patch | 3 tomcat10-10.1.55/debian/patches/0025-invalid-configuration-exit-status.patch | 2 tomcat10-10.1.55/debian/patches/0026-easymock4-compatibility.patch | 24 tomcat10-10.1.55/debian/patches/0030-eclipse-jdt-classpath.patch | 4 tomcat10-10.1.55/debian/patches/bouncycastle.patch | 51 + tomcat10-10.1.55/debian/patches/disable-jacoco.patch | 8 tomcat10-10.1.55/debian/patches/exclude-TestJNDIRealmIntegration.patch | 6 tomcat10-10.1.55/debian/patches/series | 1 tomcat10-10.1.55/java/jakarta/el/CompositeELResolver.java | 1 tomcat10-10.1.55/java/org/apache/catalina/AccessLog.java | 2 tomcat10-10.1.55/java/org/apache/catalina/Context.java | 1 tomcat10-10.1.55/java/org/apache/catalina/JmxEnabled.java | 4 tomcat10-10.1.55/java/org/apache/catalina/WebResource.java | 40 tomcat10-10.1.55/java/org/apache/catalina/WebResourceRoot.java | 12 tomcat10-10.1.55/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java | 23 tomcat10-10.1.55/java/org/apache/catalina/authenticator/AuthenticatorBase.java | 6 tomcat10-10.1.55/java/org/apache/catalina/authenticator/DigestAuthenticator.java | 3 tomcat10-10.1.55/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java | 192 --- tomcat10-10.1.55/java/org/apache/catalina/connector/CoyoteAdapter.java | 11 tomcat10-10.1.55/java/org/apache/catalina/connector/Response.java | 48 tomcat10-10.1.55/java/org/apache/catalina/core/AprLifecycleListener.java | 105 +- tomcat10-10.1.55/java/org/apache/catalina/core/LocalStrings.properties | 1 tomcat10-10.1.55/java/org/apache/catalina/core/LocalStrings_fr.properties | 3 tomcat10-10.1.55/java/org/apache/catalina/core/LocalStrings_ja.properties | 1 tomcat10-10.1.55/java/org/apache/catalina/core/OpenSSLLifecycleListener.java | 23 tomcat10-10.1.55/java/org/apache/catalina/core/StandardContext.java | 16 tomcat10-10.1.55/java/org/apache/catalina/ha/context/ReplicatedContext.java | 10 tomcat10-10.1.55/java/org/apache/catalina/ha/tcp/ReplicationValve.java | 19 tomcat10-10.1.55/java/org/apache/catalina/ha/tcp/SendMessageData.java | 2 tomcat10-10.1.55/java/org/apache/catalina/manager/HTMLManagerServlet.java | 24 tomcat10-10.1.55/java/org/apache/catalina/manager/host/HostManagerServlet.java | 3 tomcat10-10.1.55/java/org/apache/catalina/mapper/LocalStrings.properties | 1 tomcat10-10.1.55/java/org/apache/catalina/mapper/LocalStrings_fr.properties | 1 tomcat10-10.1.55/java/org/apache/catalina/mapper/LocalStrings_ja.properties | 1 tomcat10-10.1.55/java/org/apache/catalina/mapper/Mapper.java | 2 tomcat10-10.1.55/java/org/apache/catalina/mapper/MappingData.java | 6 tomcat10-10.1.55/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java | 42 tomcat10-10.1.55/java/org/apache/catalina/realm/LockOutRealm.java | 25 tomcat10-10.1.55/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java | 10 tomcat10-10.1.55/java/org/apache/catalina/realm/RealmBase.java | 32 tomcat10-10.1.55/java/org/apache/catalina/servlets/WebdavServlet.java | 111 +- tomcat10-10.1.55/java/org/apache/catalina/tribes/group/AbsoluteOrder.java | 67 + tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java | 61 - tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties | 5 tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties | 3 tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties | 3 tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties | 1 tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties | 1 tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java | 2 tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java | 10 tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties | 4 tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java | 2 tomcat10-10.1.55/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java | 2 tomcat10-10.1.55/java/org/apache/catalina/util/ServerInfo.java | 215 ++++ tomcat10-10.1.55/java/org/apache/catalina/valves/AbstractAccessLogValve.java | 491 ++++++++- tomcat10-10.1.55/java/org/apache/catalina/valves/ExtendedAccessLogValve.java | 25 tomcat10-10.1.55/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java | 30 tomcat10-10.1.55/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties | 1 tomcat10-10.1.55/java/org/apache/catalina/webresources/AbstractArchiveResource.java | 46 tomcat10-10.1.55/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java | 47 tomcat10-10.1.55/java/org/apache/catalina/webresources/AbstractResource.java | 5 tomcat10-10.1.55/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties | 1 tomcat10-10.1.55/java/org/apache/coyote/AbstractProtocol.java | 435 ++++++++ tomcat10-10.1.55/java/org/apache/coyote/CompressionConfig.java | 34 tomcat10-10.1.55/java/org/apache/coyote/LocalStrings.properties | 1 tomcat10-10.1.55/java/org/apache/coyote/Request.java | 4 tomcat10-10.1.55/java/org/apache/coyote/Response.java | 9 tomcat10-10.1.55/java/org/apache/coyote/UpgradeProtocol.java | 10 tomcat10-10.1.55/java/org/apache/coyote/ajp/AbstractAjpProtocol.java | 120 ++ tomcat10-10.1.55/java/org/apache/coyote/ajp/AjpProcessor.java | 3 tomcat10-10.1.55/java/org/apache/coyote/http11/AbstractHttp11Protocol.java | 9 tomcat10-10.1.55/java/org/apache/coyote/http11/LocalStrings_fr.properties | 1 tomcat10-10.1.55/java/org/apache/coyote/http11/LocalStrings_ja.properties | 1 tomcat10-10.1.55/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java | 113 +- tomcat10-10.1.55/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java | 57 - tomcat10-10.1.55/java/org/apache/coyote/http11/filters/GzipOutputFilter.java | 6 tomcat10-10.1.55/java/org/apache/coyote/http2/HPackHuffman.java | 105 +- tomcat10-10.1.55/java/org/apache/coyote/http2/Hpack.java | 7 tomcat10-10.1.55/java/org/apache/coyote/http2/HpackDecoder.java | 54 - tomcat10-10.1.55/java/org/apache/coyote/http2/HpackEncoder.java | 34 tomcat10-10.1.55/java/org/apache/coyote/http2/Http2Parser.java | 38 tomcat10-10.1.55/java/org/apache/coyote/http2/Http2Protocol.java | 23 tomcat10-10.1.55/java/org/apache/coyote/http2/Http2UpgradeHandler.java | 9 tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings.properties | 11 tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_fr.properties | 2 tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_ja.properties | 2 tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_ko.properties | 1 tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties | 1 tomcat10-10.1.55/java/org/apache/coyote/http2/Stream.java | 203 ++-- tomcat10-10.1.55/java/org/apache/coyote/http2/StreamException.java | 6 tomcat10-10.1.55/java/org/apache/coyote/http2/StreamProcessor.java | 15 tomcat10-10.1.55/java/org/apache/el/ExpressionFactoryImpl.java | 2 tomcat10-10.1.55/java/org/apache/el/parser/AstValue.java | 9 tomcat10-10.1.55/java/org/apache/jasper/compiler/Compiler.java | 31 tomcat10-10.1.55/java/org/apache/jasper/compiler/Generator.java | 7 tomcat10-10.1.55/java/org/apache/jasper/compiler/JDTCompiler.java | 30 tomcat10-10.1.55/java/org/apache/jasper/compiler/PageInfo.java | 3 tomcat10-10.1.55/java/org/apache/jasper/compiler/TagFileProcessor.java | 25 tomcat10-10.1.55/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java | 37 tomcat10-10.1.55/java/org/apache/jasper/compiler/TextOptimizer.java | 1 tomcat10-10.1.55/java/org/apache/jasper/el/ELResolverImpl.java | 1 tomcat10-10.1.55/java/org/apache/juli/OneLineFormatter.java | 3 tomcat10-10.1.55/java/org/apache/naming/LocalStrings_zh_CN.properties | 1 tomcat10-10.1.55/java/org/apache/tomcat/Jar.java | 26 tomcat10-10.1.55/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings.properties | 2 tomcat10-10.1.55/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java | 7 tomcat10-10.1.55/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java | 9 tomcat10-10.1.55/java/org/apache/tomcat/dbcp/pool2/PooledObject.java | 2 tomcat10-10.1.55/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java | 1 tomcat10-10.1.55/java/org/apache/tomcat/jni/SSLConf.java | 14 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/Const.java | 138 +- tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java | 5 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java | 8 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java | 24 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/Constant.java | 8 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java | 4 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java | 2 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java | 2 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java | 2 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java | 2 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java | 2 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java | 2 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java | 2 tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/package-info.java | 4 tomcat10-10.1.55/java/org/apache/tomcat/util/buf/Asn1Parser.java | 2 tomcat10-10.1.55/java/org/apache/tomcat/util/http/HeaderUtil.java | 30 tomcat10-10.1.55/java/org/apache/tomcat/util/http/RequestUtil.java | 10 tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/ChunkExtension.java | 130 ++ tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/EntityTag.java | 4 tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/HttpParser.java | 92 + tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/LocalStrings.properties | 2 tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties | 2 tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties | 2 tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/Upgrade.java | 6 tomcat10-10.1.55/java/org/apache/tomcat/util/net/AbstractEndpoint.java | 5 tomcat10-10.1.55/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java | 12 tomcat10-10.1.55/java/org/apache/tomcat/util/net/LocalStrings.properties | 5 tomcat10-10.1.55/java/org/apache/tomcat/util/net/LocalStrings_fr.properties | 5 tomcat10-10.1.55/java/org/apache/tomcat/util/net/LocalStrings_ja.properties | 4 tomcat10-10.1.55/java/org/apache/tomcat/util/net/NioEndpoint.java | 22 tomcat10-10.1.55/java/org/apache/tomcat/util/net/SSLHostConfig.java | 102 +- tomcat10-10.1.55/java/org/apache/tomcat/util/net/SecureNio2Channel.java | 19 tomcat10-10.1.55/java/org/apache/tomcat/util/net/SecureNioChannel.java | 27 tomcat10-10.1.55/java/org/apache/tomcat/util/net/SocketWrapperBase.java | 6 tomcat10-10.1.55/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties | 1 tomcat10-10.1.55/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties | 1 tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java | 5 tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java | 25 tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/ciphers/Group.java | 11 tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java | 59 - tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/ciphers/SignatureScheme.java | 26 tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java | 47 tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java | 76 - tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java | 29 tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java | 1 tomcat10-10.1.55/java/org/apache/tomcat/util/openssl/openssl_h.java | 33 tomcat10-10.1.55/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java | 35 tomcat10-10.1.55/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java | 4 tomcat10-10.1.55/java/org/apache/tomcat/util/security/ConstantTime.java | 142 ++ tomcat10-10.1.55/java/org/apache/tomcat/websocket/Authenticator.java | 30 tomcat10-10.1.55/java/org/apache/tomcat/websocket/BasicAuthenticator.java | 4 tomcat10-10.1.55/java/org/apache/tomcat/websocket/DigestAuthenticator.java | 20 tomcat10-10.1.55/java/org/apache/tomcat/websocket/WsWebSocketContainer.java | 93 + tomcat10-10.1.55/res/ide-support/eclipse/eclipse.classpath | 3 tomcat10-10.1.55/res/ide-support/eclipse/java-compiler-errors-warnings.txt | 3 tomcat10-10.1.55/res/ide-support/idea/tomcat.iml | 27 tomcat10-10.1.55/res/ide-support/netbeans/nb-tomcat-build.properties | 2 tomcat10-10.1.55/res/ide-support/netbeans/project.xml | 2 tomcat10-10.1.55/res/maven/mvn.properties.default | 2 tomcat10-10.1.55/res/maven/mvn.properties.release | 2 tomcat10-10.1.55/res/maven/tomcat-servlet-api.pom | 2 tomcat10-10.1.55/res/openssl/openssl-tomcat.conf | 2 tomcat10-10.1.55/res/rat/rat-excludes.txt | 9 tomcat10-10.1.55/test-profiles.properties.default | 4 tomcat10-10.1.55/test/jakarta/el/TestArrayELResolver.java | 1 tomcat10-10.1.55/test/jakarta/el/TestBeanELResolver.java | 2 tomcat10-10.1.55/test/jakarta/el/TestBeanNameELResolver.java | 2 tomcat10-10.1.55/test/jakarta/el/TestMapELResolver.java | 2 tomcat10-10.1.55/test/jakarta/el/TestResourceBundleELResolver.java | 2 tomcat10-10.1.55/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java | 6 tomcat10-10.1.55/test/org/apache/catalina/authenticator/TestDigestAuthenticatorB.java | 135 ++ tomcat10-10.1.55/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java | 40 tomcat10-10.1.55/test/org/apache/catalina/connector/TestResponse.java | 45 tomcat10-10.1.55/test/org/apache/catalina/connector/TestValidateClientSessionId.java | 89 + tomcat10-10.1.55/test/org/apache/catalina/core/TestStandardContext.java | 54 + tomcat10-10.1.55/test/org/apache/catalina/filters/TestHttpHeaderSecurityFilter.java | 191 +++ tomcat10-10.1.55/test/org/apache/catalina/filters/TestRateLimitFilter.java | 94 - tomcat10-10.1.55/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java | 115 -- tomcat10-10.1.55/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java | 11 tomcat10-10.1.55/test/org/apache/catalina/filters/TesterHttpServletRequest.java | 12 tomcat10-10.1.55/test/org/apache/catalina/filters/TesterRateLimitClientBase.java | 73 + tomcat10-10.1.55/test/org/apache/catalina/manager/TestManagerWebapp.java | 2 tomcat10-10.1.55/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java | 2 tomcat10-10.1.55/test/org/apache/catalina/realm/TestLockoutRealm.java | 117 ++ tomcat10-10.1.55/test/org/apache/catalina/realm/TestRealmBase.java | 82 + tomcat10-10.1.55/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java | 121 ++ tomcat10-10.1.55/test/org/apache/catalina/servlets/TestWebdavServlet.java | 83 + tomcat10-10.1.55/test/org/apache/catalina/session/FileStoreTest.java | 100 -- tomcat10-10.1.55/test/org/apache/catalina/session/TestFileStore.java | 130 ++ tomcat10-10.1.55/test/org/apache/catalina/startup/LoggingBaseTest.java | 2 tomcat10-10.1.55/test/org/apache/catalina/startup/TomcatBaseTest.java | 6 tomcat10-10.1.55/test/org/apache/catalina/storeconfig/TestStoreConfig.java | 18 tomcat10-10.1.55/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java | 58 - tomcat10-10.1.55/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorAlgorithms.java | 162 +++ tomcat10-10.1.55/test/org/apache/catalina/util/TestServerInfo.java | 497 ++++++++++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestAccessLogValve.java | 12 tomcat10-10.1.55/test/org/apache/catalina/valves/TestAccessLogValveFile.java | 276 +++++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestJsonErrorReportValve.java | 412 ++++++++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java | 28 tomcat10-10.1.55/test/org/apache/catalina/valves/TestPersistentValve.java | 134 ++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestProxyErrorReportValve.java | 290 +++++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestSemaphoreValve.java | 438 ++++++++ tomcat10-10.1.55/test/org/apache/coyote/TestCompressionConfig.java | 60 - tomcat10-10.1.55/test/org/apache/coyote/http11/TestHttp11Processor.java | 30 tomcat10-10.1.55/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java | 95 + tomcat10-10.1.55/test/org/apache/coyote/http11/filters/TestChunkedOutputFilter.java | 123 ++ tomcat10-10.1.55/test/org/apache/coyote/http2/Http2TestBase.java | 31 tomcat10-10.1.55/test/org/apache/coyote/http2/TestHPackHuffman.java | 45 tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_6_2.java | 36 tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_1.java | 54 + tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_2.java | 121 ++ tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_3.java | 74 + tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_5.java | 84 + tomcat10-10.1.55/test/org/apache/coyote/http2/TestLargeUpload.java | 7 tomcat10-10.1.55/test/org/apache/coyote/http2/TestStreamProcessor.java | 6 tomcat10-10.1.55/test/org/apache/el/TestValueExpressionImpl.java | 30 tomcat10-10.1.55/test/org/apache/jasper/compiler/TestGenerator.java | 43 tomcat10-10.1.55/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java | 120 ++ tomcat10-10.1.55/test/org/apache/juli/TestFileHandlerNonRotatable.java | 12 tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java | 176 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java | 73 + tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java | 96 + tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java | 79 + tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java | 111 ++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java | 111 ++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java | 81 + tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java | 88 + tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java | 86 + tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java | 117 ++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TesterHttpd.java | 151 +++ tomcat10-10.1.55/test/org/apache/tomcat/jni/TesterLibraryLoad.java | 37 tomcat10-10.1.55/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java | 3 tomcat10-10.1.55/test/org/apache/tomcat/security/TestSecurity2018.java | 79 + tomcat10-10.1.55/test/org/apache/tomcat/security/TestSecurity2019.java | 185 +++ tomcat10-10.1.55/test/org/apache/tomcat/unittest/TesterRequest.java | 10 tomcat10-10.1.55/test/org/apache/tomcat/unittest/TesterResponseWithStatus.java | 36 tomcat10-10.1.55/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java | 3 tomcat10-10.1.55/test/org/apache/tomcat/util/http/parser/TestChunkExtension.java | 186 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/http/parser/TestMediaType.java | 6 tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestAlpnFallback.java | 52 + tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestLargeClientHello.java | 91 + tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestSSLHostConfig.java | 27 tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestSSLHostConfigProtocol.java | 153 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestSslHandshakeFailure.java | 83 + tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestXxxEndpoint.java | 5 tomcat10-10.1.55/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java | 103 ++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/TesterSupport.java | 15 tomcat10-10.1.55/test/org/apache/tomcat/util/net/index.db.attr | 1 tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java | 8 tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFail.java | 12 tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailInternalError.java | 111 ++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailTryLater.java | 111 ++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java | 169 +-- tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java | 271 +++++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java | 14 tomcat10-10.1.55/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java | 143 +- tomcat10-10.1.55/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java | 21 tomcat10-10.1.55/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java | 57 + tomcat10-10.1.55/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java | 61 - tomcat10-10.1.55/test/org/apache/tomcat/websocket/server/TestClose.java | 19 tomcat10-10.1.55/test/webapp/WEB-INF/classes/bug69623-a.mdd | 15 tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order-a.jspf | 17 tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order-b.jspf | 17 tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order-c.jspf | 17 tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order.jsp | 19 tomcat10-10.1.55/test/webapp/jsp/generator/external-taglib.jsp | 19 tomcat10-10.1.55/webapps/docs/changelog.xml | 470 +++++++++ tomcat10-10.1.55/webapps/docs/config/ajp.xml | 1 tomcat10-10.1.55/webapps/docs/config/cluster-interceptor.xml | 19 tomcat10-10.1.55/webapps/docs/config/filter.xml | 128 +- tomcat10-10.1.55/webapps/docs/config/http.xml | 35 tomcat10-10.1.55/webapps/docs/config/http2.xml | 21 tomcat10-10.1.55/webapps/docs/config/listeners.xml | 31 tomcat10-10.1.55/webapps/docs/config/realm.xml | 6 tomcat10-10.1.55/webapps/docs/config/valve.xml | 14 tomcat10-10.1.55/webapps/docs/ssl-howto.xml | 33 tomcat10-10.1.55/webapps/docs/windows-auth-howto.xml | 17 311 files changed, 12257 insertions(+), 1905 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpz2vn5j0q/tomcat10_10.1.52-1~deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpz2vn5j0q/tomcat10_10.1.55-1~deb13u1.dsc: no acceptable signature found diff -Nru tomcat10-10.1.52/.gitignore tomcat10-10.1.55/.gitignore --- tomcat10-10.1.52/.gitignore 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/.gitignore 2026-05-05 13:36:52.000000000 +0000 @@ -51,4 +51,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 tomcat10-10.1.52/BUILDING.txt tomcat10-10.1.55/BUILDING.txt --- tomcat10-10.1.52/BUILDING.txt 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/BUILDING.txt 2026-05-05 13:36:52.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. @@ -368,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: @@ -534,7 +534,7 @@ 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 @@ -568,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. @@ -580,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 @@ -589,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 tomcat10-10.1.52/CONTRIBUTING.md tomcat10-10.1.55/CONTRIBUTING.md --- tomcat10-10.1.52/CONTRIBUTING.md 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/CONTRIBUTING.md 2026-05-05 13:36:52.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 tomcat10-10.1.52/MERGE.txt tomcat10-10.1.55/MERGE.txt --- tomcat10-10.1.52/MERGE.txt 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/MERGE.txt 2026-05-05 13:36:52.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) Codec ----- diff -Nru tomcat10-10.1.52/README.md tomcat10-10.1.55/README.md --- tomcat10-10.1.52/README.md 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/README.md 2026-05-05 13:36:52.000000000 +0000 @@ -48,11 +48,11 @@ ### 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 @@ -74,4 +74,4 @@ ### Contributing -Please see [CONTRIBUTING](CONTRIBUTING.md) for more info. +Please see [CONTRIBUTING](CONTRIBUTING.md) for more information. diff -Nru tomcat10-10.1.52/bin/catalina.bat tomcat10-10.1.55/bin/catalina.bat --- tomcat10-10.1.52/bin/catalina.bat 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/bin/catalina.bat 2026-05-05 13:36:52.000000000 +0000 @@ -332,7 +332,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 tomcat10-10.1.52/bin/catalina.sh tomcat10-10.1.55/bin/catalina.sh --- tomcat10-10.1.52/bin/catalina.sh 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/bin/catalina.sh 2026-05-05 13:36:52.000000000 +0000 @@ -623,7 +623,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 tomcat10-10.1.52/build.properties.default tomcat10-10.1.55/build.properties.default --- tomcat10-10.1.52/build.properties.default 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/build.properties.default 2026-05-05 13:36:52.000000000 +0000 @@ -31,7 +31,7 @@ # ----- Version Control Flags ----- version.major=10 version.minor=1 -version.build=52 +version.build=55 version.patch=0 version.suffix= version.dev=-dev @@ -182,14 +182,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.12 -tomcat-native-openssl.version=3.5.4 +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=d937e04f7c9f0fa6ef82b32928fa2d59dbdac45cb58c7ba8eff4338fbd942297b1c9512a0a8ff80cf758d9b6ca5cc5cba8cefdc91507318b72afc56888aa1f3c +tomcat-native.src.checksum.value=33d626fab35cbfa7398ca90cabd99950c6362ab4e19637012850fd84ecc78184e4c6c975ece92dc8d6461b6a8c2f83221cbc7374ff154422e7722606a4a144c7 tomcat-native.win.checksum.enabled=true tomcat-native.win.checksum.algorithm=SHA-512 -tomcat-native.win.checksum.value=f743c151a1d48a1967c08f01986b1a30176cc8a388ad760cb8aac19e6956e5630d7ddff54782c2e136e8f247809d573fd48a31ed1a756923a3ad0954e2d4a3fe +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 @@ -198,15 +198,15 @@ 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 @@ -267,19 +267,19 @@ 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.3 +bytebuddy.version=1.18.8 bytebuddy.checksum.enabled=true bytebuddy.checksum.algorithm=SHA-512 -bytebuddy.checksum.value=8f35c806a25d9089a08d12a7aaf22c5bea2f356c432a21655f30a7935918b6385e1e080180b6ef5ad3638796fc3a7243220dfec08c31c1195416e6790fd797af +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 @@ -322,10 +322,10 @@ # ----- bnd, version 6.3.0 or later ----- # ----- provides OSGI metadata for JARs ----- -bnd.version=7.2.0 +bnd.version=7.2.3 bnd.checksum.enabled=true bnd.checksum.algorithm=MD5|SHA-1 -bnd.checksum.value=dea22b7afa9de21e1adb27d2e835a94c|af26ddc466eb178963d4eb800d2824f488037aec +bnd.checksum.value=56f0d8c7d872b88cfc5411f6caa96379|8eaec893e3ed1058acf5b780ce354db5b8226ade bnd.home=${base.path}/bnd-${bnd.version} bnd.jar=${bnd.home}/biz.aQute.bnd-${bnd.version}.jar @@ -372,3 +372,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 tomcat10-10.1.52/build.properties.release tomcat10-10.1.55/build.properties.release --- tomcat10-10.1.52/build.properties.release 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/build.properties.release 2026-05-05 13:36:52.000000000 +0000 @@ -24,7 +24,7 @@ version.dev= # Ensure consistent timestamps for reproducible builds. -ant.tstamp.now.iso=2026-01-23T19:29:07Z +ant.tstamp.now.iso=2026-05-05T13:33:02Z # Enable insertion of detached signatures into the Windows installer. do.codesigning=true @@ -41,14 +41,14 @@ # # Java Name: OpenJDK 64-Bit Server VM # Java Vendor: Eclipse Adoptium -# Java Version: 25.0.1+8-LTS +# Java Version: 25.0.2+10-LTS # The following is provided for information only. Builds will be repeatable # whether or not the build environment is consistent with this information. # -# OS: aarch64 Mac OS X 15.7.3 +# OS: aarch64 Mac OS X 26.3.1 # File encoding: UTF-8 # # Release Manager: schultz -release-java-version=25.0.1+8-LTS +release-java-version=25.0.2+10-LTS release-ant-version=1.10.15 diff -Nru tomcat10-10.1.52/build.xml tomcat10-10.1.55/build.xml --- tomcat10-10.1.52/build.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/build.xml 2026-05-05 13:36:52.000000000 +0000 @@ -222,6 +222,10 @@ + + + + @@ -255,6 +259,9 @@ + + + @@ -273,6 +280,9 @@ + + + @@ -343,6 +353,7 @@ + @@ -2015,6 +2026,9 @@ + + + @@ -2044,6 +2058,11 @@ + + + + + @@ -2099,13 +2118,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}"> @@ -2136,6 +2155,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2196,6 +2273,7 @@ + @@ -2225,6 +2303,8 @@ + + diff -Nru tomcat10-10.1.52/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch tomcat10-10.1.55/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch --- tomcat10-10.1.52/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch 2026-02-03 12:29:34.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/0013-dont-look-for-build-properties-in-user-home.patch 2026-06-02 13:38:19.000000000 +0000 @@ -10,11 +10,9 @@ build.xml | 1 - 1 file changed, 1 deletion(-) -diff --git a/build.xml b/build.xml -index 31c9129..404ba8c 100644 --- a/build.xml +++ b/build.xml -@@ -867,7 +867,6 @@ +@@ -878,7 +878,6 @@ diff -Nru tomcat10-10.1.52/debian/patches/0018-fix-manager-webapp.patch tomcat10-10.1.55/debian/patches/0018-fix-manager-webapp.patch --- tomcat10-10.1.52/debian/patches/0018-fix-manager-webapp.patch 2026-02-03 12:29:34.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/0018-fix-manager-webapp.patch 2026-06-02 13:38:19.000000000 +0000 @@ -13,8 +13,6 @@ webapps/host-manager/WEB-INF/manager.xml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) -diff --git a/conf/catalina.policy b/conf/catalina.policy -index 6a82bcb..9ca7802 100644 --- a/conf/catalina.policy +++ b/conf/catalina.policy @@ -188,7 +188,7 @@ grant { @@ -26,7 +24,7 @@ permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina"; permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.ha.session"; permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager"; -@@ -196,7 +196,7 @@ grant codeBase "file:${catalina.base}/webapps/manager/-" { +@@ -196,7 +196,7 @@ grant codeBase "file:${catalina.base}/we permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.util"; permission org.apache.catalina.security.DeployXmlPermission "manager"; }; @@ -35,7 +33,7 @@ permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina"; permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.ha.session"; permission java.lang.RuntimePermission "accessClassInPackage.org.apache.catalina.manager"; -@@ -211,10 +211,10 @@ grant codeBase "file:${catalina.home}/webapps/manager/-" { +@@ -211,10 +211,10 @@ grant codeBase "file:${catalina.home}/we // - default CATALINA_HOME == CATALINA_BASE // - CATALINA_HOME != CATALINA_BASE, per instance Host Manager in CATALINA_BASE // - CATALINA_HOME != CATALINA_BASE, shared Host Manager in CATALINA_HOME @@ -48,15 +46,13 @@ permission org.apache.catalina.security.DeployXmlPermission "host-manager"; }; -@@ -260,4 +260,4 @@ grant codeBase "file:${catalina.home}/webapps/host-manager/-" { +@@ -260,4 +260,4 @@ grant codeBase "file:${catalina.home}/we // // The permissions granted to a specific JAR // grant codeBase "war:file:${catalina.base}/webapps/examples.war*/WEB-INF/lib/foo.jar" { -// }; \ No newline at end of file +// }; -diff --git a/webapps/docs/manager-howto.xml b/webapps/docs/manager-howto.xml -index 3bd65b9..07512b0 100644 --- a/webapps/docs/manager-howto.xml +++ b/webapps/docs/manager-howto.xml @@ -73,7 +73,7 @@ configuration file in the @@ -68,8 +64,6 @@ @@ -23,8 +21,6 @@ -diff --git a/java/org/apache/catalina/util/ServerInfo.properties b/java/org/apache/catalina/util/ServerInfo.properties -index 3aa34f4..c8dff06 100644 --- a/java/org/apache/catalina/util/ServerInfo.properties +++ b/java/org/apache/catalina/util/ServerInfo.properties @@ -13,7 +13,7 @@ diff -Nru tomcat10-10.1.52/debian/patches/0021-dont-test-unsupported-ciphers.patch tomcat10-10.1.55/debian/patches/0021-dont-test-unsupported-ciphers.patch --- tomcat10-10.1.52/debian/patches/0021-dont-test-unsupported-ciphers.patch 2026-02-03 12:29:34.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/0021-dont-test-unsupported-ciphers.patch 2026-06-02 13:38:19.000000000 +0000 @@ -12,8 +12,6 @@ .../tomcat/util/net/openssl/ciphers/TesterOpenSSL.java | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) -diff --git a/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java b/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java -index 9b9eb5e..9d81307 100644 --- 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 @@ public class TestCipher { @@ -25,11 +23,9 @@ Set expectedCipherSuites = new HashSet<>(); for (Cipher cipher : Cipher.values()) { -diff --git a/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java b/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java -index 1c2b946..8bb4315 100644 --- 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 @@ public class TestOpenSSLCipherConfigurationParser { +@@ -608,7 +608,7 @@ public class TestOpenSSLCipherConfigurat private void testSpecification(String specification) throws Exception { // Filter out cipher suites that OpenSSL does not implement @@ -38,11 +34,9 @@ List jsseCipherListFromOpenSSL = OpenSSLCipherConfigurationParser.parseExpression(openSSLCipherList); List jsseCipherListFromParser = -diff --git a/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java b/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java -index 1c1cf5a..0dc9db8 100644 --- 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 @@ public class TesterOpenSSL { +@@ -107,6 +107,24 @@ public class TesterOpenSSL { 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 tomcat10-10.1.52/debian/patches/0023-disable-shutdown-by-socket.patch tomcat10-10.1.55/debian/patches/0023-disable-shutdown-by-socket.patch --- tomcat10-10.1.52/debian/patches/0023-disable-shutdown-by-socket.patch 2026-02-03 12:29:34.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/0023-disable-shutdown-by-socket.patch 2026-06-02 13:38:19.000000000 +0000 @@ -7,8 +7,6 @@ conf/server.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) -diff --git a/conf/server.xml b/conf/server.xml -index 0ab6264..cf8f617 100644 --- a/conf/server.xml +++ b/conf/server.xml @@ -19,7 +19,7 @@ diff -Nru tomcat10-10.1.52/debian/patches/0024-systemd-log-formatter.patch tomcat10-10.1.55/debian/patches/0024-systemd-log-formatter.patch --- tomcat10-10.1.52/debian/patches/0024-systemd-log-formatter.patch 2026-02-03 12:29:34.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/0024-systemd-log-formatter.patch 2026-06-02 13:38:19.000000000 +0000 @@ -8,9 +8,6 @@ 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 tomcat10-10.1.52/debian/patches/0025-invalid-configuration-exit-status.patch tomcat10-10.1.55/debian/patches/0025-invalid-configuration-exit-status.patch --- tomcat10-10.1.52/debian/patches/0025-invalid-configuration-exit-status.patch 2026-02-03 12:29:34.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/0025-invalid-configuration-exit-status.patch 2026-06-02 13:38:19.000000000 +0000 @@ -8,8 +8,6 @@ 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 8dfc1fb..7250ff7 100644 --- a/java/org/apache/catalina/startup/Bootstrap.java +++ b/java/org/apache/catalina/startup/Bootstrap.java @@ -470,6 +470,10 @@ public final class Bootstrap { diff -Nru tomcat10-10.1.52/debian/patches/0026-easymock4-compatibility.patch tomcat10-10.1.55/debian/patches/0026-easymock4-compatibility.patch --- tomcat10-10.1.52/debian/patches/0026-easymock4-compatibility.patch 2026-02-03 12:29:34.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/0026-easymock4-compatibility.patch 2026-06-02 13:38:19.000000000 +0000 @@ -7,11 +7,9 @@ .../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 @@ public class TestCrawlerSessionManagerValve { +@@ -57,13 +57,13 @@ public class TestCrawlerSessionManagerVa CrawlerSessionManagerValve valve = new CrawlerSessionManagerValve(); valve.setCrawlerIps("216\\.58\\.206\\.174"); valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); @@ -27,7 +25,7 @@ EasyMock.verify(request, session); } -@@ -73,13 +73,13 @@ public class TestCrawlerSessionManagerValve { +@@ -73,13 +73,13 @@ public class TestCrawlerSessionManagerVa CrawlerSessionManagerValve valve = new CrawlerSessionManagerValve(); valve.setCrawlerIps("216\\.58\\.206\\.174"); valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); @@ -43,7 +41,7 @@ EasyMock.verify(request, session); } -@@ -90,7 +90,7 @@ public class TestCrawlerSessionManagerValve { +@@ -90,7 +90,7 @@ public class TestCrawlerSessionManagerVa valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); valve.setHostAware(true); valve.setContextAware(true); @@ -52,7 +50,7 @@ verifyCrawlingLocalhost(valve, "localhost"); verifyCrawlingLocalhost(valve, "example.invalid"); -@@ -102,7 +102,7 @@ public class TestCrawlerSessionManagerValve { +@@ -102,7 +102,7 @@ public class TestCrawlerSessionManagerVa valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); valve.setHostAware(true); valve.setContextAware(true); @@ -61,7 +59,7 @@ verifyCrawlingContext(valve, "/examples"); verifyCrawlingContext(valve, null); -@@ -113,7 +113,7 @@ public class TestCrawlerSessionManagerValve { +@@ -113,7 +113,7 @@ public class TestCrawlerSessionManagerVa CrawlerSessionManagerValve valve = new CrawlerSessionManagerValve(); valve.setCrawlerIps("216\\.58\\.206\\.174"); valve.setCrawlerUserAgents(valve.getCrawlerUserAgents()); @@ -70,7 +68,7 @@ valve.setSessionInactiveInterval(0); StandardSession session = new StandardSession(TEST_MANAGER); session.setId("id"); -@@ -123,7 +123,7 @@ public class TestCrawlerSessionManagerValve { +@@ -123,7 +123,7 @@ public class TestCrawlerSessionManagerVa EasyMock.replay(request); @@ -79,7 +77,7 @@ EasyMock.verify(request); -@@ -142,7 +142,7 @@ public class TestCrawlerSessionManagerValve { +@@ -142,7 +142,7 @@ public class TestCrawlerSessionManagerVa EasyMock.replay(request, session); @@ -88,7 +86,7 @@ EasyMock.verify(request, session); } -@@ -156,14 +156,14 @@ public class TestCrawlerSessionManagerValve { +@@ -156,14 +156,14 @@ public class TestCrawlerSessionManagerVa EasyMock.replay(request, session); @@ -105,7 +103,7 @@ if (isBot) { EasyMock.expect(session.getId()).andReturn("id").times(1); session.setAttribute(EasyMock.eq(valve.getClass().getName()), -@@ -182,7 +182,7 @@ public class TestCrawlerSessionManagerValve { +@@ -182,7 +182,7 @@ public class TestCrawlerSessionManagerVa private Request createRequestExpectations(String ip, HttpSession session, boolean isBot, String hostname, String contextPath, String userAgent) { @@ -114,7 +112,7 @@ EasyMock.expect(request.getRemoteAddr()).andReturn(ip); EasyMock.expect(request.getRemoteAddr()).andReturn(ip); EasyMock.expect(request.getHost()).andReturn(simpleHostWithName(hostname)); -@@ -198,7 +198,7 @@ public class TestCrawlerSessionManagerValve { +@@ -198,7 +198,7 @@ public class TestCrawlerSessionManagerVa } private Host simpleHostWithName(String hostname) { @@ -123,7 +121,7 @@ EasyMock.expect(host.getName()).andReturn(hostname); EasyMock.replay(host); return host; -@@ -208,7 +208,7 @@ public class TestCrawlerSessionManagerValve { +@@ -208,7 +208,7 @@ public class TestCrawlerSessionManagerVa if (contextPath == null) { return null; } diff -Nru tomcat10-10.1.52/debian/patches/0030-eclipse-jdt-classpath.patch tomcat10-10.1.55/debian/patches/0030-eclipse-jdt-classpath.patch --- tomcat10-10.1.52/debian/patches/0030-eclipse-jdt-classpath.patch 2026-02-03 12:29:34.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/0030-eclipse-jdt-classpath.patch 2026-06-02 13:38:19.000000000 +0000 @@ -7,11 +7,9 @@ build.xml | 1 + 1 file changed, 1 insertion(+) -diff --git a/build.xml b/build.xml -index 5f30263..7d05c45 100644 --- a/build.xml +++ b/build.xml -@@ -233,6 +233,7 @@ +@@ -237,6 +237,7 @@ diff -Nru tomcat10-10.1.52/debian/patches/bouncycastle.patch tomcat10-10.1.55/debian/patches/bouncycastle.patch --- tomcat10-10.1.52/debian/patches/bouncycastle.patch 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/bouncycastle.patch 2026-06-02 13:38:19.000000000 +0000 @@ -0,0 +1,51 @@ +From: Markus Koschany +Date: Thu, 16 Apr 2026 21:54:06 +0200 +Subject: bouncycastle + +Small adjustments due to older bouncycastle version in bullseye. + +Forwarded: not-needed +--- + .../tomcat/util/net/ocsp/TesterOcspResponderServlet.java | 10 ++++++++-- + 1 file changed, 8 insertions(+), 2 deletions(-) + +--- a/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java ++++ b/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java +@@ -65,6 +65,9 @@ import org.bouncycastle.operator.DigestC + import org.bouncycastle.operator.OperatorCreationException; + import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; ++import org.bouncycastle.asn1.x509.CRLReason; ++import org.bouncycastle.asn1.ocsp.RevokedInfo; ++import org.bouncycastle.asn1.ASN1GeneralizedTime; + + /* + * Based on https://github.com/wdawson/revoker - ALv2 licensed +@@ -216,6 +219,9 @@ public class TesterOcspResponderServlet + Req[] requests = ocspReq.getRequestList(); + for (Req request : requests) { + CertificateID certificateID = request.getCertID(); ++ ASN1GeneralizedTime revocationTime = new ASN1GeneralizedTime(new Date()); ++ CRLReason reason = CRLReason.lookup(CRLReason.keyCompromise); ++ RevokedInfo revokedInfo = new RevokedInfo(revocationTime, reason); + if (fixedResponse == null) { + switch (certificateID.getSerialNumber().intValue()) { + // TODO read index.db rather than hard-code certificate serial numbers +@@ -228,7 +234,7 @@ public class TesterOcspResponderServlet + case 4097: + case 4099: + case 4102: +- responseBuilder.addResponse(certificateID, new RevokedStatus(new Date(0))); ++ responseBuilder.addResponse(certificateID, new RevokedStatus(revokedInfo)); + break; + default: + responseBuilder.addResponse(certificateID, new UnknownStatus()); +@@ -239,7 +245,7 @@ public class TesterOcspResponderServlet + responseBuilder.addResponse(certificateID, CertificateStatus.GOOD); + break; + case REVOKED: +- responseBuilder.addResponse(certificateID, new RevokedStatus(new Date(0))); ++ responseBuilder.addResponse(certificateID, new RevokedStatus(revokedInfo)); + break; + case TRY_LATER: + // NO-OP diff -Nru tomcat10-10.1.52/debian/patches/disable-jacoco.patch tomcat10-10.1.55/debian/patches/disable-jacoco.patch --- tomcat10-10.1.52/debian/patches/disable-jacoco.patch 2026-02-03 12:29:34.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/disable-jacoco.patch 2026-06-02 13:38:19.000000000 +0000 @@ -7,11 +7,9 @@ build.xml | 12 ------------ 1 file changed, 12 deletions(-) -diff --git a/build.xml b/build.xml -index 7d05c45..969b037 100644 --- a/build.xml +++ b/build.xml -@@ -2167,10 +2167,6 @@ +@@ -2244,10 +2244,6 @@ @@ -22,7 +20,7 @@ @@ -30,7 +28,7 @@ -@@ -3853,15 +3848,8 @@ Configured for ${release.asfusername} to release Tomcat ${version.major}.${versi +@@ -3960,15 +3955,8 @@ Configured for ${release.asfusername} to diff -Nru tomcat10-10.1.52/debian/patches/exclude-TestJNDIRealmIntegration.patch tomcat10-10.1.55/debian/patches/exclude-TestJNDIRealmIntegration.patch --- tomcat10-10.1.52/debian/patches/exclude-TestJNDIRealmIntegration.patch 2026-02-03 12:29:34.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/exclude-TestJNDIRealmIntegration.patch 2026-06-02 13:38:19.000000000 +0000 @@ -10,11 +10,9 @@ build.xml | 2 ++ 1 file changed, 2 insertions(+) -diff --git a/build.xml b/build.xml -index 969b037..5c56c29 100644 --- a/build.xml +++ b/build.xml -@@ -1945,6 +1945,7 @@ +@@ -1956,6 +1956,7 @@ @@ -22,7 +20,7 @@ -@@ -2235,6 +2236,7 @@ +@@ -2315,6 +2316,7 @@ tests. See below for more details. --> diff -Nru tomcat10-10.1.52/debian/patches/series tomcat10-10.1.55/debian/patches/series --- tomcat10-10.1.52/debian/patches/series 2026-02-03 12:29:34.000000000 +0000 +++ tomcat10-10.1.55/debian/patches/series 2026-06-02 13:38:19.000000000 +0000 @@ -13,3 +13,4 @@ 0030-eclipse-jdt-classpath.patch disable-jacoco.patch exclude-TestJNDIRealmIntegration.patch +bouncycastle.patch diff -Nru tomcat10-10.1.52/java/jakarta/el/CompositeELResolver.java tomcat10-10.1.55/java/jakarta/el/CompositeELResolver.java --- tomcat10-10.1.52/java/jakarta/el/CompositeELResolver.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/jakarta/el/CompositeELResolver.java 2026-05-05 13:36:52.000000000 +0000 @@ -234,6 +234,7 @@ this.guaranteeIterator(); } + @SuppressWarnings("removal") private void guaranteeIterator() { while (this.itr == null && this.idx < this.size) { this.itr = this.resolvers[this.idx].getFeatureDescriptors(this.context, this.base); diff -Nru tomcat10-10.1.52/java/org/apache/catalina/AccessLog.java tomcat10-10.1.55/java/org/apache/catalina/AccessLog.java --- tomcat10-10.1.52/java/org/apache/catalina/AccessLog.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/AccessLog.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/Context.java tomcat10-10.1.55/java/org/apache/catalina/Context.java --- tomcat10-10.1.52/java/org/apache/catalina/Context.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/Context.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/JmxEnabled.java tomcat10-10.1.55/java/org/apache/catalina/JmxEnabled.java --- tomcat10-10.1.52/java/org/apache/catalina/JmxEnabled.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/JmxEnabled.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/WebResource.java tomcat10-10.1.55/java/org/apache/catalina/WebResource.java --- tomcat10-10.1.52/java/org/apache/catalina/WebResource.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/WebResource.java 2026-05-05 13:36:52.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. */ @@ -153,11 +183,15 @@ URL getCodeBase(); /** + * 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() @@ -165,6 +199,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 tomcat10-10.1.52/java/org/apache/catalina/WebResourceRoot.java tomcat10-10.1.55/java/org/apache/catalina/WebResourceRoot.java --- tomcat10-10.1.52/java/org/apache/catalina/WebResourceRoot.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/WebResourceRoot.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java tomcat10-10.1.55/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java --- tomcat10-10.1.52/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/ant/AbstractCatalinaCommandTask.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/authenticator/AuthenticatorBase.java tomcat10-10.1.55/java/org/apache/catalina/authenticator/AuthenticatorBase.java --- tomcat10-10.1.52/java/org/apache/catalina/authenticator/AuthenticatorBase.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/authenticator/AuthenticatorBase.java 2026-05-05 13:36:52.000000000 +0000 @@ -529,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; diff -Nru tomcat10-10.1.52/java/org/apache/catalina/authenticator/DigestAuthenticator.java tomcat10-10.1.55/java/org/apache/catalina/authenticator/DigestAuthenticator.java --- tomcat10-10.1.52/java/org/apache/catalina/authenticator/DigestAuthenticator.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/authenticator/DigestAuthenticator.java 2026-05-05 13:36:52.000000000 +0000 @@ -42,6 +42,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; /** @@ -640,7 +641,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 tomcat10-10.1.52/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java tomcat10-10.1.55/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java --- tomcat10-10.1.52/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java 2026-05-05 13:36:52.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 } @@ -179,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")); @@ -304,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 tomcat10-10.1.52/java/org/apache/catalina/connector/CoyoteAdapter.java tomcat10-10.1.55/java/org/apache/catalina/connector/CoyoteAdapter.java --- tomcat10-10.1.52/java/org/apache/catalina/connector/CoyoteAdapter.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/connector/CoyoteAdapter.java 2026-05-05 13:36:52.000000000 +0000 @@ -558,9 +558,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) @@ -1112,6 +1113,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(); @@ -1286,7 +1289,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 tomcat10-10.1.52/java/org/apache/catalina/connector/Response.java tomcat10-10.1.55/java/org/apache/catalina/connector/Response.java --- tomcat10-10.1.52/java/org/apache/catalina/connector/Response.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/connector/Response.java 2026-05-05 13:36:52.000000000 +0000 @@ -768,6 +768,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); } @@ -780,13 +797,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 tomcat10-10.1.52/java/org/apache/catalina/core/AprLifecycleListener.java tomcat10-10.1.55/java/org/apache/catalina/core/AprLifecycleListener.java --- tomcat10-10.1.52/java/org/apache/catalina/core/AprLifecycleListener.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/core/AprLifecycleListener.java 2026-05-05 13:36:52.000000000 +0000 @@ -66,15 +66,13 @@ private static final int TCN_1_REQUIRED_MINOR = 3; private static final int TCN_1_REQUIRED_PATCH = 4; - private static final int TCN_1_RECOMMENDED_MINOR = 3; - private static final int TCN_1_RECOMMENDED_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 = 12; + protected static final int TCN_RECOMMENDED_PV = 14; // ---------------------------------------------- Properties @@ -124,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); } @@ -265,7 +353,7 @@ int rcver; if (tcnMajor == 1) { rqver = 1000 + TCN_1_REQUIRED_MINOR * 100 + TCN_1_REQUIRED_PATCH; - rcver = 1000 + TCN_1_RECOMMENDED_MINOR * 100 + TCN_1_RECOMMENDED_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; @@ -290,13 +378,8 @@ return; } if (tcnVersion < rcver) { - if (tcnMajor == 1) { - initInfoLogMessages.add(sm.getString("aprListener.tcnVersion.1", Library.versionString(), - "1." + TCN_1_RECOMMENDED_MINOR + "." + TCN_1_RECOMMENDED_PATCH)); - } else { - initInfoLogMessages.add(sm.getString("aprListener.tcnVersion", Library.versionString(), - TCN_RECOMMENDED_MAJOR + "." + TCN_RECOMMENDED_MINOR + "." + TCN_RECOMMENDED_PV)); - } + initInfoLogMessages.add(sm.getString("aprListener.tcnVersion", Library.versionString(), + TCN_RECOMMENDED_MAJOR + "." + TCN_RECOMMENDED_MINOR + "." + TCN_RECOMMENDED_PV)); } initInfoLogMessages diff -Nru tomcat10-10.1.52/java/org/apache/catalina/core/LocalStrings.properties tomcat10-10.1.55/java/org/apache/catalina/core/LocalStrings.properties --- tomcat10-10.1.52/java/org/apache/catalina/core/LocalStrings.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/core/LocalStrings.properties 2026-05-05 13:36:52.000000000 +0000 @@ -94,7 +94,6 @@ 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.tcnVersion.1=An older version [{0}] of the Apache Tomcat Native library is installed, while Tomcat recommends a minimum version of [{1}] of Tomcat Native 1.x aprListener.tooLateForFIPSMode=Cannot setFIPSMode: SSL has already been initialized aprListener.tooLateForSSLEngine=Cannot setSSLEngine: SSL has already been initialized aprListener.tooLateForSSLRandomSeed=Cannot setSSLRandomSeed: SSL has already been initialized diff -Nru tomcat10-10.1.52/java/org/apache/catalina/core/LocalStrings_fr.properties tomcat10-10.1.55/java/org/apache/catalina/core/LocalStrings_fr.properties --- tomcat10-10.1.52/java/org/apache/catalina/core/LocalStrings_fr.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/core/LocalStrings_fr.properties 2026-05-05 13:36:52.000000000 +0000 @@ -91,6 +91,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é @@ -276,7 +277,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 tomcat10-10.1.52/java/org/apache/catalina/core/LocalStrings_ja.properties tomcat10-10.1.55/java/org/apache/catalina/core/LocalStrings_ja.properties --- tomcat10-10.1.52/java/org/apache/catalina/core/LocalStrings_ja.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/core/LocalStrings_ja.properties 2026-05-05 13:36:52.000000000 +0000 @@ -91,6 +91,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は既に初期化されています。 diff -Nru tomcat10-10.1.52/java/org/apache/catalina/core/OpenSSLLifecycleListener.java tomcat10-10.1.55/java/org/apache/catalina/core/OpenSSLLifecycleListener.java --- tomcat10-10.1.52/java/org/apache/catalina/core/OpenSSLLifecycleListener.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/core/OpenSSLLifecycleListener.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/core/StandardContext.java tomcat10-10.1.55/java/org/apache/catalina/core/StandardContext.java --- tomcat10-10.1.52/java/org/apache/catalina/core/StandardContext.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/core/StandardContext.java 2026-05-05 13:36:52.000000000 +0000 @@ -280,7 +280,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 @@ -2185,12 +2185,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 tomcat10-10.1.52/java/org/apache/catalina/ha/context/ReplicatedContext.java tomcat10-10.1.55/java/org/apache/catalina/ha/context/ReplicatedContext.java --- tomcat10-10.1.52/java/org/apache/catalina/ha/context/ReplicatedContext.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/ha/context/ReplicatedContext.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/ha/tcp/ReplicationValve.java tomcat10-10.1.55/java/org/apache/catalina/ha/tcp/ReplicationValve.java --- tomcat10-10.1.52/java/org/apache/catalina/ha/tcp/ReplicationValve.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/ha/tcp/ReplicationValve.java 2026-05-05 13:36:52.000000000 +0000 @@ -363,16 +363,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 tomcat10-10.1.52/java/org/apache/catalina/ha/tcp/SendMessageData.java tomcat10-10.1.55/java/org/apache/catalina/ha/tcp/SendMessageData.java --- tomcat10-10.1.52/java/org/apache/catalina/ha/tcp/SendMessageData.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/ha/tcp/SendMessageData.java 2026-05-05 13:36:52.000000000 +0000 @@ -26,6 +26,8 @@ /** + * Data about a message send operation. + * * @param message The message to send * @param destination Member destination * @param exception Associated error diff -Nru tomcat10-10.1.52/java/org/apache/catalina/manager/HTMLManagerServlet.java tomcat10-10.1.55/java/org/apache/catalina/manager/HTMLManagerServlet.java --- tomcat10-10.1.52/java/org/apache/catalina/manager/HTMLManagerServlet.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/manager/HTMLManagerServlet.java 2026-05-05 13:36:52.000000000 +0000 @@ -1170,34 +1170,34 @@ "\n" + "\n" + " \n" + " \n" + "\n" + "\n" + " \n" + " \n" + "\n" + "\n" + " \n" + " \n" + "\n" + "\n" + " \n" + " \n" + "\n" + "\n" + @@ -1224,10 +1224,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" + @@ -1260,10 +1260,10 @@ "
\n" + - " {2}\n" + + " \n" + " \n" + - " \n" + + " \n" + "
\n" + "\n" + " \n" + " \n" + "\n" + "\n" + diff -Nru tomcat10-10.1.52/java/org/apache/catalina/manager/host/HostManagerServlet.java tomcat10-10.1.55/java/org/apache/catalina/manager/host/HostManagerServlet.java --- tomcat10-10.1.52/java/org/apache/catalina/manager/host/HostManagerServlet.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/manager/host/HostManagerServlet.java 2026-05-05 13:36:52.000000000 +0000 @@ -66,6 +66,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 tomcat10-10.1.52/java/org/apache/catalina/mapper/LocalStrings.properties tomcat10-10.1.55/java/org/apache/catalina/mapper/LocalStrings.properties --- tomcat10-10.1.52/java/org/apache/catalina/mapper/LocalStrings.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/mapper/LocalStrings.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/mapper/LocalStrings_fr.properties tomcat10-10.1.55/java/org/apache/catalina/mapper/LocalStrings_fr.properties --- tomcat10-10.1.52/java/org/apache/catalina/mapper/LocalStrings_fr.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/mapper/LocalStrings_fr.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/mapper/LocalStrings_ja.properties tomcat10-10.1.55/java/org/apache/catalina/mapper/LocalStrings_ja.properties --- tomcat10-10.1.52/java/org/apache/catalina/mapper/LocalStrings_ja.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/mapper/LocalStrings_ja.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/mapper/Mapper.java tomcat10-10.1.55/java/org/apache/catalina/mapper/Mapper.java --- tomcat10-10.1.52/java/org/apache/catalina/mapper/Mapper.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/mapper/Mapper.java 2026-05-05 13:36:52.000000000 +0000 @@ -698,7 +698,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 tomcat10-10.1.52/java/org/apache/catalina/mapper/MappingData.java tomcat10-10.1.55/java/org/apache/catalina/mapper/MappingData.java --- tomcat10-10.1.52/java/org/apache/catalina/mapper/MappingData.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/mapper/MappingData.java 2026-05-05 13:36:52.000000000 +0000 @@ -57,4 +57,10 @@ redirectPath.recycle(); matchType = null; } + + @Override + public final String toString() { + return "MappingData[" + host + ":" + context + ":" + wrapper + "]"; + } + } diff -Nru tomcat10-10.1.52/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java tomcat10-10.1.55/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java --- tomcat10-10.1.52/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/realm/LockOutRealm.java tomcat10-10.1.55/java/org/apache/catalina/realm/LockOutRealm.java --- tomcat10-10.1.52/java/org/apache/catalina/realm/LockOutRealm.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/realm/LockOutRealm.java 2026-05-05 13:36:52.000000000 +0000 @@ -19,6 +19,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; @@ -71,6 +72,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 { @@ -205,6 +211,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); @@ -227,6 +236,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); } @@ -236,6 +248,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)) { @@ -337,6 +352,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 tomcat10-10.1.52/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java tomcat10-10.1.55/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java --- tomcat10-10.1.52/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/realm/RealmBase.java tomcat10-10.1.55/java/org/apache/catalina/realm/RealmBase.java --- tomcat10-10.1.52/java/org/apache/catalina/realm/RealmBase.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/realm/RealmBase.java 2026-05-05 13:36:52.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; @@ -424,7 +425,7 @@ "digestA2:" + digestA2 + " Server digest:" + serverDigest); } - if (serverDigest.equals(clientDigest)) { + if (ConstantTime.equals(serverDigest, clientDigest, true)) { return getPrincipal(username); } @@ -666,8 +667,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(); @@ -677,6 +676,7 @@ continue; } + boolean matched = false; for (int k = 0; k < patterns.length && !matched; k++) { String pattern = patterns[k]; if (pattern.startsWith("*.")) { @@ -686,19 +686,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]); } } } @@ -1146,12 +1145,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 tomcat10-10.1.52/java/org/apache/catalina/servlets/WebdavServlet.java tomcat10-10.1.55/java/org/apache/catalina/servlets/WebdavServlet.java --- tomcat10-10.1.52/java/org/apache/catalina/servlets/WebdavServlet.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/servlets/WebdavServlet.java 2026-05-05 13:36:52.000000000 +0000 @@ -174,6 +174,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 */ @@ -200,6 +202,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. */ @@ -213,6 +221,7 @@ "\n \n" + " \n"; + /** * Simple date format for the creation date ISO representation (partial). */ @@ -225,6 +234,7 @@ */ protected static final String LOCK_SCHEME = "urn:uuid:"; + // ----------------------------------------------------- Instance Variables /** @@ -245,6 +255,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)? */ @@ -291,6 +304,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")); } @@ -835,12 +852,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) { @@ -1001,6 +1014,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 @@ -1241,6 +1289,7 @@ WebResource resource = resources.getResource(path); if (!checkIfHeaders(req, resp, resource)) { resp.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED); + return; } deleteResource(path, req, resp, true); } @@ -1394,14 +1443,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(); @@ -1888,6 +1934,9 @@ } String hrefPath = hrefUri.getPath(); + if (hrefPath == null) { + return null; + } // Avoid path traversals if (!hrefPath.equals(RequestUtil.normalize(hrefPath))) { @@ -2050,6 +2099,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))) { @@ -3017,6 +3070,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 tomcat10-10.1.52/java/org/apache/catalina/tribes/group/AbsoluteOrder.java tomcat10-10.1.55/java/org/apache/catalina/tribes/group/AbsoluteOrder.java --- tomcat10-10.1.52/java/org/apache/catalina/tribes/group/AbsoluteOrder.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/group/AbsoluteOrder.java 2026-05-05 13:36:52.000000000 +0000 @@ -45,13 +45,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; @@ -59,6 +70,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; @@ -66,10 +82,19 @@ members.sort(comp); } + /** + * A comparator for absolute ordering of members. + */ public static class AbsoluteComparator implements Comparator, Serializable { private static final long serialVersionUID = 1L; + /** + * Default constructor. + */ + public AbsoluteComparator() { + } + @Override public int compare(Member m1, Member m2) { int result = compareIps(m1, m2); @@ -82,18 +107,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) { @@ -108,10 +161,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 tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java --- tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/EncryptInterceptor.java 2026-05-05 13:36:52.000000000 +0000 @@ -21,6 +21,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; @@ -41,7 +42,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 @@ -149,7 +149,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. */ @@ -313,33 +314,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 tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties --- tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties --- tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_fr.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties --- tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ja.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties --- tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_ko.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties --- tomcat10-10.1.52/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/group/interceptors/LocalStrings_zh_CN.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java --- tomcat10-10.1.52/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/AbstractStreamProvider.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java --- tomcat10-10.1.52/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/KubernetesMembershipProvider.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties --- tomcat10-10.1.52/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/LocalStrings.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java --- tomcat10-10.1.52/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/membership/cloud/TokenStreamProvider.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java tomcat10-10.1.55/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java --- tomcat10-10.1.52/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java 2026-05-05 13:36:52.000000000 +0000 @@ -49,6 +49,8 @@ import org.apache.juli.logging.LogFactory; /** + * An abstract replicated map implementation. + * * @param The type of Key * @param The type of Value */ diff -Nru tomcat10-10.1.52/java/org/apache/catalina/util/ServerInfo.java tomcat10-10.1.55/java/org/apache/catalina/util/ServerInfo.java --- tomcat10-10.1.52/java/org/apache/catalina/util/ServerInfo.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/util/ServerInfo.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/valves/AbstractAccessLogValve.java tomcat10-10.1.55/java/org/apache/catalina/valves/AbstractAccessLogValve.java --- tomcat10-10.1.52/java/org/apache/catalina/valves/AbstractAccessLogValve.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/valves/AbstractAccessLogValve.java 2026-05-05 13:36:52.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; @@ -1160,9 +1353,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) { @@ -1173,11 +1374,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()); } @@ -1188,9 +1386,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) { @@ -1209,7 +1415,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 { @@ -1221,10 +1427,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: @@ -1267,18 +1481,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 @@ -1304,9 +1528,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) { @@ -1316,16 +1548,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) { @@ -1339,18 +1580,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) { @@ -1378,15 +1628,6 @@ this.style = 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 - */ - public ElapsedTimeElement(boolean micros, boolean millis) { - this(micros ? Style.MICROSECONDS : millis ? Style.MILLISECONDS : Style.SECONDS); - } - @Override public void addElement(CharArrayWriter buf, Request request, Response response, long time) { style.append(buf, time); @@ -1394,9 +1635,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(); @@ -1410,26 +1659,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) { @@ -1446,13 +1723,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('-'); } @@ -1460,9 +1745,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; @@ -1487,11 +1780,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; } @@ -1503,11 +1801,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; } @@ -1528,11 +1831,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; } @@ -1566,11 +1874,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; } @@ -1593,11 +1906,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; } @@ -1623,11 +1941,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; } @@ -1656,9 +1979,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) { @@ -1699,20 +2030,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": @@ -1789,6 +2127,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) { @@ -1842,7 +2187,7 @@ } else if ("fracsec".equals(name)) { return new ElapsedTimeElement(ElapsedTimeElement.Style.SECONDS_FRACTIONAL); } else { - return new ElapsedTimeElement(false, false); + return new ElapsedTimeElement(ElapsedTimeElement.Style.SECONDS); } default: return new StringElement("???"); @@ -1867,7 +2212,7 @@ case 'B': return new ByteSentElement(false); case 'D': - return new ElapsedTimeElement(true, false); + return new ElapsedTimeElement(ElapsedTimeElement.Style.MICROSECONDS); case 'F': return new FirstByteTimeElement(); case 'h': @@ -1891,7 +2236,7 @@ case 't': return new DateAndTimeElement(); case 'T': - return new ElapsedTimeElement(false, false); + return new ElapsedTimeElement(ElapsedTimeElement.Style.SECONDS); case 'u': return new UserElement(); case 'U': @@ -1937,10 +2282,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 tomcat10-10.1.52/java/org/apache/catalina/valves/ExtendedAccessLogValve.java tomcat10-10.1.55/java/org/apache/catalina/valves/ExtendedAccessLogValve.java --- tomcat10-10.1.52/java/org/apache/catalina/valves/ExtendedAccessLogValve.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/valves/ExtendedAccessLogValve.java 2026-05-05 13:36:52.000000000 +0000 @@ -588,27 +588,24 @@ } else if ("query".equals(token)) { 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('-'); + public void addElement(CharArrayWriter buf, Request request, Response response, long time) { + String query = null; + if (request != null) { + query = request.getQueryString(); } + appendQueryString(query, buf, false, true, true); } }; } } else { 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()); + public void addElement(CharArrayWriter buf, Request request, Response response, long time) { + if (request != null) { + escapeAndAppend(request.getRequestURI(), buf); + appendQueryString(request.getQueryString(), buf, true, true, false); + } else { + buf.append('-'); } } }; diff -Nru tomcat10-10.1.52/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java tomcat10-10.1.55/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java --- tomcat10-10.1.52/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/valves/LoadBalancerDrainingValve.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties tomcat10-10.1.55/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties --- tomcat10-10.1.52/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/valves/LocalStrings_zh_CN.properties 2026-05-05 13:36:52.000000000 +0000 @@ -131,6 +131,7 @@ jdbcAccessLogValve.close=无法关闭数据库。 jdbcAccessLogValve.exception=执行插入访问项时发生异常 +persistentValve.acquireInterrupted=对 [{0}] 的请求未能获取每个会话的信号量,因为在等待许可时被中断。 persistentValve.filter.failure=无法编译filter=[{0}] remoteCidrValve.invalid=为[{0}]提供的配置无效。有关详细信息,请参阅以前的消息 diff -Nru tomcat10-10.1.52/java/org/apache/catalina/webresources/AbstractArchiveResource.java tomcat10-10.1.55/java/org/apache/catalina/webresources/AbstractArchiveResource.java --- tomcat10-10.1.52/java/org/apache/catalina/webresources/AbstractArchiveResource.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/webresources/AbstractArchiveResource.java 2026-05-05 13:36:52.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; @@ -40,6 +43,18 @@ private boolean readCerts = false; private Certificate[] certificates; + /* + * The expectation is that this will be deprecated and then removed once the SecurityManager has been fully removed + * 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) { super(archiveResourceSet.getRoot(), webAppPath); @@ -65,18 +80,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; } @@ -246,6 +281,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; @@ -309,6 +350,11 @@ return is.markSupported(); } + /** + * Returns the certificates. + * + * @return the certificates + */ public Certificate[] getCertificates() { return jarEntry.getCertificates(); } diff -Nru tomcat10-10.1.52/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java tomcat10-10.1.55/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java --- tomcat10-10.1.52/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/webresources/AbstractArchiveResourceSet.java 2026-05-05 13:36:52.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 @@ -334,6 +378,9 @@ } } + /** + * Closes the JAR file. + */ protected void closeJarFile() { synchronized (archiveLock) { archiveUseCount--; diff -Nru tomcat10-10.1.52/java/org/apache/catalina/webresources/AbstractResource.java tomcat10-10.1.55/java/org/apache/catalina/webresources/AbstractResource.java --- tomcat10-10.1.52/java/org/apache/catalina/webresources/AbstractResource.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/webresources/AbstractResource.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties tomcat10-10.1.55/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties --- tomcat10-10.1.52/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/catalina/webresources/LocalStrings_zh_CN.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/AbstractProtocol.java tomcat10-10.1.55/java/org/apache/coyote/AbstractProtocol.java --- tomcat10-10.1.52/java/org/apache/coyote/AbstractProtocol.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/AbstractProtocol.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/CompressionConfig.java tomcat10-10.1.55/java/org/apache/coyote/CompressionConfig.java --- tomcat10-10.1.52/java/org/apache/coyote/CompressionConfig.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/CompressionConfig.java 2026-05-05 13:36:52.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; + } /** @@ -215,9 +245,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 tomcat10-10.1.52/java/org/apache/coyote/LocalStrings.properties tomcat10-10.1.55/java/org/apache/coyote/LocalStrings.properties --- tomcat10-10.1.52/java/org/apache/coyote/LocalStrings.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/LocalStrings.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/Request.java tomcat10-10.1.55/java/org/apache/coyote/Request.java --- tomcat10-10.1.52/java/org/apache/coyote/Request.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/Request.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/Response.java tomcat10-10.1.55/java/org/apache/coyote/Response.java --- tomcat10-10.1.52/java/org/apache/coyote/Response.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/Response.java 2026-05-05 13:36:52.000000000 +0000 @@ -415,8 +415,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; @@ -425,12 +423,11 @@ try { 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 tomcat10-10.1.52/java/org/apache/coyote/UpgradeProtocol.java tomcat10-10.1.55/java/org/apache/coyote/UpgradeProtocol.java --- tomcat10-10.1.52/java/org/apache/coyote/UpgradeProtocol.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/UpgradeProtocol.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/ajp/AbstractAjpProtocol.java tomcat10-10.1.55/java/org/apache/coyote/ajp/AbstractAjpProtocol.java --- tomcat10-10.1.52/java/org/apache/coyote/ajp/AbstractAjpProtocol.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/ajp/AbstractAjpProtocol.java 2026-05-05 13:36:52.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; } @@ -179,10 +209,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; } @@ -190,14 +230,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; } @@ -208,15 +263,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; @@ -225,42 +295,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(sm.getString("ajpprotocol.noUpgradeHandler", @@ -268,6 +375,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 tomcat10-10.1.52/java/org/apache/coyote/ajp/AjpProcessor.java tomcat10-10.1.55/java/org/apache/coyote/ajp/AjpProcessor.java --- tomcat10-10.1.52/java/org/apache/coyote/ajp/AjpProcessor.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/ajp/AjpProcessor.java 2026-05-05 13:36:52.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. @@ -836,7 +837,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 tomcat10-10.1.52/java/org/apache/coyote/http11/AbstractHttp11Protocol.java tomcat10-10.1.55/java/org/apache/coyote/http11/AbstractHttp11Protocol.java --- tomcat10-10.1.52/java/org/apache/coyote/http11/AbstractHttp11Protocol.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http11/AbstractHttp11Protocol.java 2026-05-05 13:36:52.000000000 +0000 @@ -402,6 +402,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 tomcat10-10.1.52/java/org/apache/coyote/http11/LocalStrings_fr.properties tomcat10-10.1.55/java/org/apache/coyote/http11/LocalStrings_fr.properties --- tomcat10-10.1.52/java/org/apache/coyote/http11/LocalStrings_fr.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http11/LocalStrings_fr.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http11/LocalStrings_ja.properties tomcat10-10.1.55/java/org/apache/coyote/http11/LocalStrings_ja.properties --- tomcat10-10.1.52/java/org/apache/coyote/http11/LocalStrings_ja.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http11/LocalStrings_ja.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java tomcat10-10.1.55/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java --- tomcat10-10.1.52/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java 2026-05-05 13:36:52.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); } diff -Nru tomcat10-10.1.52/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java tomcat10-10.1.55/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java --- tomcat10-10.1.52/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http11/filters/GzipOutputFilter.java tomcat10-10.1.55/java/org/apache/coyote/http11/filters/GzipOutputFilter.java --- tomcat10-10.1.52/java/org/apache/coyote/http11/filters/GzipOutputFilter.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http11/filters/GzipOutputFilter.java 2026-05-05 13:36:52.000000000 +0000 @@ -157,11 +157,13 @@ @Override public void flush() throws IOException { - /* NOOP */} + // NOOP + } @Override public void close() throws IOException { - /* NOOP */} + // NOOP + } } diff -Nru tomcat10-10.1.52/java/org/apache/coyote/http2/HPackHuffman.java tomcat10-10.1.55/java/org/apache/coyote/http2/HPackHuffman.java --- tomcat10-10.1.52/java/org/apache/coyote/http2/HPackHuffman.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/HPackHuffman.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http2/Hpack.java tomcat10-10.1.55/java/org/apache/coyote/http2/Hpack.java --- tomcat10-10.1.52/java/org/apache/coyote/http2/Hpack.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/Hpack.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http2/HpackDecoder.java tomcat10-10.1.55/java/org/apache/coyote/http2/HpackDecoder.java --- tomcat10-10.1.52/java/org/apache/coyote/http2/HpackDecoder.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/HpackDecoder.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http2/HpackEncoder.java tomcat10-10.1.55/java/org/apache/coyote/http2/HpackEncoder.java --- tomcat10-10.1.52/java/org/apache/coyote/http2/HpackEncoder.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/HpackEncoder.java 2026-05-05 13:36:52.000000000 +0000 @@ -124,6 +124,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); @@ -136,8 +152,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) != ':') { @@ -211,23 +229,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 tomcat10-10.1.52/java/org/apache/coyote/http2/Http2Parser.java tomcat10-10.1.55/java/org/apache/coyote/http2/Http2Parser.java --- tomcat10-10.1.52/java/org/apache/coyote/http2/Http2Parser.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/Http2Parser.java 2026-05-05 13:36:52.000000000 +0000 @@ -264,6 +264,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) { @@ -272,11 +278,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); @@ -316,8 +321,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); } @@ -540,6 +548,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 @@ -652,12 +662,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); @@ -666,10 +670,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 tomcat10-10.1.52/java/org/apache/coyote/http2/Http2Protocol.java tomcat10-10.1.55/java/org/apache/coyote/http2/Http2Protocol.java --- tomcat10-10.1.52/java/org/apache/coyote/http2/Http2Protocol.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/Http2Protocol.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http2/Http2UpgradeHandler.java tomcat10-10.1.55/java/org/apache/coyote/http2/Http2UpgradeHandler.java --- tomcat10-10.1.52/java/org/apache/coyote/http2/Http2UpgradeHandler.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/Http2UpgradeHandler.java 2026-05-05 13:36:52.000000000 +0000 @@ -140,6 +140,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) { @@ -173,6 +175,8 @@ pingManager.initiateDisabled = protocol.getInitiatePingDisabled(); + drainTimeout = protocol.getDrainTimeout(); + // Initial HTTP request becomes stream 1. if (coyoteRequest != null) { if (log.isTraceEnabled()) { @@ -543,7 +547,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); } @@ -1185,7 +1189,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 tomcat10-10.1.52/java/org/apache/coyote/http2/LocalStrings.properties tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings.properties --- tomcat10-10.1.52/java/org/apache/coyote/http2/LocalStrings.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http2/LocalStrings_fr.properties tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_fr.properties --- tomcat10-10.1.52/java/org/apache/coyote/http2/LocalStrings_fr.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_fr.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http2/LocalStrings_ja.properties tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_ja.properties --- tomcat10-10.1.52/java/org/apache/coyote/http2/LocalStrings_ja.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_ja.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http2/LocalStrings_ko.properties tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_ko.properties --- tomcat10-10.1.52/java/org/apache/coyote/http2/LocalStrings_ko.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_ko.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties --- tomcat10-10.1.52/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/LocalStrings_zh_CN.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/coyote/http2/Stream.java tomcat10-10.1.55/java/org/apache/coyote/http2/Stream.java --- tomcat10-10.1.52/java/org/apache/coyote/http2/Stream.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/Stream.java 2026-05-05 13:36:52.000000000 +0000 @@ -25,7 +25,6 @@ import java.security.PrivilegedExceptionAction; 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; @@ -49,6 +48,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; @@ -327,19 +327,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; } } @@ -350,7 +354,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) == ':'; @@ -375,51 +383,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; } @@ -438,15 +459,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; } @@ -477,9 +499,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 { @@ -496,45 +516,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())); + 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); } } @@ -558,12 +580,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 @@ -587,8 +629,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; } @@ -599,17 +641,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, 0, mimeHeaders, true, Constants.DEFAULT_HEADERS_FRAME_SIZE); diff -Nru tomcat10-10.1.52/java/org/apache/coyote/http2/StreamException.java tomcat10-10.1.55/java/org/apache/coyote/http2/StreamException.java --- tomcat10-10.1.52/java/org/apache/coyote/http2/StreamException.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/StreamException.java 2026-05-05 13:36:52.000000000 +0000 @@ -31,6 +31,12 @@ } + StreamException(String msg, Http2Error error, int streamId, Throwable cause) { + super(msg, error, cause); + this.streamId = streamId; + } + + int getStreamId() { return streamId; } diff -Nru tomcat10-10.1.52/java/org/apache/coyote/http2/StreamProcessor.java tomcat10-10.1.55/java/org/apache/coyote/http2/StreamProcessor.java --- tomcat10-10.1.52/java/org/apache/coyote/http2/StreamProcessor.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/coyote/http2/StreamProcessor.java 2026-05-05 13:36:52.000000000 +0000 @@ -519,19 +519,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 tomcat10-10.1.52/java/org/apache/el/ExpressionFactoryImpl.java tomcat10-10.1.55/java/org/apache/el/ExpressionFactoryImpl.java --- tomcat10-10.1.52/java/org/apache/el/ExpressionFactoryImpl.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/el/ExpressionFactoryImpl.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/el/parser/AstValue.java tomcat10-10.1.55/java/org/apache/el/parser/AstValue.java --- tomcat10-10.1.52/java/org/apache/el/parser/AstValue.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/el/parser/AstValue.java 2026-05-05 13:36:52.000000000 +0000 @@ -92,33 +92,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 tomcat10-10.1.52/java/org/apache/jasper/compiler/Compiler.java tomcat10-10.1.55/java/org/apache/jasper/compiler/Compiler.java --- tomcat10-10.1.52/java/org/apache/jasper/compiler/Compiler.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/jasper/compiler/Compiler.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/jasper/compiler/Generator.java tomcat10-10.1.55/java/org/apache/jasper/compiler/Generator.java --- tomcat10-10.1.52/java/org/apache/jasper/compiler/Generator.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/jasper/compiler/Generator.java 2026-05-05 13:36:52.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; @@ -556,7 +557,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()) { @@ -573,8 +574,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(".*")) { diff -Nru tomcat10-10.1.52/java/org/apache/jasper/compiler/JDTCompiler.java tomcat10-10.1.55/java/org/apache/jasper/compiler/JDTCompiler.java --- tomcat10-10.1.52/java/org/apache/jasper/compiler/JDTCompiler.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/jasper/compiler/JDTCompiler.java 2026-05-05 13:36:52.000000000 +0000 @@ -349,10 +349,20 @@ // This is checked against the actual version below. settings.put(CompilerOptions.OPTION_Source, "24"); } else if (opt.equals("25")) { - // Constant not available in latest ECJ version shipped with - // Tomcat. May be supported in a snapshot build. + // Constant not available in latest ECJ version that runs on + // Java 11. // This is checked against the actual version below. settings.put(CompilerOptions.OPTION_Source, "25"); + } else if (opt.equals("26")) { + // Constant not available in latest ECJ version. + // May be supported in a snapshot build. + // This is checked against the actual version below. + settings.put(CompilerOptions.OPTION_Source, "26"); + } else if (opt.equals("27")) { + // Constant not available in latest ECJ version. + // 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_11); @@ -451,11 +461,23 @@ settings.put(CompilerOptions.OPTION_TargetPlatform, "24"); settings.put(CompilerOptions.OPTION_Compliance, "24"); } else if (opt.equals("25")) { - // Constant not available in latest ECJ version shipped with - // Tomcat. May be supported in a snapshot build. + // Constant not available in latest ECJ version that runs on + // Java 11. // This is checked against the actual version below. settings.put(CompilerOptions.OPTION_TargetPlatform, "25"); settings.put(CompilerOptions.OPTION_Compliance, "25"); + } else if (opt.equals("26")) { + // Constant not available in latest ECJ version. + // 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. + // May be supported in a snapshot build. + // This is checked against the actual version below. + 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_11); diff -Nru tomcat10-10.1.52/java/org/apache/jasper/compiler/PageInfo.java tomcat10-10.1.55/java/org/apache/jasper/compiler/PageInfo.java --- tomcat10-10.1.52/java/org/apache/jasper/compiler/PageInfo.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/jasper/compiler/PageInfo.java 2026-05-05 13:36:52.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; @@ -113,7 +114,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 tomcat10-10.1.52/java/org/apache/jasper/compiler/TagFileProcessor.java tomcat10-10.1.55/java/org/apache/jasper/compiler/TagFileProcessor.java --- tomcat10-10.1.52/java/org/apache/jasper/compiler/TagFileProcessor.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/jasper/compiler/TagFileProcessor.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java tomcat10-10.1.55/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java --- tomcat10-10.1.52/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/jasper/compiler/TagLibraryInfoImpl.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/jasper/compiler/TextOptimizer.java tomcat10-10.1.55/java/org/apache/jasper/compiler/TextOptimizer.java --- tomcat10-10.1.52/java/org/apache/jasper/compiler/TextOptimizer.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/jasper/compiler/TextOptimizer.java 2026-05-05 13:36:52.000000000 +0000 @@ -21,6 +21,7 @@ import org.apache.jasper.TrimSpacesOption; /** + * Optimizes text in JSP pages. */ public class TextOptimizer { diff -Nru tomcat10-10.1.52/java/org/apache/jasper/el/ELResolverImpl.java tomcat10-10.1.55/java/org/apache/jasper/el/ELResolverImpl.java --- tomcat10-10.1.52/java/org/apache/jasper/el/ELResolverImpl.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/jasper/el/ELResolverImpl.java 2026-05-05 13:36:52.000000000 +0000 @@ -106,6 +106,7 @@ return elResolver.isReadOnly(context, base, property); } + @SuppressWarnings("removal") @Override public Iterator getFeatureDescriptors(ELContext context, Object base) { return elResolver.getFeatureDescriptors(context, base); diff -Nru tomcat10-10.1.52/java/org/apache/juli/OneLineFormatter.java tomcat10-10.1.55/java/org/apache/juli/OneLineFormatter.java --- tomcat10-10.1.52/java/org/apache/juli/OneLineFormatter.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/juli/OneLineFormatter.java 2026-05-05 13:36:52.000000000 +0000 @@ -24,7 +24,6 @@ import java.lang.management.ThreadMXBean; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Map.Entry; import java.util.logging.Formatter; import java.util.logging.LogManager; import java.util.logging.LogRecord; @@ -270,7 +269,7 @@ } @Override - protected boolean removeEldestEntry(Entry eldest) { + protected boolean removeEldestEntry(Map.Entry eldest) { return (size() > cacheSize); } } diff -Nru tomcat10-10.1.52/java/org/apache/naming/LocalStrings_zh_CN.properties tomcat10-10.1.55/java/org/apache/naming/LocalStrings_zh_CN.properties --- tomcat10-10.1.52/java/org/apache/naming/LocalStrings_zh_CN.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/naming/LocalStrings_zh_CN.properties 2026-05-05 13:36:52.000000000 +0000 @@ -22,6 +22,7 @@ namingContext.alreadyBound=名称[{0}]已在此上下文中绑定 namingContext.contextExpected=名称 [{0}] 未被绑定到一个域 +namingContext.failResolvingReference=解析名称为 [{0}] 的引用时发生意外的异常。 namingContext.invalidName=名称无效 namingContext.nameNotBound=名称[{0}]未在此上下文中绑定。找不到[{1}]。 namingContext.noAbsoluteName=无法为此命名空间生成绝对名称 diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/Jar.java tomcat10-10.1.55/java/org/apache/tomcat/Jar.java --- tomcat10-10.1.52/java/org/apache/tomcat/Jar.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/Jar.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings.properties tomcat10-10.1.55/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings.properties --- tomcat10-10.1.52/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/dbcp/dbcp2/LocalStrings.properties 2026-05-05 13:36:52.000000000 +0000 @@ -5,7 +5,7 @@ # (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 +# 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, diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java tomcat10-10.1.55/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java --- tomcat10-10.1.52/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/dbcp/dbcp2/datasources/PerUserPoolDataSourceFactory.java 2026-05-05 13:36:52.000000000 +0000 @@ -31,13 +31,6 @@ public class PerUserPoolDataSourceFactory extends InstanceKeyDataSourceFactory { private static final String PER_USER_POOL_CLASSNAME = PerUserPoolDataSource.class.getName(); - /** - * Constructs a new instance. - */ - public PerUserPoolDataSourceFactory() { - // empty - } - @SuppressWarnings("unchecked") // Avoid warnings on deserialization @Override protected InstanceKeyDataSource getNewInstance(final Reference ref) throws IOException, ClassNotFoundException { diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java tomcat10-10.1.55/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java --- tomcat10-10.1.52/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/dbcp/dbcp2/managed/BasicManagedDataSource.java 2026-05-05 13:36:52.000000000 +0000 @@ -67,13 +67,6 @@ /** Transaction Synchronization Registry */ private transient TransactionSynchronizationRegistry transactionSynchronizationRegistry; - /** - * Constructs a new instance. - */ - public BasicManagedDataSource() { - // empty - } - @Override protected synchronized ConnectionFactory createConnectionFactory() throws SQLException { if (transactionManager == null) { @@ -115,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 tomcat10-10.1.52/java/org/apache/tomcat/dbcp/pool2/PooledObject.java tomcat10-10.1.55/java/org/apache/tomcat/dbcp/pool2/PooledObject.java --- tomcat10-10.1.52/java/org/apache/tomcat/dbcp/pool2/PooledObject.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/dbcp/pool2/PooledObject.java 2026-05-05 13:36:52.000000000 +0000 @@ -31,6 +31,7 @@ * @param the type of object in the pool. * @since 2.0 */ +@SuppressWarnings("deprecation") public interface PooledObject extends Comparable> { /** @@ -273,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 tomcat10-10.1.52/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java tomcat10-10.1.55/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java --- tomcat10-10.1.52/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/dbcp/pool2/impl/BaseObjectPoolConfig.java 2026-05-05 13:36:52.000000000 +0000 @@ -30,6 +30,7 @@ * @param Type of element pooled. * @since 2.0 */ +@SuppressWarnings("deprecation") public abstract class BaseObjectPoolConfig extends BaseObject implements Cloneable { /** diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/jni/SSLConf.java tomcat10-10.1.55/java/org/apache/tomcat/jni/SSLConf.java --- tomcat10-10.1.52/java/org/apache/tomcat/jni/SSLConf.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/jni/SSLConf.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/Const.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/Const.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/Const.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/Const.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/AnnotationElementValue.java 2026-05-05 13:36:52.000000000 +0000 @@ -28,6 +28,11 @@ this.annotationEntry = annotationEntry; } + /** + * Gets the annotation entry. + * + * @return the annotation entry. + */ public AnnotationEntry getAnnotationEntry() { return annotationEntry; } diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/AnnotationEntry.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ClassParser.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/Constant.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/Constant.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/Constant.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/Constant.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantClass.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantDouble.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantFloat.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantInteger.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantLong.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/ConstantPool.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/JavaClass.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/classfile/SimpleElementValue.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/package-info.java tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/package-info.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/bcel/package-info.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/bcel/package-info.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/buf/Asn1Parser.java tomcat10-10.1.55/java/org/apache/tomcat/util/buf/Asn1Parser.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/buf/Asn1Parser.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/buf/Asn1Parser.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/http/HeaderUtil.java tomcat10-10.1.55/java/org/apache/tomcat/util/http/HeaderUtil.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/http/HeaderUtil.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/http/HeaderUtil.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/http/RequestUtil.java tomcat10-10.1.55/java/org/apache/tomcat/util/http/RequestUtil.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/http/RequestUtil.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/http/RequestUtil.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/ChunkExtension.java tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/ChunkExtension.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/ChunkExtension.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/ChunkExtension.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/EntityTag.java tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/EntityTag.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/EntityTag.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/EntityTag.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/HttpParser.java tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/HttpParser.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/HttpParser.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/HttpParser.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/LocalStrings.properties tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/LocalStrings.properties --- tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/LocalStrings.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/LocalStrings.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties --- tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/LocalStrings_fr.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties --- tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/LocalStrings_ja.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/Upgrade.java tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/Upgrade.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/http/parser/Upgrade.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/http/parser/Upgrade.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/AbstractEndpoint.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/AbstractEndpoint.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/AbstractEndpoint.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/AbstractEndpoint.java 2026-05-05 13:36:52.000000000 +0000 @@ -61,6 +61,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. */ @@ -531,7 +533,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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/AbstractJsseEndpoint.java 2026-05-05 13:36:52.000000000 +0000 @@ -91,6 +91,12 @@ @Override 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); @@ -128,15 +134,15 @@ List clientRequestedApplicationProtocols) { List clientRequestedProtocols = clientRequestedProtocolsThreadLocal.get(); if (clientRequestedProtocols == null) { - clientRequestedProtocols = new ArrayList(); + clientRequestedProtocols = new ArrayList<>(); } List clientSupportedGroups = clientSupportedGroupsThreadLocal.get(); if (clientSupportedGroups == null) { - clientSupportedGroups = new ArrayList(); + clientSupportedGroups = new ArrayList<>(); } List clientSignatureSchemes = clientSignatureSchemesThreadLocal.get(); if (clientSignatureSchemes == null) { - clientSignatureSchemes = new ArrayList(); + clientSignatureSchemes = new ArrayList<>(); } SSLHostConfig sslHostConfig = getSSLHostConfig(sniHostName); diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/net/LocalStrings.properties tomcat10-10.1.55/java/org/apache/tomcat/util/net/LocalStrings.properties --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/LocalStrings.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/LocalStrings.properties 2026-05-05 13:36:52.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,14 +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.ignoreTls13Ciphersuite=The TLS 1.3 cipher suite [{0}] included in the TLS 1.2 and below ciphers 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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/LocalStrings_fr.properties tomcat10-10.1.55/java/org/apache/tomcat/util/net/LocalStrings_fr.properties --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/LocalStrings_fr.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/LocalStrings_fr.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/LocalStrings_ja.properties tomcat10-10.1.55/java/org/apache/tomcat/util/net/LocalStrings_ja.properties --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/LocalStrings_ja.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/LocalStrings_ja.properties 2026-05-05 13:36:52.000000000 +0000 @@ -123,6 +123,7 @@ 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=直接設定 - 場所が不明 @@ -154,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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/NioEndpoint.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/NioEndpoint.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/NioEndpoint.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/NioEndpoint.java 2026-05-05 13:36:52.000000000 +0000 @@ -1375,6 +1375,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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/SSLHostConfig.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/SSLHostConfig.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/SSLHostConfig.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/SSLHostConfig.java 2026-05-05 13:36:52.000000000 +0000 @@ -57,7 +57,7 @@ // 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_12 = "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. @@ -111,6 +111,7 @@ private boolean certificateVerificationDepthConfigured = false; 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; @@ -134,7 +135,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; @@ -398,51 +399,65 @@ if (ciphersList != null) { if (ciphersList.contains(":")) { // OpenSSL format - StringBuilder sb = new StringBuilder(); + 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.ignoreTls13Ciphersuite", trimmed)); + log.warn(sm.getString("sslHostConfig.handleTls13CiphersuiteInCiphers", trimmed)); + if (sbCipherSuitesFromCiphers.length() > 0) { + sbCipherSuitesFromCiphers.append(':'); + } + sbCipherSuitesFromCiphers.append(trimmed); } else { - if (sb.length() > 0) { - sb.append(':'); + if (sbCiphers.length() > 0) { + sbCiphers.append(':'); } - sb.append(trimmed); + sbCiphers.append(trimmed); } } - this.ciphers = sb.toString(); + 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 sb = new StringBuilder(); + 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.ignoreTls13Ciphersuite", trimmed)); - continue; - } - String openSSLName = OpenSSLCipherConfigurationParser.jsseToOpenSSL(trimmed); - if (openSSLName == null) { - // Not a JSSE name. Maybe an OpenSSL name or alias - openSSLName = trimmed; - } - if (sb.length() > 0) { - sb.append(':'); + log.warn(sm.getString("sslHostConfig.handleTls13CiphersuiteInCiphers", trimmed)); + if (sbCipherSuitesFromCiphers.length() > 0) { + 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.length() > 0) { + sbCiphers.append(':'); + } + sbCiphers.append(openSSLName); } - sb.append(openSSLName); } } - this.ciphers = sb.toString(); + this.ciphers = sbCiphers.toString(); + this.cipherSuitesFromCiphers = sbCipherSuitesFromCiphers.toString(); } } else { this.ciphers = null; + this.cipherSuitesFromCiphers = null; } this.cipherList = null; this.jsseCipherNames = null; + this.cipherSuiteList = null; } @@ -464,13 +479,13 @@ /** * 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) { - Set jsseCiphers = new HashSet<>(); + Set jsseCiphers = new LinkedHashSet<>(); jsseCiphers.addAll(getCipherSuiteList()); jsseCiphers.addAll(getCipherList()); jsseCipherNames = OpenSSLCipherConfigurationParser.convertForJSSE(jsseCiphers); @@ -528,7 +543,14 @@ * @return An OpenSSL cipher suite string for the current configuration. */ public String getCipherSuites() { - return cipherSuites; + StringBuilder sb = new StringBuilder(cipherSuites); + if (cipherSuitesFromCiphers != null && !cipherSuitesFromCiphers.isEmpty()) { + if (sb.length() > 0) { + sb.append(':'); + } + sb.append(cipherSuitesFromCiphers); + } + return sb.toString(); } @@ -697,19 +719,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; } @@ -717,6 +731,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; } diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/net/SecureNio2Channel.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/SecureNio2Channel.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/SecureNio2Channel.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/SecureNio2Channel.java 2026-05-05 13:36:52.000000000 +0000 @@ -136,13 +136,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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/SecureNioChannel.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/SecureNioChannel.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/SecureNioChannel.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/SecureNioChannel.java 2026-05-05 13:36:52.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 @@ -498,6 +500,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 @@ -614,7 +619,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? @@ -701,7 +707,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? @@ -832,7 +839,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")); } @@ -875,7 +883,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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/SocketWrapperBase.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/SocketWrapperBase.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/SocketWrapperBase.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/SocketWrapperBase.java 2026-05-05 13:36:52.000000000 +0000 @@ -924,6 +924,8 @@ /** * Internal state tracker for vectored operations. + * + * @param The attachment type */ protected abstract class OperationState implements Runnable { protected final boolean read; @@ -1014,6 +1016,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 @@ -1378,7 +1382,7 @@ try { long timeoutExpiry = System.nanoTime() + unit.toNanos(timeout); long timeoutMillis = unit.toMillis(timeout); - // Spurious wake-ups are possible. Keep waiting until state changes or timeout expires. + // Spurious wake-ups are possible. Keep waiting until state changes or timeout expires. while (state.state == CompletionState.PENDING && timeoutMillis > 0) { state.wait(unit.toMillis(timeout)); timeoutMillis = (timeoutExpiry - System.nanoTime()) / 1_000_000; diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties tomcat10-10.1.55/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/jsse/LocalStrings_fr.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties tomcat10-10.1.55/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/jsse/LocalStrings_ja.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java 2026-05-05 13:36:52.000000000 +0000 @@ -131,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)) { @@ -432,9 +430,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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java 2026-05-05 13:36:52.000000000 +0000 @@ -101,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); @@ -137,6 +136,7 @@ private final OpenSSLState state; private final Cleanable cleanable; + private ByteBuffer buf = ByteBuffer.allocateDirect(MAX_ENCRYPTED_PACKET_LENGTH); private enum Accepted { NOT, @@ -229,6 +229,7 @@ // internal errors can cause shutdown without marking the engine closed isInboundDone = isOutboundDone = engineClosed = true; cleanable.clean(); + ByteBufferUtils.cleanDirectBuffer(buf); } } @@ -255,7 +256,6 @@ return sslWrote; } } else { - ByteBuffer buf = ByteBuffer.allocateDirect(len); try { final long addr = Buffer.address(buf); @@ -276,7 +276,6 @@ } } finally { buf.clear(); - ByteBufferUtils.cleanDirectBuffer(buf); } } @@ -303,7 +302,6 @@ return netWrote; } } else { - ByteBuffer buf = ByteBuffer.allocateDirect(len); try { final long addr = Buffer.address(buf); @@ -321,7 +319,6 @@ } } finally { buf.clear(); - ByteBufferUtils.cleanDirectBuffer(buf); } } @@ -350,7 +347,6 @@ final int pos = dst.position(); final int limit = dst.limit(); final int len = Math.min(MAX_ENCRYPTED_PACKET_LENGTH, limit - pos); - final ByteBuffer buf = ByteBuffer.allocateDirect(len); try { final long addr = Buffer.address(buf); @@ -366,7 +362,6 @@ } } finally { buf.clear(); - ByteBufferUtils.cleanDirectBuffer(buf); } } @@ -391,7 +386,6 @@ checkLastError(); } } else { - final ByteBuffer buf = ByteBuffer.allocateDirect(pending); try { final long addr = Buffer.address(buf); @@ -408,7 +402,6 @@ } } finally { buf.clear(); - ByteBufferUtils.cleanDirectBuffer(buf); } } @@ -822,9 +815,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); } @@ -843,7 +833,6 @@ if (destroyed) { return; } - boolean sslv2 = false; boolean sslv3 = false; boolean tlsv1 = false; boolean tlsv1_1 = false; @@ -852,9 +841,7 @@ if (!IMPLEMENTED_PROTOCOLS_SET.contains(p)) { throw new IllegalArgumentException(sm.getString("engine.unsupportedProtocol", p)); } - if (p.equals(Constants.SSL_PROTO_SSLv2)) { - sslv2 = true; - } else if (p.equals(Constants.SSL_PROTO_SSLv3)) { + if (p.equals(Constants.SSL_PROTO_SSLv3)) { sslv3 = true; } else if (p.equals(Constants.SSL_PROTO_TLSv1)) { tlsv1 = true; @@ -866,10 +853,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); } diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/ciphers/Group.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/ciphers/Group.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/ciphers/Group.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/ciphers/Group.java 2026-05-05 13:36:52.000000000 +0000 @@ -21,8 +21,9 @@ /** * All the supported named groups for TLS 1.3. - * @see The supported groups - * registry + * + * @see The supported + * groups registry */ public enum Group { @@ -40,6 +41,9 @@ ffdhe6144(0x0103), ffdhe8192(0x0104), + // SM2 Curve + curveSM2(0x0029), + // Post-Quantum Key Exchange MLKEM512(0x0200), MLKEM768(0x0201), @@ -48,7 +52,8 @@ // Hybrid Key Exchange SecP256r1MLKEM768(0x11EB), X25519MLKEM768(0x11EC), - SecP384r1MLKEM1024(0x11ED); + SecP384r1MLKEM1024(0x11ED), + curveSM2MLKEM768(0x11EE); private final int id; diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/ciphers/OpenSSLCipherConfigurationParser.java 2026-05-05 13:36:52.000000000 +0000 @@ -100,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"; @@ -212,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"; @@ -284,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"; @@ -332,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"; @@ -446,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))); @@ -493,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)); @@ -522,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, @@ -532,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))); @@ -560,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)); diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/ciphers/SignatureScheme.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/ciphers/SignatureScheme.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/ciphers/SignatureScheme.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/ciphers/SignatureScheme.java 2026-05-05 13:36:52.000000000 +0000 @@ -21,8 +21,9 @@ /** * All the signature schemes for TLS 1.3. - * @see The signature schemes - * registry + * + * @see The + * signature schemes registry */ public enum SignatureScheme { @@ -57,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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java 2026-05-05 13:36:52.000000000 +0000 @@ -193,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)) { @@ -267,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; @@ -534,7 +532,8 @@ } if (maxTlsVersion >= TLS1_3_VERSION()) { try { - if (SSL_CTX_set_ciphersuites(state.sslCtx, localArena.allocateFrom(sslHostConfig.getCipherSuites())) <= 0) { + if (SSL_CTX_set_ciphersuites(state.sslCtx, + localArena.allocateFrom(sslHostConfig.getCipherSuites())) <= 0) { tls13Warning = sm.getString("engine.failedCipherSuite", sslHostConfig.getCipherSuites()); } else { ciphersSet = true; @@ -601,6 +600,7 @@ } else if (log.isDebugEnabled()) { log.debug(sm.getString("openssl.addedClientCaCert", caCert.toString())); } + X509_free(x509CACert); } } else if (sslHostConfig.getCaCertificateFile() != null || sslHostConfig.getCaCertificatePath() != null) { // Client certificate verification based on trusted CA files and dirs @@ -612,7 +612,8 @@ localArena .allocateFrom(SSLHostConfig.adjustRelativePath(sslHostConfig.getCaCertificatePath())) : MemorySegment.NULL; - if (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); @@ -688,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); } @@ -811,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)); @@ -1073,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; @@ -1181,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); @@ -1250,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; @@ -1299,6 +1299,7 @@ } if (SSL_CTX_add0_chain_cert(state.sslCtx, x509certChain) <= 0) { logLastError("openssl.errorAddingCertificate"); + X509_free(x509certChain); return false; } } @@ -1371,8 +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, ocspSoftFail, ocspTimeout, ocspVerifyFlags); + sslHostConfig.getCertificateVerification() == CertificateVerification.OPTIONAL_NO_CA, noOcspCheck, + ocspSoftFail, ocspTimeout, ocspVerifyFlags); } @Override diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java 2026-05-05 13:36:52.000000000 +0000 @@ -87,9 +87,9 @@ 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 @@ -97,6 +97,7 @@ 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; @@ -184,8 +185,8 @@ */ OpenSSLEngine(Cleaner cleaner, MemorySegment sslCtx, String fallbackApplicationProtocol, boolean clientMode, OpenSSLSessionContext sessionContext, boolean alpn, boolean initialized, int certificateVerificationDepth, - boolean certificateVerificationOptionalNoCA, boolean noOcspCheck, boolean ocspSoftFail, - int ocspTimeout, int ocspVerifyFlags) { + boolean certificateVerificationOptionalNoCA, boolean noOcspCheck, boolean ocspSoftFail, int ocspTimeout, + int ocspVerifyFlags) { if (sslCtx == null) { throw new IllegalArgumentException(sm.getString("engine.noSSLContext")); } @@ -412,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 @@ -540,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; @@ -739,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); } @@ -761,7 +759,6 @@ if (destroyed) { return; } - boolean sslv2 = false; boolean sslv3 = false; boolean tlsv1 = false; boolean tlsv1_1 = false; @@ -772,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; @@ -782,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()); } @@ -836,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; } @@ -858,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; } } @@ -1143,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 || errnum == X509_V_OK()) && (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()); } @@ -1214,7 +1205,8 @@ if (openssl_h_Compatibility.OPENSSL && !openssl_h_Compatibility.OPENSSL3) { issuer = openssl_h_Compatibility.X509_STORE_CTX_get0_current_issuer(x509ctx); } else { - MemorySegment x509IssuerPointer = localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); + MemorySegment x509IssuerPointer = + localArena.allocateFrom(ValueLayout.ADDRESS, MemorySegment.NULL); int res = X509_STORE_CTX_get1_issuer(x509IssuerPointer, x509ctx, x509); if (res > 0) { issuer = x509IssuerPointer.get(ValueLayout.ADDRESS, 0); @@ -1244,7 +1236,8 @@ for (String urlString : urls) { try { URL url = (new URI(urlString)).toURL(); - ocspResponse = processOCSPRequest(state, 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))); @@ -1300,6 +1293,7 @@ private static int processOCSPRequest(EngineState state, URL url, MemorySegment issuer, MemorySegment x509, MemorySegment /* X509_STORE_CTX */ x509ctx, Arena localArena) { 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; @@ -1312,20 +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); @@ -1347,12 +1345,17 @@ 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(); @@ -1370,7 +1373,8 @@ 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) { + 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(); } @@ -1397,6 +1401,8 @@ return V_OCSP_CERTSTATUS_UNKNOWN(); } return status; + } else { + X509_STORE_CTX_set_error(x509ctx, X509_V_ERR_UNABLE_TO_GET_CRL()); } } } catch (IOException ioe) { diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java 2026-05-05 13:36:52.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; @@ -427,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()) { @@ -504,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 tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLUtil.java 2026-05-05 13:36:52.000000000 +0000 @@ -105,5 +105,4 @@ throw e; } } - } diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/openssl/openssl_h.java tomcat10-10.1.55/java/org/apache/tomcat/util/openssl/openssl_h.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/openssl/openssl_h.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/openssl/openssl_h.java 2026-05-05 13:36:52.000000000 +0000 @@ -3705,7 +3705,8 @@ } private static class X509_STORE_CTX_get1_issuer { - public static final FunctionDescriptor DESC = FunctionDescriptor.of(openssl_h.C_INT, openssl_h.C_POINTER, openssl_h.C_POINTER, openssl_h.C_POINTER); + public static final FunctionDescriptor DESC = + FunctionDescriptor.of(openssl_h.C_INT, openssl_h.C_POINTER, openssl_h.C_POINTER, openssl_h.C_POINTER); public static final MemorySegment ADDR = openssl_h.findOrThrow("X509_STORE_CTX_get1_issuer"); @@ -5945,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$) { diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java tomcat10-10.1.55/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java 2026-05-05 13:36:52.000000000 +0000 @@ -22,14 +22,15 @@ 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. */ +@SuppressWarnings("javadoc") 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; @@ -40,6 +41,7 @@ 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"); @@ -148,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 tomcat10-10.1.52/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java tomcat10-10.1.55/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/openssl/openssl_h_Macros.java 2026-05-05 13:36:52.000000000 +0000 @@ -466,8 +466,8 @@ if (openssl_h_Compatibility.BORINGSSL) { class Holder { static final String NAME = "SSL_CTX_set1_groups_list"; - static final FunctionDescriptor DESC = FunctionDescriptor.of(openssl_h.C_LONG, openssl_h.C_POINTER, - openssl_h.C_POINTER); + static final FunctionDescriptor DESC = + FunctionDescriptor.of(openssl_h.C_LONG, openssl_h.C_POINTER, openssl_h.C_POINTER); static final MethodHandle MH = Linker.nativeLinker().downcallHandle(openssl_h.findOrThrow(NAME), DESC); } var mh$ = Holder.MH; diff -Nru tomcat10-10.1.52/java/org/apache/tomcat/util/security/ConstantTime.java tomcat10-10.1.55/java/org/apache/tomcat/util/security/ConstantTime.java --- tomcat10-10.1.52/java/org/apache/tomcat/util/security/ConstantTime.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/util/security/ConstantTime.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/websocket/Authenticator.java tomcat10-10.1.55/java/org/apache/tomcat/websocket/Authenticator.java --- tomcat10-10.1.52/java/org/apache/tomcat/websocket/Authenticator.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/websocket/Authenticator.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/websocket/BasicAuthenticator.java tomcat10-10.1.55/java/org/apache/tomcat/websocket/BasicAuthenticator.java --- tomcat10-10.1.52/java/org/apache/tomcat/websocket/BasicAuthenticator.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/websocket/BasicAuthenticator.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/websocket/DigestAuthenticator.java tomcat10-10.1.55/java/org/apache/tomcat/websocket/DigestAuthenticator.java --- tomcat10-10.1.52/java/org/apache/tomcat/websocket/DigestAuthenticator.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/websocket/DigestAuthenticator.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/java/org/apache/tomcat/websocket/WsWebSocketContainer.java tomcat10-10.1.55/java/org/apache/tomcat/websocket/WsWebSocketContainer.java --- tomcat10-10.1.52/java/org/apache/tomcat/websocket/WsWebSocketContainer.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/java/org/apache/tomcat/websocket/WsWebSocketContainer.java 2026-05-05 13:36:52.000000000 +0000 @@ -74,6 +74,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; import org.apache.tomcat.util.security.KeyStoreUtil; @@ -179,40 +180,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)) { @@ -230,12 +232,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; } } @@ -243,21 +245,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; @@ -293,19 +297,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.getStatus() != 200) { throw new DeploymentException(sm.getString("wsWebSocketContainer.proxyConnectFail", selectedProxy, Integer.toString(httpResponse.getStatus()))); } + // 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 @@ -325,10 +333,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); @@ -341,6 +349,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.getHandshakeResponse().getHeaders().get(Constants.LOCATION_HEADER_NAME); @@ -353,7 +364,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); @@ -374,14 +385,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.getHandshakeResponse(); clientEndpointConfiguration.getConfigurator().afterResponse(handshakeResponse); @@ -424,7 +441,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) { @@ -468,9 +485,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", @@ -495,15 +513,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 tomcat10-10.1.52/res/ide-support/eclipse/eclipse.classpath tomcat10-10.1.55/res/ide-support/eclipse/eclipse.classpath --- tomcat10-10.1.52/res/ide-support/eclipse/eclipse.classpath 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/res/ide-support/eclipse/eclipse.classpath 2026-05-05 13:36:52.000000000 +0000 @@ -40,5 +40,8 @@ + + + diff -Nru tomcat10-10.1.52/res/ide-support/eclipse/java-compiler-errors-warnings.txt tomcat10-10.1.55/res/ide-support/eclipse/java-compiler-errors-warnings.txt --- tomcat10-10.1.52/res/ide-support/eclipse/java-compiler-errors-warnings.txt 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/res/ide-support/eclipse/java-compiler-errors-warnings.txt 2026-05-05 13:36:52.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 tomcat10-10.1.52/res/ide-support/idea/tomcat.iml tomcat10-10.1.55/res/ide-support/idea/tomcat.iml --- tomcat10-10.1.52/res/ide-support/idea/tomcat.iml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/res/ide-support/idea/tomcat.iml 2026-05-05 13:36:52.000000000 +0000 @@ -144,6 +144,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru tomcat10-10.1.52/res/ide-support/netbeans/nb-tomcat-build.properties tomcat10-10.1.55/res/ide-support/netbeans/nb-tomcat-build.properties --- tomcat10-10.1.52/res/ide-support/netbeans/nb-tomcat-build.properties 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/res/ide-support/netbeans/nb-tomcat-build.properties 2026-05-05 13:36:52.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 tomcat10-10.1.52/res/ide-support/netbeans/project.xml tomcat10-10.1.55/res/ide-support/netbeans/project.xml --- tomcat10-10.1.52/res/ide-support/netbeans/project.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/res/ide-support/netbeans/project.xml 2026-05-05 13:36:52.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/XFGxe6_gx1/tomcat10-10.1.52/res/install-win/Uninstall.exe.sig and /srv/release.debian.org/tmp/ziwITPmTS3/tomcat10-10.1.55/res/install-win/Uninstall.exe.sig differ Binary files /srv/release.debian.org/tmp/XFGxe6_gx1/tomcat10-10.1.52/res/install-win/tomcat-installer.exe.sig and /srv/release.debian.org/tmp/ziwITPmTS3/tomcat10-10.1.55/res/install-win/tomcat-installer.exe.sig differ diff -Nru tomcat10-10.1.52/res/maven/mvn.properties.default tomcat10-10.1.55/res/maven/mvn.properties.default --- tomcat10-10.1.52/res/maven/mvn.properties.default 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/res/maven/mvn.properties.default 2026-05-05 13:36:52.000000000 +0000 @@ -39,7 +39,7 @@ maven.asf.release.repo.repositoryId=apache.releases.https # Release version info -maven.asf.release.deploy.version=10.1.52 +maven.asf.release.deploy.version=10.1.54 #Where do we load the libraries from tomcat.lib.path=../../output/build/lib diff -Nru tomcat10-10.1.52/res/maven/mvn.properties.release tomcat10-10.1.55/res/maven/mvn.properties.release --- tomcat10-10.1.52/res/maven/mvn.properties.release 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/res/maven/mvn.properties.release 2026-05-05 13:36:52.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=10.1.52 +maven.asf.release.deploy.version=10.1.55 # Re-use the same GPG executable. gpg.exec=/usr/local/bin/gpg diff -Nru tomcat10-10.1.52/res/maven/tomcat-servlet-api.pom tomcat10-10.1.55/res/maven/tomcat-servlet-api.pom --- tomcat10-10.1.52/res/maven/tomcat-servlet-api.pom 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/res/maven/tomcat-servlet-api.pom 2026-05-05 13:36:52.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 tomcat10-10.1.52/res/openssl/openssl-tomcat.conf tomcat10-10.1.55/res/openssl/openssl-tomcat.conf --- tomcat10-10.1.52/res/openssl/openssl-tomcat.conf 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/res/openssl/openssl-tomcat.conf 2026-05-05 13:36:52.000000000 +0000 @@ -220,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 diff -Nru tomcat10-10.1.52/res/rat/rat-excludes.txt tomcat10-10.1.55/res/rat/rat-excludes.txt --- tomcat10-10.1.52/res/rat/rat-excludes.txt 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/res/rat/rat-excludes.txt 2026-05-05 13:36:52.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 @@ -88,6 +89,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 @@ -107,6 +111,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 tomcat10-10.1.52/test/jakarta/el/TestArrayELResolver.java tomcat10-10.1.55/test/jakarta/el/TestArrayELResolver.java --- tomcat10-10.1.52/test/jakarta/el/TestArrayELResolver.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/jakarta/el/TestArrayELResolver.java 2026-05-05 13:36:52.000000000 +0000 @@ -486,6 +486,7 @@ } + @SuppressWarnings("removal") @Deprecated(forRemoval = true, since = "Tomcat 10.1.0") @Test public void testGetFeatureDescriptors() { diff -Nru tomcat10-10.1.52/test/jakarta/el/TestBeanELResolver.java tomcat10-10.1.55/test/jakarta/el/TestBeanELResolver.java --- tomcat10-10.1.52/test/jakarta/el/TestBeanELResolver.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/jakarta/el/TestBeanELResolver.java 2026-05-05 13:36:52.000000000 +0000 @@ -366,6 +366,7 @@ BeanELResolver resolver = new BeanELResolver(); ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + @SuppressWarnings("removal") Iterator result = resolver.getFeatureDescriptors(context, null); Assert.assertNull(result); @@ -380,6 +381,7 @@ BeanELResolver resolver = new BeanELResolver(); ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + @SuppressWarnings("removal") Iterator result = resolver.getFeatureDescriptors(context, new Bean()); while (result.hasNext()) { diff -Nru tomcat10-10.1.52/test/jakarta/el/TestBeanNameELResolver.java tomcat10-10.1.55/test/jakarta/el/TestBeanNameELResolver.java --- tomcat10-10.1.52/test/jakarta/el/TestBeanNameELResolver.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/jakarta/el/TestBeanNameELResolver.java 2026-05-05 13:36:52.000000000 +0000 @@ -450,6 +450,7 @@ BeanNameELResolver resolver = createBeanNameELResolver(); ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + @SuppressWarnings("removal") Object result = resolver.getFeatureDescriptors(context, null); Assert.assertNull(result); @@ -464,6 +465,7 @@ public void testGetFeatureDescriptors02() { BeanNameELResolver resolver = createBeanNameELResolver(); + @SuppressWarnings("removal") Object result = resolver.getFeatureDescriptors(null, new Object()); Assert.assertNull(result); diff -Nru tomcat10-10.1.52/test/jakarta/el/TestMapELResolver.java tomcat10-10.1.55/test/jakarta/el/TestMapELResolver.java --- tomcat10-10.1.52/test/jakarta/el/TestMapELResolver.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/jakarta/el/TestMapELResolver.java 2026-05-05 13:36:52.000000000 +0000 @@ -226,6 +226,7 @@ MapELResolver mapELResolver = new MapELResolver(); ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + @SuppressWarnings("removal") Iterator result = mapELResolver.getFeatureDescriptors(context, new Object()); Assert.assertNull(result); @@ -242,6 +243,7 @@ Map map = new HashMap<>(); map.put("key", "value"); + @SuppressWarnings("removal") Iterator result = mapELResolver.getFeatureDescriptors(context, map); while (result.hasNext()) { diff -Nru tomcat10-10.1.52/test/jakarta/el/TestResourceBundleELResolver.java tomcat10-10.1.55/test/jakarta/el/TestResourceBundleELResolver.java --- tomcat10-10.1.52/test/jakarta/el/TestResourceBundleELResolver.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/jakarta/el/TestResourceBundleELResolver.java 2026-05-05 13:36:52.000000000 +0000 @@ -217,6 +217,7 @@ ResourceBundleELResolver resolver = new ResourceBundleELResolver(); ELContext context = new StandardELContext(ELManager.getExpressionFactory()); + @SuppressWarnings("removal") Iterator result = resolver.getFeatureDescriptors(context, new Object()); Assert.assertNull(result); @@ -232,6 +233,7 @@ ELContext context = new StandardELContext(ELManager.getExpressionFactory()); ResourceBundle resourceBundle = new TesterResourceBundle(new Object[][] { { "key", "value" } }); + @SuppressWarnings("removal") Iterator result = resolver.getFeatureDescriptors(context, resourceBundle); while (result.hasNext()) { diff -Nru tomcat10-10.1.52/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java tomcat10-10.1.55/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java --- tomcat10-10.1.52/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/authenticator/TestDigestAuthenticatorB.java tomcat10-10.1.55/test/org/apache/catalina/authenticator/TestDigestAuthenticatorB.java --- tomcat10-10.1.52/test/org/apache/catalina/authenticator/TestDigestAuthenticatorB.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/authenticator/TestDigestAuthenticatorB.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java tomcat10-10.1.55/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java --- tomcat10-10.1.52/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/connector/TestResponse.java tomcat10-10.1.55/test/org/apache/catalina/connector/TestResponse.java --- tomcat10-10.1.52/test/org/apache/catalina/connector/TestResponse.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/connector/TestResponse.java 2026-05-05 13:36:52.000000000 +0000 @@ -739,6 +739,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(); @@ -1018,4 +1036,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 tomcat10-10.1.52/test/org/apache/catalina/connector/TestValidateClientSessionId.java tomcat10-10.1.55/test/org/apache/catalina/connector/TestValidateClientSessionId.java --- tomcat10-10.1.52/test/org/apache/catalina/connector/TestValidateClientSessionId.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/connector/TestValidateClientSessionId.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/core/TestStandardContext.java tomcat10-10.1.55/test/org/apache/catalina/core/TestStandardContext.java --- tomcat10-10.1.52/test/org/apache/catalina/core/TestStandardContext.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/core/TestStandardContext.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/filters/TestHttpHeaderSecurityFilter.java tomcat10-10.1.55/test/org/apache/catalina/filters/TestHttpHeaderSecurityFilter.java --- tomcat10-10.1.52/test/org/apache/catalina/filters/TestHttpHeaderSecurityFilter.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/filters/TestHttpHeaderSecurityFilter.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/filters/TestRateLimitFilter.java tomcat10-10.1.55/test/org/apache/catalina/filters/TestRateLimitFilter.java --- tomcat10-10.1.52/test/org/apache/catalina/filters/TestRateLimitFilter.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/filters/TestRateLimitFilter.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java tomcat10-10.1.55/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java --- tomcat10-10.1.52/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/filters/TestRateLimitFilterWithExactRateLimiter.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java tomcat10-10.1.55/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java --- tomcat10-10.1.52/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/filters/TestRemoteCIDRFilter.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/filters/TesterHttpServletRequest.java tomcat10-10.1.55/test/org/apache/catalina/filters/TesterHttpServletRequest.java --- tomcat10-10.1.52/test/org/apache/catalina/filters/TesterHttpServletRequest.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/filters/TesterHttpServletRequest.java 2026-05-05 13:36:52.000000000 +0000 @@ -57,6 +57,7 @@ private String serverName; private int serverPort; private String contentType; + private boolean secure = false; @Override public Object getAttribute(String name) { @@ -190,14 +191,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 tomcat10-10.1.52/test/org/apache/catalina/filters/TesterRateLimitClientBase.java tomcat10-10.1.55/test/org/apache/catalina/filters/TesterRateLimitClientBase.java --- tomcat10-10.1.52/test/org/apache/catalina/filters/TesterRateLimitClientBase.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/filters/TesterRateLimitClientBase.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/manager/TestManagerWebapp.java tomcat10-10.1.55/test/org/apache/catalina/manager/TestManagerWebapp.java --- tomcat10-10.1.52/test/org/apache/catalina/manager/TestManagerWebapp.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/manager/TestManagerWebapp.java 2026-05-05 13:36:52.000000000 +0000 @@ -537,7 +537,7 @@ Assert.assertTrue(appRoot.mkdirs() && appRoot.isDirectory()); addDeleteOnTearDown(appRoot); - try (@SuppressWarnings("unused") TomcatBaseTest.ContainerInjector ignored = + try (TomcatBaseTest.ContainerInjector ignored = TomcatBaseTest.ContainerInjector.inject(ctx.getParent(), c -> c.getPath().equals("/bug57700"), c -> { diff -Nru tomcat10-10.1.52/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java tomcat10-10.1.55/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java --- tomcat10-10.1.52/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/nonblocking/TestNonBlockingAPI.java 2026-05-05 13:36:52.000000000 +0000 @@ -598,7 +598,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 tomcat10-10.1.52/test/org/apache/catalina/realm/TestLockoutRealm.java tomcat10-10.1.55/test/org/apache/catalina/realm/TestLockoutRealm.java --- tomcat10-10.1.52/test/org/apache/catalina/realm/TestLockoutRealm.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/realm/TestLockoutRealm.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/realm/TestRealmBase.java tomcat10-10.1.55/test/org/apache/catalina/realm/TestRealmBase.java --- tomcat10-10.1.52/test/org/apache/catalina/realm/TestRealmBase.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/realm/TestRealmBase.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java tomcat10-10.1.55/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java --- tomcat10-10.1.52/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/servlets/TestWebdavBoundedByteArrayOutputStream.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/servlets/TestWebdavServlet.java tomcat10-10.1.55/test/org/apache/catalina/servlets/TestWebdavServlet.java --- tomcat10-10.1.52/test/org/apache/catalina/servlets/TestWebdavServlet.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/servlets/TestWebdavServlet.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/session/FileStoreTest.java tomcat10-10.1.55/test/org/apache/catalina/session/FileStoreTest.java --- tomcat10-10.1.52/test/org/apache/catalina/session/FileStoreTest.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/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 tomcat10-10.1.52/test/org/apache/catalina/session/TestFileStore.java tomcat10-10.1.55/test/org/apache/catalina/session/TestFileStore.java --- tomcat10-10.1.52/test/org/apache/catalina/session/TestFileStore.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/session/TestFileStore.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/startup/LoggingBaseTest.java tomcat10-10.1.55/test/org/apache/catalina/startup/LoggingBaseTest.java --- tomcat10-10.1.52/test/org/apache/catalina/startup/LoggingBaseTest.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/startup/LoggingBaseTest.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/startup/TomcatBaseTest.java tomcat10-10.1.55/test/org/apache/catalina/startup/TomcatBaseTest.java --- tomcat10-10.1.52/test/org/apache/catalina/startup/TomcatBaseTest.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/startup/TomcatBaseTest.java 2026-05-05 13:36:52.000000000 +0000 @@ -480,7 +480,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()); @@ -557,7 +557,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]); @@ -596,7 +596,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 tomcat10-10.1.52/test/org/apache/catalina/storeconfig/TestStoreConfig.java tomcat10-10.1.55/test/org/apache/catalina/storeconfig/TestStoreConfig.java --- tomcat10-10.1.52/test/org/apache/catalina/storeconfig/TestStoreConfig.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/storeconfig/TestStoreConfig.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java tomcat10-10.1.55/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java --- tomcat10-10.1.52/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptor.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorAlgorithms.java tomcat10-10.1.55/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorAlgorithms.java --- tomcat10-10.1.52/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorAlgorithms.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/tribes/group/interceptors/TestEncryptInterceptorAlgorithms.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/util/TestServerInfo.java tomcat10-10.1.55/test/org/apache/catalina/util/TestServerInfo.java --- tomcat10-10.1.52/test/org/apache/catalina/util/TestServerInfo.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/util/TestServerInfo.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/valves/TestAccessLogValve.java tomcat10-10.1.55/test/org/apache/catalina/valves/TestAccessLogValve.java --- tomcat10-10.1.52/test/org/apache/catalina/valves/TestAccessLogValve.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestAccessLogValve.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/valves/TestAccessLogValveFile.java tomcat10-10.1.55/test/org/apache/catalina/valves/TestAccessLogValveFile.java --- tomcat10-10.1.52/test/org/apache/catalina/valves/TestAccessLogValveFile.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestAccessLogValveFile.java 2026-05-05 13:36:52.000000000 +0000 @@ -0,0 +1,276 @@ +/* + * 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()); + + // Sporadic test failures if backgroundProcess is called + // directly after getUrl(). + Thread.sleep(50); + // 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 tomcat10-10.1.52/test/org/apache/catalina/valves/TestJsonErrorReportValve.java tomcat10-10.1.55/test/org/apache/catalina/valves/TestJsonErrorReportValve.java --- tomcat10-10.1.52/test/org/apache/catalina/valves/TestJsonErrorReportValve.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestJsonErrorReportValve.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java tomcat10-10.1.55/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java --- tomcat10-10.1.52/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestLoadBalancerDrainingValve.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/valves/TestPersistentValve.java tomcat10-10.1.55/test/org/apache/catalina/valves/TestPersistentValve.java --- tomcat10-10.1.52/test/org/apache/catalina/valves/TestPersistentValve.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestPersistentValve.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/catalina/valves/TestProxyErrorReportValve.java tomcat10-10.1.55/test/org/apache/catalina/valves/TestProxyErrorReportValve.java --- tomcat10-10.1.52/test/org/apache/catalina/valves/TestProxyErrorReportValve.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestProxyErrorReportValve.java 2026-05-05 13:36:52.000000000 +0000 @@ -0,0 +1,290 @@ +/* + * 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 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.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 = null; + Valve[] valves = host.getPipeline().getValves(); + for (Valve valveCandidate : valves) { + if (PROXY_VALVE.equals(valveCandidate.getClass().getName())) { + valve = (ProxyErrorReportValve)valveCandidate; + break; + } + } + Assert.assertNotNull(valve); + 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 = null; + Valve[] valves = host.getPipeline().getValves(); + for (Valve valveCandidate : valves) { + if (PROXY_VALVE.equals(valveCandidate.getClass().getName())) { + valve = (ProxyErrorReportValve)valveCandidate; + break; + } + } + Assert.assertNotNull(valve); + 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 { + + 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 { + + 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 { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + throw new RuntimeException("Test exception"); + } + } +} diff -Nru tomcat10-10.1.52/test/org/apache/catalina/valves/TestSemaphoreValve.java tomcat10-10.1.55/test/org/apache/catalina/valves/TestSemaphoreValve.java --- tomcat10-10.1.52/test/org/apache/catalina/valves/TestSemaphoreValve.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/catalina/valves/TestSemaphoreValve.java 2026-05-05 13:36:52.000000000 +0000 @@ -0,0 +1,438 @@ +/* + * 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.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 { + + 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(); + } + + 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 tomcat10-10.1.52/test/org/apache/coyote/TestCompressionConfig.java tomcat10-10.1.55/test/org/apache/coyote/TestCompressionConfig.java --- tomcat10-10.1.52/test/org/apache/coyote/TestCompressionConfig.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/TestCompressionConfig.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/coyote/http11/TestHttp11Processor.java tomcat10-10.1.55/test/org/apache/coyote/http11/TestHttp11Processor.java --- tomcat10-10.1.52/test/org/apache/coyote/http11/TestHttp11Processor.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http11/TestHttp11Processor.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java tomcat10-10.1.55/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java --- tomcat10-10.1.52/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/coyote/http11/filters/TestChunkedOutputFilter.java tomcat10-10.1.55/test/org/apache/coyote/http11/filters/TestChunkedOutputFilter.java --- tomcat10-10.1.52/test/org/apache/coyote/http11/filters/TestChunkedOutputFilter.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http11/filters/TestChunkedOutputFilter.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/coyote/http2/Http2TestBase.java tomcat10-10.1.55/test/org/apache/coyote/http2/Http2TestBase.java --- tomcat10-10.1.52/test/org/apache/coyote/http2/Http2TestBase.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http2/Http2TestBase.java 2026-05-05 13:36:52.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); } } @@ -1456,7 +1473,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 tomcat10-10.1.52/test/org/apache/coyote/http2/TestHPackHuffman.java tomcat10-10.1.55/test/org/apache/coyote/http2/TestHPackHuffman.java --- tomcat10-10.1.52/test/org/apache/coyote/http2/TestHPackHuffman.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http2/TestHPackHuffman.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/coyote/http2/TestHttp2Section_6_2.java tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_6_2.java --- tomcat10-10.1.52/test/org/apache/coyote/http2/TestHttp2Section_6_2.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_6_2.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/coyote/http2/TestHttp2Section_8_1.java tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_1.java --- tomcat10-10.1.52/test/org/apache/coyote/http2/TestHttp2Section_8_1.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_1.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/coyote/http2/TestHttp2Section_8_2.java tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_2.java --- tomcat10-10.1.52/test/org/apache/coyote/http2/TestHttp2Section_8_2.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_2.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/coyote/http2/TestHttp2Section_8_3.java tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_3.java --- tomcat10-10.1.52/test/org/apache/coyote/http2/TestHttp2Section_8_3.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_3.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/coyote/http2/TestHttp2Section_8_5.java tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_5.java --- tomcat10-10.1.52/test/org/apache/coyote/http2/TestHttp2Section_8_5.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http2/TestHttp2Section_8_5.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/coyote/http2/TestLargeUpload.java tomcat10-10.1.55/test/org/apache/coyote/http2/TestLargeUpload.java --- tomcat10-10.1.52/test/org/apache/coyote/http2/TestLargeUpload.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http2/TestLargeUpload.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/coyote/http2/TestStreamProcessor.java tomcat10-10.1.55/test/org/apache/coyote/http2/TestStreamProcessor.java --- tomcat10-10.1.52/test/org/apache/coyote/http2/TestStreamProcessor.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/coyote/http2/TestStreamProcessor.java 2026-05-05 13:36:52.000000000 +0000 @@ -295,10 +295,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()); } @@ -573,7 +570,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 tomcat10-10.1.52/test/org/apache/el/TestValueExpressionImpl.java tomcat10-10.1.55/test/org/apache/el/TestValueExpressionImpl.java --- tomcat10-10.1.52/test/org/apache/el/TestValueExpressionImpl.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/el/TestValueExpressionImpl.java 2026-05-05 13:36:52.000000000 +0000 @@ -25,6 +25,7 @@ import jakarta.el.ELContext; import jakarta.el.ExpressionFactory; +import jakarta.el.PropertyNotFoundException; import jakarta.el.ValueExpression; import jakarta.el.ValueReference; @@ -327,4 +328,33 @@ Assert.assertNotNull(result); Assert.assertTrue(result.isEmpty()); } + + + @Test + public void testBug69948() { + ExpressionFactory factory = ExpressionFactory.newInstance(); + ELContext context = new ELContextImpl(factory); + + 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 tomcat10-10.1.52/test/org/apache/jasper/compiler/TestGenerator.java tomcat10-10.1.55/test/org/apache/jasper/compiler/TestGenerator.java --- tomcat10-10.1.52/test/org/apache/jasper/compiler/TestGenerator.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/jasper/compiler/TestGenerator.java 2026-05-05 13:36:52.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; import org.apache.tomcat.util.compat.JreCompat; @@ -839,6 +844,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 tomcat10-10.1.52/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java tomcat10-10.1.55/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java --- tomcat10-10.1.52/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/jasper/compiler/TestTagLibraryInfoImpl.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/juli/TestFileHandlerNonRotatable.java tomcat10-10.1.55/test/org/apache/juli/TestFileHandlerNonRotatable.java --- tomcat10-10.1.52/test/org/apache/juli/TestFileHandlerNonRotatable.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/juli/TestFileHandlerNonRotatable.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java --- tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/HttpdIntegrationBaseTest.java 2026-05-05 13:36:52.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}\n" + + "PidFile %{CONF_DIR}/httpd.pid\n" + + "LoadModule authz_core_module modules/mod_authz_core.so\n" + + + (JrePlatform.IS_WINDOWS ? + + "ErrorLog \"|C:/Windows/System32/more.com\"\n" + + : + + "LoadModule unixd_module modules/mod_unixd.so\n" + + "LoadModule mpm_event_module modules/mod_mpm_event.so\n" + + "ErrorLog /dev/stderr\n" + + ) + + + "LogLevel warn\n" + + "ServerName localhost:%{HTTPD_PORT}\n" + ; + + 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 tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java --- tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestBasicProxy.java 2026-05-05 13:36:52.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\n" + + "LoadModule proxy_http_module modules/mod_proxy_http.so\n" + + "LoadModule headers_module modules/mod_headers.so\n" + + "ProxyRequests Off\n" + + "ProxyPreserveHost On\n" + + "ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "RequestHeader set X-Forwarded-For 140.211.11.130\n" + + "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 tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java --- tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestChunkedTransferEncodingWithProxy.java 2026-05-05 13:36:52.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\n" + + "SetEnv proxy-sendchunked 1\n" + + "LoadModule proxy_module modules/mod_proxy.so\n" + + "LoadModule proxy_http_module modules/mod_proxy_http.so\n" + + "ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "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 tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java --- tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestErrorHandling.java 2026-05-05 13:36:52.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.integration.httpd; + +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\n" + + "LoadModule proxy_http_module modules/mod_proxy_http.so\n" + + "ProxyRequests Off\n" + + "ProxyPreserveHost On\n" + + "ProxyPass / http://localhost:%{TOMCAT_PORT}/\n" + + "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() { + 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 tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java --- tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestFullReverseProxy.java 2026-05-05 13:36:52.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\n" + + "LoadModule proxy_http_module modules/mod_proxy_http.so\n" + + "LoadModule headers_module modules/mod_headers.so\n" + + "LoadModule ssl_module modules/mod_ssl.so\n" + + "SSLSessionCache none\n" + + "Listen %{HTTPD_SSL_PORT} https\n" + + "\n" + + "ServerName localhost:%{HTTPD_SSL_PORT}\n" + + "SSLEngine on\n" + + "SSLCertificateFile \"%{SSL_CERT_FILE}\"\n" + + "SSLCertificateKeyFile \"%{SSL_KEY_FILE}\"\n" + + "ProxyRequests Off\n" + + "ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "RequestHeader set SSL_CLIENT_CERT \"%{SSL_CLIENT_CERT}s\"\n" + + "RequestHeader set SSL_CIPHER \"%{SSL_CIPHER}s\"\n" + + "RequestHeader set SSL_SESSION_ID \"%{SSL_SESSION_ID}s\"\n" + + "RequestHeader set SSL_CIPHER_USEKEYSIZE \"%{SSL_CIPHER_USEKEYSIZE}s\"\n" + + "SSLVerifyClient optional\n" + + "SSLCACertificateFile \"%{SSL_CA_CERT_FILE}\"\n" + + "SSLOptions +ExportCertData\n" + + "ProxyAddHeaders Off\n" + + "RequestHeader set X-Forwarded-For 140.211.11.130\n" + + "RequestHeader set X-Forwarded-Proto https\n" + + "RequestHeader set X-Forwarded-Host whoamI.tomcat\n" + + "" + ; + + @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 tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java --- tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestLargePayloadWithProxy.java 2026-05-05 13:36:52.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\n" + + "LoadModule proxy_http_module modules/mod_proxy_http.so\n" + + "ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "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 tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java --- tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestRemoteIpValveWithProxy.java 2026-05-05 13:36:52.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\n" + + "LoadModule proxy_http_module modules/mod_proxy_http.so\n" + + "LoadModule headers_module modules/mod_headers.so\n" + + "ProxyRequests Off\n" + + "ProxyPreserveHost On\n" + + "ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "ProxyAddHeaders Off\n" + + "RequestHeader set X-Forwarded-For 140.211.11.130\n" + + "RequestHeader set X-Forwarded-Proto https\n" + + "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 tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java --- tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy01.java 2026-05-05 13:36:52.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\n" + + "LoadModule proxy_http_module modules/mod_proxy_http.so\n" + + "LoadModule headers_module modules/mod_headers.so\n" + + "LoadModule ssl_module modules/mod_ssl.so\n" + + "SSLSessionCache none\n" + + "Listen %{HTTPD_SSL_PORT} https\n" + + "\n" + + "ServerName localhost:%{HTTPD_SSL_PORT}\n" + + "SSLEngine on\n" + + "SSLCertificateFile \"%{SSL_CERT_FILE}\"\n" + + "SSLCertificateKeyFile \"%{SSL_KEY_FILE}\"\n" + + "ProxyRequests Off\n" + + "ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "RequestHeader set SSL_CLIENT_CERT \"%{SSL_CLIENT_CERT}s\"\n" + + "RequestHeader set SSL_CIPHER \"%{SSL_CIPHER}s\"\n" + + "RequestHeader set SSL_SESSION_ID \"%{SSL_SESSION_ID}s\"\n" + + "RequestHeader set SSL_CIPHER_USEKEYSIZE \"%{SSL_CIPHER_USEKEYSIZE}s\"\n" + + "" + ; + + @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 tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java --- tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestSSLValveWithProxy02.java 2026-05-05 13:36:52.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\n" + + "LoadModule proxy_http_module modules/mod_proxy_http.so\n" + + "LoadModule headers_module modules/mod_headers.so\n" + + "LoadModule ssl_module modules/mod_ssl.so\n" + + "SSLSessionCache none\n" + + "Listen %{HTTPD_SSL_PORT} https\n" + + "\n" + + "ServerName localhost:%{HTTPD_SSL_PORT}\n" + + "SSLEngine on\n" + + "SSLCertificateFile \"%{SSL_CERT_FILE}\"\n" + + "SSLCertificateKeyFile \"%{SSL_KEY_FILE}\"\n" + + "ProxyRequests Off\n" + + "ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "RequestHeader set SSL_CLIENT_CERT \"%{SSL_CLIENT_CERT}s\"\n" + + "RequestHeader set SSL_CIPHER \"%{SSL_CIPHER}s\"\n" + + "RequestHeader set SSL_SESSION_ID \"%{SSL_SESSION_ID}s\"\n" + + "RequestHeader set SSL_CIPHER_USEKEYSIZE \"%{SSL_CIPHER_USEKEYSIZE}s\"\n" + + "SSLVerifyClient optional\n" + + "SSLCACertificateFile \"%{SSL_CA_CERT_FILE}\"\n" + + "SSLOptions +ExportCertData\n" + + "" + ; + + @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 tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java --- tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TestSessionWithProxy.java 2026-05-05 13:36:52.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\n" + + "LoadModule proxy_http_module modules/mod_proxy_http.so\n" + + "LoadModule headers_module modules/mod_headers.so\n" + + "LoadModule ssl_module modules/mod_ssl.so\n" + + "SSLSessionCache none\n" + + "ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "Listen %{HTTPD_SSL_PORT} https\n" + + "\n" + + "ServerName localhost:%{HTTPD_SSL_PORT}\n" + + "SSLEngine on\n" + + "SSLCertificateFile \"%{SSL_CERT_FILE}\"\n" + + "SSLCertificateKeyFile \"%{SSL_KEY_FILE}\"\n" + + "ProxyPass /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "ProxyPassReverse /endpoint http://localhost:%{TOMCAT_PORT}/%{SERVLET_NAME}\n" + + "RequestHeader set X-Forwarded-Proto https\n" + + "" + ; + + @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 tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TesterHttpd.java tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TesterHttpd.java --- tomcat10-10.1.52/test/org/apache/tomcat/integration/httpd/TesterHttpd.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/integration/httpd/TesterHttpd.java 2026-05-05 13:36:52.000000000 +0000 @@ -0,0 +1,151 @@ +/* + * 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.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +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(new BufferedReader(new InputStreamReader(p.getInputStream())), System.out, swallowOutput); + redirect(new BufferedReader(new InputStreamReader(p.getErrorStream())), 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 (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 tomcat10-10.1.52/test/org/apache/tomcat/jni/TesterLibraryLoad.java tomcat10-10.1.55/test/org/apache/tomcat/jni/TesterLibraryLoad.java --- tomcat10-10.1.52/test/org/apache/tomcat/jni/TesterLibraryLoad.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/jni/TesterLibraryLoad.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java tomcat10-10.1.55/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java --- tomcat10-10.1.52/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/security/TestSecurity2017Ocsp.java 2026-05-05 13:36:52.000000000 +0000 @@ -16,7 +16,6 @@ */ package org.apache.tomcat.security; -import java.io.IOException; import java.net.SocketException; import javax.net.ssl.SSLHandshakeException; @@ -51,7 +50,7 @@ ocspResponder = new TesterOcspResponder(); try { ocspResponder.start(); - } catch (IOException ioe) { + } catch (Exception e) { ocspResponder = null; } } diff -Nru tomcat10-10.1.52/test/org/apache/tomcat/security/TestSecurity2018.java tomcat10-10.1.55/test/org/apache/tomcat/security/TestSecurity2018.java --- tomcat10-10.1.52/test/org/apache/tomcat/security/TestSecurity2018.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/security/TestSecurity2018.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/security/TestSecurity2019.java tomcat10-10.1.55/test/org/apache/tomcat/security/TestSecurity2019.java --- tomcat10-10.1.52/test/org/apache/tomcat/security/TestSecurity2019.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/security/TestSecurity2019.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/unittest/TesterRequest.java tomcat10-10.1.55/test/org/apache/tomcat/unittest/TesterRequest.java --- tomcat10-10.1.52/test/org/apache/tomcat/unittest/TesterRequest.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/unittest/TesterRequest.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/unittest/TesterResponseWithStatus.java tomcat10-10.1.55/test/org/apache/tomcat/unittest/TesterResponseWithStatus.java --- tomcat10-10.1.52/test/org/apache/tomcat/unittest/TesterResponseWithStatus.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/unittest/TesterResponseWithStatus.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java tomcat10-10.1.55/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/http/TestRequestUtilNormalize.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/http/parser/TestChunkExtension.java tomcat10-10.1.55/test/org/apache/tomcat/util/http/parser/TestChunkExtension.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/http/parser/TestChunkExtension.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/http/parser/TestChunkExtension.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/http/parser/TestMediaType.java tomcat10-10.1.55/test/org/apache/tomcat/util/http/parser/TestMediaType.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/http/parser/TestMediaType.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/http/parser/TestMediaType.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestAlpnFallback.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestAlpnFallback.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestAlpnFallback.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestAlpnFallback.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestLargeClientHello.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestLargeClientHello.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestLargeClientHello.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestLargeClientHello.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestSSLHostConfig.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestSSLHostConfig.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestSSLHostConfig.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestSSLHostConfig.java 2026-05-05 13:36:52.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,9 @@ // 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()); } @@ -81,9 +93,12 @@ SSLHostConfig hc = new SSLHostConfig(); Cipher c = Cipher.TLS_AES_128_CCM_SHA256; - // Single TLSv1.3 name - should be filtered out + // 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()); } @@ -93,9 +108,12 @@ 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 + // 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()); } @@ -105,9 +123,12 @@ 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 + // 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()); } diff -Nru tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestSSLHostConfigProtocol.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestSSLHostConfigProtocol.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestSSLHostConfigProtocol.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestSSLHostConfigProtocol.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestSslHandshakeFailure.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestSslHandshakeFailure.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestSslHandshakeFailure.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestSslHandshakeFailure.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestXxxEndpoint.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestXxxEndpoint.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/TestXxxEndpoint.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/TestXxxEndpoint.java 2026-05-05 13:36:52.000000000 +0000 @@ -32,8 +32,7 @@ import org.apache.tomcat.util.compat.JreCompat; /** - * Test case for the Endpoint implementations. The testing framework will ensure - * that each implementation is tested. + * Test case for the Endpoint implementations. The testing framework will ensure that each implementation is tested. */ public class TestXxxEndpoint extends TomcatBaseTest { @@ -49,7 +48,7 @@ tomcat.getConnector().stop(); Exception e = null; - try (ServerSocket s = new ServerSocket(port, 100, InetAddress.getByName("localhost"))){ + try (ServerSocket s = new ServerSocket(port, 100, InetAddress.getByName("localhost"))) { } catch (Exception e1) { e = e1; } diff -Nru tomcat10-10.1.52/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/TesterSupport.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/TesterSupport.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/TesterSupport.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/TesterSupport.java 2026-05-05 13:36:52.000000000 +0000 @@ -128,7 +128,7 @@ } public static void initSsl(Tomcat tomcat) { - // By default, use JSSE JSSE trust + // By default, use JSSE trust initSsl(tomcat, false); } @@ -656,6 +656,7 @@ private final SSLSocketFactory delegate; private String[] ciphers = null; + private String[] protocols = null; public ClientSSLSocketFactory(SSLSocketFactory delegate) { @@ -671,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); @@ -722,6 +732,9 @@ if (ciphers != null) { ((SSLSocket) socket).setEnabledCipherSuites(ciphers); } + if (protocols != null) { + ((SSLSocket) socket).setEnabledProtocols(protocols); + } return socket; } } diff -Nru tomcat10-10.1.52/test/org/apache/tomcat/util/net/index.db.attr tomcat10-10.1.55/test/org/apache/tomcat/util/net/index.db.attr --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/index.db.attr 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/index.db.attr 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -unique_subject = no diff -Nru tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspEnabled.java 2026-05-05 13:36:52.000000000 +0000 @@ -16,7 +16,7 @@ */ package org.apache.tomcat.util.net.ocsp; -import java.io.IOException; +import java.net.SocketException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -43,8 +43,8 @@ ocspResponder = new TesterOcspResponder(); try { ocspResponder.start(); - } catch (IOException ioe) { - ocspResponder = null; + } catch (Exception e) { + e.printStackTrace(); } } @@ -112,7 +112,7 @@ if (handshakeFailureExpected) { Assert.fail("Handshake did not fail when expected to do so."); } - } catch (SSLHandshakeException e) { + } catch (SSLHandshakeException | SocketException e) { if (!handshakeFailureExpected) { Assert.fail("Handshake failed when not expected to do so."); } diff -Nru tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFail.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFail.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFail.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFail.java 2026-05-05 13:36:52.000000000 +0000 @@ -18,6 +18,7 @@ import java.net.SocketException; +import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import org.junit.Test; @@ -47,13 +48,10 @@ public void testNoResponderWithoutSoftFail() throws Exception { try { doTest(false, false, ClientCertificateVerification.ENABLED, false, Boolean.FALSE); - } catch (SocketException se) { - // NIO2 may throw a SocketException rather than a SSLHandshakeException - if (getTomcatInstance().getConnector().getProtocolHandlerClassName().contains("Nio2")) { - throw new SSLHandshakeException(se.getMessage()); - } else { - throw se; - } + } 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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailInternalError.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailInternalError.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailInternalError.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailInternalError.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailTryLater.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailTryLater.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailTryLater.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TestOcspSoftFailTryLater.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponder.java 2026-05-05 13:36:52.000000000 +0000 @@ -16,110 +16,97 @@ */ package org.apache.tomcat.util.net.ocsp; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintStream; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.junit.Assert; - -import org.apache.tomcat.util.net.TesterSupport; - -/* - * The OpenSSL ocsp tool is great, but it does generate a lot of output. That output needs to be swallowed, else the - * process will freeze when the output buffers (stdout and stderr) are full. - * - * There is a command line option to redirect stdout (which could be redirected to /dev/null), but there is no option to - * redirect stderr. Therefore, this class uses a couple of dedicated threads to read stdout and stderr. By default, the - * output is ignored, but it can be dumped to Java's stdout/stderr if required for debugging purposes. - */ -public class TesterOcspResponder { +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; - private static List ocspArgs = Arrays.asList("ocsp", "-port", "8888", "-text", "-index", - TesterSupport.DB_INDEX, "-CA", TesterSupport.CA_CERT_PEM, "-rkey", TesterSupport.OCSP_RESPONDER_RSA_KEY, - "-rsigner", TesterSupport.OCSP_RESPONDER_RSA_CERT, "-nmin", "60"); - - private Process p; - - public void start() throws IOException { - if (p != null) { - throw new IllegalStateException("Already started"); - } - - String openSSLPath = System.getProperty("tomcat.test.openssl.path"); - String openSSLLibPath = null; - if (openSSLPath == null || openSSLPath.length() == 0) { - openSSLPath = "openssl"; - } else { - // Explicit OpenSSL path may also need explicit lib path - // (e.g. Gump needs this) - openSSLLibPath = openSSLPath.substring(0, openSSLPath.lastIndexOf('/')); - openSSLLibPath = openSSLLibPath + "/../:" + openSSLLibPath + "/../lib:" + openSSLLibPath + "/../lib64"; - } - List cmd = new ArrayList<>(); - cmd.add(openSSLPath); - cmd.addAll(ocspArgs); - - ProcessBuilder pb = new ProcessBuilder(cmd.toArray(new String[0])); - - if (openSSLLibPath != null) { - Map env = pb.environment(); - String libraryPath = env.get("LD_LIBRARY_PATH"); - if (libraryPath == null) { - libraryPath = openSSLLibPath; - } else { - libraryPath = libraryPath + ":" + openSSLLibPath; - } - env.put("LD_LIBRARY_PATH", libraryPath); - } +public class TesterOcspResponder { - p = pb.start(); + private OcspResponse fixedResponse; - redirect(new BufferedReader(new InputStreamReader(p.getInputStream())) , System.out, true); - redirect(new BufferedReader(new InputStreamReader(p.getErrorStream())), System.err, true); + private File catalinaBase; + private Tomcat ocspResponder; - Assert.assertTrue(p.isAlive()); + public void setFixedResponse(OcspResponse fixedResponse) { + this.fixedResponse = fixedResponse; } - public void stop() { - if (p == null) { - throw new IllegalStateException("Not started"); + 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"); } - 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); + // 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()); + } - 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]; + // Start the responder + ocspResponder.start(); + } + + public void stop() { + if (ocspResponder != null) { try { - int read; - while ((read = r.read(cbuf)) > 0) { - if (!swallow) { - os.print(new String(cbuf, 0, read)); - } - } - } catch (IOException ignore) { - // Ignore + 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); + } + } - }).start(); + public enum OcspResponse { + OK, + REVOKED, + UNKNOWN, + TRY_LATER, + INTERNAL_ERROR } } diff -Nru tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/ocsp/TesterOcspResponderServlet.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/openssl/ciphers/TestCipher.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/openssl/ciphers/TestOpenSSLCipherConfigurationParser.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java tomcat10-10.1.55/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/net/openssl/ciphers/TesterOpenSSL.java 2026-05-05 13:36:52.000000000 +0000 @@ -49,7 +49,9 @@ } catch (IOException ioe) { versionString = ""; } - if (versionString.startsWith("OpenSSL 4.0.")) { + 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; @@ -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())) { diff -Nru tomcat10-10.1.52/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java tomcat10-10.1.55/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java --- tomcat10-10.1.52/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/util/scan/TestStandardJarScanner.java 2026-05-05 13:36:52.000000000 +0000 @@ -17,13 +17,19 @@ 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.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; @@ -56,6 +62,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 tomcat10-10.1.52/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java tomcat10-10.1.55/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java --- tomcat10-10.1.52/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/websocket/TesterWebSocketClientProxy.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/org/apache/tomcat/websocket/server/TestClose.java tomcat10-10.1.55/test/org/apache/tomcat/websocket/server/TestClose.java --- tomcat10-10.1.52/test/org/apache/tomcat/websocket/server/TestClose.java 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/org/apache/tomcat/websocket/server/TestClose.java 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/webapp/WEB-INF/classes/bug69623-a.mdd tomcat10-10.1.55/test/webapp/WEB-INF/classes/bug69623-a.mdd --- tomcat10-10.1.52/test/webapp/WEB-INF/classes/bug69623-a.mdd 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test/webapp/WEB-INF/classes/bug69623-a.mdd 2026-05-05 13:36:52.000000000 +0000 @@ -1 +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 diff -Nru tomcat10-10.1.52/test/webapp/jsp/generator/dependants-order-a.jspf tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order-a.jspf --- tomcat10-10.1.52/test/webapp/jsp/generator/dependants-order-a.jspf 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order-a.jspf 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/webapp/jsp/generator/dependants-order-b.jspf tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order-b.jspf --- tomcat10-10.1.52/test/webapp/jsp/generator/dependants-order-b.jspf 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order-b.jspf 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/webapp/jsp/generator/dependants-order-c.jspf tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order-c.jspf --- tomcat10-10.1.52/test/webapp/jsp/generator/dependants-order-c.jspf 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order-c.jspf 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/webapp/jsp/generator/dependants-order.jsp tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order.jsp --- tomcat10-10.1.52/test/webapp/jsp/generator/dependants-order.jsp 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/webapp/jsp/generator/dependants-order.jsp 2026-05-05 13:36:52.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 tomcat10-10.1.52/test/webapp/jsp/generator/external-taglib.jsp tomcat10-10.1.55/test/webapp/jsp/generator/external-taglib.jsp --- tomcat10-10.1.52/test/webapp/jsp/generator/external-taglib.jsp 1970-01-01 00:00:00.000000000 +0000 +++ tomcat10-10.1.55/test/webapp/jsp/generator/external-taglib.jsp 2026-05-05 13:36:52.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 tomcat10-10.1.52/test-profiles.properties.default tomcat10-10.1.55/test-profiles.properties.default --- tomcat10-10.1.52/test-profiles.properties.default 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/test-profiles.properties.default 2026-05-05 13:36:52.000000000 +0000 @@ -98,3 +98,7 @@ # 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 tomcat10-10.1.52/webapps/docs/changelog.xml tomcat10-10.1.55/webapps/docs/changelog.xml --- tomcat10-10.1.52/webapps/docs/changelog.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/webapps/docs/changelog.xml 2026-05-05 13:36:52.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) + + + + + + + Align buffer reuse of the OpenSSLEngine for tomcat-native with the FFM + code. (remm) + + + 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) + + + +
    +
    + + + + 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) + + + Fix case sensitive handling of the protocol host name. (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 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) + + + +
    +
    @@ -112,6 +568,10 @@ 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) +
    @@ -283,6 +743,9 @@ Update Tomcat Native to 2.0.12. (markt) + + Update bnd to 7.2.0. (markt) + Add property "gpg.sign.files" to optionally disable release artefact signing with GPG. (rjung) @@ -617,7 +1080,7 @@ (markt) - Store HTTP request headers using the original case for the header name + Store HTTP trailer headers using the original case for the header name rather than forcing it to lower case. (markt) @@ -7365,9 +7828,6 @@ Update the packaged version of the Tomcat Native Library to 1.2.30. Also update the minimum recommended version to 1.2.30. (markt) - - Update bnd to 7.2.0. (markt) -
    diff -Nru tomcat10-10.1.52/webapps/docs/config/ajp.xml tomcat10-10.1.55/webapps/docs/config/ajp.xml --- tomcat10-10.1.52/webapps/docs/config/ajp.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/webapps/docs/config/ajp.xml 2026-05-05 13:36:52.000000000 +0000 @@ -559,6 +559,7 @@

    Only requests from workers with this secret keyword will be accepted. + The secret keyword must consist only of standard ASCII characters. The default value is null. This attribute must be specified with a non-null, non-zero length value unless secretRequired is explicitly configured to be diff -Nru tomcat10-10.1.52/webapps/docs/config/cluster-interceptor.xml tomcat10-10.1.55/webapps/docs/config/cluster-interceptor.xml --- tomcat10-10.1.52/webapps/docs/config/cluster-interceptor.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/webapps/docs/config/cluster-interceptor.xml 2026-05-05 13:36:52.000000000 +0000 @@ -214,18 +214,23 @@

    - The encryption algorithm to be used, including the mode and padding. Please see +

    The encryption algorithm to be used, including the mode and padding. + Please see https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html - for the standard JCA names that can be used. + for the standard JCA names that can be used. Algorithms from other JCA + providers are also supported.

    - EncryptInterceptor currently supports the following +

    The EncryptInterceptor currently supports the following standard block-cipher modes: - CBC, OFB, CFB, and GCM. + CBC, CFB, OFB and GCM. Of these, it is recommended that GCM is always + used.

    - The length of the key will specify the flavor of the encryption algorithm - to be used, if applicable (e.g. AES-128 versus AES-256). +

    The length of the key will specify the flavor of the encryption + algorithm to be used, if applicable (e.g. AES-128 versus AES-256).

    - The default algorithm is AES/CBC/PKCS5Padding. +

    The default algorithm is AES/CBC/PKCS5Padding for + backwards compatability but it is recommended that + AES/GCM/NoPadding is used.

    The key to be used with the encryption algorithm. diff -Nru tomcat10-10.1.52/webapps/docs/config/filter.xml tomcat10-10.1.55/webapps/docs/config/filter.xml --- tomcat10-10.1.52/webapps/docs/config/filter.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/webapps/docs/config/filter.xml 2026-05-05 13:36:52.000000000 +0000 @@ -913,10 +913,32 @@ -

    There are a number of HTTP headers that can be added to the response to - improve the security of the connection. This filter provides a mechanism for - adding those headers. Note that security related headers with more complex - requirements, like CORS, are implemented as separate Filters.

    +

    The HTTP Header Security Filter adds security-related HTTP response + headers to help protect against common web vulnerabilities. It supports + three independent protections, each of which can be enabled or disabled + individually:

    + +
      +
    • HTTP Strict Transport Security (HSTS) - instructs + browsers to only access the site over HTTPS, preventing protocol + downgrade attacks and cookie hijacking. The + Strict-Transport-Security header is only added to responses + for secure (HTTPS) requests. See + RFC 6797 for further + details.
    • +
    • Click-jacking protection - sets the + X-Frame-Options header to control whether the page can be + rendered in a frame, iframe, or object, preventing click-jacking + attacks.
    • +
    • Content-Type sniffing protection - sets the + X-Content-Type-Options header to nosniff, + preventing browsers from MIME-sniffing the response away from the + declared content type.
    • +
    + +

    Note that security-related headers with more complex requirements, such + as CORS, are implemented as separate filters.

    +
    @@ -928,6 +950,39 @@ + + +

    To enable the filter with a one-year HSTS policy including subdomains, + click-jacking protection allowing same-origin framing, and content-type + sniffing protection:

    + + + httpHeaderSecurity + + org.apache.catalina.filters.HttpHeaderSecurityFilter + + true + + hstsMaxAgeSeconds + 31536000 + + + hstsIncludeSubDomains + true + + + antiClickJackingOption + SAMEORIGIN + + + + httpHeaderSecurity + /* +]]> + +
    + +

    The HTTP Header Security Filter supports the following initialization @@ -936,58 +991,61 @@ -

    Will an HTTP Strict Transport Security (HSTS) header - (Strict-Transport-Security) be set on the response for - secure requests. Any HSTS header already present will be replaced. See - RFC 6797 for further - details of HSTS. If not specified, the default value of - true will be used.

    +

    Should the Strict-Transport-Security header be set on + the response for secure requests? The header is only added when the + request is secure (HTTPS). Any HSTS header already present will be + replaced. If not specified, the default value of true + will be used.

    -

    The max age value that should be used in the HSTS header. Negative - values will be treated as zero. If not specified, the default value of - 0 will be used.

    +

    The max age value (in seconds) to use in the HSTS header. This + tells the browser how long to remember that the site should only be + accessed over HTTPS. Negative values will be treated as zero. If not + specified, the default value of 0 will be used.

    -

    Should the includeSubDomains parameter be included in the HSTS - header. If not specified, the default value of false will - be used.

    +

    Should the includeSubDomains directive be included in + the HSTS header? When enabled, the HSTS policy applies to all + subdomains as well. If not specified, the default value of + false will be used.

    -

    Should the preload parameter be included in the HSTS header. If not - specified, the default value of false will be used. See - https://hstspreload.org for - important information about this parameter.

    +

    Should the preload directive be included in the HSTS + header? This signals to browser vendors that the domain should be + included in built-in HSTS preload lists. If not specified, the default + value of false will be used. See + https://hstspreload.org for + requirements and important information about this parameter.

    -

    Should the anti click-jacking header (X-Frame-Options) - be set on the response. Any anti click-jacking header already present - will be replaced. If not specified, the default value of - true will be used.

    +

    Should the X-Frame-Options header be set on the + response? Any existing header with the same name will be replaced. If + not specified, the default value of true will be used.

    -

    What value should be used for the anticlick-jacking header? Must be - one of DENY, SAMEORIGIN, - ALLOW-FROM (case-insensitive). If not specified, the - default value of DENY will be used.

    +

    The value to use for the X-Frame-Options header. Must + be one of DENY, SAMEORIGIN, or + ALLOW-FROM (case-insensitive). If not specified, the + default value of DENY will be used.

    -

    If ALLOW-FROM is used for antiClickJackingOption, - what URI should be allowed? If not specified, the default value of an - empty string will be used.

    +

    When ALLOW-FROM is used for + antiClickJackingOption, this specifies the URI that is + permitted to frame the page. Must be a valid URI. If not specified, the + default value of an empty string will be used.

    -

    Should the header that blocks content type sniffing - (X-Content-Type-Options) be set on every response. If - already present, the header will be replaced. If not specified, the - default value of true will be used.

    +

    Should the X-Content-Type-Options: nosniff header be + set on every response? Any existing header with the same name will be + replaced. If not specified, the default value of true + will be used.

    diff -Nru tomcat10-10.1.52/webapps/docs/config/http.xml tomcat10-10.1.55/webapps/docs/config/http.xml --- tomcat10-10.1.52/webapps/docs/config/http.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/webapps/docs/config/http.xml 2026-05-05 13:36:52.000000000 +0000 @@ -674,6 +674,18 @@ used.

    + +

    A comma-separated list of content encodings that indicate + already-compressed content. When the response already has a + Content-Encoding header with one of these values, compression + will not be applied to prevent double compression. This attribute is only + used if compression is set to on or + force.

    +

    If not specified, the default values is + br,compress,dcb,dcz,deflate,gzip,pack2000-gzip,zstd, which + includes all commonly used compression algorithms.

    +
    +

    The value is a regular expression (using java.util.regex) matching the user-agent header of HTTP clients for which @@ -1376,10 +1388,14 @@

    Only the TLSv1.2 and below ciphers that are supported by the SSL implementation will be used. Any ciphers in the list derived from a non-default cipher string that are not supported by the SSL implementation - or are TLSv1.3 cipher suites will be ignored and logged in a - WARNING message when the Connector starts. The warning can be - avoided by providing an explicit list of TLSv1.2 and below ciphers that - are supported by the configured SSL implementation.

    + will be ignored and logged in a WARNING message when the + Connector starts. Any entires in the list that are TLSv1.3 cipher suites + will be removed from the cipher list, added to the end of the cipher suite + list (they will not be added to the cipher suite list from Tomact 12.0.x + onwards) and logged in a WARNING message when the Connector + starts. The warnings can be avoided by providing an explicit list of + TLSv1.2 and below ciphers that are supported by the configured SSL + implementation.

    If not specified, a default (using the OpenSSL notation) of HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!kRSA:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256 will be used.

    @@ -1427,12 +1443,10 @@
    -

    JSSE only.

    Allows only allowing certain named groups. The value should be a case sensitive comma separated list of the names of the groups.

    -

    . If not specified, the default named groups of the provider will be - used, and any named groups specified by the client will be passed to it. -

    +

    The default is the value of the jdk.tls.namedGroups + system property.

    @@ -1500,7 +1514,7 @@
  • TLSv1.2
  • TLSv1.3
  • all
  • Each token in the list can be prefixed with a plus sign ("+") or a minus sign ("-"). A plus sign adds the protocol, a minus sign - removes it form the current list. The list is built starting from + removes it from the current list. The list is built starting from an empty list.

    The token all is an alias for SSLv2Hello,TLSv1,TLSv1.1,TLSv1.2,TLSv1.3.

    @@ -1511,8 +1525,7 @@ based secure connector it will always support SSLv2Hello. If a single protocol is specified it will not support SSLv2Hello.

    -

    Note that SSLv2 and SSLv3 are inherently - unsafe.

    +

    Note that SSLv3 is inherently unsafe.

    If not specified, the default value of all will be used.

    diff -Nru tomcat10-10.1.52/webapps/docs/config/http2.xml tomcat10-10.1.55/webapps/docs/config/http2.xml --- tomcat10-10.1.52/webapps/docs/config/http2.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/webapps/docs/config/http2.xml 2026-05-05 13:36:52.000000000 +0000 @@ -44,15 +44,6 @@ the Servlet API is fundamentally blocking, each HTTP/2 stream requires a dedicated container thread for the duration of that stream.

    -

    Requests processed using HTTP/2 will have the following additional request - attributes available:

    -
      -
    • org.apache.coyote.connectionID will return the HTTP/2 - connection ID
    • -
    • org.apache.coyote.streamID will return the HTTP/2 stream - ID
    • -
    - @@ -88,6 +79,18 @@ If not specified, this attribute is set to false.

    + +

    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 might have already buffered frames for opening new streams on a + connection. If not specified, this attribute is set to 0.

    +
    +

    Controls the initial size of the flow control window for streams that Tomcat advertises to clients. If not specified, the default value of diff -Nru tomcat10-10.1.52/webapps/docs/config/listeners.xml tomcat10-10.1.55/webapps/docs/config/listeners.xml --- tomcat10-10.1.52/webapps/docs/config/listeners.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/webapps/docs/config/listeners.xml 2026-05-05 13:36:52.000000000 +0000 @@ -428,10 +428,23 @@ -

    The StoreConfig Lifecycle Listener configures a - StoreConfig MBean that may be used to save the current server configuration - in server.xml or the current configuration for a web application in a - context.xml file.

    +

    The StoreConfig Lifecycle Listener enables persisting + runtime configuration changes. When Tomcat's configuration is + modified at runtime, for example, adding or removing a virtual host + through the Host Manager, those changes only exist in memory and are lost + on restart. This listener provides the ability to write the current + in-memory configuration to server.xml (or an individual web + application's context.xml), so that runtime changes are + preserved across restarts.

    + +

    When this listener is enabled, configuration persistence can be + triggered through the + Host Manager and + Manager applications. + The listener compares the current configuration against + default values and only writes attributes that differ from the defaults. + Before overwriting server.xml, a backup of the existing file + is created.

    This listener must only be nested within Server elements.

    @@ -442,16 +455,18 @@ -

    The name of the IStoreConfig implementation to use. If +

    The name of the IStoreConfig implementation to use. + This is an extension point for custom implementations. If not specified the default of org.apache.catalina.storeconfig.StoreConfig will be used.

    -

    The URL of the configuration file that configures how the - IStoreConfig is to save the configuration. If not specified - the built in resource +

    The URL of the configuration file that defines how each server + component type (connectors, hosts, realms, valves, etc.) should be + serialized to XML. This is an extension point for custom serialization + rules. If not specified the built in resource /org/apache/catalina/storeconfig/server-registry.xml will be used.

    diff -Nru tomcat10-10.1.52/webapps/docs/config/realm.xml tomcat10-10.1.55/webapps/docs/config/realm.xml --- tomcat10-10.1.52/webapps/docs/config/realm.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/webapps/docs/config/realm.xml 2026-05-05 13:36:52.000000000 +0000 @@ -1009,6 +1009,12 @@ 1000.

    + +

    The manner in which user names will be treated when making locking + decisions. Defaults to false, meaning user names will be + treated in a case insensitive manner.

    +
    +

    The number of times in a row a user has to fail authentication to be locked out. Defaults to 5.

    diff -Nru tomcat10-10.1.52/webapps/docs/config/valve.xml tomcat10-10.1.55/webapps/docs/config/valve.xml --- tomcat10-10.1.52/webapps/docs/config/valve.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/webapps/docs/config/valve.xml 2026-05-05 13:36:52.000000000 +0000 @@ -287,7 +287,7 @@
  • %A - Local IP address
  • %b - Bytes sent, excluding HTTP headers, or '-' if zero
  • %B - Bytes sent, excluding HTTP headers
  • -
  • %D - Time taken to process the request in microseconds
  • +
  • %D - Time taken to process the request, in microseconds
  • %F - Time taken to commit the response, in milliseconds
  • %h - Remote host name (or IP address if enableLookups for the connector is false)
  • @@ -2189,14 +2189,10 @@
    -

    A fix introduced in Java 8 update 40 ( - JDK-8048194) - onwards broke SPNEGO authentication for IE with Tomcat running on - Windows 2008 R2 servers. This option enables a work-around that allows - SPNEGO authentication to continue working. The work-around should not - impact other configurations so it is enabled by default. If necessary, - the workaround can be disabled by setting this attribute to - false.

    +

    This attribute is hard-coded to false as the work around + it enabled is no longer required. The attribute will be removed from + Tomcat 12 onwards. Any attempt to set the attribute will be ignored. +

    diff -Nru tomcat10-10.1.52/webapps/docs/ssl-howto.xml tomcat10-10.1.55/webapps/docs/ssl-howto.xml --- tomcat10-10.1.52/webapps/docs/ssl-howto.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/webapps/docs/ssl-howto.xml 2026-05-05 13:36:52.000000000 +0000 @@ -537,6 +537,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 tomcat10-10.1.52/webapps/docs/windows-auth-howto.xml tomcat10-10.1.55/webapps/docs/windows-auth-howto.xml --- tomcat10-10.1.52/webapps/docs/windows-auth-howto.xml 2026-01-23 19:33:36.000000000 +0000 +++ tomcat10-10.1.55/webapps/docs/windows-auth-howto.xml 2026-05-05 13:36:52.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).

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