001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS.
026 */
027package org.opends.dsml.protocol;
028
029
030import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI;
031import static org.opends.server.protocols.ldap.LDAPResultCode.
032  CLIENT_SIDE_CONNECT_ERROR;
033import static org.opends.server.util.ServerConstants.SASL_MECHANISM_PLAIN;
034import static org.opends.messages.CoreMessages.
035  INFO_RESULT_CLIENT_SIDE_ENCODING_ERROR;
036import static org.opends.messages.CoreMessages.INFO_RESULT_AUTHORIZATION_DENIED;
037
038import java.io.BufferedInputStream;
039import java.io.IOException;
040import java.io.InputStream;
041import java.io.OutputStream;
042import java.io.StringReader;
043import java.net.URL;
044import java.text.ParseException;
045import java.util.ArrayList;
046import java.util.Enumeration;
047import java.util.HashSet;
048import java.util.Iterator;
049import java.util.LinkedHashSet;
050import java.util.List;
051import java.util.StringTokenizer;
052import java.util.concurrent.atomic.AtomicBoolean;
053import java.util.concurrent.atomic.AtomicInteger;
054import java.util.logging.Level;
055import java.util.logging.Logger;
056
057import javax.servlet.ServletConfig;
058import javax.servlet.ServletException;
059import javax.servlet.http.HttpServlet;
060import javax.servlet.http.HttpServletRequest;
061import javax.servlet.http.HttpServletResponse;
062import javax.xml.XMLConstants;
063import javax.xml.bind.JAXBContext;
064import javax.xml.bind.JAXBElement;
065import javax.xml.bind.JAXBException;
066import javax.xml.bind.Marshaller;
067import javax.xml.bind.Unmarshaller;
068import javax.xml.parsers.DocumentBuilder;
069import javax.xml.parsers.DocumentBuilderFactory;
070import javax.xml.parsers.ParserConfigurationException;
071import javax.xml.parsers.SAXParserFactory;
072import javax.xml.soap.*;
073import javax.xml.soap.SOAPConstants;
074import javax.xml.validation.Schema;
075import javax.xml.validation.SchemaFactory;
076
077import org.forgerock.i18n.LocalizableMessage;
078import org.forgerock.opendj.ldap.ByteString;
079import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
080import org.forgerock.opendj.ldap.SearchScope;
081import org.opends.server.controls.ProxiedAuthV2Control;
082import org.opends.server.core.DirectoryServer;
083import org.opends.server.protocols.ldap.LDAPConstants;
084import org.opends.server.protocols.ldap.LDAPFilter;
085import org.opends.server.protocols.ldap.LDAPMessage;
086import org.opends.server.protocols.ldap.LDAPResultCode;
087import org.opends.server.protocols.ldap.SearchRequestProtocolOp;
088import org.opends.server.schema.SchemaConstants;
089import org.opends.server.tools.LDAPConnection;
090import org.opends.server.tools.LDAPConnectionException;
091import org.opends.server.tools.LDAPConnectionOptions;
092import org.opends.server.tools.SSLConnectionException;
093import org.opends.server.tools.SSLConnectionFactory;
094import org.opends.server.types.LDAPException;
095import org.opends.server.util.Base64;
096
097import org.w3c.dom.Document;
098import org.xml.sax.Attributes;
099import org.xml.sax.EntityResolver;
100import org.xml.sax.InputSource;
101import org.xml.sax.SAXException;
102import org.xml.sax.SAXNotRecognizedException;
103import org.xml.sax.SAXNotSupportedException;
104import org.xml.sax.XMLReader;
105import org.xml.sax.helpers.DefaultHandler;
106
107/**
108 * This class provides the entry point for the DSML request.
109 * It parses the SOAP request, calls the appropriate class
110 * which performs the LDAP operation, and returns the response
111 * as a DSML response.
112 */
113public class DSMLServlet extends HttpServlet {
114  private static final String PKG_NAME = "org.opends.dsml.protocol";
115  private static final String PORT = "ldap.port";
116  private static final String HOST = "ldap.host";
117  private static final String USERDN = "ldap.userdn";
118  private static final String USERPWD = "ldap.userpassword";
119  private static final String USESSL = "ldap.usessl";
120  private static final String USESTARTTLS = "ldap.usestarttls";
121  private static final String TRUSTSTOREPATH = "ldap.truststore.path";
122  private static final String TRUSTSTOREPASSWORD = "ldap.truststore.password";
123  private static final String TRUSTALLCERTS = "ldap.trustall";
124  private static final String USEHTTPAUTHZID = "ldap.authzidtypeisid";
125  private static final String EXOPSTRINGPREFIX = "ldap.exop.string.";
126  private static final long serialVersionUID = -3748022009593442973L;
127  private static final AtomicInteger nextMessageID = new AtomicInteger(1);
128
129  // definitions of return error messages
130  private static final String MALFORMED_REQUEST = "malformedRequest";
131  private static final String NOT_ATTEMPTED = "notAttempted";
132  private static final String AUTHENTICATION_FAILED = "authenticationFailed";
133  private static final String COULD_NOT_CONNECT = "couldNotConnect";
134  private static final String GATEWAY_INTERNAL_ERROR = "gatewayInternalError";
135  private static final String UNRESOLVABLE_URI = "unresolvableURI";
136
137  // definitions of onError values
138  private static final String ON_ERROR_EXIT = "exit";
139
140  private static JAXBContext jaxbContext;
141  private static Schema schema;
142
143  /** Prevent multiple logging when trying to set unavailable/unsupported parser features */
144  private static AtomicBoolean logFeatureWarnings = new AtomicBoolean(false);
145
146  private String hostName;
147  private Integer port;
148  private String userDN;
149  private String userPassword;
150  private Boolean useSSL;
151  private Boolean useStartTLS;
152  private String trustStorePathValue;
153  private String trustStorePasswordValue;
154  private Boolean trustAll;
155  private Boolean useHTTPAuthzID;
156  private HashSet<String> exopStrings = new HashSet<>();
157
158  /**
159   * This method will be called by the Servlet Container when
160   * this servlet is being placed into service.
161   *
162   * @param config - the <CODE>ServletConfig</CODE> object that
163   *               contains configuration information for this servlet.
164   * @throws ServletException If an error occurs during processing.
165   */
166  @Override
167  public void init(ServletConfig config) throws ServletException {
168
169    try {
170      hostName = config.getServletContext().getInitParameter(HOST);
171
172      port = Integer.valueOf(config.getServletContext().getInitParameter(PORT));
173
174      userDN = config.getServletContext().getInitParameter(USERDN);
175
176      userPassword = config.getServletContext().getInitParameter(USERPWD);
177
178      useSSL = Boolean.valueOf(
179          config.getServletContext().getInitParameter(USESSL));
180
181      useStartTLS = Boolean.valueOf(
182          config.getServletContext().getInitParameter(USESTARTTLS));
183
184      trustStorePathValue =
185          config.getServletContext().getInitParameter(TRUSTSTOREPATH);
186
187      trustStorePasswordValue =
188          config.getServletContext().getInitParameter(TRUSTSTOREPASSWORD);
189
190      trustAll = Boolean.valueOf(
191          config.getServletContext().getInitParameter(TRUSTALLCERTS));
192
193      useHTTPAuthzID = Boolean.valueOf(
194          config.getServletContext().getInitParameter(USEHTTPAUTHZID));
195
196      /*
197       * Find all the param-names matching the pattern:
198       * ldap.exop.string.1.2.3.4.5
199       * and if the value's true then mark that OID (1.2.3.4.5) as one returning
200       * a string value.
201       */
202      Enumeration names = config.getServletContext().getInitParameterNames();
203      while (names.hasMoreElements())
204      {
205        String name = (String) names.nextElement();
206        if (name.startsWith(EXOPSTRINGPREFIX) &&
207            Boolean.valueOf(config.getServletContext().getInitParameter(name)))
208        {
209          exopStrings.add(name.substring(EXOPSTRINGPREFIX.length()));
210        }
211      }
212
213      // allow the use of anyURI values in adds and modifies
214      System.setProperty("mapAnyUriToUri", "true");
215
216      if(jaxbContext==null)
217      {
218        jaxbContext = JAXBContext.newInstance(PKG_NAME, getClass().getClassLoader());
219      }
220      // assign the DSMLv2 schema for validation
221      if(schema==null)
222      {
223        URL url = getClass().getResource("/resources/DSMLv2.xsd");
224        if ( url != null ) {
225          SchemaFactory sf = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI);
226          schema = sf.newSchema(url);
227        }
228      }
229
230      DirectoryServer.bootstrapClient();
231    } catch (Exception je) {
232      je.printStackTrace();
233      throw new ServletException(je.getMessage());
234    }
235  }
236
237
238
239  /**
240   * Check if using the proxy authz control will work, by using it to read
241   * the Root DSE.
242   *
243   * @param connection The authenticated LDAP connection used to check.
244   * @param authorizationID The authorization ID, in the format
245   *                        "u:&lt;userid&gt;" or "dn:&lt;DN&gt;".
246   * @return a configured proxy authz control.
247   * @throws  LDAPConnectionException  If an error occurs during the check.
248   *
249   */
250  private org.opends.server.types.Control checkAuthzControl(
251      LDAPConnection connection, String authorizationID)
252      throws LDAPConnectionException
253  {
254    LinkedHashSet<String>attributes = new LinkedHashSet<>(1);
255    attributes.add(SchemaConstants.NO_ATTRIBUTES);
256    ArrayList<org.opends.server.types.Control> controls = new ArrayList<>(1);
257    org.opends.server.types.Control proxyAuthzControl =
258        new ProxiedAuthV2Control(true, ByteString.valueOf(authorizationID));
259    controls.add(proxyAuthzControl);
260
261    try
262    {
263      SearchRequestProtocolOp protocolOp = new SearchRequestProtocolOp(
264          ByteString.empty(), SearchScope.BASE_OBJECT,
265          DereferenceAliasesPolicy.NEVER, 0, 0, true,
266          LDAPFilter.objectClassPresent(), attributes);
267      byte opType;
268      LDAPMessage msg =
269        new LDAPMessage(DSMLServlet.nextMessageID(), protocolOp, controls);
270      connection.getLDAPWriter().writeMessage(msg);
271      do {
272        LDAPMessage responseMessage = connection.getLDAPReader().
273            readMessage();
274        opType = responseMessage.getProtocolOpType();
275        switch (opType)
276        {
277        case LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE:
278          switch (responseMessage.getSearchResultDoneProtocolOp().
279              getResultCode())
280          {
281            default:
282              LocalizableMessage m = INFO_RESULT_AUTHORIZATION_DENIED.get();
283              throw new LDAPConnectionException(m, CLIENT_SIDE_CONNECT_ERROR,
284                  null);
285            case LDAPResultCode.SUCCESS:
286              return proxyAuthzControl;
287          }
288        }
289      } while (true);
290    }
291    catch (LDAPException | IOException ie)
292    {
293      LocalizableMessage m = INFO_RESULT_CLIENT_SIDE_ENCODING_ERROR.get();
294      throw new LDAPConnectionException(m, CLIENT_SIDE_CONNECT_ERROR, null, ie);
295    }
296  }
297
298  /**
299   * The HTTP POST operation. This servlet expects a SOAP message
300   * with a DSML request payload.
301   *
302   * @param req Information about the request received from the client.
303   * @param res Information about the response to send to the client.
304   * @throws ServletException If an error occurs during servlet processing.
305   * @throws IOException   If an error occurs while interacting with the client.
306   */
307  @Override
308  public void doPost(HttpServletRequest req, HttpServletResponse res)
309  throws ServletException, IOException {
310    LDAPConnectionOptions connOptions = new LDAPConnectionOptions();
311    connOptions.setUseSSL(useSSL);
312    connOptions.setStartTLS(useStartTLS);
313
314    LDAPConnection connection = null;
315    BatchRequest batchRequest = null;
316
317    // Keep the Servlet input stream buffered in case the SOAP un-marshalling
318    // fails, the SAX parsing will be able to retrieve the requestID even if
319    // the XML is malformed by resetting the input stream.
320    BufferedInputStream is = new BufferedInputStream(req.getInputStream(),
321                                                     65536);
322    if ( is.markSupported() ) {
323      is.mark(65536);
324    }
325
326    // Create response in the beginning as it might be used if the parsing
327    // fails.
328    ObjectFactory objFactory = new ObjectFactory();
329    BatchResponse batchResponse = objFactory.createBatchResponse();
330    List<JAXBElement<?>> batchResponses = batchResponse.getBatchResponses();
331
332    // Thi sis only used for building the response
333    Document doc = createSafeDocument();
334
335    MessageFactory messageFactory = null;
336    String messageContentType = null;
337
338    if (useSSL || useStartTLS)
339    {
340      SSLConnectionFactory sslConnectionFactory = new SSLConnectionFactory();
341      try
342      {
343        sslConnectionFactory.init(trustAll, null, null, null,
344                                  trustStorePathValue, trustStorePasswordValue);
345      }
346      catch(SSLConnectionException e)
347      {
348        batchResponses.add(
349          createErrorResponse(objFactory,
350            new LDAPException(LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR,
351              LocalizableMessage.raw(
352              "Invalid SSL or TLS configuration to connect to LDAP server."))));
353      }
354      connOptions.setSSLConnectionFactory(sslConnectionFactory);
355    }
356
357    SOAPBody soapBody = null;
358
359    MimeHeaders mimeHeaders = new MimeHeaders();
360    Enumeration en = req.getHeaderNames();
361    String bindDN = null;
362    String bindPassword = null;
363    boolean authenticationInHeader = false;
364    boolean authenticationIsID = false;
365    while (en.hasMoreElements()) {
366      String headerName = (String) en.nextElement();
367      String headerVal = req.getHeader(headerName);
368      if (headerName.equalsIgnoreCase("content-type")) {
369        try
370        {
371          if (headerVal.startsWith(SOAPConstants.SOAP_1_1_CONTENT_TYPE))
372          {
373            messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_1_PROTOCOL);
374            messageContentType = SOAPConstants.SOAP_1_1_CONTENT_TYPE;
375          }
376          else if (headerVal.startsWith(SOAPConstants.SOAP_1_2_CONTENT_TYPE))
377          {
378            MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL);
379            messageContentType = SOAPConstants.SOAP_1_2_CONTENT_TYPE;
380          }
381          else {
382            throw new ServletException("Content-Type does not match SOAP 1.1 or SOAP 1.2");
383          }
384        }
385        catch (SOAPException e)
386        {
387          throw new ServletException(e.getMessage());
388        }
389      } else if (headerName.equalsIgnoreCase("authorization") && headerVal.startsWith("Basic "))
390      {
391        authenticationInHeader = true;
392        String authorization = headerVal.substring(6).trim();
393        try {
394          String unencoded = new String(Base64.decode(authorization));
395          int colon = unencoded.indexOf(':');
396          if (colon > 0) {
397            if (useHTTPAuthzID)
398            {
399              connOptions.setSASLMechanism("mech=" + SASL_MECHANISM_PLAIN);
400              connOptions.addSASLProperty(
401                  "authid=u:" + unencoded.substring(0, colon).trim());
402              authenticationIsID = true;
403            }
404            else
405            {
406              bindDN = unencoded.substring(0, colon).trim();
407            }
408            bindPassword = unencoded.substring(colon + 1);
409          }
410        } catch (ParseException ex) {
411          // user/DN:password parsing error
412          batchResponses.add(
413            createErrorResponse(objFactory,
414                  new LDAPException(LDAPResultCode.INVALID_CREDENTIALS,
415                  LocalizableMessage.raw(ex.getMessage()))));
416          break;
417        }
418      }
419      StringTokenizer tk = new StringTokenizer(headerVal, ",");
420      while (tk.hasMoreTokens()) {
421        mimeHeaders.addHeader(headerName, tk.nextToken().trim());
422      }
423    }
424
425    if ( ! authenticationInHeader ) {
426      // if no authentication, set default user from web.xml
427      if (userDN != null)
428      {
429        bindDN = userDN;
430        if (userPassword != null)
431        {
432          bindPassword = userPassword;
433        }
434        else
435        {
436          batchResponses.add(
437              createErrorResponse(objFactory,
438                    new LDAPException(LDAPResultCode.INVALID_CREDENTIALS,
439                    LocalizableMessage.raw("Invalid configured credentials."))));
440        }
441      }
442      else
443      {
444        bindDN = "";
445        bindPassword = "";
446      }
447    } else {
448      // otherwise if DN or password is null, send back an error
449      if (((!authenticationIsID && bindDN == null) || bindPassword == null)
450         && batchResponses.isEmpty()) {
451        batchResponses.add(
452              createErrorResponse(objFactory,
453                    new LDAPException(LDAPResultCode.INVALID_CREDENTIALS,
454                    LocalizableMessage.raw("Unable to retrieve credentials."))));
455      }
456    }
457
458    // if an error already occurred, the list is not empty
459    if ( batchResponses.isEmpty() ) {
460      try {
461        SOAPMessage message = messageFactory.createMessage(mimeHeaders, is);
462        soapBody = message.getSOAPBody();
463      } catch (SOAPException ex) {
464        // SOAP was unable to parse XML successfully
465        batchResponses.add(
466          createXMLParsingErrorResponse(is,
467                                        objFactory,
468                                        batchResponse,
469                                        String.valueOf(ex.getCause())));
470      }
471    }
472
473    if ( soapBody != null ) {
474      Iterator it = soapBody.getChildElements();
475      while (it.hasNext()) {
476        Object obj = it.next();
477        if (!(obj instanceof SOAPElement)) {
478          continue;
479        }
480        // Parse and unmarshall the SOAP object - the implementation prevents the use of a
481        // DOCTYPE and xincludes, so should be safe. There is no way to configure a more
482        // restrictive parser.
483        SOAPElement se = (SOAPElement) obj;
484        JAXBElement<BatchRequest> batchRequestElement = null;
485        try {
486          Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
487          unmarshaller.setSchema(schema);
488          batchRequestElement = unmarshaller.unmarshal(se, BatchRequest.class);
489        } catch (JAXBException e) {
490          // schema validation failed
491          batchResponses.add(createXMLParsingErrorResponse(is,
492                                                       objFactory,
493                                                       batchResponse,
494                                                       String.valueOf(e)));
495        }
496        if ( batchRequestElement != null ) {
497          boolean authzInBind = false;
498          boolean authzInControl = false;
499          batchRequest = batchRequestElement.getValue();
500
501          /*
502           *  Process optional authRequest (i.e. use authz)
503           */
504          if (batchRequest.authRequest != null) {
505            if (authenticationIsID) {
506              // If we are using SASL, then use the bind authz.
507              connOptions.addSASLProperty("authzid=" +
508                  batchRequest.authRequest.getPrincipal());
509              authzInBind = true;
510            } else {
511              // If we are using simple then we have to do some work after
512              // the bind.
513              authzInControl = true;
514            }
515          }
516          // set requestID in response
517          batchResponse.setRequestID(batchRequest.getRequestID());
518          org.opends.server.types.Control proxyAuthzControl = null;
519
520          boolean connected = false;
521
522          if ( connection == null ) {
523            connection = new LDAPConnection(hostName, port, connOptions);
524            try {
525
526              connection.connectToHost(bindDN, bindPassword);
527              if (authzInControl)
528              {
529                proxyAuthzControl = checkAuthzControl(connection,
530                    batchRequest.authRequest.getPrincipal());
531              }
532              if (authzInBind || authzInControl)
533              {
534                LDAPResult authResponse = objFactory.createLDAPResult();
535                ResultCode code = ResultCodeFactory.create(objFactory,
536                    LDAPResultCode.SUCCESS);
537                authResponse.setResultCode(code);
538                batchResponses.add(
539                    objFactory.createBatchResponseAuthResponse(authResponse));
540              }
541              connected = true;
542            } catch (LDAPConnectionException e) {
543              // if connection failed, return appropriate error response
544              batchResponses.add(createErrorResponse(objFactory, e));
545            }
546          }
547          if ( connected ) {
548            List<DsmlMessage> list = batchRequest.getBatchRequests();
549
550            for (DsmlMessage request : list) {
551              JAXBElement<?> result = performLDAPRequest(connection, objFactory, proxyAuthzControl, request);
552              if ( result != null ) {
553                batchResponses.add(result);
554              }
555              // evaluate response to check if an error occurred
556              Object o = result.getValue();
557              if ( o instanceof ErrorResponse ) {
558                if ( ON_ERROR_EXIT.equals(batchRequest.getOnError()) ) {
559                  break;
560                }
561              } else if ( o instanceof LDAPResult ) {
562                int code = ((LDAPResult)o).getResultCode().getCode();
563                if ( code != LDAPResultCode.SUCCESS
564                  && code != LDAPResultCode.REFERRAL
565                  && code != LDAPResultCode.COMPARE_TRUE
566                  && code != LDAPResultCode.COMPARE_FALSE && ON_ERROR_EXIT.equals(batchRequest.getOnError()) )
567                {
568                  break;
569                }
570              }
571            }
572          }
573          // close connection to LDAP server
574          if ( connection != null ) {
575            connection.close(nextMessageID);
576          }
577        }
578      }
579    }
580    try {
581      Marshaller marshaller = jaxbContext.createMarshaller();
582      marshaller.marshal(objFactory.createBatchResponse(batchResponse), doc);
583      sendResponse(doc, messageFactory, messageContentType, res);
584    } catch (Exception e) {
585      e.printStackTrace();
586    }
587
588  }
589
590
591
592  /**
593   * Safely set a feature on an XMLReader instance.
594   *
595   * @param xmlReader The reader to configure.
596   * @param feature The feature string to set.
597   * @param flag The value to set the feature to.
598   */
599  private void safeSetFeature(XMLReader xmlReader, String feature, boolean flag)
600  {
601    try
602    {
603      xmlReader.setFeature(feature, flag);
604    }
605    catch (SAXNotSupportedException e)
606    {
607      if (logFeatureWarnings.compareAndSet(false, true))
608      {
609        Logger.getLogger(PKG_NAME).log(Level.SEVERE, "XMLReader unsupported feature " + feature);
610      }
611    }
612    catch (SAXNotRecognizedException e)
613    {
614      if (logFeatureWarnings.compareAndSet(false, true))
615      {
616        Logger.getLogger(PKG_NAME).log(Level.SEVERE, "XMLReader unrecognized feature " + feature);
617      }
618    }
619  }
620
621
622
623  /**
624   * Returns an error response after a parsing error. The response has the
625   * requestID of the batch request, the error response message of the parsing
626   * exception message and the type 'malformed request'.
627   *
628   * @param is the XML InputStream to parse
629   * @param objFactory the object factory
630   * @param batchResponse the JAXB object to fill in
631   * @param parserErrorMessage the parsing error message
632   *
633   * @return a JAXBElement that contains an ErrorResponse
634   */
635  private JAXBElement<ErrorResponse> createXMLParsingErrorResponse(
636                                                    InputStream is,
637                                                    ObjectFactory objFactory,
638                                                    BatchResponse batchResponse,
639                                                    String parserErrorMessage) {
640    ErrorResponse errorResponse = objFactory.createErrorResponse();
641    DSMLContentHandler contentHandler = new DSMLContentHandler();
642
643    try
644    {
645      // try alternative XML parsing using SAX to retrieve requestID value
646      final XMLReader xmlReader = createSafeXMLReader();
647      xmlReader.setContentHandler(contentHandler);
648      is.reset();
649
650      xmlReader.parse(new InputSource(is));
651    }
652    catch (ParserConfigurationException | SAXException | IOException e)
653    {
654      // ignore
655    }
656    if ( parserErrorMessage!= null ) {
657      errorResponse.setMessage(parserErrorMessage);
658    }
659    batchResponse.setRequestID(contentHandler.requestID);
660
661    errorResponse.setType(MALFORMED_REQUEST);
662
663    return objFactory.createBatchResponseErrorResponse(errorResponse);
664  }
665
666  /**
667   * Returns an error response with attributes set according to the exception
668   * provided as argument.
669   *
670   * @param objFactory the object factory
671   * @param t the exception that occurred
672   *
673   * @return a JAXBElement that contains an ErrorResponse
674   */
675  private JAXBElement<ErrorResponse> createErrorResponse(ObjectFactory objFactory, Throwable t) {
676    // potential exceptions are IOException, LDAPException, DecodeException
677
678    ErrorResponse errorResponse = objFactory.createErrorResponse();
679    errorResponse.setMessage(String.valueOf(t));
680
681    if ( t instanceof LDAPException ) {
682      switch(((LDAPException)t).getResultCode()) {
683        case LDAPResultCode.AUTHORIZATION_DENIED:
684        case LDAPResultCode.INAPPROPRIATE_AUTHENTICATION:
685        case LDAPResultCode.INVALID_CREDENTIALS:
686        case LDAPResultCode.STRONG_AUTH_REQUIRED:
687          errorResponse.setType(AUTHENTICATION_FAILED);
688          break;
689
690        case LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR:
691          errorResponse.setType(COULD_NOT_CONNECT);
692          break;
693
694        case LDAPResultCode.UNWILLING_TO_PERFORM:
695          errorResponse.setType(NOT_ATTEMPTED);
696          break;
697
698        default:
699          errorResponse.setType(MALFORMED_REQUEST);
700          break;
701      }
702    } else if ( t instanceof LDAPConnectionException ) {
703      errorResponse.setType(COULD_NOT_CONNECT);
704    } else if ( t instanceof IOException ) {
705      errorResponse.setType(UNRESOLVABLE_URI);
706    } else {
707      errorResponse.setType(GATEWAY_INTERNAL_ERROR);
708    }
709
710    return objFactory.createBatchResponseErrorResponse(errorResponse);
711  }
712
713  /**
714   * Performs the LDAP operation and sends back the result (if any). In case
715   * of error, an error response is returned.
716   *
717   * @param connection a connected connection
718   * @param objFactory the object factory
719   * @param proxyAuthzControl a proxy authz control, or null
720   * @param request the JAXB request to perform
721   *
722   * @return null for an abandon request, the expect result for all other
723   *         requests or an error in case of unexpected behaviour.
724   */
725  private JAXBElement<?> performLDAPRequest(LDAPConnection connection,
726                                            ObjectFactory objFactory,
727                                            org.opends.server.types.Control proxyAuthzControl,
728                                            DsmlMessage request) {
729    ArrayList<org.opends.server.types.Control> controls = new ArrayList<>(1);
730    if (proxyAuthzControl != null)
731    {
732      controls.add(proxyAuthzControl);
733    }
734    try {
735      if (request instanceof SearchRequest) {
736        // Process the search request.
737        SearchRequest sr = (SearchRequest) request;
738        DSMLSearchOperation ds = new DSMLSearchOperation(connection);
739        SearchResponse searchResponse = ds.doSearch(objFactory, sr, controls);
740        return objFactory.createBatchResponseSearchResponse(searchResponse);
741      } else if (request instanceof AddRequest) {
742        // Process the add request.
743        AddRequest ar = (AddRequest) request;
744        DSMLAddOperation addOp = new DSMLAddOperation(connection);
745        LDAPResult addResponse = addOp.doOperation(objFactory, ar, controls);
746        return objFactory.createBatchResponseAddResponse(addResponse);
747      } else if (request instanceof AbandonRequest) {
748        // Process the abandon request.
749        AbandonRequest ar = (AbandonRequest) request;
750        DSMLAbandonOperation ao = new DSMLAbandonOperation(connection);
751        ao.doOperation(objFactory, ar, controls);
752        return null;
753      } else if (request instanceof ExtendedRequest) {
754        // Process the extended request.
755        ExtendedRequest er = (ExtendedRequest) request;
756        DSMLExtendedOperation eo = new DSMLExtendedOperation(connection,
757            exopStrings);
758        ExtendedResponse extendedResponse = eo.doOperation(objFactory, er,
759            controls);
760        return objFactory.createBatchResponseExtendedResponse(extendedResponse);
761
762      } else if (request instanceof DelRequest) {
763        // Process the delete request.
764        DelRequest dr = (DelRequest) request;
765        DSMLDeleteOperation delOp = new DSMLDeleteOperation(connection);
766        LDAPResult delResponse = delOp.doOperation(objFactory, dr, controls);
767        return objFactory.createBatchResponseDelResponse(delResponse);
768      } else if (request instanceof CompareRequest) {
769        // Process the compare request.
770        CompareRequest cr = (CompareRequest) request;
771        DSMLCompareOperation compareOp =
772                new DSMLCompareOperation(connection);
773        LDAPResult compareResponse = compareOp.doOperation(objFactory, cr,
774            controls);
775        return objFactory.createBatchResponseCompareResponse(compareResponse);
776      } else if (request instanceof ModifyDNRequest) {
777        // Process the Modify DN request.
778        ModifyDNRequest mr = (ModifyDNRequest) request;
779        DSMLModifyDNOperation moddnOp =
780                new DSMLModifyDNOperation(connection);
781        LDAPResult moddnResponse = moddnOp.doOperation(objFactory, mr,
782            controls);
783        return objFactory.createBatchResponseModDNResponse(moddnResponse);
784      } else if (request instanceof ModifyRequest) {
785        // Process the Modify request.
786        ModifyRequest modr = (ModifyRequest) request;
787        DSMLModifyOperation modOp = new DSMLModifyOperation(connection);
788        LDAPResult modResponse = modOp.doOperation(objFactory, modr, controls);
789        return objFactory.createBatchResponseModifyResponse(modResponse);
790      } else if (request instanceof AuthRequest) {
791        // Process the Auth request.
792        // Only returns an BatchResponse with an AuthResponse containing the
793        // LDAP result code AUTH_METHOD_NOT_SUPPORTED
794        ResultCode resultCode = objFactory.createResultCode();
795        resultCode.setCode(LDAPResultCode.AUTH_METHOD_NOT_SUPPORTED);
796
797        LDAPResult ldapResult = objFactory.createLDAPResult();
798        ldapResult.setResultCode(resultCode);
799
800        return objFactory.createBatchResponseAuthResponse(ldapResult);
801      }
802    } catch (Throwable t) {
803      return createErrorResponse(objFactory, t);
804    }
805    // should never happen as the schema was validated
806    return null;
807  }
808
809
810  /**
811   * Send a response back to the client. This could be either a SOAP fault
812   * or a correct DSML response.
813   *
814   * @param doc   The document to include in the response.
815   * @param messageFactory  The SOAP message factory.
816   * @param contentType  The MIME content type to send appropriate for the MessageFactory
817   * @param res   Information about the HTTP response to the client.
818   *
819   * @throws IOException   If an error occurs while interacting with the client.
820   * @throws SOAPException If an encoding or decoding error occurs.
821   */
822  private void sendResponse(Document doc, MessageFactory messageFactory, String contentType, HttpServletResponse res)
823    throws IOException, SOAPException {
824
825    SOAPMessage reply = messageFactory.createMessage();
826    SOAPHeader header = reply.getSOAPHeader();
827    header.detachNode();
828    SOAPBody replyBody = reply.getSOAPBody();
829
830    res.setHeader("Content-Type", contentType);
831
832    replyBody.addDocument(doc);
833
834    reply.saveChanges();
835
836    OutputStream os = res.getOutputStream();
837    reply.writeTo(os);
838    os.flush();
839  }
840
841
842  /**
843   * Retrieves a message ID that may be used for the next LDAP message sent to
844   * the Directory Server.
845   *
846   * @return  A message ID that may be used for the next LDAP message sent to
847   *          the Directory Server.
848   */
849  public static int nextMessageID() {
850    int nextID = nextMessageID.getAndIncrement();
851    if (nextID == Integer.MAX_VALUE) {
852      nextMessageID.set(1);
853    }
854
855    return nextID;
856  }
857
858  /**
859   * Safely set a feature on an DocumentBuilderFactory instance.
860   *
861   * @param factory The DocumentBuilderFactory to configure.
862   * @param feature The feature string to set.
863   * @param flag The value to set the feature to.
864   */
865  private void safeSetFeature(DocumentBuilderFactory factory, String feature, boolean flag)
866  {
867    try
868    {
869      factory.setFeature(feature, flag);
870    }
871    catch (ParserConfigurationException e) {
872      if (logFeatureWarnings.compareAndSet(false, true))
873      {
874        Logger.getLogger(PKG_NAME).log(Level.SEVERE, "DocumentBuilderFactory unsupported feature " + feature);
875      }
876    }
877  }
878
879  /**
880   * Create a Document object that is safe against XML External Entity (XXE) Processing
881   * attacks.
882   *
883   * @return A Document object
884   * @throws ServletException if a Document object could not be created.
885   */
886  private Document createSafeDocument()
887          throws ServletException
888  {
889    final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
890    try
891    {
892      dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
893    }
894    catch (ParserConfigurationException e)
895    {
896      if (logFeatureWarnings.compareAndSet(false, true)) {
897        Logger.getLogger(PKG_NAME).log(Level.SEVERE, "DocumentBuilderFactory cannot be configured securely");
898      }
899    }
900    dbf.setXIncludeAware(false);
901    dbf.setNamespaceAware(true);
902    dbf.setValidating(true);
903    safeSetFeature(dbf, "http://apache.org/xml/features/disallow-doctype-decl", true);
904    safeSetFeature(dbf, "http://xml.org/sax/features/external-general-entities", false);
905    safeSetFeature(dbf, "http://xml.org/sax/features/external-parameter-entities", false);
906    dbf.setExpandEntityReferences(false);
907
908    final DocumentBuilder db;
909    try
910    {
911      db = dbf.newDocumentBuilder();
912    }
913    catch (ParserConfigurationException e)
914    {
915      throw new ServletException(e.getMessage());
916    }
917    db.setEntityResolver(new SafeEntityResolver());
918    return db.newDocument();
919
920  }
921
922  /**
923   * Create an XMLReader that is safe against XML External Entity (XXE) Processing attacks.
924   *
925   * @return an XMLReader
926   * @throws ParserConfigurationException if we cannot obtain a parser.
927   * @throws SAXException if we cannot obtain a parser.
928   */
929  private XMLReader createSafeXMLReader()
930          throws ParserConfigurationException, SAXException
931  {
932    final SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
933    // Ensure we are doing basic secure processing.
934    saxParserFactory.setXIncludeAware(false);
935    saxParserFactory.setNamespaceAware(true);
936    saxParserFactory.setValidating(false);
937
938    // Configure a safe XMLReader appropriate for SOAP.
939    final XMLReader xmlReader = saxParserFactory.newSAXParser().getXMLReader();
940    safeSetFeature(xmlReader, XMLConstants.FEATURE_SECURE_PROCESSING, true);
941    safeSetFeature(xmlReader, "http://apache.org/xml/features/disallow-doctype-decl", true);
942    safeSetFeature(xmlReader, "http://xml.org/sax/features/external-general-entities", false);
943    safeSetFeature(xmlReader, "http://xml.org/sax/features/external-parameter-entities", false);
944    xmlReader.setEntityResolver(new SafeEntityResolver());
945    return xmlReader;
946  }
947
948  /**
949   * This class is used when an XML request is malformed to retrieve the
950   * requestID value using an event XML parser.
951   */
952  private class DSMLContentHandler extends DefaultHandler {
953    private String requestID;
954    /**
955     * This function fetches the requestID value of the batchRequest xml
956     * element and call the default implementation (super).
957     */
958    @Override
959    public void startElement(String uri, String localName, String qName,
960                             Attributes attributes) throws SAXException {
961      if ( requestID==null && localName.equals("batchRequest") ) {
962        requestID = attributes.getValue("requestID");
963      }
964      super.startElement(uri, localName, qName, attributes);
965    }
966  }
967
968  /**
969   * This is defensive - we prevent entity resolving by configuration, but
970   * just in case, we ensure that nothing resolves.
971   */
972  private class SafeEntityResolver implements EntityResolver
973  {
974    @Override
975    public InputSource resolveEntity(String publicId, String systemId)
976    {
977      return new InputSource(new StringReader(""));
978    }
979  }
980}
981