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:<userid>" or "dn:<DN>". 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