From 218108b7e8097ac40db22340263c34c7426548ce Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 03 Aug 2026 20:51:08 +0000
Subject: [PATCH] [#809] Fix DSML gateway NPE on abandonRequest and on missing Content-Type (#811)
---
opendj-dsml-servlet/src/main/java/org/opends/dsml/protocol/DSMLServlet.java | 124 ++++++++++++++++++++++++++++-------------
1 files changed, 84 insertions(+), 40 deletions(-)
diff --git a/opendj-dsml-servlet/src/main/java/org/opends/dsml/protocol/DSMLServlet.java b/opendj-dsml-servlet/src/main/java/org/opends/dsml/protocol/DSMLServlet.java
index 46630f4..8163dba 100644
--- a/opendj-dsml-servlet/src/main/java/org/opends/dsml/protocol/DSMLServlet.java
+++ b/opendj-dsml-servlet/src/main/java/org/opends/dsml/protocol/DSMLServlet.java
@@ -42,8 +42,6 @@
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.logging.Level;
-import java.util.logging.Logger;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
@@ -106,6 +104,12 @@
* It parses the SOAP request, calls the appropriate class
* which performs the LDAP operation, and returns the response
* as a DSML response.
+ * <p>
+ * Everything is logged through {@code getServletContext().log()}: it is the
+ * only sink which survives at runtime, as
+ * {@code LDAPConnection.connectToHost()} turns {@code java.util.logging} off
+ * for the whole JVM on every non-verbose connection, and the war ships
+ * {@code slf4j-api} without any provider.
*/
public class DSMLServlet extends HttpServlet {
private static final String PKG_NAME = "org.opends.dsml.protocol";
@@ -165,6 +169,9 @@
*/
@Override
public void init(ServletConfig config) throws ServletException {
+ // Let GenericServlet keep the configuration: getServletContext() relies on
+ // it, and it is the only logging facility available at runtime.
+ super.init(config);
try {
hostName = stringValue(config, HOST);
port = Integer.valueOf(stringValue(config, PORT));
@@ -333,7 +340,6 @@
connOptions.setUseSSL(useSSL);
connOptions.setStartTLS(useStartTLS);
- LDAPConnection connection = null;
BatchRequest batchRequest = null;
// Keep the Servlet input stream buffered in case the SOAP un-marshalling
@@ -400,9 +406,8 @@
messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL);
messageContentType = SOAPConstants.SOAP_1_2_CONTENT_TYPE;
}
- else {
- throw new ServletException("Content-Type does not match SOAP 1.1 or SOAP 1.2");
- }
+ // An unsupported Content-Type leaves the message factory unset: the
+ // request is rejected as malformed once all the headers are read.
}
catch (SOAPException e)
{
@@ -430,12 +435,14 @@
bindPassword = unencoded.substring(colon + 1);
}
} catch (final LocalizedIllegalArgumentException ex) {
- // user/DN:password parsing error
+ // user/DN:password parsing error. Keep reading the headers: the
+ // Content-Type may still be ahead, and it decides which SOAP
+ // version the error is reported with.
batchResponses.add(
createErrorResponse(objFactory,
new LDAPException(LDAPResultCode.INVALID_CREDENTIALS,
LocalizableMessage.raw(ex.getMessage()))));
- break;
+ continue;
}
}
StringTokenizer tk = new StringTokenizer(headerVal, ",");
@@ -477,6 +484,31 @@
}
}
+ if ( messageFactory == null ) {
+ // The request carries no Content-Type header, or one which matches
+ // neither SOAP 1.1 nor SOAP 1.2: it cannot be parsed. Fall back to
+ // SOAP 1.1 for the response and reject the request as malformed,
+ // unless an error has already been reported.
+ try
+ {
+ messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_1_PROTOCOL);
+ messageContentType = SOAPConstants.SOAP_1_1_CONTENT_TYPE;
+ }
+ catch (SOAPException e)
+ {
+ throw new ServletException(e.getMessage());
+ }
+ if ( batchResponses.isEmpty() ) {
+ // Nothing has been read from the stream yet, so the SAX pass can still
+ // recover the requestID and let the client correlate the reply.
+ batchResponses.add(
+ createXMLParsingErrorResponse(is,
+ objFactory,
+ batchResponse,
+ "Content-Type does not match SOAP 1.1 or SOAP 1.2"));
+ }
+ }
+
// if an error already occurred, the list is not empty
if ( batchResponses.isEmpty() ) {
try {
@@ -520,6 +552,13 @@
boolean authzInControl = false;
batchRequest = batchRequestElement.getValue();
+ // The connection options are shared by all the batch requests of this
+ // SOAP body, so the authzid of the previous one must not survive into
+ // the bind of this one: it would run under an authorization identity
+ // it never asked for, and addSASLProperty() appends to the values of
+ // a key, which SASL PLAIN rejects as a multi-valued authzid.
+ connOptions.getSASLProperties().remove("authzid");
+
/*
* Process optional authRequest (i.e. use authz)
*/
@@ -541,10 +580,12 @@
boolean connected = false;
- if ( connection == null ) {
- connection = new LDAPConnection(hostName, port, connOptions);
+ // Each batch request gets its own connection: the previous one has
+ // been closed by the finally block below.
+ LDAPConnection connection =
+ new LDAPConnection(hostName, port, connOptions);
+ try {
try {
-
connection.connectToHost(bindDN, bindPassword);
if (authzInControl)
{
@@ -565,35 +606,37 @@
// if connection failed, return appropriate error response
batchResponses.add(createErrorResponse(objFactory, e));
}
- }
- if ( connected ) {
- List<DsmlMessage> list = batchRequest.getBatchRequests();
+ if ( connected ) {
+ List<DsmlMessage> list = batchRequest.getBatchRequests();
- for (DsmlMessage request : list) {
- JAXBElement<?> result = performLDAPRequest(connection, objFactory, proxyAuthzControl, request);
- if ( result != null ) {
- batchResponses.add(result);
- }
- // evaluate response to check if an error occurred
- Object o = result.getValue();
- if ( o instanceof ErrorResponse ) {
- if ( ON_ERROR_EXIT.equals(batchRequest.getOnError()) ) {
- break;
+ for (DsmlMessage request : list) {
+ JAXBElement<?> result = performLDAPRequest(connection, objFactory, proxyAuthzControl, request);
+ if ( result == null ) {
+ // an abandon request does not produce any response element
+ continue;
}
- } else if ( o instanceof LDAPResult ) {
- int code = ((LDAPResult)o).getResultCode().getCode();
- if ( code != LDAPResultCode.SUCCESS
- && code != LDAPResultCode.REFERRAL
- && code != LDAPResultCode.COMPARE_TRUE
- && code != LDAPResultCode.COMPARE_FALSE && ON_ERROR_EXIT.equals(batchRequest.getOnError()) )
- {
- break;
+ batchResponses.add(result);
+ // evaluate response to check if an error occurred
+ Object o = result.getValue();
+ if ( o instanceof ErrorResponse ) {
+ if ( ON_ERROR_EXIT.equals(batchRequest.getOnError()) ) {
+ break;
+ }
+ } else if ( o instanceof LDAPResult ) {
+ int code = ((LDAPResult)o).getResultCode().getCode();
+ if ( code != LDAPResultCode.SUCCESS
+ && code != LDAPResultCode.REFERRAL
+ && code != LDAPResultCode.COMPARE_TRUE
+ && code != LDAPResultCode.COMPARE_FALSE && ON_ERROR_EXIT.equals(batchRequest.getOnError()) )
+ {
+ break;
+ }
}
}
}
- }
- // close connection to LDAP server
- if ( connection != null ) {
+ } finally {
+ // close connection to LDAP server, whatever happened while
+ // processing the batch
connection.close(nextMessageID);
}
}
@@ -604,7 +647,8 @@
marshaller.marshal(objFactory.createBatchResponse(batchResponse), doc);
sendResponse(doc, messageFactory, messageContentType, res);
} catch (Exception e) {
- e.printStackTrace();
+ // The client gets an empty response: at least make the cause visible.
+ getServletContext().log("Unable to send the DSML response", e);
}
}
@@ -628,14 +672,14 @@
{
if (logFeatureWarnings.compareAndSet(false, true))
{
- Logger.getLogger(PKG_NAME).log(Level.SEVERE, "XMLReader unsupported feature " + feature);
+ getServletContext().log("XMLReader unsupported feature " + feature);
}
}
catch (SAXNotRecognizedException e)
{
if (logFeatureWarnings.compareAndSet(false, true))
{
- Logger.getLogger(PKG_NAME).log(Level.SEVERE, "XMLReader unrecognized feature " + feature);
+ getServletContext().log("XMLReader unrecognized feature " + feature);
}
}
}
@@ -893,7 +937,7 @@
catch (ParserConfigurationException e) {
if (logFeatureWarnings.compareAndSet(false, true))
{
- Logger.getLogger(PKG_NAME).log(Level.SEVERE, "DocumentBuilderFactory unsupported feature " + feature);
+ getServletContext().log("DocumentBuilderFactory unsupported feature " + feature);
}
}
}
@@ -916,7 +960,7 @@
catch (ParserConfigurationException e)
{
if (logFeatureWarnings.compareAndSet(false, true)) {
- Logger.getLogger(PKG_NAME).log(Level.SEVERE, "DocumentBuilderFactory cannot be configured securely");
+ getServletContext().log("DocumentBuilderFactory cannot be configured securely");
}
}
dbf.setXIncludeAware(false);
--
Gitblit v1.10.0