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
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2006-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.dsml.protocol;
028    
029    
030    import java.io.BufferedInputStream;
031    import java.io.InputStream;
032    import java.text.ParseException;
033    import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI;
034    import javax.xml.bind.JAXBException;
035    import org.opends.messages.Message;
036    import org.opends.server.core.DirectoryServer;
037    import org.opends.server.protocols.ldap.LDAPResultCode;
038    import org.opends.server.tools.LDAPConnection;
039    import org.opends.server.tools.LDAPConnectionOptions;
040    import org.opends.server.util.Base64;
041    import org.w3c.dom.Document;
042    
043    import javax.servlet.ServletConfig;
044    import javax.servlet.ServletException;
045    import javax.servlet.http.HttpServlet;
046    import javax.servlet.http.HttpServletRequest;
047    import javax.servlet.http.HttpServletResponse;
048    import javax.xml.bind.JAXBContext;
049    import javax.xml.bind.JAXBElement;
050    import javax.xml.bind.Marshaller;
051    import javax.xml.bind.Unmarshaller;
052    import javax.xml.parsers.DocumentBuilder;
053    import javax.xml.parsers.DocumentBuilderFactory;
054    import javax.xml.soap.*;
055    import java.io.IOException;
056    import java.io.OutputStream;
057    import java.net.URL;
058    import java.util.Enumeration;
059    import java.util.Iterator;
060    import java.util.List;
061    import java.util.StringTokenizer;
062    import java.util.concurrent.atomic.AtomicInteger;
063    import javax.xml.validation.SchemaFactory;
064    import org.opends.server.tools.LDAPConnectionException;
065    import org.opends.server.types.LDAPException;
066    import org.xml.sax.Attributes;
067    import org.xml.sax.InputSource;
068    import org.xml.sax.SAXException;
069    import org.xml.sax.XMLReader;
070    import org.xml.sax.helpers.DefaultHandler;
071    import org.xml.sax.helpers.XMLReaderFactory;
072    
073    
074    /**
075     * This class provides the entry point for the DSML request.
076     * It parses the SOAP request, calls the appropriate class
077     * which performs the LDAP operation, and returns the response
078     * as a DSML response.
079     */
080    public class DSMLServlet extends HttpServlet {
081      private static final String PKG_NAME = "org.opends.dsml.protocol";
082      private static final String PORT = "ldap.port";
083      private static final String HOST = "ldap.host";
084      private static final long serialVersionUID = -3748022009593442973L;
085      private static final AtomicInteger nextMessageID = new AtomicInteger(1);
086    
087      // definitions of return error messages
088      private static final String MALFORMED_REQUEST = "malformedRequest";
089      private static final String NOT_ATTEMPTED = "notAttempted";
090      private static final String AUTHENTICATION_FAILED = "authenticationFailed";
091      private static final String COULD_NOT_CONNECT = "couldNotConnect";
092      private static final String GATEWAY_INTERNAL_ERROR = "gatewayInternalError";
093    
094      private static final String UNKNOWN_ERROR = "Unknown error";
095    
096      // definitions of onError values
097      private static final String ON_ERROR_RESUME = "resume";
098      private static final String ON_ERROR_EXIT = "exit";
099    
100      private Unmarshaller unmarshaller;
101      private Marshaller marshaller;
102      private ObjectFactory objFactory;
103      private MessageFactory messageFactory;
104      private DocumentBuilder db;
105    
106      // this extends the default handler of SAX parser. It helps to retrieve the
107      // requestID value when the xml request is malformed and thus unparsable
108      // using SOAP or JAXB.
109      private DSMLContentHandler contentHandler;
110    
111      private String hostName;
112      private Integer port;
113    
114      /**
115       * This method will be called by the Servlet Container when
116       * this servlet is being placed into service.
117       *
118       * @param config - the <CODE>ServletConfig</CODE> object that
119       *               contains configutation information for this servlet.
120       * @throws ServletException If an error occurs during processing.
121       */
122      public void init(ServletConfig config) throws ServletException {
123    
124        try {
125          hostName = config.getServletContext().getInitParameter(HOST);
126    
127          port = new Integer(config.getServletContext().getInitParameter(PORT));
128    
129          JAXBContext jaxbContext = JAXBContext.newInstance(PKG_NAME);
130          unmarshaller = jaxbContext.createUnmarshaller();
131          // assign the DSMLv2 schema for validation
132          URL schema = getClass().getResource("/resources/DSMLv2.xsd");
133          if ( schema != null ) {
134            SchemaFactory sf = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI);
135            unmarshaller.setSchema(sf.newSchema(schema));
136          }
137    
138          marshaller = jaxbContext.createMarshaller();
139    
140          objFactory = new ObjectFactory();
141          messageFactory = MessageFactory.newInstance();
142          DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
143          dbf.setNamespaceAware(true);
144          db = dbf.newDocumentBuilder();
145    
146          this.contentHandler = new DSMLContentHandler();
147    
148          DirectoryServer.bootstrapClient();
149        } catch (Exception je) {
150          je.printStackTrace();
151          throw new ServletException(je.getMessage());
152        }
153      }
154    
155      /**
156       * The HTTP POST operation. This servlet expects a SOAP message
157       * with a DSML request payload.
158       *
159       * @param req Information about the request received from the client.
160       * @param res Information about the response to send to the client.
161       * @throws ServletException If an error occurs during servlet processing.
162       * @throws IOException   If an error occurs while interacting with the client.
163       */
164      public void doPost(HttpServletRequest req, HttpServletResponse res)
165      throws ServletException, IOException {
166        LDAPConnectionOptions connOptions = new LDAPConnectionOptions();
167        LDAPConnection connection = null;
168        BatchRequest batchRequest = null;
169    
170        // Keep the Servlet input stream buffered in case the SOAP unmarshalling
171        // fails, the SAX parsing will be able to retrieve the requestID even if
172        // the XML is malmformed by resetting the input stream.
173        BufferedInputStream is = new BufferedInputStream(req.getInputStream(),
174                                                         65536);
175        if ( is.markSupported() ) {
176          is.mark(65536);
177        }
178    
179        // Create response in the beginning as it might be used if the parsing
180        // failes.
181        BatchResponse batchResponse = objFactory.createBatchResponse();
182        List<JAXBElement<?>> batchResponses = batchResponse.getBatchResponses();
183        Document doc = db.newDocument();
184    
185        SOAPBody soapBody = null;
186    
187        MimeHeaders mimeHeaders = new MimeHeaders();
188        Enumeration en = req.getHeaderNames();
189        String bindDN = null;
190        String bindPassword = null;
191        boolean authorizationInHeader = false;
192        while (en.hasMoreElements()) {
193          String headerName = (String) en.nextElement();
194          String headerVal = req.getHeader(headerName);
195          if (headerName.equalsIgnoreCase("authorization")) {
196            if (headerVal.startsWith("Basic ")) {
197              authorizationInHeader = true;
198              String authorization = headerVal.substring(6).trim();
199              try {
200                String unencoded = new String(Base64.decode(authorization));
201                int colon = unencoded.indexOf(':');
202                if (colon > 0) {
203                  bindDN = unencoded.substring(0, colon).trim();
204                  bindPassword = unencoded.substring(colon + 1);
205                }
206              } catch (ParseException ex) {
207                // DN:password parsing error
208                batchResponses.add(
209                  createErrorResponse(
210                        new LDAPException(LDAPResultCode.INVALID_CREDENTIALS,
211                        Message.raw(ex.getMessage()))));
212                break;
213              }
214            }
215          }
216          StringTokenizer tk = new StringTokenizer(headerVal, ",");
217          while (tk.hasMoreTokens()) {
218            mimeHeaders.addHeader(headerName, tk.nextToken().trim());
219          }
220        }
221    
222        if ( ! authorizationInHeader ) {
223          // if no authorization, set default user
224          bindDN = "";
225          bindPassword = "";
226        } else {
227          // otherwise if DN or password is null, send back an error
228          if ( (bindDN == null || bindPassword == null)
229             && batchResponses.size()==0) {
230            batchResponses.add(
231                  createErrorResponse(
232                        new LDAPException(LDAPResultCode.INVALID_CREDENTIALS,
233                        Message.raw("Unable to retrieve credentials."))));
234          }
235        }
236    
237        // if an error already occured, the list is not empty
238        if ( batchResponses.size() == 0 ) {
239          try {
240            SOAPMessage message = messageFactory.createMessage(mimeHeaders, is);
241            soapBody = message.getSOAPBody();
242          } catch (SOAPException ex) {
243            // SOAP was unable to parse XML successfully
244            batchResponses.add(
245              createXMLParsingErrorResponse(is,
246                                            batchResponse,
247                                            String.valueOf(ex.getCause())));
248          }
249        }
250    
251        if ( soapBody != null ) {
252          Iterator it = soapBody.getChildElements();
253          while (it.hasNext()) {
254            Object obj = it.next();
255            if (!(obj instanceof SOAPElement)) {
256              continue;
257            }
258            SOAPElement se = (SOAPElement) obj;
259            JAXBElement<BatchRequest> batchRequestElement = null;
260            try {
261              batchRequestElement = unmarshaller.unmarshal(se, BatchRequest.class);
262            } catch (JAXBException e) {
263              // schema validation failed
264              batchResponses.add(createXMLParsingErrorResponse(is,
265                                                           batchResponse,
266                                                           String.valueOf(e)));
267            }
268            if ( batchRequestElement != null ) {
269              batchRequest = batchRequestElement.getValue();
270    
271              // set requestID in response
272              batchResponse.setRequestID(batchRequest.getRequestID());
273    
274              boolean connected = false;
275              if ( connection == null ) {
276                connection = new LDAPConnection(hostName, port, connOptions);
277                try {
278                  connection.connectToHost(bindDN, bindPassword);
279                  connected = true;
280                } catch (LDAPConnectionException e) {
281                  // if connection failed, return appropriate error response
282                  batchResponses.add(createErrorResponse(e));
283                }
284              }
285              if ( connected ) {
286                List<DsmlMessage> list = batchRequest.getBatchRequests();
287    
288                for (DsmlMessage request : list) {
289                  JAXBElement<?> result = performLDAPRequest(connection, request);
290                  if ( result != null ) {
291                    batchResponses.add(result);
292                  }
293                  // evaluate response to check if an error occured
294                  Object o = result.getValue();
295                  if ( o instanceof ErrorResponse ) {
296                    if ( ON_ERROR_EXIT.equals(batchRequest.getOnError()) ) {
297                      break;
298                    }
299                  } else if ( o instanceof LDAPResult ) {
300                    int code = ((LDAPResult)o).getResultCode().getCode();
301                    if ( code != LDAPResultCode.SUCCESS
302                      && code != LDAPResultCode.REFERRAL
303                      && code != LDAPResultCode.COMPARE_TRUE
304                      && code != LDAPResultCode.COMPARE_FALSE ) {
305                      if ( ON_ERROR_EXIT.equals(batchRequest.getOnError()) ) {
306                        break;
307                      }
308                    }
309                  }
310                }
311              }
312              // close connection to LDAP server
313              if ( connection != null ) {
314                connection.close(nextMessageID);
315              }
316            }
317          }
318        }
319        try {
320          marshaller.marshal(objFactory.createBatchResponse(batchResponse), doc);
321          sendResponse(doc, res);
322        } catch (Exception e) {
323          e.printStackTrace();
324        }
325    
326      }
327    
328      /**
329       * Returns an error response after a parsing error. The response has the
330       * requestID of the batch request, the error response message of the parsing
331       * exception message and the type 'malformed request'.
332       *
333       * @param is the xml InputStream to parse
334       * @param batchResponse the JAXB object to fill in
335       * @param parserErrorMessage the parsing error message
336       *
337       * @return a JAXBElement that contains an ErrorResponse
338       */
339      private JAXBElement<ErrorResponse> createXMLParsingErrorResponse(
340                                                        InputStream is,
341                                                        BatchResponse batchResponse,
342                                                        String parserErrorMessage) {
343        ErrorResponse errorResponse = objFactory.createErrorResponse();
344    
345        try {
346          // try alternative XML parsing using SAX to retrieve requestID value
347          XMLReader xmlReader = XMLReaderFactory.createXMLReader();
348          // clear previous match
349          this.contentHandler.requestID = null;
350          xmlReader.setContentHandler(this.contentHandler);
351          is.reset();
352    
353          xmlReader.parse(new InputSource(is));
354        } catch (Throwable e) {
355          // document is unparsable so will jump here
356        }
357        if ( parserErrorMessage!= null ) {
358          errorResponse.setMessage(parserErrorMessage);
359        }
360        batchResponse.setRequestID(this.contentHandler.requestID);
361    
362        errorResponse.setType(MALFORMED_REQUEST);
363    
364        return objFactory.createBatchResponseErrorResponse(errorResponse);
365      }
366    
367      /**
368       * Returns an error response with attributes set according to the exception
369       * provided as argument.
370       *
371       * @param t the exception that occured
372       *
373       * @return a JAXBElement that contains an ErrorResponse
374       */
375      private JAXBElement<ErrorResponse> createErrorResponse(Throwable t) {
376        // potential exceptions are IOException, LDAPException, ASN1Exception
377    
378        ErrorResponse errorResponse = objFactory.createErrorResponse();
379        errorResponse.setMessage(String.valueOf(t));
380    
381        if ( t instanceof LDAPException ) {
382          switch(((LDAPException)t).getResultCode()) {
383            case LDAPResultCode.AUTHORIZATION_DENIED:
384            case LDAPResultCode.INAPPROPRIATE_AUTHENTICATION:
385            case LDAPResultCode.INVALID_CREDENTIALS:
386            case LDAPResultCode.STRONG_AUTH_REQUIRED:
387              errorResponse.setType(AUTHENTICATION_FAILED);
388              break;
389    
390            case LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR:
391              errorResponse.setType(COULD_NOT_CONNECT);
392              break;
393    
394            case LDAPResultCode.UNWILLING_TO_PERFORM:
395              errorResponse.setType(NOT_ATTEMPTED);
396              break;
397    
398            default:
399              errorResponse.setType(UNKNOWN_ERROR);
400              break;
401          }
402        } else if ( t instanceof LDAPConnectionException ) {
403          errorResponse.setType(COULD_NOT_CONNECT);
404        } else {
405          errorResponse.setType(GATEWAY_INTERNAL_ERROR);
406        }
407    
408        return objFactory.createBatchResponseErrorResponse(errorResponse);
409      }
410    
411      /**
412       * Performs the LDAP operation and sends back the result (if any). In case
413       * of error, an error reponse is returned.
414       *
415       * @param connection a connected connection
416       * @param request the JAXB request to perform
417       *
418       * @return null for an abandon request, the expect result for all other
419       *         requests or an error in case of unexpected behaviour.
420       */
421      private JAXBElement<?> performLDAPRequest(LDAPConnection connection,
422                                                DsmlMessage request) {
423        try {
424          if (request instanceof SearchRequest) {
425            // Process the search request.
426            SearchRequest sr = (SearchRequest) request;
427            DSMLSearchOperation ds = new DSMLSearchOperation(connection);
428            SearchResponse searchResponse = ds.doSearch(objFactory, sr);
429    
430            return objFactory.createBatchResponseSearchResponse(searchResponse);
431          } else if (request instanceof AddRequest) {
432            // Process the add request.
433            AddRequest ar = (AddRequest) request;
434            DSMLAddOperation addOp = new DSMLAddOperation(connection);
435            LDAPResult addResponse = addOp.doOperation(objFactory, ar);
436            return objFactory.createBatchResponseAddResponse(addResponse);
437          } else if (request instanceof AbandonRequest) {
438            // Process the abandon request.
439            AbandonRequest ar = (AbandonRequest) request;
440            DSMLAbandonOperation ao = new DSMLAbandonOperation(connection);
441            LDAPResult abandonResponse = ao.doOperation(objFactory, ar);
442            return null;
443          } else if (request instanceof ExtendedRequest) {
444            // Process the extended request.
445            ExtendedRequest er = (ExtendedRequest) request;
446            DSMLExtendedOperation eo = new DSMLExtendedOperation(connection);
447            ExtendedResponse extendedResponse = eo.doOperation(objFactory, er);
448            return objFactory.createBatchResponseExtendedResponse(extendedResponse);
449    
450          } else if (request instanceof DelRequest) {
451            // Process the delete request.
452            DelRequest dr = (DelRequest) request;
453            DSMLDeleteOperation delOp = new DSMLDeleteOperation(connection);
454            LDAPResult delResponse = delOp.doOperation(objFactory, dr);
455            return objFactory.createBatchResponseDelResponse(delResponse);
456          } else if (request instanceof CompareRequest) {
457            // Process the compare request.
458            CompareRequest cr = (CompareRequest) request;
459            DSMLCompareOperation compareOp =
460                    new DSMLCompareOperation(connection);
461            LDAPResult compareResponse = compareOp.doOperation(objFactory, cr);
462            return objFactory.createBatchResponseCompareResponse(compareResponse);
463          } else if (request instanceof ModifyDNRequest) {
464            // Process the Modify DN request.
465            ModifyDNRequest mr = (ModifyDNRequest) request;
466            DSMLModifyDNOperation moddnOp =
467                    new DSMLModifyDNOperation(connection);
468            LDAPResult moddnResponse = moddnOp.doOperation(objFactory, mr);
469            return objFactory.createBatchResponseModDNResponse(moddnResponse);
470          } else if (request instanceof ModifyRequest) {
471            // Process the Modify request.
472            ModifyRequest modr = (ModifyRequest) request;
473            DSMLModifyOperation modOp = new DSMLModifyOperation(connection);
474            LDAPResult modResponse = modOp.doOperation(objFactory, modr);
475            return objFactory.createBatchResponseModifyResponse(modResponse);
476          } else if (request instanceof AuthRequest) {
477            // Process the Auth request.
478            // Only returns an BatchReponse with an AuthResponse containing the
479            // LDAP result code AUTH_METHOD_NOT_SUPPORTED
480            ResultCode resultCode = objFactory.createResultCode();
481            resultCode.setCode(LDAPResultCode.AUTH_METHOD_NOT_SUPPORTED);
482    
483            LDAPResult ldapResult = objFactory.createLDAPResult();
484            ldapResult.setResultCode(resultCode);
485    
486            return objFactory.createBatchResponseAuthResponse(ldapResult);
487          }
488        } catch (Throwable t) {
489          return createErrorResponse(t);
490        }
491        // should never happen as the schema was validated
492        return null;
493      }
494    
495    
496      /**
497       * Send a response back to the client. This could be either a SOAP fault
498       * or a correct DSML response.
499       *
500       * @param doc   The document to include in the response.
501       * @param res   Information about the HTTP response to the client.
502       *
503       * @throws IOException   If an error occurs while interacting with the client.
504       * @throws SOAPException If an encoding or decoding error occurs.
505       */
506      private void sendResponse(Document doc, HttpServletResponse res)
507        throws IOException, SOAPException {
508    
509        SOAPMessage reply = messageFactory.createMessage();
510        SOAPHeader header = reply.getSOAPHeader();
511        header.detachNode();
512        SOAPBody replyBody = reply.getSOAPBody();
513    
514        res.setHeader("Content-Type", "text/xml");
515    
516        SOAPElement bodyElement = replyBody.addDocument(doc);
517    
518        reply.saveChanges();
519    
520        OutputStream os = res.getOutputStream();
521        reply.writeTo(os);
522        os.flush();
523      }
524    
525    
526      /**
527       * Retrieves a message ID that may be used for the next LDAP message sent to
528       * the Directory Server.
529       *
530       * @return  A message ID that may be used for the next LDAP message sent to
531       *          the Directory Server.
532       */
533      public static int nextMessageID() {
534        int nextID = nextMessageID.getAndIncrement();
535        if (nextID == Integer.MAX_VALUE) {
536          nextMessageID.set(1);
537        }
538    
539        return nextID;
540      }
541    
542      /**
543       * This class is used when a xml request is malformed to retrieve the
544       * requestID value using an event xml parser.
545       */
546      private static class DSMLContentHandler extends DefaultHandler {
547        private String requestID;
548        /*
549         * This function fetches the requestID value of the batchRequest xml
550         * element and call the default implementation (super).
551         */
552        public void startElement(String uri, String localName, String qName,
553                                 Attributes attributes) throws SAXException {
554          if ( requestID==null && localName.equals("batchRequest") ) {
555            requestID = attributes.getValue("requestID");
556          }
557          super.startElement(uri, localName, qName, attributes);
558        }
559      }
560    }
561