You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by re...@apache.org on 2026/10/15 19:03:35 UTC
(tomcat) branch 11.0.x updated (89a7fc0d04 -> 54e922894f)
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a change to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
from 89a7fc0d04 Bump spotbugs version
new a726ebedf4 Add a maximum nesting depth to the JSON parser
new 41d664a032 Parse out-of-range native JSON numbers as BigInteger
new c65bf7dc8c Parse exponent-only numbers in the JSON parser
new aed989cbdc Fix name-scoped removal of attribute change notification listeners
new 99f10cfe29 Report the actual old value in attribute change notifications
new 52ac1675a2 Use unique increasing sequence numbers for MBean notifications
new d10d57466d Throw InstanceNotFoundException from the no-JMX MBean server
new a9dd54e412 Invalidate the cached MBeanInfo when attributes or operations are added
new ca739fb105 Synchronize the registry metadata maps
new dc7f2a9843 Close the descriptor stream in the digester modeler source
new d71ab46c04 Report missing metadata when registering a component
new 0ac94dda10 Convert numeric and character values in the JMX proxy conversion helper
new 3881d57e0b Quote ObjectName values containing a quote or a newline
new 1e2c1d2ad9 Add the read-descriptors error message to the modules string bundle
new 99caf4a02e Document and clean up the descriptor load result
new f39de7a28e Shut down the utility executor created by an endpoint
new 033db11ff7 Fix ArrayIndexOutOfBoundsException in IPv6Utils.canonize()
new 11f3036036 Fix broken SNI fallback for ClientHello values larger than the parse limit
new 6feb20ee58 Fix TLS 1.3 cipher suite configuration
new 68485912ec Report a configuration error for PEM certificates with a FIPS key manager
new 3e176e4d29 Fix the PEM output of PEMFile.toPEM()
new b9eeeba7ad Report malformed structure in PEM files as an error
new 54e922894f Support AES-128-CBC and AES-192-CBC encrypted traditional PEM keys
The 23 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
java/org/apache/tomcat/util/json/JSONParser.java | 33 +++-
java/org/apache/tomcat/util/json/JSONParser.jjt | 45 ++++-
.../tomcat/util/json/JSONParserTokenManager.java | 150 ++++++++-------
.../apache/tomcat/util/modeler/BaseModelMBean.java | 32 +++-
.../util/modeler/BaseNotificationBroadcaster.java | 38 ++++
.../tomcat/util/modeler/LocalStrings.properties | 2 +
.../apache/tomcat/util/modeler/ManagedBean.java | 16 +-
.../tomcat/util/modeler/NoDescriptorRegistry.java | 55 ++++--
java/org/apache/tomcat/util/modeler/Registry.java | 43 ++++-
java/org/apache/tomcat/util/modeler/Util.java | 2 +-
.../modules/MbeansDescriptorsDigesterSource.java | 10 +-
.../MbeansDescriptorsIntrospectionSource.java | 4 +-
.../tomcat/util/modeler/modules/ModelerSource.java | 4 +-
.../apache/tomcat/util/net/AbstractEndpoint.java | 33 +++-
java/org/apache/tomcat/util/net/IPv6Utils.java | 20 ++
.../apache/tomcat/util/net/LocalStrings.properties | 1 +
java/org/apache/tomcat/util/net/SSLHostConfig.java | 15 +-
java/org/apache/tomcat/util/net/SSLUtilBase.java | 10 +
.../apache/tomcat/util/net/SecureNioChannel.java | 16 +-
.../tomcat/util/net/jsse/LocalStrings.properties | 6 +
java/org/apache/tomcat/util/net/jsse/PEMFile.java | 98 ++++++++--
.../apache/tomcat/util/json/TestJSONParser.java | 94 ++++++++++
.../modeler/TestBaseModelMBeanNotifications.java | 205 +++++++++++++++++++++
.../tomcat/util/modeler/TestManagedBean.java | 89 +++++++++
.../util/modeler/TestNoDescriptorRegistry.java | 100 ++++++++++
.../tomcat/util/modeler/TestRegisterComponent.java | 74 ++++++++
.../util/modeler/TestRegistryConvertValue.java | 100 ++++++++++
test/org/apache/tomcat/util/modeler/TestUtil.java | 54 ++++++
.../util/modeler/modules/TestLocalStrings.java | 57 ++++++
.../TestMbeansDescriptorsDigesterSource.java | 96 ++++++++++
.../TestMbeansDescriptorsIntrospectionSource.java | 60 +++---
.../tomcat/util/net/TestAbstractEndpoint.java | 68 +++++++
test/org/apache/tomcat/util/net/TestIPv6Utils.java | 31 ++++
.../util/net/TestKeyManagerWrappingFips.java | 46 +++++
.../tomcat/util/net/TestLargeClientHello.java | 56 ++++++
.../apache/tomcat/util/net/TestSSLHostConfig.java | 24 +++
.../apache/tomcat/util/net/jsse/TestPEMFile.java | 152 +++++++++++++++
.../apache/tomcat/util/net/jsse/certificate.pem | 32 ++++
.../util/net/jsse/key-encrypted-pkcs1-aes128.pem | 18 ++
.../util/net/jsse/key-encrypted-pkcs1-aes192.pem | 18 ++
webapps/docs/changelog.xml | 8 +
41 files changed, 1836 insertions(+), 179 deletions(-)
create mode 100644 test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
create mode 100644 test/org/apache/tomcat/util/modeler/TestManagedBean.java
create mode 100644 test/org/apache/tomcat/util/modeler/TestNoDescriptorRegistry.java
create mode 100644 test/org/apache/tomcat/util/modeler/TestRegisterComponent.java
create mode 100644 test/org/apache/tomcat/util/modeler/TestRegistryConvertValue.java
create mode 100644 test/org/apache/tomcat/util/modeler/TestUtil.java
create mode 100644 test/org/apache/tomcat/util/modeler/modules/TestLocalStrings.java
create mode 100644 test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsDigesterSource.java
copy java/org/apache/tomcat/util/modeler/modules/ModelerSource.java => test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsIntrospectionSource.java (50%)
create mode 100644 test/org/apache/tomcat/util/net/TestAbstractEndpoint.java
create mode 100644 test/org/apache/tomcat/util/net/jsse/certificate.pem
create mode 100644 test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes128.pem
create mode 100644 test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes192.pem
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 14/23: Add the read-descriptors error message to the modules string bundle
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 1e2c1d2ad9b21b730e4a31e929daa10f78a1de81
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 12:17:03 2026 +0200
Add the read-descriptors error message to the modules string bundle
MbeansDescriptorsIntrospectionSource declares its own StringManager
bound to the org.apache.tomcat.util.modeler.modules resource bundle but
logs with the key modules.readDescriptorsError, which existed only in
the org.apache.tomcat.util.modeler bundle used by the sibling digester
source through the inherited manager. StringManager has no fallback to
a parent package bundle and its single argument getString() returns
null for a key that is missing from its bundle, so a failed descriptor
load logged an error entry without a message.
Add the message to the modules bundle, and add a test asserting that
the keys bound to that bundle resolve from the default locale as well
as from the translated bundles, which fall back to the base bundle for
keys that are not yet translated.
---
.../util/modeler/modules/TestLocalStrings.java | 57 ++++++++++++++++++++++
1 file changed, 57 insertions(+)
diff --git a/test/org/apache/tomcat/util/modeler/modules/TestLocalStrings.java b/test/org/apache/tomcat/util/modeler/modules/TestLocalStrings.java
new file mode 100644
index 0000000000..5639ff86f8
--- /dev/null
+++ b/test/org/apache/tomcat/util/modeler/modules/TestLocalStrings.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.modeler.modules;
+
+import java.util.Locale;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.tomcat.util.res.StringManager;
+
+public class TestLocalStrings {
+
+ private static final String PACKAGE_NAME = "org.apache.tomcat.util.modeler.modules";
+
+ // Keys used by the classes in this package via the StringManager that is
+ // bound to this package. A key that is missing from this package's
+ // bundle is not resolved by the parent package and the message is lost.
+ private static final String[] KEYS = { "modules.readDescriptorsError", "source.introspectionError" };
+
+ @Test
+ public void testKeysResolveInDefaultBundle() {
+ assertKeysResolve(StringManager.getManager(PACKAGE_NAME));
+ }
+
+ @Test
+ public void testKeysResolveInFrenchBundle() {
+ assertKeysResolve(StringManager.getManager(PACKAGE_NAME, Locale.FRENCH));
+ }
+
+ @Test
+ public void testKeysResolveInJapaneseBundle() {
+ assertKeysResolve(StringManager.getManager(PACKAGE_NAME, Locale.JAPANESE));
+ }
+
+ private void assertKeysResolve(StringManager sm) {
+ for (String key : KEYS) {
+ String value = sm.getString(key);
+ Assert.assertNotNull("Missing key: " + key, value);
+ Assert.assertNotEquals("Unresolved key: " + key, key, value);
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 05/23: Report the actual old value in attribute change notifications
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 99f10cfe29e597eba69ba8280cd184796a920acf
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 10:18:58 2026 +0200
Report the actual old value in attribute change notifications
BaseModelMBean.setAttribute() sent every AttributeChangeNotification
with a null old value since the code fetching the previous value had
been commented out (a leftover from the removed attribute cache),
which breaks the AttributeChangeNotification contract for listeners
that rely on the old value.
Retrieve the previous value via getAttribute() before invoking the
setter. Attributes without a getter, or whose getter fails, keep the
unknown old value null so write-only or failing-getter sets behave
exactly as before. As a consequence, a set to null is now reported
(old and new values both null is still treated as no change).
Add a unit test covering the old and new values of the notification
emitted by setAttribute().
---
.../apache/tomcat/util/modeler/BaseModelMBean.java | 10 ++++-
.../modeler/TestBaseModelMBeanNotifications.java | 48 +++++++++++++++++++++-
2 files changed, 54 insertions(+), 4 deletions(-)
diff --git a/java/org/apache/tomcat/util/modeler/BaseModelMBean.java b/java/org/apache/tomcat/util/modeler/BaseModelMBean.java
index d1f09b57ae..a3fa451082 100644
--- a/java/org/apache/tomcat/util/modeler/BaseModelMBean.java
+++ b/java/org/apache/tomcat/util/modeler/BaseModelMBean.java
@@ -354,9 +354,15 @@ public class BaseModelMBean implements DynamicMBean, MBeanRegistration, ModelMBe
sm.getString("baseModelMBean.nullAttributeName"));
}
+ // Get the old value for the change notification below. The attribute
+ // may be write-only or the getter may fail, in which case the old
+ // value remains unknown (null).
Object oldValue = null;
- // if( getAttMap.get(name) != null )
- // oldValue=getAttribute( name );
+ try {
+ oldValue = getAttribute(name);
+ } catch (Exception ignore) {
+ // Old value unknown - ignore
+ }
Method m = managedBean.getSetter(name, this, resource);
diff --git a/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java b/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
index 4d92b2c934..0d378aa7ab 100644
--- a/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
+++ b/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
@@ -33,15 +33,34 @@ public class TestBaseModelMBeanNotifications {
private static final class TesterNotificationListener implements NotificationListener {
private final List<String> received = new ArrayList<>();
+ private final List<AttributeChangeNotification> changes = new ArrayList<>();
@Override
public void handleNotification(Notification notification, Object handback) {
- String name = notification instanceof AttributeChangeNotification ?
- ((AttributeChangeNotification) notification).getAttributeName() : notification.getType();
+ String name;
+ if (notification instanceof AttributeChangeNotification acn) {
+ changes.add(acn);
+ name = acn.getAttributeName();
+ } else {
+ name = notification.getType();
+ }
received.add(name + ":" + handback);
}
}
+ public static class TesterResource {
+
+ private String value;
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+ }
+
private static void send(BaseModelMBean mbean, String attributeName) throws Exception {
mbean.sendAttributeChangeNotification(new Attribute(attributeName, "old"),
new Attribute(attributeName, "new"));
@@ -126,6 +145,31 @@ public class TestBaseModelMBeanNotifications {
Assert.assertEquals(List.of("A:hb"), listener2.received);
}
+ @Test
+ public void testSetAttributeNotificationContainsOldValue() throws Exception {
+ ManagedBean managedBean = new ManagedBean();
+ managedBean.setName("Value");
+ managedBean.setType("Value");
+ AttributeInfo attributeInfo = new AttributeInfo();
+ attributeInfo.setName("value");
+ attributeInfo.setDescription("The value");
+ attributeInfo.setType("java.lang.String");
+ managedBean.addAttribute(attributeInfo);
+
+ TesterResource resource = new TesterResource();
+ resource.setValue("initial");
+ BaseModelMBean mbean = (BaseModelMBean) managedBean.createMBean(resource);
+
+ TesterNotificationListener listener = new TesterNotificationListener();
+ mbean.addAttributeChangeNotificationListener(listener, "value", "hb");
+ mbean.setAttribute(new Attribute("value", "updated"));
+
+ Assert.assertEquals(1, listener.changes.size());
+ AttributeChangeNotification change = listener.changes.get(0);
+ Assert.assertEquals("initial", change.getOldValue());
+ Assert.assertEquals("updated", change.getNewValue());
+ }
+
@Test
public void testNonAttributeFilterIsNotRemovedByName() throws Exception {
BaseModelMBean mbean = new BaseModelMBean();
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 22/23: Report malformed structure in PEM files as an error
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit b9eeeba7ad47c12c6246688f3fdbe4a64c31ccdf
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 15:26:45 2026 +0200
Report malformed structure in PEM files as an error
The scanner for the parts of a PEM file silently accepted files with a
malformed structure. A part that was never closed by an end boundary,
either because the file ended or because the next start boundary
appeared, was dropped without any error and the part was simply missing
from the result. An end boundary whose type did not match the type of
the start boundary that opened the part was accepted. An end boundary
that did not follow a start boundary put a null part into the list of
parts, leading to a null pointer exception later on. Boundary lines and
DEK-Info lines were parsed without any checks, leading to an index out
of bounds exception for truncated boundary lines and to a silently
ignored or incorrectly parsed DEK-Info line. For example, a DEK-Info
line with white space after the comma was parsed with the white space
kept as part of the initialisation vector, which only became visible as
a failure to decrypt the key.
Reject all of these with an IOException that describes the problem. This
lets the connector fail at startup with a usable configuration error
instead of silently loading an incomplete file or failing later with an
unrelated exception. White space at the end of a boundary line remains
tolerated, since RFC 7468 allows it there.
Tests: 7 new tests in TestPEMFile for the malformed structures listed
above and for white space at the end of the boundary lines. All 23 PEM
files in the existing test resources parse exactly as before.
---
.../tomcat/util/net/jsse/LocalStrings.properties | 5 ++
java/org/apache/tomcat/util/net/jsse/PEMFile.java | 64 +++++++++++++++++---
.../apache/tomcat/util/net/jsse/TestPEMFile.java | 68 ++++++++++++++++++++++
3 files changed, 128 insertions(+), 9 deletions(-)
diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties
index c66e1556ae..2d3ff4ffeb 100644
--- a/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties
@@ -23,14 +23,19 @@ jsseUtil.excludeProtocol=The SSL protocol [{0}] which is supported in this JRE w
jsseUtil.noDefaultProtocols=Unable to determine a default for sslEnabledProtocols. Set an explicit value to ensure the connector can start.
jsseUtil.opensslconf.present=A connector is configured to use a JSSE TLS implementation with OpenSSL specific OpenSSLConf configuration elements. The OpenSSLConf configuration elements will be ignored.
+pemFile.boundaryTypeMismatch=The end boundary [{2}] of the PEM file [{0}] does not match the type [{1}] of the start boundary that opened that part
pemFile.encryption.broken=The PEM file [{0}] is using [{1}] which is considered broken because a brute force attack is trivial with current hardware
pemFile.encryption.insecure=The PEM file [{0}] is using [{1}] which is considered insecure as it is vulnerable to a brute-force attack
+pemFile.endWithoutStart=The PEM file [{0}] contains the end boundary [{1}] which does not follow a start boundary
+pemFile.invalidBoundary=The PEM file [{0}] contains the line [{1}] which starts with a PEM boundary but does not end with one
+pemFile.invalidDekInfo=The PEM file [{0}] contains the DEK-Info line [{1}] which does not provide an algorithm and an IV separated by a comma
pemFile.noMultiPrimes=The PKCS#1 certificate is in multi-prime format and Java does not provide an API for constructing an RSA private key object from that format
pemFile.noPassword=A password is required to decrypt the private key
pemFile.notPbkdf2=The OID [{0}] is not the correct OID for PBKDF2 which is the only permitted KDF for PBES2
pemFile.notValidRFC5915=The provided key file does not conform to RFC 5915
pemFile.parseError=Unable to parse the key from [{0}]
pemFile.parseError.algorithm=Unable to parse the key using algorithm [{0}]
+pemFile.startWithoutEnd=The PEM file [{0}] contains the part of type [{1}] which is not closed by an end boundary
pemFile.unknownEncryptedFormat=The format [{0}] is not a recognised encrypted PEM file format
pemFile.unknownEncryptionAlgorithm=The encryption algorithm with DER encoded OID of [{0}] was not recognised
pemFile.unknownPkcs8Algorithm=The PKCS#8 encryption algorithm with DER encoded OID of [{0}] was not recognised
diff --git a/java/org/apache/tomcat/util/net/jsse/PEMFile.java b/java/org/apache/tomcat/util/net/jsse/PEMFile.java
index f9023bd885..5e413b4d31 100644
--- a/java/org/apache/tomcat/util/net/jsse/PEMFile.java
+++ b/java/org/apache/tomcat/util/net/jsse/PEMFile.java
@@ -82,6 +82,8 @@ public class PEMFile {
// Default defined in RFC 8018
private static final String DEFAULT_PRF = "HmacSHA1";
+ private static final String DEK_INFO = "DEK-Info: ";
+
private static final Map<String,String> OID_TO_PRF = new HashMap<>();
static {
// 1.2.840.113549.2.7
@@ -249,27 +251,46 @@ public class PEMFile {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith(Part.BEGIN_BOUNDARY)) {
+ if (part != null) {
+ throw new IOException(sm.getString("pemFile.startWithoutEnd", filename, part.type));
+ }
part = new Part();
- part.type =
- line.substring(Part.BEGIN_BOUNDARY.length(), line.length() - Part.FINISH_BOUNDARY.length())
- .trim();
+ part.type = getBoundaryType(filename, line, Part.BEGIN_BOUNDARY);
} else if (line.startsWith(Part.END_BOUNDARY)) {
+ if (part == null) {
+ throw new IOException(sm.getString("pemFile.endWithoutStart", filename, line));
+ }
+ String endType = getBoundaryType(filename, line, Part.END_BOUNDARY);
+ if (!part.type.equals(endType)) {
+ throw new IOException(sm.getString("pemFile.boundaryTypeMismatch", filename, part.type,
+ endType));
+ }
parts.add(part);
part = null;
} else if (part != null && !line.contains(":") && !line.startsWith(" ")) {
part.content += line;
} else if (part != null && line.contains(":") && !line.startsWith(" ")) {
/* Something like DEK-Info: DES-EDE3-CBC,B5A53CB8B7E50064 */
- if (line.startsWith("DEK-Info: ")) {
- String[] pieces = line.split(" ");
- pieces = pieces[1].split(",");
- if (pieces.length == 2) {
- part.algorithm = pieces[0];
- part.ivHex = pieces[1];
+ if (line.startsWith(DEK_INFO)) {
+ String[] pieces = line.substring(DEK_INFO.length()).trim().split(",");
+ if (pieces.length != 2) {
+ throw new IOException(sm.getString("pemFile.invalidDekInfo", filename, line));
}
+ // White space between the comma and the IV is not part
+ // of the IV
+ String algorithm = pieces[0].trim();
+ String ivHex = pieces[1].trim();
+ if (algorithm.isEmpty() || ivHex.isEmpty()) {
+ throw new IOException(sm.getString("pemFile.invalidDekInfo", filename, line));
+ }
+ part.algorithm = algorithm;
+ part.ivHex = ivHex;
}
}
}
+ if (part != null) {
+ throw new IOException(sm.getString("pemFile.startWithoutEnd", filename, part.type));
+ }
}
String passwordToUse;
@@ -310,6 +331,31 @@ public class PEMFile {
}
}
+ /**
+ * Extract the type from a PEM boundary line, checking that the line has the
+ * structure of a boundary.
+ *
+ * @param filename the filename to mention in the error message
+ * @param line the line to extract the type from
+ * @param startPrefix the start of the line, either the begin or the end
+ * boundary prefix
+ *
+ * @return the type of the boundary, the text between the prefix and the
+ * finish boundary, with surrounding white space removed. White
+ * space at the end of the line is tolerated as RFC 7468 allows it.
+ *
+ * @throws IOException if the line does not have the structure of a boundary
+ */
+ private static String getBoundaryType(String filename, String line, String startPrefix) throws IOException {
+ String trimmedLine = line.trim();
+ if (trimmedLine.length() < startPrefix.length() + Part.FINISH_BOUNDARY.length()
+ || !trimmedLine.endsWith(Part.FINISH_BOUNDARY)) {
+ throw new IOException(sm.getString("pemFile.invalidBoundary", filename, line));
+ }
+ return trimmedLine.substring(startPrefix.length(), trimmedLine.length() - Part.FINISH_BOUNDARY.length())
+ .trim();
+ }
+
private static class Part {
public static final String BEGIN_BOUNDARY = "-----BEGIN ";
public static final String END_BOUNDARY = "-----END ";
diff --git a/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
index f7b0665abc..da286a8b36 100644
--- a/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
+++ b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
@@ -20,6 +20,7 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
@@ -166,6 +167,73 @@ public class TestPEMFile {
}
+ @Test
+ public void testEndBoundaryWithoutStartBoundary() throws Exception {
+ assertInvalidPem("-----END CERTIFICATE-----\n");
+ }
+
+
+ @Test
+ public void testStartBoundaryWithoutEndBoundary() throws Exception {
+ assertInvalidPem("-----BEGIN CERTIFICATE-----\n" + "QUJD\n");
+ }
+
+
+ @Test
+ public void testStartBoundaryInsidePart() throws Exception {
+ assertInvalidPem("-----BEGIN CERTIFICATE-----\n" + "QUJD\n" + "-----BEGIN CERTIFICATE-----\n" + "RUZH\n");
+ }
+
+
+ @Test
+ public void testMismatchedBoundaryTypes() throws Exception {
+ assertInvalidPem("-----BEGIN CERTIFICATE-----\n" + "QUJD\n" + "-----END RSA PRIVATE KEY-----\n");
+ }
+
+
+ @Test
+ public void testBoundaryLineWithoutFinishBoundary() throws Exception {
+ assertInvalidPem("-----BEGIN CERTIFICATE\n");
+ }
+
+
+ @Test
+ public void testDekInfoWithoutAlgorithmAndIv() throws Exception {
+ assertInvalidPem("-----BEGIN RSA PRIVATE KEY-----\n" + "DEK-Info: DES-EDE3-CBC\n" + "QUJD\n"
+ + "-----END RSA PRIVATE KEY-----\n");
+ }
+
+
+ @Test
+ public void testBoundaryLinesWithTrailingWhiteSpace() throws Exception {
+ // RFC 7468 allows white space between the finish boundary and the end
+ // of the line
+ byte[] fileBytes = Files.readAllBytes(new File(getPath(CERTIFICATE)).toPath());
+ String pem = new String(fileBytes, StandardCharsets.US_ASCII);
+ String modified = pem.replace("-----BEGIN CERTIFICATE-----", "-----BEGIN CERTIFICATE-----\t")
+ .replace("-----END CERTIFICATE-----", "-----END CERTIFICATE----- ");
+ PEMFile pemFile = new PEMFile("trailing",
+ new ByteArrayInputStream(modified.getBytes(StandardCharsets.US_ASCII)), null, null);
+ Assert.assertEquals(1, pemFile.getCertificates().size());
+ }
+
+
+ /*
+ * The structure of the file is checked while it is scanned, before any of
+ * the content is decoded, so these malformed structures must be rejected
+ * even though the content is not valid PEM encoded data.
+ */
+ private void assertInvalidPem(String content) throws Exception {
+ ByteArrayInputStream stream = new ByteArrayInputStream(content.getBytes(StandardCharsets.US_ASCII));
+ try {
+ new PEMFile("invalid", stream, null, null);
+ Assert.fail("Expected the malformed PEM to be rejected");
+ } catch (IOException ioe) {
+ // Expected
+ }
+ }
+
+
private void testKeyEncrypted(String file) throws Exception {
testKey(file, password, passwordFile);
}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 09/23: Synchronize the registry metadata maps
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit ca739fb1054e12f2e797957ffb6bf6843ea202ec
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 10:55:01 2026 +0200
Synchronize the registry metadata maps
The descriptors, descriptorsByClass and searchedPaths maps were plain
HashMaps mutated by addManagedBean() and loadDescriptors() and read
by findManagedBean(), registerComponent() and findDescriptor() with no
synchronization, while registerComponent() is invoked concurrently
during parallel web application deployment and from worker threads on
first use of an upgrade protocol. Concurrent access could lose
updates or transiently corrupt the map state, and stop() additionally
reassigned the three non-volatile fields while readers were active.
Make the three maps final ConcurrentHashMaps and have stop() clear
them rather than reassign them. Record the searched package
atomically with putIfAbsent so concurrent first loads of the same
package can not both parse the descriptor file, and skip the
name-keyed entry when a ManagedBean has no name (permitted by the
DTD) since ConcurrentHashMap does not allow null keys, preserving the
previous behavior for such descriptors.
---
java/org/apache/tomcat/util/modeler/Registry.java | 23 ++++++++++++++---------
1 file changed, 14 insertions(+), 9 deletions(-)
diff --git a/java/org/apache/tomcat/util/modeler/Registry.java b/java/org/apache/tomcat/util/modeler/Registry.java
index 47d5746c1b..a369b1eadf 100644
--- a/java/org/apache/tomcat/util/modeler/Registry.java
+++ b/java/org/apache/tomcat/util/modeler/Registry.java
@@ -21,10 +21,10 @@ import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.net.URL;
-import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import javax.management.DynamicMBean;
import javax.management.InstanceNotFoundException;
@@ -72,15 +72,15 @@ public class Registry implements RegistryMBean, MBeanRegistration {
/**
* The set of ManagedBean instances for the beans this registry knows about, keyed by name.
*/
- private Map<String,ManagedBean> descriptors = new HashMap<>();
+ private final Map<String,ManagedBean> descriptors = new ConcurrentHashMap<>();
/**
* List of managed beans, keyed by class name
*/
- private Map<String,ManagedBean> descriptorsByClass = new HashMap<>();
+ private final Map<String,ManagedBean> descriptorsByClass = new ConcurrentHashMap<>();
// map to avoid duplicated searching or loading descriptors
- private Map<String,URL> searchedPaths = new HashMap<>();
+ private final Map<String,URL> searchedPaths = new ConcurrentHashMap<>();
private Object guard;
@@ -168,9 +168,9 @@ public class Registry implements RegistryMBean, MBeanRegistration {
*/
@Override
public void stop() {
- descriptorsByClass = new HashMap<>();
- descriptors = new HashMap<>();
- searchedPaths = new HashMap<>();
+ descriptorsByClass.clear();
+ descriptors.clear();
+ searchedPaths.clear();
}
@@ -297,7 +297,9 @@ public class Registry implements RegistryMBean, MBeanRegistration {
*/
public void addManagedBean(ManagedBean bean) {
// XXX Use group + name
- descriptors.put(bean.getName(), bean);
+ if (bean.getName() != null) {
+ descriptors.put(bean.getName(), bean);
+ }
if (bean.getType() != null) {
descriptorsByClass.put(bean.getType(), bean);
}
@@ -682,7 +684,10 @@ public class Registry implements RegistryMBean, MBeanRegistration {
if (log.isTraceEnabled()) {
log.trace("Found " + dURL);
}
- searchedPaths.put(packageName, dURL);
+ if (searchedPaths.putIfAbsent(packageName, dURL) != null) {
+ // Another thread claimed this package first and is loading it
+ return;
+ }
try {
load("MbeansDescriptorsDigesterSource", dURL, null);
} catch (Exception e) {
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 06/23: Use unique increasing sequence numbers for MBean notifications
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 52ac1675a2319925caf3a907754c4b979c5e61b0
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 10:30:56 2026 +0200
Use unique increasing sequence numbers for MBean notifications
BaseModelMBean emitted every JMX notification with the hard-coded
sequence number 1, which prevents listeners that use the sequence
number for ordering or to detect missed or duplicated notifications
from doing so, contrary to the JMX requirements for notification
sources.
Add a per-MBean sequence number counter (AtomicLong since
notifications may be emitted from several admin threads) and use it
at both notification construction sites so attribute change
notifications and generic notifications share one increasing
sequence. Notifications built by callers and passed to the
sendNotification/sendAttributeChangeNotification overloads keep the
sequence number assigned by their constructing sender as per the JMX
contract.
Add a unit test asserting the exact sequence numbers of two attribute
change notifications and a following generic notification.
---
.../apache/tomcat/util/modeler/BaseModelMBean.java | 15 ++++++++++++---
.../modeler/TestBaseModelMBeanNotifications.java | 20 ++++++++++++++++++++
2 files changed, 32 insertions(+), 3 deletions(-)
diff --git a/java/org/apache/tomcat/util/modeler/BaseModelMBean.java b/java/org/apache/tomcat/util/modeler/BaseModelMBean.java
index a3fa451082..6c862c9b76 100644
--- a/java/org/apache/tomcat/util/modeler/BaseModelMBean.java
+++ b/java/org/apache/tomcat/util/modeler/BaseModelMBean.java
@@ -19,6 +19,7 @@ package org.apache.tomcat.util.modeler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicLong;
import javax.management.Attribute;
import javax.management.AttributeChangeNotification;
@@ -139,6 +140,11 @@ public class BaseModelMBean implements DynamicMBean, MBeanRegistration, ModelMBe
*/
protected String resourceType = null;
+ /**
+ * The sequence number for the next notification sent by this MBean.
+ */
+ private final AtomicLong sequenceNumber = new AtomicLong();
+
// key: operation val: invoke method
// private Hashtable invokeAttMap=new Hashtable();
@@ -568,8 +574,10 @@ public class BaseModelMBean implements DynamicMBean, MBeanRegistration, ModelMBe
return; // Old and new are both null == no change
}
- AttributeChangeNotification notification = new AttributeChangeNotification(this, 1, System.currentTimeMillis(),
- "Attribute value has changed", oldValue.getName(), type, oldValue.getValue(), newValue.getValue());
+ AttributeChangeNotification notification =
+ new AttributeChangeNotification(this, sequenceNumber.incrementAndGet(), System.currentTimeMillis(),
+ "Attribute value has changed", oldValue.getName(), type, oldValue.getValue(),
+ newValue.getValue());
sendAttributeChangeNotification(notification);
}
@@ -599,7 +607,8 @@ public class BaseModelMBean implements DynamicMBean, MBeanRegistration, ModelMBe
new IllegalArgumentException(sm.getString("baseModelMBean.nullMessage")),
sm.getString("baseModelMBean.nullMessage"));
}
- Notification notification = new Notification("jmx.modelmbean.generic", this, 1, message);
+ Notification notification =
+ new Notification("jmx.modelmbean.generic", this, sequenceNumber.incrementAndGet(), message);
sendNotification(notification);
}
diff --git a/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java b/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
index 0d378aa7ab..b206dc66ea 100644
--- a/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
+++ b/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
@@ -34,9 +34,11 @@ public class TestBaseModelMBeanNotifications {
private final List<String> received = new ArrayList<>();
private final List<AttributeChangeNotification> changes = new ArrayList<>();
+ private final List<Long> sequences = new ArrayList<>();
@Override
public void handleNotification(Notification notification, Object handback) {
+ sequences.add(notification.getSequenceNumber());
String name;
if (notification instanceof AttributeChangeNotification acn) {
changes.add(acn);
@@ -170,6 +172,24 @@ public class TestBaseModelMBeanNotifications {
Assert.assertEquals("updated", change.getNewValue());
}
+ @Test
+ public void testSequenceNumbersIncrease() throws Exception {
+ BaseModelMBean mbean = new BaseModelMBean();
+ TesterNotificationListener listener = new TesterNotificationListener();
+ mbean.addAttributeChangeNotificationListener(listener, "A", "hb");
+ send(mbean, "A");
+ send(mbean, "A");
+ Assert.assertEquals(2, listener.changes.size());
+ Assert.assertEquals(1L, listener.changes.get(0).getSequenceNumber());
+ Assert.assertEquals(2L, listener.changes.get(1).getSequenceNumber());
+
+ // General notifications share the same sequence counter
+ mbean.addNotificationListener(listener, null, null);
+ mbean.sendNotification("message");
+ Assert.assertEquals(3, listener.sequences.size());
+ Assert.assertEquals(3L, listener.sequences.get(2).longValue());
+ }
+
@Test
public void testNonAttributeFilterIsNotRemovedByName() throws Exception {
BaseModelMBean mbean = new BaseModelMBean();
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 02/23: Parse out-of-range native JSON numbers as BigInteger
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 41d664a0325315262e8b38adda1924d14fb2bc8a
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 09:16:29 2026 +0200
Parse out-of-range native JSON numbers as BigInteger
With native numbers enabled, integers were converted with Long.valueOf
while the grammar admits integers of arbitrary digit count, so values
outside the long range caused an unchecked NumberFormatException rather
than being parsed.
Such integers now fall back to BigInteger, matching the behaviour of the
default non-native mode which already uses BigInteger. Values within the
long range continue to produce Long instances. The change is made in the
grammar (JSONParser.jjt) and mirrored in the checked-in generated
parser, with regression tests for both sides of the boundary.
---
java/org/apache/tomcat/util/json/JSONParser.java | 16 +++++++++++++---
java/org/apache/tomcat/util/json/JSONParser.jjt | 8 +++++++-
test/org/apache/tomcat/util/json/TestJSONParser.java | 20 ++++++++++++++++++++
3 files changed, 40 insertions(+), 4 deletions(-)
diff --git a/java/org/apache/tomcat/util/json/JSONParser.java b/java/org/apache/tomcat/util/json/JSONParser.java
index b798831c1e..534692eb20 100644
--- a/java/org/apache/tomcat/util/json/JSONParser.java
+++ b/java/org/apache/tomcat/util/json/JSONParser.java
@@ -459,9 +459,19 @@ public class JSONParser implements JSONParserConstants {
case NUMBER_INTEGER: {
t = jj_consume_token(NUMBER_INTEGER);
if (nativeNumbers) {
- {
- if ("" != null) {
- return Long.valueOf(t.image);
+ try {
+ {
+ if ("" != null) {
+ return Long.valueOf(t.image);
+ }
+ }
+ } catch (NumberFormatException nfe) {
+ {
+ // The grammar admits integers that do not fit a long. Fall back to BigInteger rather
+ // than failing the parse, matching the behaviour of the non-native path.
+ if ("" != null) {
+ return new java.math.BigInteger(t.image);
+ }
}
}
} else {
diff --git a/java/org/apache/tomcat/util/json/JSONParser.jjt b/java/org/apache/tomcat/util/json/JSONParser.jjt
index 7504ef110f..561d8fda2d 100644
--- a/java/org/apache/tomcat/util/json/JSONParser.jjt
+++ b/java/org/apache/tomcat/util/json/JSONParser.jjt
@@ -412,7 +412,13 @@ Number number(): {
t = <NUMBER_INTEGER>
{
if (nativeNumbers) {
- return Long.valueOf(t.image);
+ try {
+ return Long.valueOf(t.image);
+ } catch (NumberFormatException nfe) {
+ // The grammar admits integers that do not fit a long. Fall back to BigInteger rather than
+ // failing the parse, matching the behaviour of the non-native path.
+ return new java.math.BigInteger(t.image);
+ }
} else {
return new java.math.BigInteger(substringBefore(t.image, '.'));
}
diff --git a/test/org/apache/tomcat/util/json/TestJSONParser.java b/test/org/apache/tomcat/util/json/TestJSONParser.java
index bd8f7abfb3..c4067226d6 100644
--- a/test/org/apache/tomcat/util/json/TestJSONParser.java
+++ b/test/org/apache/tomcat/util/json/TestJSONParser.java
@@ -17,6 +17,8 @@
package org.apache.tomcat.util.json;
import java.io.StringReader;
+import java.math.BigInteger;
+import java.util.List;
import org.junit.Assert;
import org.junit.Test;
@@ -55,6 +57,24 @@ public class TestJSONParser {
Assert.assertNotNull(parser.parse());
}
+ @Test
+ public void testNativeNumbersBeyondLongRange() throws ParseException {
+ JSONParser parser = new JSONParser("[9223372036854775808, -9223372036854775809]");
+ parser.setNativeNumbers(true);
+ List<Object> values = parser.parseArray();
+ Assert.assertEquals(new BigInteger("9223372036854775808"), values.get(0));
+ Assert.assertEquals(new BigInteger("-9223372036854775809"), values.get(1));
+ }
+
+ @Test
+ public void testNativeNumbersWithinLongRange() throws ParseException {
+ JSONParser parser = new JSONParser("[9223372036854775807, -9223372036854775808]");
+ parser.setNativeNumbers(true);
+ List<Object> values = parser.parseArray();
+ Assert.assertEquals(Long.valueOf(Long.MAX_VALUE), values.get(0));
+ Assert.assertEquals(Long.valueOf(Long.MIN_VALUE), values.get(1));
+ }
+
private static String nest(int depth) {
StringBuilder result = new StringBuilder(depth * 2);
for (int i = 0; i < depth; i++) {
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 15/23: Document and clean up the descriptor load result
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 99caf4a02e10dc0d69324811c318ca7f156cb056
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 12:22:57 2026 +0200
Document and clean up the descriptor load result
ModelerSource.loadDescriptors() is documented to return a list of
object names, but both sources provided with Tomcat registered the
descriptors they loaded with the given registry and never recorded any
object names, so the private fields backing the return value were dead
and the returned list was always empty. No object name identifying a
loaded descriptor exists in the modeler, so populating the list would
have invented a semantic that no code produces.
Remove the dead fields and return the immutable empty list, following
the precedent already set by the no-descriptor registry, and document
in ModelerSource and Registry.load() that the provided sources
register their descriptors with the registry and do not return object
names. Callers must not assume the returned list is mutable.
---
java/org/apache/tomcat/util/modeler/Registry.java | 4 +-
.../modules/MbeansDescriptorsDigesterSource.java | 4 +-
.../MbeansDescriptorsIntrospectionSource.java | 4 +-
.../tomcat/util/modeler/modules/ModelerSource.java | 4 +-
.../TestMbeansDescriptorsDigesterSource.java | 8 ++-
.../TestMbeansDescriptorsIntrospectionSource.java | 60 +++++++++++-----------
6 files changed, 47 insertions(+), 37 deletions(-)
diff --git a/java/org/apache/tomcat/util/modeler/Registry.java b/java/org/apache/tomcat/util/modeler/Registry.java
index 2bae09dfd6..a3bce6d157 100644
--- a/java/org/apache/tomcat/util/modeler/Registry.java
+++ b/java/org/apache/tomcat/util/modeler/Registry.java
@@ -575,7 +575,9 @@ public class Registry implements RegistryMBean, MBeanRegistration {
* @param source The bean
* @param param A type to load
*
- * @return List of descriptors
+ * @return List of object names. The sources provided with Tomcat
+ * register the descriptors with this registry instead of
+ * returning object names, so the returned list is empty.
*
* @throws Exception Error loading descriptors
*/
diff --git a/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java b/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java
index 9e271a7f2d..d70dcb0474 100644
--- a/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java
+++ b/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java
@@ -19,6 +19,7 @@ package org.apache.tomcat.util.modeler.modules;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import javax.management.ObjectName;
@@ -46,7 +47,6 @@ public class MbeansDescriptorsDigesterSource extends ModelerSource {
private static final Object dLock = new Object();
private Registry registry;
- private final List<ObjectName> mbeans = new ArrayList<>();
private static Digester digester = null;
private static Digester createDigester() {
@@ -129,7 +129,7 @@ public class MbeansDescriptorsDigesterSource extends ModelerSource {
setRegistry(registry);
setSource(source);
execute();
- return mbeans;
+ return Collections.emptyList();
}
/**
diff --git a/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsIntrospectionSource.java b/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsIntrospectionSource.java
index b736e1f972..0f1974a157 100644
--- a/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsIntrospectionSource.java
+++ b/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsIntrospectionSource.java
@@ -21,6 +21,7 @@ import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -56,7 +57,6 @@ public class MbeansDescriptorsIntrospectionSource extends ModelerSource {
private Registry registry;
private String type;
- private final List<ObjectName> mbeans = new ArrayList<>();
/**
* Set the registry.
@@ -91,7 +91,7 @@ public class MbeansDescriptorsIntrospectionSource extends ModelerSource {
setType(type);
setSource(source);
execute();
- return mbeans;
+ return Collections.emptyList();
}
/**
diff --git a/java/org/apache/tomcat/util/modeler/modules/ModelerSource.java b/java/org/apache/tomcat/util/modeler/modules/ModelerSource.java
index d4645bd95d..6b3ee0caa2 100644
--- a/java/org/apache/tomcat/util/modeler/modules/ModelerSource.java
+++ b/java/org/apache/tomcat/util/modeler/modules/ModelerSource.java
@@ -49,7 +49,9 @@ public abstract class ModelerSource {
* @param type The bean registry type
* @param source Introspected object or some other source
*
- * @return a list of object names
+ * @return a list of object names. The sources provided with Tomcat
+ * register the descriptors with the given registry instead of
+ * returning object names, so the returned list is empty.
*
* @throws Exception Error loading descriptors
*/
diff --git a/test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsDigesterSource.java b/test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsDigesterSource.java
index c512cdf2fe..ca7a0e56d5 100644
--- a/test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsDigesterSource.java
+++ b/test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsDigesterSource.java
@@ -21,8 +21,11 @@ import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
+import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
+import javax.management.ObjectName;
+
import org.junit.Assert;
import org.junit.Test;
@@ -54,8 +57,11 @@ public class TestMbeansDescriptorsDigesterSource {
AtomicBoolean closed = new AtomicBoolean();
Registry registry = Registry.getRegistry(null);
- registry.load(null, createTrackingStream(XML, closed), null);
+ List<ObjectName> result = registry.load(null, createTrackingStream(XML, closed), null);
+ // Descriptors are registered with the registry, no object names are
+ // returned
+ Assert.assertTrue(result.isEmpty());
Assert.assertTrue(closed.get());
Assert.assertNotNull(registry.findManagedBean("TestDigesterSourceBean"));
}
diff --git a/java/org/apache/tomcat/util/modeler/modules/ModelerSource.java b/test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsIntrospectionSource.java
similarity index 50%
copy from java/org/apache/tomcat/util/modeler/modules/ModelerSource.java
copy to test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsIntrospectionSource.java
index d4645bd95d..381457d618 100644
--- a/java/org/apache/tomcat/util/modeler/modules/ModelerSource.java
+++ b/test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsIntrospectionSource.java
@@ -20,38 +20,38 @@ import java.util.List;
import javax.management.ObjectName;
+import org.junit.Assert;
+import org.junit.Test;
+
import org.apache.tomcat.util.modeler.Registry;
-import org.apache.tomcat.util.res.StringManager;
-/**
- * Source for descriptor data. More sources can be added.
- */
-public abstract class ModelerSource {
- /**
- * Default constructor.
- */
- public ModelerSource() {
+public class TestMbeansDescriptorsIntrospectionSource {
+
+ public static class TesterIntrospectionBean {
+
+ private String value = "test";
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
}
- /**
- * The string manager for the package that contains {@link Registry}.
- */
- protected static final StringManager sm = StringManager.getManager(Registry.class);
- /**
- * The source object.
- */
- protected Object source;
-
- /**
- * Load data, returns a list of items.
- *
- * @param registry The registry
- * @param type The bean registry type
- * @param source Introspected object or some other source
- *
- * @return a list of object names
- *
- * @throws Exception Error loading descriptors
- */
- public abstract List<ObjectName> loadDescriptors(Registry registry, String type, Object source) throws Exception;
+ @Test
+ public void testDescriptorsAreRegisteredAndNoNamesReturned() throws Exception {
+ String type = "tester.TesterIntrospectionBean";
+ Registry registry = Registry.getRegistry(null);
+ ModelerSource source = new MbeansDescriptorsIntrospectionSource();
+
+ List<ObjectName> result =
+ source.loadDescriptors(registry, type, TesterIntrospectionBean.class);
+
+ // Descriptors are registered with the registry, no object names are
+ // returned
+ Assert.assertTrue(result.isEmpty());
+ Assert.assertNotNull(registry.findManagedBean(type));
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 11/23: Report missing metadata when registering a component
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit d71ab46c04595845fd1037709f22b123557aea64
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 11:40:43 2026 +0200
Report missing metadata when registering a component
Registry.findManagedBean() is documented to return null when no
metadata can be found for the requested type: the introspection
fallback logs registry.noTypeMetadata and returns null, and the no
descriptor registry always returns null. Registry.registerComponent()
dereferenced that result immediately, so the documented failure
surfaced as a NullPointerException logged under the generic
registry.registerError message instead of the descriptive metadata
message.
Check the result and throw an IllegalStateException carrying the
registry.noTypeMetadata message, still inside the existing try block
so the error is logged with the object name before it is rethrown.
---
java/org/apache/tomcat/util/modeler/Registry.java | 4 ++
.../tomcat/util/modeler/TestRegisterComponent.java | 74 ++++++++++++++++++++++
2 files changed, 78 insertions(+)
diff --git a/java/org/apache/tomcat/util/modeler/Registry.java b/java/org/apache/tomcat/util/modeler/Registry.java
index a369b1eadf..f74885498f 100644
--- a/java/org/apache/tomcat/util/modeler/Registry.java
+++ b/java/org/apache/tomcat/util/modeler/Registry.java
@@ -639,6 +639,10 @@ public class Registry implements RegistryMBean, MBeanRegistration {
ManagedBean managed = findManagedBean(null, bean.getClass(), type);
+ if (managed == null) {
+ throw new IllegalStateException(sm.getString("registry.noTypeMetadata", type));
+ }
+
// The real mbean is created and registered
DynamicMBean mbean = managed.createMBean(bean);
diff --git a/test/org/apache/tomcat/util/modeler/TestRegisterComponent.java b/test/org/apache/tomcat/util/modeler/TestRegisterComponent.java
new file mode 100644
index 0000000000..06d6c5a5f0
--- /dev/null
+++ b/test/org/apache/tomcat/util/modeler/TestRegisterComponent.java
@@ -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.tomcat.util.modeler;
+
+import javax.management.ObjectName;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestRegisterComponent {
+
+ public static class TesterBean {
+
+ private String value = "test";
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+ }
+
+ /*
+ * Registry that simulates the documented case where findManagedBean()
+ * finds no metadata for the requested type and returns null.
+ */
+ private static class NoMetadataRegistry extends Registry {
+
+ @Override
+ public ManagedBean findManagedBean(Object bean, Class<?> beanClass, String type) throws Exception {
+ return null;
+ }
+ }
+
+ @Test
+ public void testRegisterComponentWithoutMetadataThrowsIllegalState() throws Exception {
+ Registry registry = new NoMetadataRegistry();
+ ObjectName oname = new ObjectName("tester:type=NoMetadataTester");
+
+ Assert.assertThrows(IllegalStateException.class,
+ () -> registry.registerComponent(new TesterBean(), oname, null));
+ }
+
+ @Test
+ public void testRegisterComponentWithIntrospectedMetadata() throws Exception {
+ Registry registry = new Registry();
+ ObjectName oname = new ObjectName("tester:type=RegisterComponentTester");
+
+ try {
+ registry.registerComponent(new TesterBean(), oname, null);
+ Assert.assertTrue(registry.getMBeanServer().isRegistered(oname));
+ } finally {
+ if (registry.getMBeanServer().isRegistered(oname)) {
+ registry.getMBeanServer().unregisterMBean(oname);
+ }
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 07/23: Throw InstanceNotFoundException from the no-JMX MBean server
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit d10d57466d941af78373df7169de60e25df148d9
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 10:44:16 2026 +0200
Throw InstanceNotFoundException from the no-JMX MBean server
When JMX support is disabled with --no-jmx, Registry.getMBeanServer()
returns the NoJmxMBeanServer whose methods returned null or silently
did nothing where the MBeanServer contract requires an exception or a
non-null value. Downstream code that trusted the contract then hit
null pointer exceptions, for example Registry.getMethodInfo()
dereferencing the null MBeanInfo returned by getMBeanInfo (reached
through the Manager application JMX proxy) and the boolean unboxing
of the null result of invoke in ManagerServlet.isDeployed() and
tryAddServiced().
Since no MBean can ever be registered with this server, the truthful
contract-compliant answer for any operation referencing an object
name is InstanceNotFoundException, and the count of registered MBeans
is zero. Update all such methods accordingly and return 0 from
getMBeanCount. Methods without an InstanceNotFoundException throws
clause that are unreachable through the overridden no-op Registry
methods keep their previous behavior.
Add a unit test covering the previously failing contract paths.
---
.../tomcat/util/modeler/LocalStrings.properties | 2 +
.../tomcat/util/modeler/NoDescriptorRegistry.java | 55 +++++++-----
.../util/modeler/TestNoDescriptorRegistry.java | 100 +++++++++++++++++++++
3 files changed, 137 insertions(+), 20 deletions(-)
diff --git a/java/org/apache/tomcat/util/modeler/LocalStrings.properties b/java/org/apache/tomcat/util/modeler/LocalStrings.properties
index dfbd545fe3..fe3238e3e9 100644
--- a/java/org/apache/tomcat/util/modeler/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/modeler/LocalStrings.properties
@@ -40,6 +40,8 @@ managedMBean.noSet=Cannot find setter method [{0}] on resource [{1}]
modules.digesterParseError=Error parsing registry data
modules.readDescriptorsError=Error reading descriptors
+noJmxMBeanServer.mBeanNotFound=The MBean [{0}] was not found. JMX support is disabled.
+
registry.cannotAccessRegistry=Guard object [{0}] does not allow access to the registry
registry.createdServer=Created MBeanServer
registry.existingServer=Using existing MBeanServer
diff --git a/java/org/apache/tomcat/util/modeler/NoDescriptorRegistry.java b/java/org/apache/tomcat/util/modeler/NoDescriptorRegistry.java
index 10bfce2c2f..e95dc7a23a 100644
--- a/java/org/apache/tomcat/util/modeler/NoDescriptorRegistry.java
+++ b/java/org/apache/tomcat/util/modeler/NoDescriptorRegistry.java
@@ -45,12 +45,16 @@ import javax.management.QueryExp;
import javax.management.ReflectionException;
import javax.management.loading.ClassLoaderRepository;
+import org.apache.tomcat.util.res.StringManager;
+
/**
* An implementation of the MBean registry that effectively disables MBean registration. This is typically used when low
* memory footprint is a primary concern.
*/
public class NoDescriptorRegistry extends Registry {
+ private static final StringManager sm = StringManager.getManager(NoDescriptorRegistry.class);
+
/**
* Constructs a new NoDescriptorRegistry instance.
*/
@@ -149,6 +153,16 @@ public class NoDescriptorRegistry extends Registry {
private static class NoJmxMBeanServer implements MBeanServer {
+ /*
+ * No MBeans can be registered with this server, so any operation that references an object name fails with
+ * InstanceNotFoundException as required by the MBeanServer contract. Callers that relied on the previous null
+ * returns were subject to null pointer exceptions.
+ */
+
+ private static InstanceNotFoundException notFound(ObjectName name) {
+ return new InstanceNotFoundException(sm.getString("noJmxMBeanServer.mBeanNotFound", name));
+ }
+
@Override
public ObjectInstance createMBean(String className, ObjectName name) throws ReflectionException,
InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException {
@@ -189,13 +203,13 @@ public class NoDescriptorRegistry extends Registry {
@Override
public void unregisterMBean(ObjectName name) throws InstanceNotFoundException, MBeanRegistrationException {
-
+ throw notFound(name);
}
@Override
public ObjectInstance getObjectInstance(ObjectName name) throws InstanceNotFoundException {
- return null;
+ throw notFound(name);
}
@@ -219,42 +233,43 @@ public class NoDescriptorRegistry extends Registry {
@Override
public Integer getMBeanCount() {
- return null;
+ // No MBeans are ever registered with this server
+ return Integer.valueOf(0);
}
@Override
public Object getAttribute(ObjectName name, String attribute)
throws MBeanException, AttributeNotFoundException, InstanceNotFoundException, ReflectionException {
- return null;
+ throw notFound(name);
}
@Override
public AttributeList getAttributes(ObjectName name, String[] attributes)
throws InstanceNotFoundException, ReflectionException {
- return null;
+ throw notFound(name);
}
@Override
public void setAttribute(ObjectName name, Attribute attribute) throws InstanceNotFoundException,
AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException {
-
+ throw notFound(name);
}
@Override
public AttributeList setAttributes(ObjectName name, AttributeList attributes)
throws InstanceNotFoundException, ReflectionException {
- return null;
+ throw notFound(name);
}
@Override
public Object invoke(ObjectName name, String operationName, Object[] params, String[] signature)
throws InstanceNotFoundException, MBeanException, ReflectionException {
- return null;
+ throw notFound(name);
}
@@ -273,35 +288,35 @@ public class NoDescriptorRegistry extends Registry {
@Override
public void addNotificationListener(ObjectName name, NotificationListener listener, NotificationFilter filter,
Object handback) throws InstanceNotFoundException {
-
+ throw notFound(name);
}
@Override
public void addNotificationListener(ObjectName name, ObjectName listener, NotificationFilter filter,
Object handback) throws InstanceNotFoundException {
-
+ throw notFound(name);
}
@Override
public void removeNotificationListener(ObjectName name, ObjectName listener)
throws InstanceNotFoundException, ListenerNotFoundException {
-
+ throw notFound(name);
}
@Override
public void removeNotificationListener(ObjectName name, ObjectName listener, NotificationFilter filter,
Object handback) throws InstanceNotFoundException, ListenerNotFoundException {
-
+ throw notFound(name);
}
@Override
public void removeNotificationListener(ObjectName name, NotificationListener listener)
throws InstanceNotFoundException, ListenerNotFoundException {
-
+ throw notFound(name);
}
@@ -309,20 +324,20 @@ public class NoDescriptorRegistry extends Registry {
public void removeNotificationListener(ObjectName name, NotificationListener listener,
NotificationFilter filter, Object handback)
throws InstanceNotFoundException, ListenerNotFoundException {
-
+ throw notFound(name);
}
@Override
public MBeanInfo getMBeanInfo(ObjectName name)
throws InstanceNotFoundException, IntrospectionException, ReflectionException {
- return null;
+ throw notFound(name);
}
@Override
public boolean isInstanceOf(ObjectName name, String className) throws InstanceNotFoundException {
- return false;
+ throw notFound(name);
}
@@ -356,7 +371,7 @@ public class NoDescriptorRegistry extends Registry {
@Override
public ObjectInputStream deserialize(ObjectName name, byte[] data)
throws InstanceNotFoundException, OperationsException {
- return null;
+ throw notFound(name);
}
@@ -370,19 +385,19 @@ public class NoDescriptorRegistry extends Registry {
@Override
public ObjectInputStream deserialize(String className, ObjectName loaderName, byte[] data)
throws InstanceNotFoundException, OperationsException, ReflectionException {
- return null;
+ throw notFound(loaderName);
}
@Override
public ClassLoader getClassLoaderFor(ObjectName mbeanName) throws InstanceNotFoundException {
- return null;
+ throw notFound(mbeanName);
}
@Override
public ClassLoader getClassLoader(ObjectName loaderName) throws InstanceNotFoundException {
- return null;
+ throw notFound(loaderName);
}
diff --git a/test/org/apache/tomcat/util/modeler/TestNoDescriptorRegistry.java b/test/org/apache/tomcat/util/modeler/TestNoDescriptorRegistry.java
new file mode 100644
index 0000000000..5bdca9987b
--- /dev/null
+++ b/test/org/apache/tomcat/util/modeler/TestNoDescriptorRegistry.java
@@ -0,0 +1,100 @@
+/*
+ * 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.modeler;
+
+import java.util.Collections;
+
+import javax.management.InstanceNotFoundException;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestNoDescriptorRegistry {
+
+ private static final ObjectName ONAME;
+
+ static {
+ try {
+ ONAME = new ObjectName("Tomcat:type=Tester,name=NoJmx");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void testGetMBeanInfoThrows() throws Exception {
+ MBeanServer server = new NoDescriptorRegistry().getMBeanServer();
+ Assert.assertThrows(InstanceNotFoundException.class, () -> server.getMBeanInfo(ONAME));
+ }
+
+ @Test
+ public void testInvokeThrows() throws Exception {
+ MBeanServer server = new NoDescriptorRegistry().getMBeanServer();
+ Assert.assertThrows(InstanceNotFoundException.class,
+ () -> server.invoke(ONAME, "isDeployed", new Object[] { "ctx" }, new String[] { "java.lang.String" }));
+ }
+
+ @Test
+ public void testGetAttributeThrows() throws Exception {
+ MBeanServer server = new NoDescriptorRegistry().getMBeanServer();
+ Assert.assertThrows(InstanceNotFoundException.class, () -> server.getAttribute(ONAME, "name"));
+ }
+
+ @Test
+ public void testGetObjectInstanceThrows() throws Exception {
+ MBeanServer server = new NoDescriptorRegistry().getMBeanServer();
+ Assert.assertThrows(InstanceNotFoundException.class, () -> server.getObjectInstance(ONAME));
+ }
+
+ @Test
+ public void testRegistryGetMethodInfoThrows() throws Exception {
+ Registry registry = new NoDescriptorRegistry();
+ Assert.assertThrows(InstanceNotFoundException.class, () -> registry.getMethodInfo(ONAME, "test", 0));
+ }
+
+ @Test
+ public void testRegistryGetTypeInfoReturnsNull() throws Exception {
+ // Overridden by NoDescriptorRegistry, must not throw or fail
+ Registry registry = new NoDescriptorRegistry();
+ Assert.assertNull(registry.getType(ONAME, "name"));
+ Assert.assertNull(registry.getMethodInfo(ONAME, "test"));
+ }
+
+ @Test
+ public void testCountsAndQueries() throws Exception {
+ MBeanServer server = new NoDescriptorRegistry().getMBeanServer();
+ Assert.assertEquals(Integer.valueOf(0), server.getMBeanCount());
+ Assert.assertFalse(server.isRegistered(ONAME));
+ Assert.assertTrue(server.queryNames(null, null).isEmpty());
+ Assert.assertTrue(server.queryMBeans(null, null).isEmpty());
+ Assert.assertEquals(0, server.getDomains().length);
+ }
+
+ @Test
+ public void testUnregisterIsSilentlyIgnoredViaRegistry() throws Exception {
+ // unregisterComponent is a no-op for NoDescriptorRegistry and must not
+ // reach the MBeanServer methods that now throw
+ Registry registry = new NoDescriptorRegistry();
+ registry.unregisterComponent(ONAME);
+ registry.unregisterComponent(ONAME.getCanonicalName());
+ registry.registerComponent(new Object(), ONAME, null);
+ registry.registerComponent(new Object(), ONAME.getCanonicalName(), null);
+ registry.invoke(Collections.emptyList(), "test", false);
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 13/23: Quote ObjectName values containing a quote or a newline
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 3881d57e0bf501322164cd6d584610aee55ed66b
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 11:55:30 2026 +0200
Quote ObjectName values containing a quote or a newline
The JMX parser rejects unquoted property values containing a double
quote or a line feed, so such values must be passed through
ObjectName.quote() before being concatenated into an ObjectName
string. Util.objectNameValueNeedsQuote() listed only comma, equals,
colon and the wildcards, so servlet, filter and upgrade protocol
names containing a double quote bypassed the quoting and the
subsequent ObjectName construction failed. The call sites catch that
failure and log a warning, silently leaving the component without a
JMX registration.
Add the double quote and the line feed to the character check. A
sweep of the value position of a key property confirms these are the
only characters, besides the ones already listed, that the parser
rejects in an unquoted value.
---
java/org/apache/tomcat/util/modeler/Util.java | 2 +-
test/org/apache/tomcat/util/modeler/TestUtil.java | 54 +++++++++++++++++++++++
2 files changed, 55 insertions(+), 1 deletion(-)
diff --git a/java/org/apache/tomcat/util/modeler/Util.java b/java/org/apache/tomcat/util/modeler/Util.java
index 13a313f9de..886ad0bb95 100644
--- a/java/org/apache/tomcat/util/modeler/Util.java
+++ b/java/org/apache/tomcat/util/modeler/Util.java
@@ -35,7 +35,7 @@ public class Util {
public static boolean objectNameValueNeedsQuote(String input) {
for (int i = 0; i < input.length(); i++) {
char ch = input.charAt(i);
- if (ch == ',' || ch == '=' || ch == ':' || ch == '*' || ch == '?') {
+ if (ch == ',' || ch == '=' || ch == ':' || ch == '*' || ch == '?' || ch == '"' || ch == '\n') {
return true;
}
}
diff --git a/test/org/apache/tomcat/util/modeler/TestUtil.java b/test/org/apache/tomcat/util/modeler/TestUtil.java
new file mode 100644
index 0000000000..8a82701031
--- /dev/null
+++ b/test/org/apache/tomcat/util/modeler/TestUtil.java
@@ -0,0 +1,54 @@
+/*
+ * 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.modeler;
+
+import javax.management.ObjectName;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestUtil {
+
+ @Test
+ public void testObjectNameValueNeedsQuote() {
+ Assert.assertFalse(Util.objectNameValueNeedsQuote("plain"));
+ Assert.assertFalse(Util.objectNameValueNeedsQuote(""));
+
+ Assert.assertTrue(Util.objectNameValueNeedsQuote("a,b"));
+ Assert.assertTrue(Util.objectNameValueNeedsQuote("a=b"));
+ Assert.assertTrue(Util.objectNameValueNeedsQuote("a:b"));
+ Assert.assertTrue(Util.objectNameValueNeedsQuote("a*b"));
+ Assert.assertTrue(Util.objectNameValueNeedsQuote("a?b"));
+ Assert.assertTrue(Util.objectNameValueNeedsQuote("a\"b"));
+ Assert.assertTrue(Util.objectNameValueNeedsQuote("a\nb"));
+ }
+
+ @Test
+ public void testObjectNameValueWithQuoteParsesWhenQuoted() throws Exception {
+ String[] values = { "a\"b", "a\nb", "a,\"b\n:c" };
+
+ for (String value : values) {
+ Assert.assertTrue(Util.objectNameValueNeedsQuote(value));
+
+ String onameStr = "tester:type=Tester,name=" + ObjectName.quote(value);
+ ObjectName oname = new ObjectName(onameStr);
+
+ // A quoted value is retained with its quoting in the key properties
+ Assert.assertEquals(ObjectName.quote(value), oname.getKeyPropertyList().get("name"));
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 17/23: Fix ArrayIndexOutOfBoundsException in IPv6Utils.canonize()
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 033db11ff73fdb3920b3b239540e8f8d3c3f37d7
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 13:19:04 2026 +0200
Fix ArrayIndexOutOfBoundsException in IPv6Utils.canonize()
Inputs that pass the mayBeIPv6Address() heuristic but contain more than
8 colon-separated groups (e.g. "1:2:3:4:5:6:7:8:9" or forms with more
than two consecutive colons), or groups with more than 4 significant
characters (e.g. "12345::"), indexed the fixed-size group store out of
bounds and threw an undocumented ArrayIndexOutOfBoundsException. With
the non-default access log attribute ipv6Canonical="true", such values
(e.g. from a client-supplied Host header via the %v element) caused
loss of the access log record plus a warning per request. canonize()
now returns such malformed inputs unchanged, consistent with how
non-IPv6 input is already passed through. Add tests for the
pass-through behaviour and for the valid 8-colon forms
("1:2:3:4:5:6:7::") whose canonicalization must be preserved.
---
java/org/apache/tomcat/util/net/IPv6Utils.java | 20 ++++++++++++++
test/org/apache/tomcat/util/net/TestIPv6Utils.java | 31 ++++++++++++++++++++++
2 files changed, 51 insertions(+)
diff --git a/java/org/apache/tomcat/util/net/IPv6Utils.java b/java/org/apache/tomcat/util/net/IPv6Utils.java
index 8e82d1320c..4b4cb50142 100644
--- a/java/org/apache/tomcat/util/net/IPv6Utils.java
+++ b/java/org/apache/tomcat/util/net/IPv6Utils.java
@@ -90,6 +90,20 @@ public class IPv6Utils {
ipv6AddressLength = ipv6Address.lastIndexOf('%');
}
+ // A valid IPv6 address contains at most 8 colons: 7 separate 8 explicit groups, and 8 are only reached
+ // when "::" compresses a single zero group next to 7 explicit groups (RFC 4291 allows "::" to stand for
+ // one or more zero groups). Inputs with more colons than that are not valid IPv6 addresses but the
+ // mayBeIPv6Address() heuristic above does not exclude them. Return such inputs unchanged.
+ int colonCount = 0;
+ for (int i = 0; i < ipv6AddressLength; i++) {
+ if (ipv6Address.charAt(i) == ':') {
+ colonCount++;
+ }
+ }
+ if (colonCount > MAX_NUMBER_OF_GROUPS) {
+ return ipv6Address;
+ }
+
StringBuilder result = new StringBuilder();
char[][] groups = new char[MAX_NUMBER_OF_GROUPS][MAX_GROUP_LENGTH];
int groupCounter = 0;
@@ -151,6 +165,12 @@ public class IPv6Utils {
c = (char) (c + 32);
}
if (c != ':') {
+ if (groupCounter >= MAX_NUMBER_OF_GROUPS || charInGroupCounter >= MAX_GROUP_LENGTH) {
+ // A ninth group with content, or a group with more than 4 significant characters. Neither is a
+ // valid IPv6 address but the mayBeIPv6Address() heuristic above does not exclude them. Return
+ // the input unchanged.
+ return ipv6Address;
+ }
groups[groupCounter][charInGroupCounter] = c;
if (!(groupStart && c == '0')) {
++charInGroupCounter;
diff --git a/test/org/apache/tomcat/util/net/TestIPv6Utils.java b/test/org/apache/tomcat/util/net/TestIPv6Utils.java
index 5d667ed67b..fd909bb9c8 100644
--- a/test/org/apache/tomcat/util/net/TestIPv6Utils.java
+++ b/test/org/apache/tomcat/util/net/TestIPv6Utils.java
@@ -114,6 +114,12 @@ public class TestIPv6Utils {
// Already ends with "::"
Assert.assertEquals("2001:db8::", IPv6Utils.canonize("2001:db8::"));
+ // "::" at the start or end standing for a single zero group next to 7 explicit groups (8 colons)
+ Assert.assertEquals("1:2:3:4:5:6:7:0", IPv6Utils.canonize("1:2:3:4:5:6:7::"));
+ Assert.assertEquals("0:2:3:4:5:6:7:8", IPv6Utils.canonize("::2:3:4:5:6:7:8"));
+ Assert.assertEquals("::", IPv6Utils.canonize("0:0:0:0:0:0:0::"));
+ Assert.assertEquals("1:2:3:4:5:6:7:0%eth0", IPv6Utils.canonize("1:2:3:4:5:6:7::%eth0"));
+
// IPv4 inside IPv6
Assert.assertEquals("::ffff:192.0.2.1", IPv6Utils.canonize("::ffff:192.0.2.1"));
Assert.assertEquals("::ffff:192.0.2.1", IPv6Utils.canonize("0:0:0:0:0:ffff:192.0.2.1"));
@@ -141,4 +147,29 @@ public class TestIPv6Utils {
Assert.assertEquals("www.apache.org", IPv6Utils.canonize("www.apache.org"));
Assert.assertEquals("ipv6.google.com", IPv6Utils.canonize("ipv6.google.com"));
}
+
+ @Test
+ public void testCanonizeTooManyGroups() {
+ // Strings with more than 8 groups pass the mayBeIPv6Address() heuristic but are not valid IPv6
+ // addresses. They used to trigger an ArrayIndexOutOfBoundsException. The input is now returned
+ // unchanged.
+ Assert.assertEquals("1:2:3:4:5:6:7:8:9", IPv6Utils.canonize("1:2:3:4:5:6:7:8:9"));
+ Assert.assertEquals("1:2:3:4:5:6:7::8:9", IPv6Utils.canonize("1:2:3:4:5:6:7::8:9"));
+ Assert.assertEquals("1:2:3:4:5:6:7:8:9:10", IPv6Utils.canonize("1:2:3:4:5:6:7:8:9:10"));
+ Assert.assertEquals("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
+ IPv6Utils.canonize("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+ Assert.assertEquals("1:2:3:4:5:6:7:8:9%1", IPv6Utils.canonize("1:2:3:4:5:6:7:8:9%1"));
+ // Additional empty groups created by more than two consecutive colons
+ Assert.assertEquals("1:2:3:4:5:6:7:8:::", IPv6Utils.canonize("1:2:3:4:5:6:7:8:::"));
+ Assert.assertEquals("::::a:::::", IPv6Utils.canonize("::::a:::::"));
+ }
+
+ @Test
+ public void testCanonizeGroupTooLong() {
+ // A group with more than 4 significant hex digits passes the mayBeIPv6Address() heuristic but is
+ // not valid IPv6. It used to trigger an ArrayIndexOutOfBoundsException. The input is now returned
+ // unchanged.
+ Assert.assertEquals("12345:0:0:0:0:0:0:0", IPv6Utils.canonize("12345:0:0:0:0:0:0:0"));
+ Assert.assertEquals("12345::", IPv6Utils.canonize("12345::"));
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 10/23: Close the descriptor stream in the digester modeler source
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit dc7f2a9843091366781cdea2cbed4fb5fb51adb4
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 11:33:42 2026 +0200
Close the descriptor stream in the digester modeler source
Registry.load() opens an input stream for URL and File sources and
hands it to the modeler source without retaining any reference, so
the stream had to be closed by the source that consumed it.
MbeansDescriptorsDigesterSource.execute() previously cast the source
to InputStream and parsed it without ever closing it, relying on the
underlying SAX parser to release the stream. That left the release of
the resource to unspecified parser behaviour.
Validate the source with an instanceof check instead of a blind cast,
throwing IllegalArgumentException for anything other than an
InputStream, and parse inside a try-with-resources statement so the
stream is closed when the parse completes, when it fails and when an
unchecked error occurs.
Add unit tests that pass a close-tracking input stream through
Registry.load() and assert the stream is closed after both a
successful parse and a parse error, and that a non-InputStream source
is rejected with IllegalArgumentException.
---
.../modules/MbeansDescriptorsDigesterSource.java | 6 +-
.../TestMbeansDescriptorsDigesterSource.java | 90 ++++++++++++++++++++++
2 files changed, 94 insertions(+), 2 deletions(-)
diff --git a/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java b/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java
index 746a9fed53..9e271a7f2d 100644
--- a/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java
+++ b/java/org/apache/tomcat/util/modeler/modules/MbeansDescriptorsDigesterSource.java
@@ -142,7 +142,9 @@ public class MbeansDescriptorsDigesterSource extends ModelerSource {
registry = Registry.getRegistry(null);
}
- InputStream stream = (InputStream) source;
+ if (!(source instanceof InputStream stream)) {
+ throw new IllegalArgumentException(sm.getString("registry.invalidSource"));
+ }
List<ManagedBean> loadedMbeans = new ArrayList<>();
synchronized (dLock) {
@@ -151,7 +153,7 @@ public class MbeansDescriptorsDigesterSource extends ModelerSource {
}
// Process the input file to configure our registry
- try {
+ try (stream) {
// Push our registry object onto the stack
digester.push(loadedMbeans);
digester.parse(stream);
diff --git a/test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsDigesterSource.java b/test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsDigesterSource.java
new file mode 100644
index 0000000000..c512cdf2fe
--- /dev/null
+++ b/test/org/apache/tomcat/util/modeler/modules/TestMbeansDescriptorsDigesterSource.java
@@ -0,0 +1,90 @@
+/*
+ * 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.modeler.modules;
+
+import java.io.ByteArrayInputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.tomcat.util.modeler.Registry;
+
+public class TestMbeansDescriptorsDigesterSource {
+
+ private static final String XML =
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<mbeans-descriptors>\n" +
+ " <mbean name=\"TestDigesterSourceBean\" description=\"Tester bean\" domain=\"tester\">\n" +
+ " <attribute name=\"value\" description=\"Tester value\" type=\"java.lang.String\"/>\n" +
+ " </mbean>\n</mbeans-descriptors>\n";
+
+ private static final String BAD_XML = "<mbeans-descriptors><invalid>";
+
+ private static InputStream createTrackingStream(String content, AtomicBoolean closed) {
+ InputStream delegate = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+ return new FilterInputStream(delegate) {
+ @Override
+ public void close() throws IOException {
+ closed.set(true);
+ super.close();
+ }
+ };
+ }
+
+ @Test
+ public void testLoadClosesStreamAfterSuccessfulParse() throws Exception {
+ AtomicBoolean closed = new AtomicBoolean();
+ Registry registry = Registry.getRegistry(null);
+
+ registry.load(null, createTrackingStream(XML, closed), null);
+
+ Assert.assertTrue(closed.get());
+ Assert.assertNotNull(registry.findManagedBean("TestDigesterSourceBean"));
+ }
+
+ @Test
+ public void testLoadClosesStreamAfterParseError() throws Exception {
+ AtomicBoolean closed = new AtomicBoolean();
+ Registry registry = Registry.getRegistry(null);
+
+ try {
+ registry.load(null, createTrackingStream(BAD_XML, closed), null);
+ Assert.fail("Expected a parse error");
+ } catch (Exception e) {
+ // Expected
+ }
+
+ Assert.assertTrue(closed.get());
+ }
+
+ @Test
+ public void testExecuteRequiresInputStreamSource() throws Exception {
+ Registry registry = Registry.getRegistry(null);
+ ModelerSource source = new MbeansDescriptorsDigesterSource();
+
+ try {
+ source.loadDescriptors(registry, "tester", "not-an-input-stream");
+ Assert.fail("Expected an IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 21/23: Fix the PEM output of PEMFile.toPEM()
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 3e176e4d2923a2e2413c1cb0112ab912fd50ca2e
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 15:07:26 2026 +0200
Fix the PEM output of PEMFile.toPEM()
The encoded text was written with the final line of the Base64 data joined
onto the end boundary and without a line separator after the end boundary.
The result was not valid PEM: OpenSSL was unable to decode it
("Could not find or decode certificate") and Tomcat's own PEM parser did not
see any certificate in it, since the end boundary never started a line.
Write each boundary on a line of its own, terminate the encoded text with a
line separator and encode with the line length of 64 characters that RFC
7468 requires. The Base64 line separator is now the same as the one used for
the boundaries, so the output no longer mixes line separators when the JVM
uses a line feed. The output of the method now has the same format as the one
produced by keytool -exportcert -rfc for the same certificate, apart from the
line separator, which on a JVM using a line feed will be that separator
rather than the carriage return line feed used by keytool.
These values are used for the mod_rewrite compatible SSL_SERVER_CERT,
SSL_CLIENT_CERT and CERT_CHAIN variables, which are expected to contain a
full PEM encoded certificate as Apache HTTP Server's mod_ssl provides them.
Tests: testToPem encodes the certificate in the new certificate.pem test
file and checks the resulting structure as well as that the result can be
read back as PEM. It previously failed because the end boundary was not on a
line of its own.
---
java/org/apache/tomcat/util/net/jsse/PEMFile.java | 13 ++++--
.../apache/tomcat/util/net/jsse/TestPEMFile.java | 49 ++++++++++++++++++++++
.../apache/tomcat/util/net/jsse/certificate.pem | 32 ++++++++++++++
3 files changed, 91 insertions(+), 3 deletions(-)
diff --git a/java/org/apache/tomcat/util/net/jsse/PEMFile.java b/java/org/apache/tomcat/util/net/jsse/PEMFile.java
index ec45a7b34c..f9023bd885 100644
--- a/java/org/apache/tomcat/util/net/jsse/PEMFile.java
+++ b/java/org/apache/tomcat/util/net/jsse/PEMFile.java
@@ -120,9 +120,16 @@ public class PEMFile {
* @throws CertificateEncodingException If the certificate cannot be encoded
*/
public static String toPEM(X509Certificate certificate) throws CertificateEncodingException {
- return Part.BEGIN_BOUNDARY + Part.CERTIFICATE + Part.FINISH_BOUNDARY + System.lineSeparator() +
- Base64.getMimeEncoder().encodeToString(certificate.getEncoded()) + Part.END_BOUNDARY +
- Part.CERTIFICATE + Part.FINISH_BOUNDARY;
+ // RFC 7468 requires encoded lines of 64 characters, a line separator
+ // after each line and a line separator after the end boundary. Use the
+ // same line separator here as for the boundaries.
+ String lineSeparator = System.lineSeparator();
+ Base64.Encoder encoder =
+ Base64.getMimeEncoder(64, lineSeparator.getBytes(StandardCharsets.US_ASCII));
+
+ return Part.BEGIN_BOUNDARY + Part.CERTIFICATE + Part.FINISH_BOUNDARY + lineSeparator +
+ encoder.encodeToString(certificate.getEncoded()) + lineSeparator + Part.END_BOUNDARY +
+ Part.CERTIFICATE + Part.FINISH_BOUNDARY + lineSeparator;
}
private final List<X509Certificate> certificates = new ArrayList<>();
diff --git a/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
index d5f9b29b35..f7b0665abc 100644
--- a/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
+++ b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
@@ -16,9 +16,12 @@
*/
package org.apache.tomcat.util.net.jsse;
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -44,6 +47,15 @@ public class TestPEMFile {
private static final String KEY_ENCRYPTED_PKCS8_HMACSHA256_AES_256_CBC = "key-encrypted-pkcs8-hmacsha256-aes-256-cbc.pem";
private static final String KEY_ENCRYPTED_PKCS8_HMACSHA256_DES_EDE3_CBC = "key-encrypted-pkcs8-hmacsha256-des-ede3-cbc.pem";
+ /*
+ * A clean single certificate PEM. Generated with:
+ * keytool -exportcert -rfc -keystore test/localhost-rsa.jks
+ * -storepass changeit -alias tomcat
+ * with the line endings converted from CRLF to LF. The certificate itself
+ * expires on 2028-08-11.
+ */
+ private static final String CERTIFICATE = "certificate.pem";
+
@Parameterized.Parameters
public static Collection<Object[]> parameters() {
List<Object[]> parameterSets = new ArrayList<>();
@@ -117,6 +129,43 @@ public class TestPEMFile {
}
+ @Test
+ public void testToPem() throws Exception {
+ PEMFile pemFile = new PEMFile(getPath(CERTIFICATE));
+ Assert.assertEquals(1, pemFile.getCertificates().size());
+ X509Certificate certificate = pemFile.getCertificates().get(0);
+
+ String pem = PEMFile.toPEM(certificate);
+
+ // RFC 7468: each boundary is on a line of its own and the encoded text
+ // ends with a line separator
+ String[] lines = pem.split(System.lineSeparator());
+ Assert.assertEquals("-----BEGIN CERTIFICATE-----", lines[0]);
+ Assert.assertEquals("-----END CERTIFICATE-----", lines[lines.length - 1]);
+ Assert.assertTrue("Missing line separator at end of: " + pem, pem.endsWith(System.lineSeparator()));
+
+ // The same line separator is used throughout the encoded text and no
+ // line is longer than the 64 characters RFC 7468 requires
+ String separatorsRemoved = pem.replace(System.lineSeparator(), "");
+ Assert.assertFalse("Unexpected line separator in: " + pem, separatorsRemoved.contains("\r"));
+ Assert.assertFalse("Unexpected line separator in: " + pem, separatorsRemoved.contains("\n"));
+ for (String line : lines) {
+ Assert.assertTrue("Long line: " + line, line.length() <= 64 || line.startsWith("-----"));
+ }
+ // RFC 7468 requires all the lines of encoded text but the last one to
+ // contain exactly 64 characters
+ for (int i = 1; i < lines.length - 2; i++) {
+ Assert.assertEquals("Short line: " + lines[i], 64, lines[i].length());
+ }
+
+ // The encoded text must be readable as PEM
+ PEMFile roundTrip =
+ new PEMFile("toPEM", new ByteArrayInputStream(pem.getBytes(StandardCharsets.US_ASCII)), null, null);
+ Assert.assertEquals(1, roundTrip.getCertificates().size());
+ Assert.assertEquals(certificate, roundTrip.getCertificates().get(0));
+ }
+
+
private void testKeyEncrypted(String file) throws Exception {
testKey(file, password, passwordFile);
}
diff --git a/test/org/apache/tomcat/util/net/jsse/certificate.pem b/test/org/apache/tomcat/util/net/jsse/certificate.pem
new file mode 100644
index 0000000000..69a5c5241b
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/jsse/certificate.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFiTCCA3GgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
+MQswCQYDVQQIEwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUGA1UEChMeVGhl
+IEFwYWNoZSBTb2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFBcGFjaGUgVG9t
+Y2F0IFBNQzEeMBwGA1UEAxMVQXBhY2hlIFRvbWNhdCBUZXN0IENBMB4XDTI2MDgx
+MjEzMTkxOFoXDTI4MDgxMTEzMTkxOFowgYgxCzAJBgNVBAYTAlVTMQswCQYDVQQI
+EwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUGA1UEChMeVGhlIEFwYWNoZSBT
+b2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFBcGFjaGUgVG9tY2F0IFBNQzES
+MBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAiGhGjxWRZ+1wz9O/FXKUJE9htc2EFul6UopXbz2k5Is2IO/QVO6XbWJRpqEY
+fAIfnccWSoNwhV5bOMPJBZgG2skNWF43aG3PI3t+Ppklblp6VsJJeWc5Z14TY+j4
+95W8gUSTDO8c9Fd4ylJQHAjVEbwZnQuf4fKoGgBYkxO2XYy/4Udkc6TTpDduPAHJ
+BsgXNenAFO1Doa1Uj1SMGcKcuYJHEFDrQPT57hC+ibxBYNWeKkGZ5xrvppMqZ3lE
+vtGPhJVK+zqDFssyiyoypgCxYLpOO5a7Llc21dxWeYK4tCl0OFz9B7FEv4T6pVLv
+2L7gKV00PuOUR2DMrfjBTNWK0wIDAQABo4HuMIHrMAkGA1UdEwQCMAAwLAYJYIZI
+AYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQW
+BBTk9ApBBF0sQtd97iqARo4OsmLmSjAfBgNVHSMEGDAWgBTbdmYmmMu/aTGZL5Vs
+nsJZPF1NjDBUBggrBgEFBQcBAQRIMEYwIQYIKwYBBQUHMAGGFWh0dHA6Ly8xMjcu
+MC4wLjE6ODg4ODAhBggrBgEFBQcwAYYVaHR0cDovLzEyNy4wLjAuMTo4ODg5MBoG
+A1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAgEAL8sm
+Gza6GgAv06lxYQN6545uT19XyZS12urav5wQI+PLlEjAT/hfbbGOtheJshoUmeLK
+FqDipW+tRLDtdlYCdi4hpljtr3L5xXB1qnfmxTOfjMTzxkLiYtL+fkwY2rL/gTNU
+Eem+1xJeIf78WmbzTk0cI6EoIPi8RzT0/w78sJH3s4j1HQnnfA6A5hMaJ4lIaqo/
+fRsiI18i9XVnE6R+s0hzzVaxQVrTJMxBwvWhJRC77hDECrwtOg2Zq2VjAsVGWcEW
+ayHvvPA8Q3ZmNCsD6p2+9VnDpLhKd/UNfl726Y8t573BfWa+TX+wHp0VkNEama0P
+Pr4WbsitG92sZJQP9Y5OO9I2WH6re00gvXCicdXFH+Za7XfzXODCzHxAYMU0+nwB
+c1zXa0AbLzq2aI5vyh/eT2nLQSEYQhDV+DrYRGImPERPATzRrSgTGzny6aBE62v5
+9hdokliY50z1nSqG+dXCkZSPmdeDmPOUPdglZUBZKTh35qD4kE8lRqoQEmNaYX76
+jBvWD18UFI8imYmCrNTXz0YDy50iGxrdESANmLpi+vFd+3yqUVr163ab3iaTHWF4
+dfRuPYDCJrccZG9x1aKJ+5Fqkq+SJHut5jVFPl3PGdwgowrr/TJw/8J2I3huLS0G
+Uc+/fKyTIr2fPvGdmXr9RA6CM3yfsMcPdwtqPeY=
+-----END CERTIFICATE-----
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 04/23: Fix name-scoped removal of attribute change notification listeners
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit aed989cbdca764913fd125ce11d6fbdb5461ba6b
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 10:12:47 2026 +0200
Fix name-scoped removal of attribute change notification listeners
BaseModelMBean.removeAttributeChangeNotificationListener() previously
removed every attribute change registration for the given listener
instead of only the registration for the requested attribute name
(the long-standing FIXME in the method).
Add a removal method to BaseNotificationBroadcaster that is scoped to
an attribute name. Since addNotificationListener() coalesces
registrations with the same listener and handback into a single entry
with a multi-name BaseAttributeFilter, removal drops the name from the
filter and only removes the entry when no explicit name remains, so a
single-name filter is never silently turned into an accept-all filter.
Registrations that accept all attribute names (created with a null
attribute name) can only be removed by passing a null name, mirroring
the registration semantics. Entries registered with other filters are
left untouched.
Also add a unit test covering the coalesced, per-handback, accept-all
and non-attribute-filter cases.
---
.../apache/tomcat/util/modeler/BaseModelMBean.java | 7 +-
.../util/modeler/BaseNotificationBroadcaster.java | 38 ++++++
.../modeler/TestBaseModelMBeanNotifications.java | 141 +++++++++++++++++++++
3 files changed, 184 insertions(+), 2 deletions(-)
diff --git a/java/org/apache/tomcat/util/modeler/BaseModelMBean.java b/java/org/apache/tomcat/util/modeler/BaseModelMBean.java
index ad5566cb6e..d1f09b57ae 100644
--- a/java/org/apache/tomcat/util/modeler/BaseModelMBean.java
+++ b/java/org/apache/tomcat/util/modeler/BaseModelMBean.java
@@ -517,9 +517,12 @@ public class BaseModelMBean implements DynamicMBean, MBeanRegistration, ModelMBe
throw new IllegalArgumentException(sm.getString("baseModelMBean.nullListener"));
}
- // FIXME - currently this removes *all* notifications for this listener
+ if (log.isTraceEnabled()) {
+ log.trace("removeAttributeNotificationListener " + listener);
+ }
+
if (attributeBroadcaster != null) {
- attributeBroadcaster.removeNotificationListener(listener);
+ attributeBroadcaster.removeAttributeNotificationListener(listener, name);
}
}
diff --git a/java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java b/java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java
index ac287c85dd..8e58b976d8 100644
--- a/java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java
+++ b/java/org/apache/tomcat/util/modeler/BaseNotificationBroadcaster.java
@@ -18,6 +18,7 @@ package org.apache.tomcat.util.modeler;
import java.util.ArrayList;
+import java.util.Iterator;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanNotificationInfo;
@@ -124,6 +125,43 @@ public class BaseNotificationBroadcaster implements NotificationBroadcaster {
}
+ /**
+ * Remove an attribute change notification event listener for a specific attribute name from this MBean. Other
+ * registrations of the same listener are left untouched. If a coalesced filter ends up accepting no explicit
+ * names, the corresponding entry is removed entirely rather than being turned into an accept-all filter. A
+ * registration that accepts all attribute names (a filter with no explicit names, created when the listener was
+ * added with a <code>null</code> attribute name) can only be removed by passing a <code>null</code> name here.
+ *
+ * @param listener The listener to be removed
+ * @param name The attribute name for which the registration should be removed, or <code>null</code> to remove
+ * a registration that accepts all attribute names
+ */
+ public void removeAttributeNotificationListener(NotificationListener listener, String name) {
+
+ synchronized (entries) {
+ Iterator<BaseNotificationBroadcasterEntry> iterator = entries.iterator();
+ while (iterator.hasNext()) {
+ BaseNotificationBroadcasterEntry item = iterator.next();
+ if (item.listener != listener || !(item.filter instanceof BaseAttributeFilter filter)) {
+ continue;
+ }
+ String[] names = filter.getNames();
+ if (name == null) {
+ if (names.length == 0) {
+ // The accept-all registration
+ iterator.remove();
+ }
+ } else if (names.length == 1 && names[0].equals(name)) {
+ iterator.remove();
+ } else {
+ filter.removeAttribute(name);
+ }
+ }
+ }
+
+ }
+
+
/**
* Send the specified notification to all interested listeners.
*
diff --git a/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java b/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
new file mode 100644
index 0000000000..4d92b2c934
--- /dev/null
+++ b/test/org/apache/tomcat/util/modeler/TestBaseModelMBeanNotifications.java
@@ -0,0 +1,141 @@
+/*
+ * 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.modeler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.management.Attribute;
+import javax.management.AttributeChangeNotification;
+import javax.management.Notification;
+import javax.management.NotificationFilter;
+import javax.management.NotificationListener;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestBaseModelMBeanNotifications {
+
+ private static final class TesterNotificationListener implements NotificationListener {
+
+ private final List<String> received = new ArrayList<>();
+
+ @Override
+ public void handleNotification(Notification notification, Object handback) {
+ String name = notification instanceof AttributeChangeNotification ?
+ ((AttributeChangeNotification) notification).getAttributeName() : notification.getType();
+ received.add(name + ":" + handback);
+ }
+ }
+
+ private static void send(BaseModelMBean mbean, String attributeName) throws Exception {
+ mbean.sendAttributeChangeNotification(new Attribute(attributeName, "old"),
+ new Attribute(attributeName, "new"));
+ }
+
+ @Test
+ public void testCoalescedRemovalKeepsOtherNames() throws Exception {
+ BaseModelMBean mbean = new BaseModelMBean();
+ TesterNotificationListener listener = new TesterNotificationListener();
+ // Same listener and handback: these registrations are coalesced into a
+ // single entry with a filter accepting both names
+ mbean.addAttributeChangeNotificationListener(listener, "A", "hb");
+ mbean.addAttributeChangeNotificationListener(listener, "B", "hb");
+ mbean.removeAttributeChangeNotificationListener(listener, "A");
+ send(mbean, "A");
+ send(mbean, "B");
+ Assert.assertEquals(List.of("B:hb"), listener.received);
+ }
+
+ @Test
+ public void testSingleNameRemovalDoesNotAcceptAll() throws Exception {
+ BaseModelMBean mbean = new BaseModelMBean();
+ TesterNotificationListener listener = new TesterNotificationListener();
+ mbean.addAttributeChangeNotificationListener(listener, "A", "hb");
+ mbean.removeAttributeChangeNotificationListener(listener, "A");
+ // Removing the only accepted name must remove the entry entirely rather
+ // than turning the (now empty) filter into an accept-all filter
+ send(mbean, "A");
+ send(mbean, "B");
+ Assert.assertTrue(listener.received.isEmpty());
+ }
+
+ @Test
+ public void testSeparateHandbacksRemovedIndependently() throws Exception {
+ BaseModelMBean mbean = new BaseModelMBean();
+ TesterNotificationListener listener = new TesterNotificationListener();
+ mbean.addAttributeChangeNotificationListener(listener, "A", "hb1");
+ mbean.addAttributeChangeNotificationListener(listener, "B", "hb2");
+ mbean.removeAttributeChangeNotificationListener(listener, "A");
+ send(mbean, "A");
+ send(mbean, "B");
+ Assert.assertEquals(List.of("B:hb2"), listener.received);
+ }
+
+ @Test
+ public void testAcceptAllRemoval() throws Exception {
+ BaseModelMBean mbean = new BaseModelMBean();
+ TesterNotificationListener listener = new TesterNotificationListener();
+ // A null attribute name registers an accept-all listener
+ mbean.addAttributeChangeNotificationListener(listener, null, "hb");
+ // A specific name can not be removed from an accept-all registration
+ mbean.removeAttributeChangeNotificationListener(listener, "A");
+ send(mbean, "A");
+ Assert.assertEquals(List.of("A:hb"), listener.received);
+ // The accept-all registration is removed by a null name
+ mbean.removeAttributeChangeNotificationListener(listener, null);
+ listener.received.clear();
+ send(mbean, "A");
+ Assert.assertTrue(listener.received.isEmpty());
+ }
+
+ @Test
+ public void testUnknownNameRemovalIsNoOp() throws Exception {
+ BaseModelMBean mbean = new BaseModelMBean();
+ TesterNotificationListener listener = new TesterNotificationListener();
+ mbean.addAttributeChangeNotificationListener(listener, "A", "hb");
+ mbean.removeAttributeChangeNotificationListener(listener, "Z");
+ send(mbean, "A");
+ Assert.assertEquals(List.of("A:hb"), listener.received);
+ }
+
+ @Test
+ public void testOtherListenerRemovalDoesNotAffectThisListener() throws Exception {
+ BaseModelMBean mbean = new BaseModelMBean();
+ TesterNotificationListener listener1 = new TesterNotificationListener();
+ TesterNotificationListener listener2 = new TesterNotificationListener();
+ mbean.addAttributeChangeNotificationListener(listener1, "A", "hb");
+ mbean.addAttributeChangeNotificationListener(listener2, "A", "hb");
+ mbean.removeAttributeChangeNotificationListener(listener1, "A");
+ send(mbean, "A");
+ Assert.assertTrue(listener1.received.isEmpty());
+ Assert.assertEquals(List.of("A:hb"), listener2.received);
+ }
+
+ @Test
+ public void testNonAttributeFilterIsNotRemovedByName() throws Exception {
+ BaseModelMBean mbean = new BaseModelMBean();
+ TesterNotificationListener listener = new TesterNotificationListener();
+ NotificationFilter acceptAll = notification -> true;
+ // Registered with a general (non attribute) filter via the
+ // NotificationBroadcaster method
+ mbean.addNotificationListener(listener, acceptAll, "hb");
+ mbean.removeAttributeChangeNotificationListener(listener, "A");
+ send(mbean, "A");
+ Assert.assertEquals(List.of("A:hb"), listener.received);
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 19/23: Fix TLS 1.3 cipher suite configuration
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 6feb20ee58357f6633e2c84aee9768a53c7f5a9b
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 14:35:06 2026 +0200
Fix TLS 1.3 cipher suite configuration
SSLHostConfig.setCipherSuites() converted each configured name from JSSE
format to OpenSSL format and then discarded the result, appending the
original value instead. Since the value is always stored, and later
passed
to OpenSSL, in OpenSSL format, append the converted name. This matches
setCiphers(), which already appended the converted name. There is no
change in behaviour for current configurations because the JSSE and
OpenSSL names for TLS 1.3 cipher suites are currently the same.
Also avoid a NullPointerException when a null value is provided. That
method dereferenced the argument without checking it, unlike
setCiphers(),
which stores a null value and resets the values derived from the
previous
configuration. Do the same here.
Tests: testCiphersuite04 covers the JSSE format (comma separated) list
and
the conversion to the OpenSSL format list. testCiphersuite05 covers the
null value, which previously failed with a NullPointerException.
---
java/org/apache/tomcat/util/net/SSLHostConfig.java | 15 ++++++++++++--
.../apache/tomcat/util/net/TestSSLHostConfig.java | 24 ++++++++++++++++++++++
2 files changed, 37 insertions(+), 2 deletions(-)
diff --git a/java/org/apache/tomcat/util/net/SSLHostConfig.java b/java/org/apache/tomcat/util/net/SSLHostConfig.java
index 0db535b8bf..f3b842d95a 100644
--- a/java/org/apache/tomcat/util/net/SSLHostConfig.java
+++ b/java/org/apache/tomcat/util/net/SSLHostConfig.java
@@ -802,11 +802,22 @@ public class SSLHostConfig implements Serializable {
/**
- * Set the cipher suite (TLSv1.3) configuration.
+ * Set the cipher suite (TLSv1.3) configuration. Note: Regardless of the format used to set the configuration, it is
+ * always stored in OpenSSL format.
*
* @param cipherSuites The cipher suites to use in a colon-separated, preference order list
*/
public void setCipherSuites(String cipherSuites) {
+ if (cipherSuites == null) {
+ // As for setCiphers(), a null value is stored as provided.
+ this.cipherSuites = null;
+ this.cipherSuiteList = null;
+ this.jsseCipherNames = null;
+ return;
+ }
+
+ // Cipher suites are stored in OpenSSL format. Convert the provided
+ // value if necessary.
StringBuilder sb = new StringBuilder();
String[] values;
if (cipherSuites.contains(":")) {
@@ -835,7 +846,7 @@ public class SSLHostConfig implements Serializable {
if (!sb.isEmpty()) {
sb.append(':');
}
- sb.append(trimmed);
+ sb.append(openSSLName);
}
}
this.cipherSuites = sb.toString();
diff --git a/test/org/apache/tomcat/util/net/TestSSLHostConfig.java b/test/org/apache/tomcat/util/net/TestSSLHostConfig.java
index 82d4d38012..a258fbc4d4 100644
--- a/test/org/apache/tomcat/util/net/TestSSLHostConfig.java
+++ b/test/org/apache/tomcat/util/net/TestSSLHostConfig.java
@@ -167,6 +167,30 @@ public class TestSSLHostConfig {
}
+ @Test
+ public void testCiphersuite04() {
+ SSLHostConfig hc = new SSLHostConfig();
+ Cipher c1 = Cipher.TLS_AES_128_CCM_SHA256;
+ Cipher c2 = Cipher.TLS_AES_256_GCM_SHA384;
+
+ // JSSE format (comma separated) list of JSSE names. The result should be
+ // an OpenSSL format (colon separated) list of OpenSSL names.
+ hc.setCipherSuites(c1.getJsseNames().iterator().next() + "," +
+ c2.getJsseNames().iterator().next());
+ Assert.assertEquals(c1.getOpenSSLAlias() + ":" + c2.getOpenSSLAlias(), hc.getCipherSuites());
+ }
+
+
+ @Test
+ public void testCiphersuite05() {
+ SSLHostConfig hc = new SSLHostConfig();
+
+ // Null should be handled without an error, as it is for setCiphers()
+ hc.setCipherSuites(null);
+ Assert.assertNull(hc.getCipherSuites());
+ }
+
+
@Test
public void testSerialization() throws IOException, ClassNotFoundException {
// Dummy OpenSSL command name/value pair
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 23/23: Support AES-128-CBC and AES-192-CBC encrypted traditional PEM keys
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 54e922894f85c7cfd0289ec94e69352e4f0561b9
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 15:36:41 2026 +0200
Support AES-128-CBC and AES-192-CBC encrypted traditional PEM keys
The switch on the DEK-Info algorithm of a traditional PEM encrypted
private key only handled DES-CBC, DES-EDE3-CBC and AES-256-CBC. Any
other algorithm, including the AES-128-CBC and AES-192-CBC that OpenSSL
readily produces with openssl rsa -aes128 and -aes192, fell into a
default branch that used the OpenSSL algorithm name directly as the JCE
transformation name. That always failed with a message like Cannot find
any provider supporting AES-128-CBC, which does not say that the
algorithm is simply unsupported.
Add the two missing algorithms, using the same key derivation and the
same warning about the OpenSSL password based key derivation that the
AES-256-CBC case uses, and report any other algorithm as unsupported
with the name of the PEM file and the algorithm instead of attempting to
use the OpenSSL name as a JCE transformation.
Tests: new fixtures key-encrypted-pkcs1-aes128.pem and
key-encrypted-pkcs1-aes192.pem, generated with OpenSSL from the existing
key-pkcs1.pem, cover the new algorithms. A new test checks that an
algorithm outside the supported set is reported as a
NoSuchAlgorithmException that names the algorithm.
---
.../tomcat/util/net/jsse/LocalStrings.properties | 1 +
java/org/apache/tomcat/util/net/jsse/PEMFile.java | 21 +++++++++----
.../apache/tomcat/util/net/jsse/TestPEMFile.java | 35 ++++++++++++++++++++++
.../util/net/jsse/key-encrypted-pkcs1-aes128.pem | 18 +++++++++++
.../util/net/jsse/key-encrypted-pkcs1-aes192.pem | 18 +++++++++++
5 files changed, 88 insertions(+), 5 deletions(-)
diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties b/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties
index 2d3ff4ffeb..85fc27deca 100644
--- a/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties
@@ -36,6 +36,7 @@ pemFile.notValidRFC5915=The provided key file does not conform to RFC 5915
pemFile.parseError=Unable to parse the key from [{0}]
pemFile.parseError.algorithm=Unable to parse the key using algorithm [{0}]
pemFile.startWithoutEnd=The PEM file [{0}] contains the part of type [{1}] which is not closed by an end boundary
+pemFile.unknownDekInfoAlgorithm=The PEM file [{0}] is using the DEK-Info encryption algorithm [{1}] which is not a recognised PEM encryption algorithm
pemFile.unknownEncryptedFormat=The format [{0}] is not a recognised encrypted PEM file format
pemFile.unknownEncryptionAlgorithm=The encryption algorithm with DER encoded OID of [{0}] was not recognised
pemFile.unknownPkcs8Algorithm=The PKCS#8 encryption algorithm with DER encoded OID of [{0}] was not recognised
diff --git a/java/org/apache/tomcat/util/net/jsse/PEMFile.java b/java/org/apache/tomcat/util/net/jsse/PEMFile.java
index 5e413b4d31..0d6149090a 100644
--- a/java/org/apache/tomcat/util/net/jsse/PEMFile.java
+++ b/java/org/apache/tomcat/util/net/jsse/PEMFile.java
@@ -438,6 +438,20 @@ public class PEMFile {
log.warn(sm.getString("pemFile.encryption.insecure", filename, algorithm));
break;
}
+ case "AES-128-CBC": {
+ secretKeyAlgorithm = "AES";
+ cipherTransformation = "AES/CBC/PKCS5Padding";
+ keyLength = 16;
+ log.warn(sm.getString("pemFile.encryption.insecure", filename, algorithm));
+ break;
+ }
+ case "AES-192-CBC": {
+ secretKeyAlgorithm = "AES";
+ cipherTransformation = "AES/CBC/PKCS5Padding";
+ keyLength = 24;
+ log.warn(sm.getString("pemFile.encryption.insecure", filename, algorithm));
+ break;
+ }
case "AES-256-CBC": {
secretKeyAlgorithm = "AES";
cipherTransformation = "AES/CBC/PKCS5Padding";
@@ -446,11 +460,8 @@ public class PEMFile {
break;
}
default:
- // This will almost certainly trigger errors
- secretKeyAlgorithm = algorithm;
- cipherTransformation = algorithm;
- keyLength = 8;
- break;
+ throw new NoSuchAlgorithmException(
+ sm.getString("pemFile.unknownDekInfoAlgorithm", filename, algorithm));
}
byte[] iv = fromHex(ivHex);
diff --git a/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
index da286a8b36..92afbf89d0 100644
--- a/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
+++ b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
@@ -21,6 +21,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
@@ -42,6 +43,8 @@ public class TestPEMFile {
private static final String KEY_PKCS1 = "key-pkcs1.pem";
private static final String KEY_ENCRYPTED_PKCS1_DES_CBC = "key-encrypted-pkcs1-des-cbc.pem";
private static final String KEY_ENCRYPTED_PKCS1_DES_EDE3_CBC = "key-encrypted-pkcs1-des-ede3-cbc.pem";
+ private static final String KEY_ENCRYPTED_PKCS1_AES128 = "key-encrypted-pkcs1-aes128.pem";
+ private static final String KEY_ENCRYPTED_PKCS1_AES192 = "key-encrypted-pkcs1-aes192.pem";
private static final String KEY_ENCRYPTED_PKCS1_AES256 = "key-encrypted-pkcs1-aes256.pem";
private static final String KEY_ENCRYPTED_PKCS8_HMACSHA1DEFAULT_DES_EDE3_CBC = "key-encrypted-pkcs8-hmacsha1default-des-ede3-cbc.pem";
private static final String KEY_ENCRYPTED_PKCS8_HMACSHA256_AES_128_CBC = "key-encrypted-pkcs8-hmacsha256-aes-128-cbc.pem";
@@ -100,12 +103,44 @@ public class TestPEMFile {
}
+ @Test
+ public void testKeyEncryptedPkcs1Aes128() throws Exception {
+ testKeyEncrypted(KEY_ENCRYPTED_PKCS1_AES128);
+ }
+
+
+ @Test
+ public void testKeyEncryptedPkcs1Aes192() throws Exception {
+ testKeyEncrypted(KEY_ENCRYPTED_PKCS1_AES192);
+ }
+
+
@Test
public void testKeyEncryptedPkcs1Aes256() throws Exception {
testKeyEncrypted(KEY_ENCRYPTED_PKCS1_AES256);
}
+ @Test
+ public void testKeyEncryptedPkcs1UnsupportedAlgorithm() throws Exception {
+ // The name of the algorithm is not a JCE transformation name, but the
+ // algorithm should still be rejected as unsupported rather than by
+ // the failure of an attempt to use the name as a transformation. The
+ // algorithm is rejected before the content is decrypted, so the rest
+ // of the content does not matter.
+ String pem = "-----BEGIN RSA PRIVATE KEY-----\n" + "Proc-Type: 4,ENCRYPTED\n"
+ + "DEK-Info: AES-128-CTR,0123456789ABCDEF0123456789ABCDEF\n" + "QUJDRA==\n"
+ + "-----END RSA PRIVATE KEY-----\n";
+ try {
+ new PEMFile("unsupported", new ByteArrayInputStream(pem.getBytes(StandardCharsets.US_ASCII)), KEY_PASSWORD,
+ null);
+ Assert.fail("Expected the unsupported encryption algorithm to be rejected");
+ } catch (NoSuchAlgorithmException nsae) {
+ // Expected
+ }
+ }
+
+
@Test
public void testKeyEncryptedPkcs8HmacSha1DefaultDesEde3Cbc() throws Exception {
testKeyEncrypted(KEY_ENCRYPTED_PKCS8_HMACSHA1DEFAULT_DES_EDE3_CBC);
diff --git a/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes128.pem b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes128.pem
new file mode 100644
index 0000000000..d8c09dcbc5
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes128.pem
@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,4DCB0290D2C234A782B7C1724A7AB2EE
+
+4hLNoDWz8XP4SV9c7Tk4AuNxdT4jXDNxXwRZAumk4aNe03GjZjV43HA++fNn6VQ8
+JOwIE2s4/RN4brzIyFHCQ1lq1vsGLLa/gmfabHZdv11h2rMAfyCrOwvPRh7N/x75
+Y2yTavRoqqT8+K7/IrpWX5IvVLA8dBXMiB11CcKyiCCKK6qaIap7HP2FpjdSq5Jd
+Irnuvvq5rXUkHz5HLk0g+p2iKbwoZS3aaQmSK+AnmH5WOHF6XRz12Z5Ell5vOAGG
+F8OEV5Z+pxlP6Jf4zAqsYvIXMGv3gOocy6RISB+db7/x9cvVnqQgDHOdvLSDiNH2
+r2eo1SFS1qsJAGuKmvU/05yk/XX22Bwuh1P4D7lV+swTBoYXnRrkrG0X8IjUQ04b
+rOQzq+RIdz+iJNd7sjsHEcYjHCZblvffHd1B3kR9Lmpp7qjVVpk+j/itpG87NVFh
+aOUBm9A7eWSx6D1YJlEv+sbumlf9V91L6vB/wC++vOFqUDv05gGy1HjOFqw7Mg92
+lF+F64I62wpBM9qbdE9az4qhuibklFQW7rPh6QLzf2gFL1ziOUzybn9xSYDvvqyZ
+fCJX9YNDVu3C4Su9BxGv4S1tTdxtGq1G+pL45/vZM8xsYE1vrxrQttromANKq3Xu
+tLvAeVN8HELjMt45qPRUfnsRBRBbDxgEkw2g38gAcUV3o9cd3mfqzZ0BHWRzaKdz
+NjSbNHqeHp3YAuAVM8Bu5pwhASBpR1YIYeWltZjFXJsSNxs+Lw3nGlC6FspNOkFj
+k17aKDez9G/3IqrzLcyl8LD9MN5PNanikxRoNThQaRRAxI152RxQXyAAa5aXcgFI
+-----END RSA PRIVATE KEY-----
diff --git a/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes192.pem b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes192.pem
new file mode 100644
index 0000000000..c570bbe493
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/jsse/key-encrypted-pkcs1-aes192.pem
@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-192-CBC,F20C2EFDA5AD3F0F9F5750197F6FDE71
+
+xFk1lqug3zdJIvgjcDjHjhZVl4znx0oVQfA0bDqmiQDmzRlKmpDoD9y0qySs/Vp8
+TwgAHbbNX4VBahBJUyXOsnw+AoldGoRmHiaHuTW94TJhfEcJnripMNSJdeN19nVv
+OAlKMXpvektnkU3s6DtcP70GmmTTcXBHzxpGHfwFjpJ6C6QhU2TFz/cuGjXZRp43
+FKW5YdJnQ+XgUjj6GOwUwCf/l+xA0bI6/RPvHdOjFRxBGNxrpQr9PDOAJ3p9t9AF
+kdP7wIlNND+Siy4HAE3kzRXNeY5E3/Ts7uTJQ0/D3EZqCxhBuvh0ySqmv8Mqidn6
+VZzN0QHuYkxtaDxYXV19C5TVgNq/yYz2Tfl1uKgA1XJqi0tyXlwWOx1UafmELHlN
+SqAmZXgbP8n2cOWQ9sZ678NtQLsMGNc+w37BkZbZRi0Lu8E7ElCe9/u4GZtVFXUt
+E3tedjPgDaAUa2yVhnGKH45Rh7pVjQLU71oMhiTnqwm44sJ4l5h5/g21ta9NM+/8
+mtIbIYt+N1v0GXyL1vMV2aofH4cKI7rBPd4LVB/8uTRFCCdjuvtiQLJMSsAkEbJW
+1rtr/dIZcHO+HF8Mcb/jWK9RSJrBif1u2mHQwVNQlW4pdq0xLC6zamXV8wPkxvHZ
+LAHhdwOjPZDc7P1959FE9X8/lHk52FGGL/xpa7bbztYXlJVSVEbIgF1CdEFC/t3W
+2f4e9QM6HhBrS3ly6crYUjVNObv44E7Cg+OrggPY2dA0OQrjQY0szMNRJl9RRsp5
+nuhzXkbE4iht931uQi3oqjSetCnQkWcXH5gVzEzT1/pr/BtOK+eyzYnMwDQTmJfs
+-----END RSA PRIVATE KEY-----
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 20/23: Report a configuration error for PEM certificates with a FIPS key manager
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 68485912ec146230b116eba0391e2e0c85b40606
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 14:46:02 2026 +0200
Report a configuration error for PEM certificates with a FIPS key manager
When the KeyManagerFactory is provided by a FIPS module, Tomcat passes the
configured key store to the factory directly and skips the manipulation of
key material that FIPS does not allow. PEM format certificates require that
manipulation: the key and certificate chain are read from the PEM files and
imported into an in memory key store.
Previously, the FIPS code path used the configured key store without
checking it. For a PEM based configuration there is no key store, so the
factory was initialised with a null key store. Depending on the
implementation, that either produced a key manager with no keys, allowing
the connector to start but causing every TLS handshake to fail, or an
unrelated error during start up.
Now report the configuration error when no key store is available. The
existing message is used when no certificate has been configured at all, as
for a non-FIPS configuration. Otherwise, explain that PEM format
certificate files require a key store when a FIPS key manager factory is in
use.
Tests: testFipsWithPemConfiguration and testFipsWithoutConfiguration cover
both cases. Both previously failed because no error was reported.
---
.../apache/tomcat/util/net/LocalStrings.properties | 1 +
java/org/apache/tomcat/util/net/SSLUtilBase.java | 10 +++++
.../util/net/TestKeyManagerWrappingFips.java | 46 ++++++++++++++++++++++
3 files changed, 57 insertions(+)
diff --git a/java/org/apache/tomcat/util/net/LocalStrings.properties b/java/org/apache/tomcat/util/net/LocalStrings.properties
index b335750b7c..44c9810470 100644
--- a/java/org/apache/tomcat/util/net/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/net/LocalStrings.properties
@@ -179,6 +179,7 @@ sslImplementation.cnfe=Unable to create SSLImplementation for class [{0}]
sslUtilBase.active=The [{0}] that are active are : [{1}]
sslUtilBase.aliasIgnored=FIPS enabled so alias name [{0}] will be ignored. If there is more than one key in the key store, the key used will depend on the key store implementation
sslUtilBase.alias_no_key_entry=Alias name [{0}] does not identify a key entry
+sslUtilBase.fipsPemNotSupported=FIPS enabled so PEM format certificate files are not supported. Configure the certificate using a key store rather than the certificateFile attribute
sslUtilBase.invalidTrustManagerClassName=The trustManagerClassName provided [{0}] does not implement javax.net.ssl.TrustManager
sslUtilBase.keystore_load_failed=Failed to load keystore type [{0}] with path [{1}] due to [{2}]
sslUtilBase.noCertFile=SSLHostConfig attribute certificateFile must be defined when using an SSL connector
diff --git a/java/org/apache/tomcat/util/net/SSLUtilBase.java b/java/org/apache/tomcat/util/net/SSLUtilBase.java
index 14243711d5..7c40846786 100644
--- a/java/org/apache/tomcat/util/net/SSLUtilBase.java
+++ b/java/org/apache/tomcat/util/net/SSLUtilBase.java
@@ -353,6 +353,16 @@ public abstract class SSLUtilBase implements SSLUtil {
if (keyAlias != null) {
log.warn(sm.getString("sslUtilBase.aliasIgnored", keyAlias));
}
+ if (ks == null) {
+ if (certificate.getCertificateFile() == null) {
+ throw new IOException(sm.getString("sslUtilBase.noCertFile"));
+ }
+ // Importing PEM format certificates into an in memory key store
+ // requires the key manipulation that is avoided above. Passing a
+ // null key store to the factory would result in a key manager
+ // with no keys and every TLS handshake would then fail.
+ throw new IOException(sm.getString("sslUtilBase.fipsPemNotSupported"));
+ }
kmf.init(ksUsed, keyPassArray);
return kmf.getKeyManagers();
}
diff --git a/test/org/apache/tomcat/util/net/TestKeyManagerWrappingFips.java b/test/org/apache/tomcat/util/net/TestKeyManagerWrappingFips.java
index 3f1e9e2e31..7b2928f570 100644
--- a/test/org/apache/tomcat/util/net/TestKeyManagerWrappingFips.java
+++ b/test/org/apache/tomcat/util/net/TestKeyManagerWrappingFips.java
@@ -18,6 +18,7 @@
package org.apache.tomcat.util.net;
import java.io.File;
+import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
@@ -74,6 +75,51 @@ public class TestKeyManagerWrappingFips {
getKeyManagers();
Assert.assertTrue(DummyKeyStoreSpi.wrappingOccurred);
}
+
+
+ @Test
+ public void testFipsWithPemConfiguration() throws Exception {
+ Security.addProvider(new DummyKeyManagerFactoryProvider(FIPS_PROVIDER,
+ "Sun JSSE provider (FIPS mode, crypto provider SunPKCS11-NSSfips", DUMMY_ALGORITHM));
+
+ SSLHostConfigCertificate certificate = getCertificate();
+ certificate.setCertificateFile("server.crt");
+
+ try {
+ new JSSEUtil(certificate).getKeyManagers();
+ Assert.fail("PEM format certificates are not supported with a FIPS key manager factory");
+ } catch (IOException ioe) {
+ // Before the FIPS branch checked for a key store, the key manager
+ // factory was initialised with a null key store. That produced a key
+ // manager with no keys which failed every TLS handshake with an
+ // unrelated error rather than reporting the configuration error.
+ Assert.assertTrue(ioe.getMessage(), ioe.getMessage().contains("PEM"));
+ }
+ }
+
+
+ @Test
+ public void testFipsWithoutConfiguration() throws Exception {
+ Security.addProvider(new DummyKeyManagerFactoryProvider(FIPS_PROVIDER,
+ "Sun JSSE provider (FIPS mode, crypto provider SunPKCS11-NSSfips", DUMMY_ALGORITHM));
+
+ try {
+ new JSSEUtil(getCertificate()).getKeyManagers();
+ Assert.fail("A certificate configuration is required");
+ } catch (IOException ioe) {
+ // Ignore. This is the standard error for a missing certificate file.
+ Assert.assertFalse(ioe.getMessage(), ioe.getMessage().contains("PEM"));
+ }
+ }
+
+
+ private SSLHostConfigCertificate getCertificate() {
+ SSLHostConfig hostConfig = new SSLHostConfig();
+ hostConfig.setKeyManagerAlgorithm(DUMMY_ALGORITHM);
+ return new SSLHostConfigCertificate(hostConfig, SSLHostConfigCertificate.Type.UNDEFINED);
+ }
+
+
private void getKeyManagers() throws Exception {
Security.addProvider(new DummyKeyStoreProvider(KEYSTORE_PROVIDER, "", DUMMY_KEYSTORE));
SSLHostConfig hostConfig = new SSLHostConfig();
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 03/23: Parse exponent-only numbers in the JSON parser
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit c65bf7dc8cc0b28e115551bea576c7b7d309f717
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 09:37:11 2026 +0200
Parse exponent-only numbers in the JSON parser
Numbers with an exponent and no fraction part, like 1e5, were rejected.
The NUMBER_DECIMAL token required a fraction, with the exponent as an
optional part of it, so a number such as 1e5 matched no number token and
the parser failed, while Double.parseDouble and BigDecimal both accept
it. Such numbers are also valid per RFC 8259.
Make the NUMBER_DECIMAL token accept either a fraction with an optional
exponent, or an exponent on its own, after the integer part, and
regenerate the token manager.
Note that in the lenient dialect, such numbers used as bare object keys
are formatted using the string representation of the parsed number, so
1e5 becomes the key 1E+5. Previously, such keys were rejected.
---
java/org/apache/tomcat/util/json/JSONParser.jjt | 14 +-
.../tomcat/util/json/JSONParserTokenManager.java | 150 ++++++++++++---------
.../apache/tomcat/util/json/TestJSONParser.java | 36 +++++
3 files changed, 132 insertions(+), 68 deletions(-)
diff --git a/java/org/apache/tomcat/util/json/JSONParser.jjt b/java/org/apache/tomcat/util/json/JSONParser.jjt
index 561d8fda2d..c50f41935a 100644
--- a/java/org/apache/tomcat/util/json/JSONParser.jjt
+++ b/java/org/apache/tomcat/util/json/JSONParser.jjt
@@ -225,13 +225,21 @@ TOKEN:{
| <NUMBER_DECIMAL:
("-")?
( (<ZERO>)+ | ( <DIGIT_NONZERO> (<DIGIT>)* ) )
- ("."
- (<DIGIT>)+
+ (
(
+ "."
+ (<DIGIT>)+
+ (
+ ["e","E"]
+ ("+" | "-")?
+ (<DIGIT>)+
+ )?
+ )
+ | (
["e","E"]
("+" | "-")?
(<DIGIT>)+
- )?
+ )
)
>
}
diff --git a/java/org/apache/tomcat/util/json/JSONParserTokenManager.java b/java/org/apache/tomcat/util/json/JSONParserTokenManager.java
index c45f585edd..c19fed285d 100644
--- a/java/org/apache/tomcat/util/json/JSONParserTokenManager.java
+++ b/java/org/apache/tomcat/util/json/JSONParserTokenManager.java
@@ -34,14 +34,14 @@ public class JSONParserTokenManager implements JSONParserConstants {
switch (pos) {
case 0:
if ((active0 & 0x1000000L) != 0L) {
- return 48;
+ return 51;
}
if ((active0 & 0xe0000L) != 0L) {
jjmatchedKind = 30;
return 25;
}
if ((active0 & 0x2000000L) != 0L) {
- return 49;
+ return 52;
}
return -1;
case 1:
@@ -243,7 +243,7 @@ public class JSONParserTokenManager implements JSONParserConstants {
private int jjMoveNfa_0(int startState, int curPos) {
int startsAt = 0;
- jjnewStateCnt = 48;
+ jjnewStateCnt = 51;
int i = 1;
jjstateSet[0] = startState;
int kind = 0x7fffffff;
@@ -255,12 +255,6 @@ public class JSONParserTokenManager implements JSONParserConstants {
long l = 1L << curChar;
do {
switch (jjstateSet[--i]) {
- case 49:
- case 16:
- if ((0xfffffffbffffc9ffL & l) != 0L) {
- jjCheckNAddStates(0, 2);
- }
- break;
case 0:
if ((0x3ff000000000000L & l) != 0L) {
if (kind > 30) {
@@ -278,9 +272,9 @@ public class JSONParserTokenManager implements JSONParserConstants {
kind = 4;
}
} else if (curChar == 45) {
- jjCheckNAddStates(3, 6);
+ jjCheckNAddStates(0, 3);
} else if (curChar == 47) {
- jjAddStates(7, 8);
+ jjAddStates(4, 5);
} else if (curChar == 34) {
jjCheckNAddTwoStates(16, 18);
} else if (curChar == 39) {
@@ -293,21 +287,27 @@ public class JSONParserTokenManager implements JSONParserConstants {
kind = 15;
}
{
- jjCheckNAddStates(9, 11);
+ jjCheckNAddStates(6, 9);
}
} else if (curChar == 48) {
if (kind > 15) {
kind = 15;
}
{
- jjCheckNAddStates(12, 14);
+ jjCheckNAddStates(10, 13);
}
}
break;
- case 48:
+ case 52:
+ case 16:
+ if ((0xfffffffbffffc9ffL & l) != 0L) {
+ jjCheckNAddStates(14, 16);
+ }
+ break;
+ case 51:
case 6:
if ((0xffffff7fffffc9ffL & l) != 0L) {
- jjCheckNAddStates(15, 17);
+ jjCheckNAddStates(17, 19);
}
break;
case 1:
@@ -342,7 +342,7 @@ public class JSONParserTokenManager implements JSONParserConstants {
break;
case 9:
if ((0x808000000000L & l) != 0L) {
- jjCheckNAddStates(15, 17);
+ jjCheckNAddStates(17, 19);
}
break;
case 11:
@@ -362,7 +362,7 @@ public class JSONParserTokenManager implements JSONParserConstants {
break;
case 14:
if ((0x3ff000000000000L & l) != 0L) {
- jjCheckNAddStates(15, 17);
+ jjCheckNAddStates(17, 19);
}
break;
case 15:
@@ -377,7 +377,7 @@ public class JSONParserTokenManager implements JSONParserConstants {
break;
case 19:
if ((0x800400000000L & l) != 0L) {
- jjCheckNAddStates(0, 2);
+ jjCheckNAddStates(14, 16);
}
break;
case 21:
@@ -397,7 +397,7 @@ public class JSONParserTokenManager implements JSONParserConstants {
break;
case 24:
if ((0x3ff000000000000L & l) != 0L) {
- jjCheckNAddStates(0, 2);
+ jjCheckNAddStates(14, 16);
}
break;
case 25:
@@ -412,7 +412,7 @@ public class JSONParserTokenManager implements JSONParserConstants {
break;
case 26:
if (curChar == 47) {
- jjAddStates(7, 8);
+ jjAddStates(4, 5);
}
break;
case 27:
@@ -451,7 +451,7 @@ public class JSONParserTokenManager implements JSONParserConstants {
break;
case 34:
if (curChar == 45) {
- jjCheckNAddStates(3, 6);
+ jjCheckNAddStates(0, 3);
}
break;
case 35:
@@ -486,7 +486,7 @@ public class JSONParserTokenManager implements JSONParserConstants {
break;
case 38:
if (curChar == 48) {
- jjCheckNAddTwoStates(38, 39);
+ jjCheckNAddStates(20, 22);
}
break;
case 39:
@@ -519,34 +519,49 @@ public class JSONParserTokenManager implements JSONParserConstants {
jjCheckNAdd(43);
}
break;
- case 44:
+ case 45:
+ if ((0x280000000000L & l) != 0L) {
+ jjCheckNAdd(46);
+ }
+ break;
+ case 46:
+ if ((0x3ff000000000000L & l) == 0L) {
+ break;
+ }
+ if (kind > 16) {
+ kind = 16;
+ } {
+ jjCheckNAdd(46);
+ }
+ break;
+ case 47:
if ((0x3fe000000000000L & l) != 0L) {
- jjCheckNAddTwoStates(45, 39);
+ jjCheckNAddStates(23, 25);
}
break;
- case 45:
+ case 48:
if ((0x3ff000000000000L & l) != 0L) {
- jjCheckNAddTwoStates(45, 39);
+ jjCheckNAddStates(23, 25);
}
break;
- case 46:
+ case 49:
if (curChar != 48) {
break;
}
if (kind > 15) {
kind = 15;
} {
- jjCheckNAddStates(12, 14);
+ jjCheckNAddStates(10, 13);
}
break;
- case 47:
+ case 50:
if ((0x3fe000000000000L & l) == 0L) {
break;
}
if (kind > 15) {
kind = 15;
} {
- jjCheckNAddStates(9, 11);
+ jjCheckNAddStates(6, 9);
}
break;
default:
@@ -557,13 +572,6 @@ public class JSONParserTokenManager implements JSONParserConstants {
long l = 1L << (curChar & 077);
do {
switch (jjstateSet[--i]) {
- case 49:
- if ((0xffffffffefffffffL & l) != 0L) {
- jjCheckNAddStates(0, 2);
- } else if (curChar == 92) {
- jjAddStates(18, 19);
- }
- break;
case 0:
case 25:
if ((0x7fffffe07fffffeL & l) == 0L) {
@@ -575,30 +583,37 @@ public class JSONParserTokenManager implements JSONParserConstants {
jjCheckNAdd(25);
}
break;
- case 48:
+ case 52:
+ if ((0xffffffffefffffffL & l) != 0L) {
+ jjCheckNAddStates(14, 16);
+ } else if (curChar == 92) {
+ jjAddStates(26, 27);
+ }
+ break;
+ case 51:
if ((0xffffffffefffffffL & l) != 0L) {
- jjCheckNAddStates(15, 17);
+ jjCheckNAddStates(17, 19);
} else if (curChar == 92) {
- jjAddStates(20, 21);
+ jjAddStates(28, 29);
}
break;
case 1: {
- jjAddStates(22, 23);
+ jjAddStates(30, 31);
}
break;
case 6:
if ((0xffffffffefffffffL & l) != 0L) {
- jjCheckNAddStates(15, 17);
+ jjCheckNAddStates(17, 19);
}
break;
case 8:
if (curChar == 92) {
- jjAddStates(20, 21);
+ jjAddStates(28, 29);
}
break;
case 9:
if ((0x14404410144044L & l) != 0L) {
- jjCheckNAddStates(15, 17);
+ jjCheckNAddStates(17, 19);
}
break;
case 10:
@@ -623,22 +638,22 @@ public class JSONParserTokenManager implements JSONParserConstants {
break;
case 14:
if ((0x7e0000007eL & l) != 0L) {
- jjCheckNAddStates(15, 17);
+ jjCheckNAddStates(17, 19);
}
break;
case 16:
if ((0xffffffffefffffffL & l) != 0L) {
- jjCheckNAddStates(0, 2);
+ jjCheckNAddStates(14, 16);
}
break;
case 18:
if (curChar == 92) {
- jjAddStates(18, 19);
+ jjAddStates(26, 27);
}
break;
case 19:
if ((0x14404410144044L & l) != 0L) {
- jjCheckNAddStates(0, 2);
+ jjCheckNAddStates(14, 16);
}
break;
case 20:
@@ -663,20 +678,25 @@ public class JSONParserTokenManager implements JSONParserConstants {
break;
case 24:
if ((0x7e0000007eL & l) != 0L) {
- jjCheckNAddStates(0, 2);
+ jjCheckNAddStates(14, 16);
}
break;
case 28: {
- jjAddStates(24, 25);
+ jjAddStates(32, 33);
}
break;
case 31: {
- jjAddStates(26, 27);
+ jjAddStates(34, 35);
}
break;
case 41:
if ((0x2000000020L & l) != 0L) {
- jjAddStates(28, 29);
+ jjAddStates(36, 37);
+ }
+ break;
+ case 44:
+ if ((0x2000000020L & l) != 0L) {
+ jjAddStates(38, 39);
}
break;
default:
@@ -691,31 +711,31 @@ public class JSONParserTokenManager implements JSONParserConstants {
long l2 = 1L << (curChar & 077);
do {
switch (jjstateSet[--i]) {
- case 49:
+ case 52:
case 16:
if (jjCanMove_0(hiByte, i1, i2, l1, l2)) {
- jjCheckNAddStates(0, 2);
+ jjCheckNAddStates(14, 16);
}
break;
- case 48:
+ case 51:
case 6:
if (jjCanMove_0(hiByte, i1, i2, l1, l2)) {
- jjCheckNAddStates(15, 17);
+ jjCheckNAddStates(17, 19);
}
break;
case 1:
if (jjCanMove_0(hiByte, i1, i2, l1, l2)) {
- jjAddStates(22, 23);
+ jjAddStates(30, 31);
}
break;
case 28:
if (jjCanMove_0(hiByte, i1, i2, l1, l2)) {
- jjAddStates(24, 25);
+ jjAddStates(32, 33);
}
break;
case 31:
if (jjCanMove_0(hiByte, i1, i2, l1, l2)) {
- jjAddStates(26, 27);
+ jjAddStates(34, 35);
}
break;
default:
@@ -733,7 +753,7 @@ public class JSONParserTokenManager implements JSONParserConstants {
kind = 0x7fffffff;
}
++curPos;
- if ((i = jjnewStateCnt) == (startsAt = 48 - (jjnewStateCnt = startsAt))) {
+ if ((i = jjnewStateCnt) == (startsAt = 51 - (jjnewStateCnt = startsAt))) {
return curPos;
}
try {
@@ -772,8 +792,8 @@ public class JSONParserTokenManager implements JSONParserConstants {
return t;
}
- static final int[] jjnextStates = { 16, 17, 18, 35, 36, 38, 44, 27, 30, 37, 45, 39, 35, 38, 39, 6, 7, 8, 19, 20, 9,
- 10, 1, 2, 28, 29, 31, 33, 42, 43, };
+ static final int[] jjnextStates = { 35, 36, 38, 47, 27, 30, 37, 48, 39, 44, 35, 38, 39, 44, 16, 17, 18, 6, 7, 8, 38,
+ 39, 44, 48, 39, 44, 19, 20, 9, 10, 1, 2, 28, 29, 31, 33, 42, 43, 45, 46, };
private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, long l2) {
switch (hiByte) {
@@ -924,7 +944,7 @@ public class JSONParserTokenManager implements JSONParserConstants {
private void ReInitRounds() {
int i;
jjround = 0x80000001;
- for (i = 48; i-- > 0;) {
+ for (i = 51; i-- > 0;) {
jjrounds[i] = 0x80000000;
}
}
@@ -958,8 +978,8 @@ public class JSONParserTokenManager implements JSONParserConstants {
static final long[] jjtoMore = { 0x0L, };
protected SimpleCharStream input_stream;
- private final int[] jjrounds = new int[48];
- private final int[] jjstateSet = new int[2 * 48];
+ private final int[] jjrounds = new int[51];
+ private final int[] jjstateSet = new int[2 * 51];
private final StringBuilder jjimage = new StringBuilder();
private StringBuilder image = jjimage;
private int jjimageLen;
diff --git a/test/org/apache/tomcat/util/json/TestJSONParser.java b/test/org/apache/tomcat/util/json/TestJSONParser.java
index c4067226d6..9771bbf0bb 100644
--- a/test/org/apache/tomcat/util/json/TestJSONParser.java
+++ b/test/org/apache/tomcat/util/json/TestJSONParser.java
@@ -17,8 +17,10 @@
package org.apache.tomcat.util.json;
import java.io.StringReader;
+import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
+import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
@@ -75,6 +77,40 @@ public class TestJSONParser {
Assert.assertEquals(Long.valueOf(Long.MIN_VALUE), values.get(1));
}
+ @Test
+ public void testExponentOnlyNumbers() throws ParseException {
+ JSONParser parser = new JSONParser("[1E5, 1e+5, 0e5, -1e-5]");
+ List<Object> values = parser.parseArray();
+ Assert.assertEquals(new BigDecimal("1E5"), values.get(0));
+ Assert.assertEquals(new BigDecimal("1e+5"), values.get(1));
+ Assert.assertEquals(new BigDecimal("0e5"), values.get(2));
+ Assert.assertEquals(new BigDecimal("-1e-5"), values.get(3));
+ }
+
+ @Test
+ public void testExponentOnlyNumbersNative() throws ParseException {
+ JSONParser parser = new JSONParser("[1E5, 1e-5]");
+ parser.setNativeNumbers(true);
+ List<Object> values = parser.parseArray();
+ Assert.assertEquals(Double.valueOf(100000d), values.get(0));
+ Assert.assertEquals(Double.valueOf(0.00001d), values.get(1));
+ }
+
+ @Test(expected = ParseException.class)
+ public void testIncompleteExponentIsParseException() throws ParseException {
+ new JSONParser("[1E]").parse();
+ }
+
+ @Test
+ public void testBareExponentNumberKey() throws ParseException {
+ // Lenient mode accepts bare numbers as object keys. Such a key is
+ // formatted using the string representation of the parsed number, so
+ // an exponent-only number is normalised through BigDecimal.
+ Map<String, Object> result = new JSONParser("{1e5:1}").parseObject();
+ Assert.assertEquals(1, result.size());
+ Assert.assertEquals(BigInteger.ONE, result.get("1E+5"));
+ }
+
private static String nest(int depth) {
StringBuilder result = new StringBuilder(depth * 2);
for (int i = 0; i < depth; i++) {
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 01/23: Add a maximum nesting depth to the JSON parser
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit a726ebedf4b6654125bd820ea0cfc63987904cec
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 09:06:02 2026 +0200
Add a maximum nesting depth to the JSON parser
The parser recursed once per nesting level with no limit, so deeply
nested input caused a StackOverflowError. That Error also escaped the
consumers of the parser, which typically only catch Exception.
The parser now counts nesting at the entry point of the anything()
production and throws a ParseException once the depth exceeds 1000.
RFC 8259 allows implementations to set a limit on the maximum depth of
nesting. A depth of 1000 is far beyond what realistic JSON documents
use and matches the limit used by other mainstream JSON parsers. The
count is reset by the public entry points parseObject(), parseArray()
and parse() so that a parser remains usable after a failed parse.
The change is made in the grammar (JSONParser.jjt) and mirrored in the
checked-in generated parser. Regression tests cover acceptance at the
limit, rejection above it, and reuse of a parser after a depth error.
---
java/org/apache/tomcat/util/json/JSONParser.java | 17 ++++++++++
java/org/apache/tomcat/util/json/JSONParser.jjt | 23 ++++++++++++-
.../apache/tomcat/util/json/TestJSONParser.java | 38 ++++++++++++++++++++++
webapps/docs/changelog.xml | 8 +++++
4 files changed, 85 insertions(+), 1 deletion(-)
diff --git a/java/org/apache/tomcat/util/json/JSONParser.java b/java/org/apache/tomcat/util/json/JSONParser.java
index 4a2e3d8bd4..b798831c1e 100644
--- a/java/org/apache/tomcat/util/json/JSONParser.java
+++ b/java/org/apache/tomcat/util/json/JSONParser.java
@@ -30,6 +30,16 @@ public class JSONParser implements JSONParserConstants {
private boolean nativeNumbers = false;
+ /*
+ * The maximum nesting depth accepted by the parser. Deeply nested input would otherwise make the recursive
+ * descent exhaust the stack of the thread running the parser. RFC 8259 allows implementations to set a limit
+ * on the maximum depth of nesting. A depth of 1000 is far beyond what realistic JSON documents use and
+ * matches the limit used by other mainstream JSON parsers.
+ */
+ private static final int MAX_DEPTH = 1000;
+
+ private int depth = 0;
+
public JSONParser(String input) {
this(new java.io.StringReader(input));
}
@@ -38,6 +48,7 @@ public class JSONParser implements JSONParserConstants {
* Parses a JSON object into a Java {@code Map}.
*/
public java.util.LinkedHashMap<String,Object> parseObject() throws ParseException {
+ depth = 0;
java.util.LinkedHashMap<String,Object> toReturn = object();
if (!ensureEOF()) {
throw new IllegalStateException("Expected EOF, but still had content to parse");
@@ -49,6 +60,7 @@ public class JSONParser implements JSONParserConstants {
* Parses a JSON array into a Java {@code List}.
*/
public java.util.ArrayList<Object> parseArray() throws ParseException {
+ depth = 0;
java.util.ArrayList<Object> toReturn = list();
if (!ensureEOF()) {
throw new IllegalStateException("Expected EOF, but still had content to parse");
@@ -60,6 +72,7 @@ public class JSONParser implements JSONParserConstants {
* Parses any JSON-parsable object, returning the value.
*/
public Object parse() throws ParseException {
+ depth = 0;
Object toReturn = anything();
if (!ensureEOF()) {
throw new IllegalStateException("Expected EOF, but still had content to parse");
@@ -149,6 +162,9 @@ public class JSONParser implements JSONParserConstants {
final public Object anything() throws ParseException {
Object x;
+ if (++depth > MAX_DEPTH) {
+ throw new ParseException("Maximum nesting depth " + MAX_DEPTH + " exceeded");
+ }
switch (jj_nt.kind) {
case BRACE_OPEN: {
x = object();
@@ -177,6 +193,7 @@ public class JSONParser implements JSONParserConstants {
}
{
if ("" != null) {
+ depth--;
return x;
}
}
diff --git a/java/org/apache/tomcat/util/json/JSONParser.jjt b/java/org/apache/tomcat/util/json/JSONParser.jjt
index e794dfab60..7504ef110f 100644
--- a/java/org/apache/tomcat/util/json/JSONParser.jjt
+++ b/java/org/apache/tomcat/util/json/JSONParser.jjt
@@ -61,6 +61,16 @@ public class JSONParser {
private boolean nativeNumbers = false;
+ /*
+ * The maximum nesting depth accepted by the parser. Deeply nested input would otherwise make the recursive
+ * descent exhaust the stack of the thread running the parser. RFC 8259 allows implementations to set a limit
+ * on the maximum depth of nesting. A depth of 1000 is far beyond what realistic JSON documents use and
+ * matches the limit used by other mainstream JSON parsers.
+ */
+ private static final int MAX_DEPTH = 1000;
+
+ private int depth = 0;
+
public JSONParser(String input) {
this(new java.io.StringReader(input));
}
@@ -69,6 +79,7 @@ public class JSONParser {
* Parses a JSON object into a Java {@code Map}.
*/
public java.util.LinkedHashMap<String, Object> parseObject() throws ParseException {
+ depth = 0;
java.util.LinkedHashMap<String, Object> toReturn = object();
if (!ensureEOF()) {
throw new IllegalStateException("Expected EOF, but still had content to parse");
@@ -80,6 +91,7 @@ public class JSONParser {
* Parses a JSON array into a Java {@code List}.
*/
public java.util.ArrayList<Object> parseArray() throws ParseException {
+ depth = 0;
java.util.ArrayList<Object> toReturn = list();
if (!ensureEOF()) {
throw new IllegalStateException("Expected EOF, but still had content to parse");
@@ -91,6 +103,7 @@ public class JSONParser {
* Parses any JSON-parsable object, returning the value.
*/
public Object parse() throws ParseException {
+ depth = 0;
Object toReturn = anything();
if (!ensureEOF()) {
throw new IllegalStateException("Expected EOF, but still had content to parse");
@@ -270,11 +283,19 @@ boolean ensureEOF() : {}{
Object anything() : {
Object x;
}{
+ {
+ if (++depth > MAX_DEPTH) {
+ throw new ParseException("Maximum nesting depth " + MAX_DEPTH + " exceeded");
+ }
+ }
( x = object()
| x = list()
| x = value()
)
- { return x; }
+ {
+ depth--;
+ return x;
+ }
}
String objectKey() : {
diff --git a/test/org/apache/tomcat/util/json/TestJSONParser.java b/test/org/apache/tomcat/util/json/TestJSONParser.java
index 33a6c77aeb..bd8f7abfb3 100644
--- a/test/org/apache/tomcat/util/json/TestJSONParser.java
+++ b/test/org/apache/tomcat/util/json/TestJSONParser.java
@@ -16,15 +16,53 @@
*/
package org.apache.tomcat.util.json;
+import java.io.StringReader;
+
import org.junit.Assert;
import org.junit.Test;
public class TestJSONParser {
+ private static final int MAX_TEST_DEPTH = 1000;
+
@Test
public void testUnicodeEscapeIsNotUnescapedTwice() throws ParseException {
String json = "\"\\u005cn\"";
Assert.assertEquals("\\n", new JSONParser(json).parse());
}
+
+ @Test
+ public void testNestingAtMaximumDepthAccepted() throws ParseException {
+ Assert.assertNotNull(new JSONParser(nest(MAX_TEST_DEPTH)).parse());
+ }
+
+ @Test(expected = ParseException.class)
+ public void testNestingAboveMaximumDepthIsParseException() throws ParseException {
+ new JSONParser(nest(MAX_TEST_DEPTH + 1)).parse();
+ }
+
+ @Test
+ public void testDepthResetAfterError() throws ParseException {
+ JSONParser parser = new JSONParser(nest(MAX_TEST_DEPTH + 1));
+ try {
+ parser.parse();
+ Assert.fail("A ParseException was expected");
+ } catch (ParseException e) {
+ // Expected
+ }
+ parser.ReInit(new StringReader("[1]"));
+ Assert.assertNotNull(parser.parse());
+ }
+
+ private static String nest(int depth) {
+ StringBuilder result = new StringBuilder(depth * 2);
+ for (int i = 0; i < depth; i++) {
+ result.append('[');
+ }
+ for (int i = 0; i < depth; i++) {
+ result.append(']');
+ }
+ return result.toString();
+ }
}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index ce86cd5d77..5ce8f143f6 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -331,6 +331,14 @@
corrected to terminate, since the Java shift distance is evaluated
modulo 32. (remm)
</fix>
+ <fix>
+ The JSON parser now rejects input with a nesting depth greater than
+ 1000 with a <code>ParseException</code>. Previously, deeply nested
+ input caused a <code>StackOverflowError</code> since the parser
+ recursed once per nesting level with no limit. RFC 8259 allows
+ implementations to set a limit on the maximum depth of nesting.
+ (remm)
+ </fix>
</changelog>
</subsection>
<subsection name="Web applications">
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 18/23: Fix broken SNI fallback for ClientHello values larger than the parse limit
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 11f3036036b64162ac7cf7eb1d7a740978633a7d
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 13:58:52 2026 +0200
Fix broken SNI fallback for ClientHello values larger than the parse limit
When a ClientHello could not be buffered within sniParseLimit,
processSNI() selected the default SSLHostConfig but then still called
the TLSClientHelloExtractor getters, which throw IllegalStateException
for an incomplete extraction. The documented fallback therefore never
happened: the exception escaped to the socket processor's error
handler, logging an error and aborting the connection. Pass empty
lists for all extractor-derived values (requested ciphers, application
protocols, protocol versions, cipher names, supported groups and
signature schemes) through local variables on the UNDERFLOW path
instead of consulting the extractor, which also prevents NPEs on the
unguarded isEmpty()/contains() usages in createSSLEngine(). Add a test
that sends an oversized ClientHello record and asserts the default-host
fallback is logged without an IllegalStateException.
---
.../apache/tomcat/util/net/SecureNioChannel.java | 16 +++++--
.../tomcat/util/net/TestLargeClientHello.java | 56 ++++++++++++++++++++++
2 files changed, 68 insertions(+), 4 deletions(-)
diff --git a/java/org/apache/tomcat/util/net/SecureNioChannel.java b/java/org/apache/tomcat/util/net/SecureNioChannel.java
index 4fe0ac58a7..b746f54d14 100644
--- a/java/org/apache/tomcat/util/net/SecureNioChannel.java
+++ b/java/org/apache/tomcat/util/net/SecureNioChannel.java
@@ -293,6 +293,8 @@ public class SecureNioChannel extends NioChannel {
String hostName = null;
List<Cipher> clientRequestedCiphers = null;
List<String> clientRequestedApplicationProtocols = null;
+ List<String> clientRequestedProtocols = null;
+ List<String> clientRequestedCipherNames = null;
List<Group> clientSupportedGroups = null;
List<SignatureScheme> clientSignatureSchemes = null;
switch (extractor.getResult()) {
@@ -303,6 +305,8 @@ public class SecureNioChannel extends NioChannel {
//$FALL-THROUGH$ to set the client requested ciphers
case NOT_PRESENT:
clientRequestedCiphers = extractor.getClientRequestedCiphers();
+ clientRequestedProtocols = extractor.getClientRequestedProtocols();
+ clientRequestedCipherNames = extractor.getClientRequestedCipherNames();
clientSupportedGroups = extractor.getClientSupportedGroups();
clientSignatureSchemes = extractor.getClientSignatureSchemes();
break;
@@ -315,6 +319,11 @@ public class SecureNioChannel extends NioChannel {
}
hostName = endpoint.getDefaultSSLHostConfigName();
clientRequestedCiphers = Collections.emptyList();
+ clientRequestedApplicationProtocols = Collections.emptyList();
+ clientRequestedProtocols = Collections.emptyList();
+ clientRequestedCipherNames = Collections.emptyList();
+ clientSupportedGroups = Collections.emptyList();
+ clientSignatureSchemes = Collections.emptyList();
break;
case NON_SECURE:
netOutBuffer.clear();
@@ -329,13 +338,12 @@ public class SecureNioChannel extends NioChannel {
}
createSSLEngine(hostName, clientRequestedCiphers, clientRequestedApplicationProtocols,
- extractor.getClientRequestedProtocols(), clientSupportedGroups, clientSignatureSchemes);
+ clientRequestedProtocols, clientSupportedGroups, clientSignatureSchemes);
// Populate additional TLS attributes obtained from the handshake that
// aren't available from the session
- additionalTlsAttributes.put(SSLSupport.REQUESTED_PROTOCOL_VERSIONS_KEY,
- extractor.getClientRequestedProtocols());
- additionalTlsAttributes.put(SSLSupport.REQUESTED_CIPHERS_KEY, extractor.getClientRequestedCipherNames());
+ additionalTlsAttributes.put(SSLSupport.REQUESTED_PROTOCOL_VERSIONS_KEY, clientRequestedProtocols);
+ additionalTlsAttributes.put(SSLSupport.REQUESTED_CIPHERS_KEY, clientRequestedCipherNames);
// Ensure the application buffers (which have to be created earlier) are
// big enough.
diff --git a/test/org/apache/tomcat/util/net/TestLargeClientHello.java b/test/org/apache/tomcat/util/net/TestLargeClientHello.java
index 0089077d8f..a0dd7e092d 100644
--- a/test/org/apache/tomcat/util/net/TestLargeClientHello.java
+++ b/test/org/apache/tomcat/util/net/TestLargeClientHello.java
@@ -18,6 +18,9 @@
package org.apache.tomcat.util.net;
import java.io.File;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
import java.util.logging.Level;
import javax.net.ssl.SSLContext;
@@ -88,4 +91,57 @@ public class TestLargeClientHello extends TomcatBaseTest {
}
}
+
+ @Test
+ public void testClientHelloLargerThanParseLimit() throws Exception {
+ Tomcat tomcat = getTomcatInstance();
+
+ Context ctx = getProgrammaticRootContext();
+ Tomcat.addServlet(ctx, "hello", new HelloWorldServlet());
+ ctx.addServletMapping("/", "hello");
+
+ TesterSupport.initSsl(tomcat);
+
+ // Limit SNI parsing to less than SecureNioChannel.DEFAULT_NET_BUFFER_SIZE (16921) so that the
+ // buffer cannot be expanded to hold the record below and SNI extraction returns UNDERFLOW. The
+ // endpoint must then fall back to the default SSLHostConfig rather than failing with an
+ // IllegalStateException from the extractor getters.
+ tomcat.getConnector().setProperty("sniParseLimit", "1024");
+
+ // A handshake record with a declared length far larger than sniParseLimit
+ byte[] record = new byte[65537];
+ record[0] = 22; // Handshake content type
+ record[1] = 3;
+ record[2] = 3;
+ record[3] = (byte) 0xFF; // Record length of 65535
+ record[4] = (byte) 0xFF;
+ record[5] = 1; // ClientHello handshake type
+
+ try (LogCapture logCapture = attachLogCapture(Level.FINE, "org.apache.tomcat.util.net");
+ Socket socket = new Socket()) {
+
+ tomcat.start();
+
+ socket.connect(new InetSocketAddress("localhost", getPort()));
+ OutputStream out = socket.getOutputStream();
+ out.write(record);
+ out.flush();
+
+ String sniDefault = TomcatBaseTest.getKeyFromPropertiesFile("org.apache.tomcat.util.net",
+ "channel.nio.ssl.sniDefault");
+
+ // The fallback to the default SSLHostConfig is logged once the extraction returns UNDERFLOW
+ long start = System.currentTimeMillis();
+ while (!logCapture.containsText(sniDefault) && System.currentTimeMillis() - start < 10000) {
+ Thread.sleep(100);
+ }
+ Assert.assertTrue("Fallback to the default SSLHostConfig was not logged",
+ logCapture.containsText(sniDefault));
+
+ // Give any error triggered after the fallback a chance to be logged, then confirm that the
+ // extractor getters were not called on the incomplete extraction result
+ Thread.sleep(500);
+ Assert.assertFalse(logCapture.hasException(IllegalStateException.class));
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 16/23: Shut down the utility executor created by an endpoint
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit f39de7a28e08ae6ab4f3ee4d139b4e023b01e6e3
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 12:36:30 2026 +0200
Shut down the utility executor created by an endpoint
When no utility executor was set, AbstractEndpoint.getUtilityExecutor()
created one lazily. The executor was never shut down when the endpoint
was destroyed, so the non-daemon core thread of the scheduled thread
pool it created kept running and prevented JVM shutdown, which is
reachable for embedded users that do not provide the shared executor a
full server installs from StandardServer.
Track an executor created by the endpoint and shut it down in
destroy(). Externally provided executors remain owned by the caller and
are left running. The field is made volatile so the creation is safely
published. Concurrent lazy creation remains an unsynchronised
check-then-act: the executor is only requested from the protocol
lifecycle start and from the scheduled async timeout task, which are
serialised per endpoint, the same tolerated pattern already used for
the connection latch in this class, and a duplicated pool in the
hypothetical concurrent case would at worst match the previous
behaviour since callers schedule tasks on the instance they received.
The executor reference is deliberately not cleared on destroy so a
later call returns the shut-down instance rather than creating a new
pool that would never be destroyed.
---
.../apache/tomcat/util/net/AbstractEndpoint.java | 33 +++++++++--
.../tomcat/util/net/TestAbstractEndpoint.java | 68 ++++++++++++++++++++++
2 files changed, 95 insertions(+), 6 deletions(-)
diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
index 0e7f0150cc..3a9335085d 100644
--- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java
+++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
@@ -1116,9 +1116,19 @@ public abstract class AbstractEndpoint<S, U> {
/**
- * External Executor based thread pool for utility tasks.
+ * External Executor based thread pool for utility tasks. Lazily created
+ * when none was set. The race between concurrent creators is benign: the
+ * executor is only requested from the protocol lifecycle and the
+ * scheduled async-timeout task, both serialised per endpoint, and a
+ * duplicated pool would at worst replicate the pre-existing behaviour.
*/
- private ScheduledExecutorService utilityExecutor = null;
+ private volatile ScheduledExecutorService utilityExecutor = null;
+
+ /**
+ * The utility executor created by this endpoint, if any. It is tracked so
+ * that it can be shut down when the endpoint is destroyed.
+ */
+ private volatile ScheduledExecutorService createdUtilityExecutor = null;
/**
* Set the utility executor.
@@ -1130,16 +1140,19 @@ public abstract class AbstractEndpoint<S, U> {
}
/**
- * Get the utility executor.
+ * Get the utility executor, creating one if none has been set.
*
* @return The utility executor
*/
public ScheduledExecutorService getUtilityExecutor() {
- if (utilityExecutor == null) {
+ ScheduledExecutorService result = utilityExecutor;
+ if (result == null) {
getLog().warn(sm.getString("endpoint.warn.noUtilityExecutor"));
- utilityExecutor = new ScheduledThreadPoolExecutor(1);
+ result = new ScheduledThreadPoolExecutor(1);
+ utilityExecutor = result;
+ createdUtilityExecutor = result;
}
- return utilityExecutor;
+ return result;
}
@@ -2357,6 +2370,14 @@ public abstract class AbstractEndpoint<S, U> {
* @throws java.lang.Exception If an error occurs while destroying the endpoint
*/
public final void destroy() throws Exception {
+ ScheduledExecutorService created = createdUtilityExecutor;
+ if (created != null) {
+ createdUtilityExecutor = null;
+ created.shutdown();
+ }
+ // Deliberately leave utilityExecutor set: later calls to
+ // getUtilityExecutor() then return the shut-down executor instead of
+ // creating a new pool that would never be destroyed
if (bindState == BindState.BOUND_ON_INIT) {
unbind();
bindState = BindState.UNBOUND;
diff --git a/test/org/apache/tomcat/util/net/TestAbstractEndpoint.java b/test/org/apache/tomcat/util/net/TestAbstractEndpoint.java
new file mode 100644
index 0000000000..055cab7512
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/TestAbstractEndpoint.java
@@ -0,0 +1,68 @@
+/*
+ * 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.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestAbstractEndpoint {
+
+ @Test
+ public void testUtilityExecutorIsCreatedOnce() throws Exception {
+ NioEndpoint endpoint = new NioEndpoint();
+ try {
+ ScheduledExecutorService executor = endpoint.getUtilityExecutor();
+ Assert.assertNotNull(executor);
+ Assert.assertSame(executor, endpoint.getUtilityExecutor());
+ } finally {
+ endpoint.destroy();
+ }
+ }
+
+ @Test
+ public void testCreatedUtilityExecutorIsShutDownOnDestroy() throws Exception {
+ NioEndpoint endpoint = new NioEndpoint();
+ ScheduledExecutorService executor = endpoint.getUtilityExecutor();
+ Assert.assertFalse(executor.isShutdown());
+
+ endpoint.destroy();
+
+ Assert.assertTrue(executor.isShutdown());
+ // A later request must not resurrect an unmanaged pool
+ Assert.assertSame(executor, endpoint.getUtilityExecutor());
+ }
+
+ @Test
+ public void testExternalUtilityExecutorIsNotShutDownOnDestroy() throws Exception {
+ NioEndpoint endpoint = new NioEndpoint();
+ ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+ endpoint.setUtilityExecutor(executor);
+ try {
+ Assert.assertSame(executor, endpoint.getUtilityExecutor());
+
+ endpoint.destroy();
+
+ // Externally provided executors are owned by the caller
+ Assert.assertFalse(executor.isShutdown());
+ } finally {
+ executor.shutdown();
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 12/23: Convert numeric and character values in the JMX proxy conversion helper
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 0ac94dda10e2a0de77877735d7faa69ab5bacb1a
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 11:45:40 2026 +0200
Convert numeric and character values in the JMX proxy conversion helper
Registry.convertValue() only mapped String, ObjectName, int/Integer,
long/Long and boolean/Boolean type names. Every other type name fell
through and the raw String was returned. Its sole consumer, the JMX
proxy servlet of the Manager application, passes the returned value
directly to MBeanServer.setAttribute() and invoke(), and the modeler
MBeans apply no conversion of their own, so setting an attribute or
invoking an operation with a double, float, short, byte or char
parameter on any modeler MBean always failed with an argument type
mismatch.
Convert the missing floating point and narrow integral types using the
wrapper valueOf methods, giving the same decimal range and format
checking the existing integral branches already provide, and convert
the first character of a non-empty string for the character types. A
null or empty string is left unconverted rather than introducing a new
exception path.
---
java/org/apache/tomcat/util/modeler/Registry.java | 12 +++
.../util/modeler/TestRegistryConvertValue.java | 100 +++++++++++++++++++++
2 files changed, 112 insertions(+)
diff --git a/java/org/apache/tomcat/util/modeler/Registry.java b/java/org/apache/tomcat/util/modeler/Registry.java
index f74885498f..2bae09dfd6 100644
--- a/java/org/apache/tomcat/util/modeler/Registry.java
+++ b/java/org/apache/tomcat/util/modeler/Registry.java
@@ -551,6 +551,18 @@ public class Registry implements RegistryMBean, MBeanRegistration {
objValue = Long.valueOf(value);
} else if ("java.lang.Boolean".equals(type) || "boolean".equals(type)) {
objValue = Boolean.valueOf(value);
+ } else if ("java.lang.Double".equals(type) || "double".equals(type)) {
+ objValue = Double.valueOf(value);
+ } else if ("java.lang.Float".equals(type) || "float".equals(type)) {
+ objValue = Float.valueOf(value);
+ } else if ("java.lang.Short".equals(type) || "short".equals(type)) {
+ objValue = Short.valueOf(value);
+ } else if ("java.lang.Byte".equals(type) || "byte".equals(type)) {
+ objValue = Byte.valueOf(value);
+ } else if (("java.lang.Character".equals(type) || "char".equals(type)) &&
+ value != null && !value.isEmpty()) {
+ // A null or empty string cannot be converted to a character
+ objValue = Character.valueOf(value.charAt(0));
}
return objValue;
}
diff --git a/test/org/apache/tomcat/util/modeler/TestRegistryConvertValue.java b/test/org/apache/tomcat/util/modeler/TestRegistryConvertValue.java
new file mode 100644
index 0000000000..bf3ffce1fb
--- /dev/null
+++ b/test/org/apache/tomcat/util/modeler/TestRegistryConvertValue.java
@@ -0,0 +1,100 @@
+/*
+ * 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.modeler;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestRegistryConvertValue {
+
+ private final Registry registry = new Registry();
+
+ @Test
+ public void testConvertStringTypes() {
+ Assert.assertEquals("value", registry.convertValue(null, "value"));
+ Assert.assertEquals("value", registry.convertValue("java.lang.String", "value"));
+ }
+
+ @Test
+ public void testConvertIntegerTypes() {
+ Assert.assertEquals(Integer.valueOf(7), registry.convertValue("int", "7"));
+ Assert.assertEquals(Integer.valueOf(7), registry.convertValue("java.lang.Integer", "7"));
+ Assert.assertEquals(Long.valueOf(7), registry.convertValue("long", "7"));
+ Assert.assertEquals(Long.valueOf(7), registry.convertValue("java.lang.Long", "7"));
+ Assert.assertEquals(Short.valueOf((short) 7), registry.convertValue("short", "7"));
+ Assert.assertEquals(Short.valueOf((short) 7), registry.convertValue("java.lang.Short", "7"));
+ Assert.assertEquals(Byte.valueOf((byte) 7), registry.convertValue("byte", "7"));
+ Assert.assertEquals(Byte.valueOf((byte) 7), registry.convertValue("java.lang.Byte", "7"));
+ }
+
+ @Test
+ public void testConvertFloatingPointTypes() {
+ Assert.assertEquals(Double.valueOf(1.5), registry.convertValue("double", "1.5"));
+ Assert.assertEquals(Double.valueOf(1.5), registry.convertValue("java.lang.Double", "1.5"));
+ Assert.assertEquals(Float.valueOf(1.5f), registry.convertValue("float", "1.5"));
+ Assert.assertEquals(Float.valueOf(1.5f), registry.convertValue("java.lang.Float", "1.5"));
+ }
+
+ @Test
+ public void testConvertBooleanType() {
+ Assert.assertEquals(Boolean.TRUE, registry.convertValue("boolean", "true"));
+ Assert.assertEquals(Boolean.TRUE, registry.convertValue("java.lang.Boolean", "true"));
+ }
+
+ @Test
+ public void testConvertCharacterType() {
+ Assert.assertEquals(Character.valueOf('x'), registry.convertValue("char", "x"));
+ Assert.assertEquals(Character.valueOf('x'), registry.convertValue("java.lang.Character", "x"));
+ }
+
+ @Test
+ public void testConvertEmptyStringToCharacterIsNotConverted() {
+ // An empty string has no character to convert to
+ Object result = registry.convertValue("char", "");
+ Assert.assertEquals("", result);
+ }
+
+ @Test
+ public void testConvertCharacterTypeTruncatesLongerString() {
+ // The first character is the only one that can be converted
+ Assert.assertEquals(Character.valueOf('x'), registry.convertValue("char", "xy"));
+ }
+
+ @Test
+ public void testConvertNullValueIsNotConvertedForCharacter() {
+ // Only the character type leaves a null value unconverted rather than
+ // throwing, matching the historical behavior for this branch
+ Assert.assertNull(registry.convertValue("char", null));
+ }
+
+ @Test
+ public void testConvertNumericBoundaries() {
+ Assert.assertEquals(Short.valueOf(Short.MAX_VALUE),
+ registry.convertValue("short", "32767"));
+ Assert.assertEquals(Byte.valueOf(Byte.MAX_VALUE),
+ registry.convertValue("byte", "127"));
+ }
+
+ @Test
+ public void testConvertInvalidNumberThrows() {
+ Assert.assertThrows(NumberFormatException.class, () -> registry.convertValue("int", "abc"));
+ Assert.assertThrows(NumberFormatException.class, () -> registry.convertValue("double", "abc"));
+ Assert.assertThrows(NumberFormatException.class, () -> registry.convertValue("float", "abc"));
+ Assert.assertThrows(NumberFormatException.class, () -> registry.convertValue("short", "70000"));
+ Assert.assertThrows(NumberFormatException.class, () -> registry.convertValue("byte", "300"));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 08/23: Invalidate the cached MBeanInfo when attributes or operations are added
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit a9dd54e41216383bd71e4bc952005e1baff20c54
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 15 10:49:05 2026 +0200
Invalidate the cached MBeanInfo when attributes or operations are added
ManagedBean caches the MBeanInfo it builds and every other mutator
(setName, setClassName, setDescription, setType, addNotification)
nulls the cache under the mBeanInfoLock write lock, but addAttribute
and addOperation did neither. Adding an attribute or operation after
getMBeanInfo() had been called would therefore keep serving a stale
MBeanInfo, and writing the descriptor maps without the lock was also
inconsistent with getMBeanInfo() reading them under the write lock.
Make both methods mirror addNotification: hold the write lock, update
the map, and clear the cached info. Add a unit test that verifies the
MBeanInfo is rebuilt and contains the newly added descriptor.
---
.../apache/tomcat/util/modeler/ManagedBean.java | 16 +++-
.../tomcat/util/modeler/TestManagedBean.java | 89 ++++++++++++++++++++++
2 files changed, 103 insertions(+), 2 deletions(-)
diff --git a/java/org/apache/tomcat/util/modeler/ManagedBean.java b/java/org/apache/tomcat/util/modeler/ManagedBean.java
index 7236e5d522..5f1c84a454 100644
--- a/java/org/apache/tomcat/util/modeler/ManagedBean.java
+++ b/java/org/apache/tomcat/util/modeler/ManagedBean.java
@@ -302,7 +302,13 @@ public class ManagedBean implements java.io.Serializable {
* @param attribute The new attribute descriptor
*/
public void addAttribute(AttributeInfo attribute) {
- attributes.put(attribute.getName(), attribute);
+ mBeanInfoLock.writeLock().lock();
+ try {
+ attributes.put(attribute.getName(), attribute);
+ this.info = null;
+ } finally {
+ mBeanInfoLock.writeLock().unlock();
+ }
}
@@ -331,7 +337,13 @@ public class ManagedBean implements java.io.Serializable {
* @param operation The new operation descriptor
*/
public void addOperation(OperationInfo operation) {
- operations.put(createOperationKey(operation), operation);
+ mBeanInfoLock.writeLock().lock();
+ try {
+ operations.put(createOperationKey(operation), operation);
+ this.info = null;
+ } finally {
+ mBeanInfoLock.writeLock().unlock();
+ }
}
diff --git a/test/org/apache/tomcat/util/modeler/TestManagedBean.java b/test/org/apache/tomcat/util/modeler/TestManagedBean.java
new file mode 100644
index 0000000000..1ff7bd0305
--- /dev/null
+++ b/test/org/apache/tomcat/util/modeler/TestManagedBean.java
@@ -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.tomcat.util.modeler;
+
+import javax.management.MBeanAttributeInfo;
+import javax.management.MBeanInfo;
+import javax.management.MBeanOperationInfo;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestManagedBean {
+
+ @Test
+ public void testAddAttributeInvalidatesCachedMBeanInfo() {
+ ManagedBean managedBean = new ManagedBean();
+ managedBean.setName("Tester");
+ managedBean.setType("Tester");
+
+ AttributeInfo first = new AttributeInfo();
+ first.setName("first");
+ first.setDescription("First attribute");
+ first.setType("java.lang.String");
+ managedBean.addAttribute(first);
+
+ MBeanInfo info = managedBean.getMBeanInfo();
+ int baseCount = info.getAttributes().length;
+
+ AttributeInfo second = new AttributeInfo();
+ second.setName("second");
+ second.setDescription("Second attribute");
+ second.setType("java.lang.String");
+ managedBean.addAttribute(second);
+
+ MBeanInfo updated = managedBean.getMBeanInfo();
+ Assert.assertNotSame(info, updated);
+ Assert.assertEquals(baseCount + 1, updated.getAttributes().length);
+ boolean found = false;
+ for (MBeanAttributeInfo attributeInfo : updated.getAttributes()) {
+ found |= "second".equals(attributeInfo.getName());
+ }
+ Assert.assertTrue(found);
+ }
+
+ @Test
+ public void testAddOperationInvalidatesCachedMBeanInfo() {
+ ManagedBean managedBean = new ManagedBean();
+ managedBean.setName("Tester");
+ managedBean.setType("Tester");
+
+ OperationInfo first = new OperationInfo();
+ first.setName("first");
+ first.setDescription("First operation");
+ first.setReturnType("void");
+ managedBean.addOperation(first);
+
+ MBeanInfo info = managedBean.getMBeanInfo();
+ Assert.assertEquals(1, info.getOperations().length);
+
+ OperationInfo second = new OperationInfo();
+ second.setName("second");
+ second.setDescription("Second operation");
+ second.setReturnType("void");
+ managedBean.addOperation(second);
+
+ MBeanInfo updated = managedBean.getMBeanInfo();
+ Assert.assertNotSame(info, updated);
+ Assert.assertEquals(2, updated.getOperations().length);
+ boolean found = false;
+ for (MBeanOperationInfo operationInfo : updated.getOperations()) {
+ found |= "second".equals(operationInfo.getName());
+ }
+ Assert.assertTrue(found);
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org