Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions opendj-dsml-servlet/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@
</dependencies>

<build><finalName>${project.groupId}.${project.artifactId}</finalName>
<testResources>
<testResource>
<directory>${basedir}/src/test/resources</directory>
</testResource>
<!-- The servlet loads the DSMLv2 schema from /resources/DSMLv2.xsd: the war
packages it under WEB-INF/classes, the tests need it on their classpath -->
<testResource>
<targetPath>resources</targetPath>
<directory>${basedir}/resources/schema</directory>
<includes>
<include>DSMLv2.xsd</include>
</includes>
</testResource>
</testResources>
<plugins>
<!-- Parse version to generate properties (major.version, minor.version, ...) -->
<plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -165,6 +169,9 @@ public class DSMLServlet extends HttpServlet {
*/
@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));
Expand Down Expand Up @@ -333,7 +340,6 @@ public void doPost(HttpServletRequest req, HttpServletResponse res)
connOptions.setUseSSL(useSSL);
connOptions.setStartTLS(useStartTLS);

LDAPConnection connection = null;
BatchRequest batchRequest = null;

// Keep the Servlet input stream buffered in case the SOAP un-marshalling
Expand All @@ -345,14 +351,16 @@ public void doPost(HttpServletRequest req, HttpServletResponse res)
is.mark(65536);
}

// Create response in the beginning as it might be used if the parsing
// fails.
// This 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();

// 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;
Expand Down Expand Up @@ -400,9 +408,8 @@ else if (headerVal.startsWith(SOAPConstants.SOAP_1_2_CONTENT_TYPE))
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)
{
Expand Down Expand Up @@ -430,12 +437,14 @@ else if (headerVal.startsWith(SOAPConstants.SOAP_1_2_CONTENT_TYPE))
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, ",");
Expand Down Expand Up @@ -477,6 +486,31 @@ else if (headerVal.startsWith(SOAPConstants.SOAP_1_2_CONTENT_TYPE))
}
}

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 {
Expand All @@ -503,23 +537,44 @@ else if (headerVal.startsWith(SOAPConstants.SOAP_1_2_CONTENT_TYPE))
// 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);
ErrorResponse errorResponse = objFactory.createErrorResponse();
errorResponse.setMessage(String.valueOf(e));
errorResponse.setType(MALFORMED_REQUEST);
elementResponses.add(
objFactory.createBatchResponseErrorResponse(errorResponse));
}
if ( batchRequestElement != null ) {
boolean authzInBind = false;
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)
*/
Expand All @@ -536,15 +591,17 @@ else if (headerVal.startsWith(SOAPConstants.SOAP_1_2_CONTENT_TYPE))
}
}
// set requestID in response
batchResponse.setRequestID(batchRequest.getRequestID());
elementResponse.setRequestID(batchRequest.getRequestID());
org.opends.server.types.Control proxyAuthzControl = null;

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)
{
Expand All @@ -557,54 +614,69 @@ else if (headerVal.startsWith(SOAPConstants.SOAP_1_2_CONTENT_TYPE))
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();

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;
if ( connected ) {
List<DsmlMessage> list = batchRequest.getBatchRequests();

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;
elementResponses.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);
}
}
}
}
try {
if ( responses.isEmpty() ) {
// An error was detected before the SOAP body was walked, or the body
// holds no batchRequest at all: reply with the upfront batchResponse.
responses.add(batchResponse);
}
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) {
e.printStackTrace();
// The client gets an empty response: at least make the cause visible.
getServletContext().log("Unable to send the DSML response", e);
}

}
Expand All @@ -628,14 +700,14 @@ private void safeSetFeature(XMLReader xmlReader, String feature, boolean flag)
{
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);
}
}
}
Expand Down Expand Up @@ -833,15 +905,17 @@ private JAXBElement<?> performLDAPRequest(LDAPConnection connection,
* 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.
*
* @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();
Expand All @@ -851,7 +925,9 @@ private void sendResponse(Document doc, MessageFactory messageFactory, String co

res.setHeader("Content-Type", contentType);

replyBody.addDocument(doc);
for (Document doc : docs) {
replyBody.addDocument(doc);
}

reply.saveChanges();

Expand Down Expand Up @@ -893,7 +969,7 @@ private void safeSetFeature(DocumentBuilderFactory factory, String feature, bool
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);
}
}
}
Expand All @@ -916,7 +992,7 @@ private Document createSafeDocument()
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);
Expand Down
Loading
Loading