From 6c0a88c2f3a42909784aa94797316d151af649e9 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 05 Aug 2026 08:31:37 +0000
Subject: [PATCH] [#824] Answer each batchRequest of a SOAP body with its own batchResponse (#836)
---
opendj-dsml-servlet/src/main/java/org/opends/dsml/protocol/DSMLServlet.java | 140 ++++++++++++++++++++++++++++++++--------------
1 files changed, 96 insertions(+), 44 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 74ea5da..df2ba81 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
@@ -406,14 +406,17 @@
is.mark(65536);
}
- // Create response in the beginning as it might be used if the parsing
- // fails.
+ // This prologue batchResponse answers everything detected before the
+ // SOAP body is walked (credentials errors, unparseable XML): those
+ // errors are built before any batchRequest is known. Each batchRequest
+ // of the body gets a batchResponse of its own, collected in responses
+ // below.
ObjectFactory objFactory = new ObjectFactory();
- BatchResponse batchResponse = objFactory.createBatchResponse();
- List<JAXBElement<?>> batchResponses = batchResponse.getBatchResponses();
+ BatchResponse prologueResponse = objFactory.createBatchResponse();
+ List<JAXBElement<?>> prologueResponses = prologueResponse.getBatchResponses();
- // Thi sis only used for building the response
- Document doc = createSafeDocument();
+ // One batchResponse per batchRequest of the SOAP body, in request order.
+ List<BatchResponse> responses = new ArrayList<>();
MessageFactory messageFactory = null;
String messageContentType = null;
@@ -428,7 +431,7 @@
}
catch(SSLConnectionException e)
{
- batchResponses.add(
+ prologueResponses.add(
createErrorResponse(objFactory,
new LDAPException(LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR,
LocalizableMessage.raw(
@@ -493,7 +496,7 @@
// 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(
+ prologueResponses.add(
createErrorResponse(objFactory,
new LDAPException(LDAPResultCode.INVALID_CREDENTIALS,
LocalizableMessage.raw(ex.getMessage()))));
@@ -517,7 +520,7 @@
}
else
{
- batchResponses.add(
+ prologueResponses.add(
createErrorResponse(objFactory,
new LDAPException(LDAPResultCode.INVALID_CREDENTIALS,
LocalizableMessage.raw("Invalid configured credentials."))));
@@ -531,19 +534,19 @@
} else {
// otherwise if DN or password is null, send back an error
if (((!authenticationIsID && bindDN == null) || bindPassword == null)
- && batchResponses.isEmpty()) {
- batchResponses.add(
+ && prologueResponses.isEmpty()) {
+ prologueResponses.add(
createErrorResponse(objFactory,
new LDAPException(LDAPResultCode.INVALID_CREDENTIALS,
LocalizableMessage.raw("Unable to retrieve credentials."))));
}
}
- if ( batchResponses.isEmpty() && req.getContentLengthLong() > requestMaxSize ) {
+ if ( prologueResponses.isEmpty() && req.getContentLengthLong() > requestMaxSize ) {
// The declared size already exceeds the cap: reject the request before
// anything reads the stream — the malformed Content-Type fallback below
// SAX-parses the whole body to recover the requestID.
- batchResponses.add(createErrorResponse(objFactory, requestSizeExceeded()));
+ prologueResponses.add(createErrorResponse(objFactory, requestSizeExceeded()));
}
if ( messageFactory == null ) {
@@ -560,36 +563,36 @@
{
throw new ServletException(e.getMessage());
}
- if ( batchResponses.isEmpty() ) {
+ if ( prologueResponses.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(
+ prologueResponses.add(
createXMLParsingErrorResponse(is,
objFactory,
- batchResponse,
+ prologueResponse,
"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() ) {
+ if ( prologueResponses.isEmpty() ) {
try {
SOAPMessage message = messageFactory.createMessage(mimeHeaders, is);
soapBody = message.getSOAPBody();
} catch (SOAPException ex) {
// SOAP was unable to parse XML successfully
- batchResponses.add(cappedStream.isLimitExceeded()
+ prologueResponses.add(cappedStream.isLimitExceeded()
? createErrorResponse(objFactory, requestSizeExceeded())
: createXMLParsingErrorResponse(is,
objFactory,
- batchResponse,
+ prologueResponse,
String.valueOf(ex.getCause())));
} catch (IOException ex) {
if ( ! cappedStream.isLimitExceeded() ) {
throw ex;
}
// The body streamed past the cap: chunked, or a lying Content-Length.
- batchResponses.add(createErrorResponse(objFactory, requestSizeExceeded()));
+ prologueResponses.add(createErrorResponse(objFactory, requestSizeExceeded()));
}
}
@@ -607,28 +610,45 @@
// (MAX_BATCH_REQUESTS), before the element is even schema-validated.
// The cap is counted over all the elements of the body, whatever
// their type, and its configured value is not echoed to the
- // unauthenticated client.
- batchResponses.add(createErrorResponse(objFactory,
- new LDAPException(LDAPResultCode.UNWILLING_TO_PERFORM,
- LocalizableMessage.raw("The SOAP body holds more elements than the configured"
- + " maximum: the remaining elements were not attempted."))));
+ // unauthenticated client. The error joins the last answered
+ // batchResponse rather than forming a root of its own: DSMLv2
+ // expects a single batchResponse per SOAP body, and under the
+ // default cap of one a separate root would give every default
+ // deployment a two-root reply. The cap is validated positive, so
+ // at least one element was answered before it could be exceeded.
+ responses.get(responses.size() - 1).getBatchResponses().add(
+ createErrorResponse(objFactory,
+ new LDAPException(LDAPResultCode.UNWILLING_TO_PERFORM,
+ LocalizableMessage.raw("The SOAP body holds more elements than the configured"
+ + " maximum: the remaining elements were not attempted."))));
break;
}
// Parse and unmarshall the SOAP object - the implementation prevents the use of a
// DOCTYPE and xincludes, so should be safe. There is no way to configure a more
// restrictive parser.
SOAPElement se = (SOAPElement) obj;
+
+ // Each batchRequest of the SOAP body is answered with its own
+ // batchResponse: a shared one would merge the elements of every
+ // batch request and keep only the last requestID.
+ BatchResponse elementResponse = objFactory.createBatchResponse();
+ List<JAXBElement<?>> elementResponses =
+ elementResponse.getBatchResponses();
+ responses.add(elementResponse);
+
JAXBElement<BatchRequest> batchRequestElement = null;
try {
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
unmarshaller.setSchema(schema);
batchRequestElement = unmarshaller.unmarshal(se, BatchRequest.class);
} catch (JAXBException e) {
- // schema validation failed
- batchResponses.add(createXMLParsingErrorResponse(is,
- objFactory,
- batchResponse,
- String.valueOf(e)));
+ // schema validation failed. The requestID is read from the element
+ // itself: the SAX pass of createXMLParsingErrorResponse() would
+ // recover the one of the first batchRequest of the body.
+ String requestID = se.getAttribute("requestID");
+ elementResponse.setRequestID(requestID.isEmpty() ? null : requestID);
+ elementResponses.add(
+ createMalformedRequestError(objFactory, String.valueOf(e)));
}
if ( batchRequestElement != null ) {
boolean authzInBind = false;
@@ -658,7 +678,7 @@
}
}
// set requestID in response
- batchResponse.setRequestID(batchRequest.getRequestID());
+ elementResponse.setRequestID(batchRequest.getRequestID());
org.opends.server.types.Control proxyAuthzControl = null;
boolean connected = false;
@@ -681,13 +701,13 @@
ResultCode code = ResultCodeFactory.create(objFactory,
LDAPResultCode.SUCCESS);
authResponse.setResultCode(code);
- batchResponses.add(
+ elementResponses.add(
objFactory.createBatchResponseAuthResponse(authResponse));
}
connected = true;
} catch (LDAPConnectionException e) {
// if connection failed, return appropriate error response
- batchResponses.add(createErrorResponse(objFactory, e));
+ elementResponses.add(createErrorResponse(objFactory, e));
}
if ( connected ) {
List<DsmlMessage> list = batchRequest.getBatchRequests();
@@ -698,7 +718,7 @@
// an abandon request does not produce any response element
continue;
}
- batchResponses.add(result);
+ elementResponses.add(result);
// evaluate response to check if an error occurred
Object o = result.getValue();
if ( o instanceof ErrorResponse ) {
@@ -726,9 +746,24 @@
}
}
try {
+ if ( !prologueResponses.isEmpty() || responses.isEmpty() ) {
+ // An error was detected before the SOAP body was walked, or the body
+ // holds no batchRequest at all: reply with the prologue batchResponse.
+ // The guards above keep both lists from being populated at once
+ // today; prepending keeps the prologue errors visible even if a
+ // future edit changes that.
+ responses.add(0, prologueResponse);
+ }
Marshaller marshaller = jaxbContext.createMarshaller();
- marshaller.marshal(objFactory.createBatchResponse(batchResponse), doc);
- sendResponse(doc, messageFactory, messageContentType, res);
+ List<Document> docs = new ArrayList<>(responses.size());
+ for (BatchResponse response : responses) {
+ // A DOM document has a single root element, so each batchResponse of
+ // the reply is marshalled into a document of its own.
+ Document doc = createSafeDocument();
+ marshaller.marshal(objFactory.createBatchResponse(response), doc);
+ docs.add(doc);
+ }
+ sendResponse(docs, messageFactory, messageContentType, res);
} catch (Exception e) {
// The client gets an empty response: at least make the cause visible.
getServletContext().log("Unable to send the DSML response", e);
@@ -787,7 +822,6 @@
ObjectFactory objFactory,
BatchResponse batchResponse,
String parserErrorMessage) {
- ErrorResponse errorResponse = objFactory.createErrorResponse();
DSMLContentHandler contentHandler = new DSMLContentHandler();
try
@@ -803,13 +837,27 @@
{
// ignore
}
- if ( parserErrorMessage!= null ) {
- errorResponse.setMessage(parserErrorMessage);
- }
batchResponse.setRequestID(contentHandler.requestID);
- errorResponse.setType(MALFORMED_REQUEST);
+ return createMalformedRequestError(objFactory, parserErrorMessage);
+ }
+ /**
+ * Returns an error response of type 'malformed request' carrying the given
+ * message, or none if it is {@code null}.
+ *
+ * @param objFactory the object factory
+ * @param message the error message, may be {@code null}
+ *
+ * @return a JAXBElement that contains an ErrorResponse
+ */
+ private JAXBElement<ErrorResponse> createMalformedRequestError(
+ ObjectFactory objFactory, String message) {
+ ErrorResponse errorResponse = objFactory.createErrorResponse();
+ if ( message != null ) {
+ errorResponse.setMessage(message);
+ }
+ errorResponse.setType(MALFORMED_REQUEST);
return objFactory.createBatchResponseErrorResponse(errorResponse);
}
@@ -974,7 +1022,8 @@
* Send a response back to the client. This could be either a SOAP fault
* or a correct DSML response.
*
- * @param doc The document to include in the response.
+ * @param docs The documents to include in the response, one per
+ * batchResponse element of the reply.
* @param messageFactory The SOAP message factory.
* @param contentType The MIME content type to send appropriate for the MessageFactory
* @param res Information about the HTTP response to the client.
@@ -982,7 +1031,8 @@
* @throws IOException If an error occurs while interacting with the client.
* @throws SOAPException If an encoding or decoding error occurs.
*/
- private void sendResponse(Document doc, MessageFactory messageFactory, String contentType, HttpServletResponse res)
+ private void sendResponse(List<Document> docs, MessageFactory messageFactory, String contentType,
+ HttpServletResponse res)
throws IOException, SOAPException {
SOAPMessage reply = messageFactory.createMessage();
@@ -992,7 +1042,9 @@
res.setHeader("Content-Type", contentType);
- replyBody.addDocument(doc);
+ for (Document doc : docs) {
+ replyBody.addDocument(doc);
+ }
reply.saveChanges();
--
Gitblit v1.10.0