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