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