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