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