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