001/* 002 * Copyright 2007-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2007-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2007-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk; 037 038 039 040import java.util.ArrayList; 041import java.util.List; 042import java.util.concurrent.LinkedBlockingQueue; 043import java.util.concurrent.TimeUnit; 044import java.util.logging.Level; 045 046import com.unboundid.asn1.ASN1Buffer; 047import com.unboundid.asn1.ASN1BufferSequence; 048import com.unboundid.asn1.ASN1Element; 049import com.unboundid.asn1.ASN1OctetString; 050import com.unboundid.asn1.ASN1Sequence; 051import com.unboundid.ldap.protocol.LDAPMessage; 052import com.unboundid.ldap.protocol.LDAPResponse; 053import com.unboundid.ldap.protocol.ProtocolOp; 054import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest; 055import com.unboundid.util.Debug; 056import com.unboundid.util.Extensible; 057import com.unboundid.util.InternalUseOnly; 058import com.unboundid.util.NotMutable; 059import com.unboundid.util.NotNull; 060import com.unboundid.util.Nullable; 061import com.unboundid.util.StaticUtils; 062import com.unboundid.util.ThreadSafety; 063import com.unboundid.util.ThreadSafetyLevel; 064import com.unboundid.util.Validator; 065 066import static com.unboundid.ldap.sdk.LDAPMessages.*; 067 068 069 070/** 071 * This class implements the processing necessary to perform an LDAPv3 extended 072 * operation, which provides a way to request actions not included in the core 073 * LDAP protocol. Subclasses can provide logic to help implement more specific 074 * types of extended operations, but it is important to note that if such 075 * subclasses include an extended request value, then the request value must be 076 * kept up-to-date if any changes are made to custom elements in that class that 077 * would impact the request value encoding. 078 */ 079@Extensible() 080@NotMutable() 081@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 082public class ExtendedRequest 083 extends LDAPRequest 084 implements ResponseAcceptor, ProtocolOp 085{ 086 /** 087 * The BER type for the extended request OID element. 088 */ 089 protected static final byte TYPE_EXTENDED_REQUEST_OID = (byte) 0x80; 090 091 092 093 /** 094 * The BER type for the extended request value element. 095 */ 096 protected static final byte TYPE_EXTENDED_REQUEST_VALUE = (byte) 0x81; 097 098 099 100 /** 101 * The serial version UID for this serializable class. 102 */ 103 private static final long serialVersionUID = 5572410770060685796L; 104 105 106 107 // The encoded value for this extended request, if available. 108 @Nullable private final ASN1OctetString value; 109 110 // The message ID from the last LDAP message sent from this request. 111 private int messageID = -1; 112 113 // The queue that will be used to receive response messages from the server. 114 @NotNull private final LinkedBlockingQueue<LDAPResponse> responseQueue = 115 new LinkedBlockingQueue<>(); 116 117 // The OID for this extended request. 118 @NotNull private final String oid; 119 120 121 122 /** 123 * Creates a new extended request with the provided OID and no value. 124 * 125 * @param oid The OID for this extended request. It must not be 126 * {@code null}. 127 */ 128 public ExtendedRequest(@NotNull final String oid) 129 { 130 super(null); 131 132 Validator.ensureNotNull(oid); 133 134 this.oid = oid; 135 136 value = null; 137 } 138 139 140 141 /** 142 * Creates a new extended request with the provided OID and no value. 143 * 144 * @param oid The OID for this extended request. It must not be 145 * {@code null}. 146 * @param controls The set of controls for this extended request. 147 */ 148 public ExtendedRequest(@NotNull final String oid, 149 @Nullable final Control[] controls) 150 { 151 super(controls); 152 153 Validator.ensureNotNull(oid); 154 155 this.oid = oid; 156 157 value = null; 158 } 159 160 161 162 /** 163 * Creates a new extended request with the provided OID and value. 164 * 165 * @param oid The OID for this extended request. It must not be 166 * {@code null}. 167 * @param value The encoded value for this extended request. It may be 168 * {@code null} if this request should not have a value. 169 */ 170 public ExtendedRequest(@NotNull final String oid, 171 @Nullable final ASN1OctetString value) 172 { 173 super(null); 174 175 Validator.ensureNotNull(oid); 176 177 this.oid = oid; 178 this.value = value; 179 } 180 181 182 183 /** 184 * Creates a new extended request with the provided OID and value. 185 * 186 * @param oid The OID for this extended request. It must not be 187 * {@code null}. 188 * @param value The encoded value for this extended request. It may be 189 * {@code null} if this request should not have a value. 190 * @param controls The set of controls for this extended request. 191 */ 192 public ExtendedRequest(@NotNull final String oid, 193 @Nullable final ASN1OctetString value, 194 @Nullable final Control[] controls) 195 { 196 super(controls); 197 198 Validator.ensureNotNull(oid); 199 200 this.oid = oid; 201 this.value = value; 202 } 203 204 205 206 /** 207 * Creates a new extended request with the information from the provided 208 * extended request. 209 * 210 * @param extendedRequest The extended request that should be used to create 211 * this new extended request. 212 */ 213 protected ExtendedRequest(@NotNull final ExtendedRequest extendedRequest) 214 { 215 super(extendedRequest.getControls()); 216 217 messageID = extendedRequest.messageID; 218 oid = extendedRequest.oid; 219 value = extendedRequest.value; 220 } 221 222 223 224 /** 225 * Retrieves the OID for this extended request. 226 * 227 * @return The OID for this extended request. 228 */ 229 @NotNull() 230 public final String getOID() 231 { 232 return oid; 233 } 234 235 236 237 /** 238 * Indicates whether this extended request has a value. 239 * 240 * @return {@code true} if this extended request has a value, or 241 * {@code false} if not. 242 */ 243 public final boolean hasValue() 244 { 245 return (value != null); 246 } 247 248 249 250 /** 251 * Retrieves the encoded value for this extended request, if available. 252 * 253 * @return The encoded value for this extended request, or {@code null} if 254 * this request does not have a value. 255 */ 256 @Nullable() 257 public final ASN1OctetString getValue() 258 { 259 return value; 260 } 261 262 263 264 /** 265 * {@inheritDoc} 266 */ 267 @Override() 268 public final byte getProtocolOpType() 269 { 270 return LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST; 271 } 272 273 274 275 /** 276 * {@inheritDoc} 277 */ 278 @Override() 279 public final void writeTo(@NotNull final ASN1Buffer writer) 280 { 281 final ASN1BufferSequence requestSequence = 282 writer.beginSequence(LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST); 283 writer.addOctetString(TYPE_EXTENDED_REQUEST_OID, oid); 284 285 if (value != null) 286 { 287 writer.addOctetString(TYPE_EXTENDED_REQUEST_VALUE, value.getValue()); 288 } 289 requestSequence.end(); 290 } 291 292 293 294 /** 295 * Encodes the extended request protocol op to an ASN.1 element. 296 * 297 * @return The ASN.1 element with the encoded extended request protocol op. 298 */ 299 @Override() 300 @NotNull() 301 public ASN1Element encodeProtocolOp() 302 { 303 // Create the extended request protocol op. 304 final ASN1Element[] protocolOpElements; 305 if (value == null) 306 { 307 protocolOpElements = new ASN1Element[] 308 { 309 new ASN1OctetString(TYPE_EXTENDED_REQUEST_OID, oid) 310 }; 311 } 312 else 313 { 314 protocolOpElements = new ASN1Element[] 315 { 316 new ASN1OctetString(TYPE_EXTENDED_REQUEST_OID, oid), 317 new ASN1OctetString(TYPE_EXTENDED_REQUEST_VALUE, value.getValue()) 318 }; 319 } 320 321 return new ASN1Sequence(LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST, 322 protocolOpElements); 323 } 324 325 326 327 /** 328 * Sends this extended request to the directory server over the provided 329 * connection and returns the associated response. 330 * 331 * @param connection The connection to use to communicate with the directory 332 * server. 333 * @param depth The current referral depth for this request. It should 334 * always be one for the initial request, and should only 335 * be incremented when following referrals. 336 * 337 * @return An LDAP result object that provides information about the result 338 * of the extended operation processing. 339 * 340 * @throws LDAPException If a problem occurs while sending the request or 341 * reading the response. 342 */ 343 @Override() 344 @NotNull() 345 protected ExtendedResult process(@NotNull final LDAPConnection connection, 346 final int depth) 347 throws LDAPException 348 { 349 setReferralDepth(depth); 350 351 if (connection.synchronousMode()) 352 { 353 return processSync(connection); 354 } 355 356 // Create the LDAP message. 357 messageID = connection.nextMessageID(); 358 final LDAPMessage message = new LDAPMessage(messageID, this, getControls()); 359 360 361 // Register with the connection reader to be notified of responses for the 362 // request that we've created. 363 connection.registerResponseAcceptor(messageID, this); 364 365 366 try 367 { 368 // Send the request to the server. 369 final long responseTimeout = getResponseTimeoutMillis(connection); 370 Debug.debugLDAPRequest(Level.INFO, this, messageID, connection); 371 372 final LDAPConnectionLogger logger = 373 connection.getConnectionOptions().getConnectionLogger(); 374 if (logger != null) 375 { 376 logger.logExtendedRequest(connection, messageID, this); 377 } 378 379 final long requestTime = System.nanoTime(); 380 connection.getConnectionStatistics().incrementNumExtendedRequests(); 381 if (this instanceof StartTLSExtendedRequest) 382 { 383 connection.sendMessage(message, 50L); 384 } 385 else 386 { 387 connection.sendMessage(message, responseTimeout); 388 } 389 390 // Wait for and process the response. 391 final LDAPResponse response; 392 try 393 { 394 if (responseTimeout > 0) 395 { 396 response = responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS); 397 } 398 else 399 { 400 response = responseQueue.take(); 401 } 402 } 403 catch (final InterruptedException ie) 404 { 405 Debug.debugException(ie); 406 Thread.currentThread().interrupt(); 407 throw new LDAPException(ResultCode.LOCAL_ERROR, 408 ERR_EXTOP_INTERRUPTED.get(connection.getHostPort()), ie); 409 } 410 411 return handleResponse(connection, response, requestTime); 412 } 413 finally 414 { 415 connection.deregisterResponseAcceptor(messageID); 416 } 417 } 418 419 420 421 /** 422 * Processes this extended operation in synchronous mode, in which the same 423 * thread will send the request and read the response. 424 * 425 * @param connection The connection to use to communicate with the directory 426 * server. 427 * 428 * @return An LDAP result object that provides information about the result 429 * of the extended processing. 430 * 431 * @throws LDAPException If a problem occurs while sending the request or 432 * reading the response. 433 */ 434 @NotNull() 435 private ExtendedResult processSync(@NotNull final LDAPConnection connection) 436 throws LDAPException 437 { 438 // Create the LDAP message. 439 messageID = connection.nextMessageID(); 440 final LDAPMessage message = 441 new LDAPMessage(messageID, this, getControls()); 442 443 444 // Send the request to the server. 445 final long requestTime = System.nanoTime(); 446 Debug.debugLDAPRequest(Level.INFO, this, messageID, connection); 447 448 final LDAPConnectionLogger logger = 449 connection.getConnectionOptions().getConnectionLogger(); 450 if (logger != null) 451 { 452 logger.logExtendedRequest(connection, messageID, this); 453 } 454 455 connection.getConnectionStatistics().incrementNumExtendedRequests(); 456 connection.sendMessage(message, getResponseTimeoutMillis(connection)); 457 458 while (true) 459 { 460 final LDAPResponse response; 461 try 462 { 463 response = connection.readResponse(messageID); 464 } 465 catch (final LDAPException le) 466 { 467 Debug.debugException(le); 468 469 if ((le.getResultCode() == ResultCode.TIMEOUT) && 470 connection.getConnectionOptions().abandonOnTimeout()) 471 { 472 connection.abandon(messageID); 473 } 474 475 throw le; 476 } 477 478 if (response instanceof IntermediateResponse) 479 { 480 final IntermediateResponseListener listener = 481 getIntermediateResponseListener(); 482 if (listener != null) 483 { 484 listener.intermediateResponseReturned( 485 (IntermediateResponse) response); 486 } 487 } 488 else 489 { 490 return handleResponse(connection, response, requestTime); 491 } 492 } 493 } 494 495 496 497 /** 498 * Performs the necessary processing for handling a response. 499 * 500 * @param connection The connection used to read the response. 501 * @param response The response to be processed. 502 * @param requestTime The time the request was sent to the server. 503 * 504 * @return The extended result. 505 * 506 * @throws LDAPException If a problem occurs. 507 */ 508 @NotNull() 509 private ExtendedResult handleResponse( 510 @NotNull final LDAPConnection connection, 511 @Nullable final LDAPResponse response, 512 final long requestTime) 513 throws LDAPException 514 { 515 if (response == null) 516 { 517 final long waitTime = 518 StaticUtils.nanosToMillis(System.nanoTime() - requestTime); 519 if (connection.getConnectionOptions().abandonOnTimeout()) 520 { 521 connection.abandon(messageID); 522 } 523 524 throw new LDAPException(ResultCode.TIMEOUT, 525 ERR_EXTENDED_CLIENT_TIMEOUT.get(waitTime, messageID, oid, 526 connection.getHostPort())); 527 } 528 529 if (response instanceof ConnectionClosedResponse) 530 { 531 final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response; 532 final String msg = ccr.getMessage(); 533 if (msg == null) 534 { 535 // The connection was closed while waiting for the response. 536 throw new LDAPException(ccr.getResultCode(), 537 ERR_CONN_CLOSED_WAITING_FOR_EXTENDED_RESPONSE.get( 538 connection.getHostPort(), toString())); 539 } 540 else 541 { 542 // The connection was closed while waiting for the response. 543 throw new LDAPException(ccr.getResultCode(), 544 ERR_CONN_CLOSED_WAITING_FOR_EXTENDED_RESPONSE_WITH_MESSAGE.get( 545 connection.getHostPort(), toString(), msg)); 546 } 547 } 548 549 connection.getConnectionStatistics().incrementNumExtendedResponses( 550 System.nanoTime() - requestTime); 551 return (ExtendedResult) response; 552 } 553 554 555 556 /** 557 * {@inheritDoc} 558 */ 559 @InternalUseOnly() 560 @Override() 561 public final void responseReceived(@NotNull final LDAPResponse response) 562 throws LDAPException 563 { 564 try 565 { 566 responseQueue.put(response); 567 } 568 catch (final Exception e) 569 { 570 Debug.debugException(e); 571 572 if (e instanceof InterruptedException) 573 { 574 Thread.currentThread().interrupt(); 575 } 576 577 throw new LDAPException(ResultCode.LOCAL_ERROR, 578 ERR_EXCEPTION_HANDLING_RESPONSE.get( 579 StaticUtils.getExceptionMessage(e)), 580 e); 581 } 582 } 583 584 585 586 /** 587 * {@inheritDoc} 588 */ 589 @Override() 590 public final int getLastMessageID() 591 { 592 return messageID; 593 } 594 595 596 597 /** 598 * {@inheritDoc} 599 */ 600 @Override() 601 @NotNull() 602 public final OperationType getOperationType() 603 { 604 return OperationType.EXTENDED; 605 } 606 607 608 609 /** 610 * {@inheritDoc}. Subclasses should override this method to return a 611 * duplicate of the appropriate type. 612 */ 613 @Override() 614 @NotNull() 615 public ExtendedRequest duplicate() 616 { 617 return duplicate(getControls()); 618 } 619 620 621 622 /** 623 * {@inheritDoc}. Subclasses should override this method to return a 624 * duplicate of the appropriate type. 625 */ 626 @Override() 627 @NotNull() 628 public ExtendedRequest duplicate(@Nullable final Control[] controls) 629 { 630 final ExtendedRequest r = new ExtendedRequest(oid, value, controls); 631 r.setResponseTimeoutMillis(getResponseTimeoutMillis(null)); 632 r.setIntermediateResponseListener(getIntermediateResponseListener()); 633 r.setReferralDepth(getReferralDepth()); 634 r.setReferralConnector(getReferralConnectorInternal()); 635 return r; 636 } 637 638 639 640 /** 641 * Retrieves the user-friendly name for the extended request, if available. 642 * If no user-friendly name has been defined, then the OID will be returned. 643 * 644 * @return The user-friendly name for this extended request, or the OID if no 645 * user-friendly name is available. 646 */ 647 @NotNull() 648 public String getExtendedRequestName() 649 { 650 // By default, we will return the OID. Subclasses should override this to 651 // provide the user-friendly name. 652 return oid; 653 } 654 655 656 657 /** 658 * {@inheritDoc} 659 */ 660 @Override() 661 public void toString(@NotNull final StringBuilder buffer) 662 { 663 buffer.append("ExtendedRequest(oid='"); 664 buffer.append(oid); 665 buffer.append('\''); 666 667 final Control[] controls = getControls(); 668 if (controls.length > 0) 669 { 670 buffer.append(", controls={"); 671 for (int i=0; i < controls.length; i++) 672 { 673 if (i > 0) 674 { 675 buffer.append(", "); 676 } 677 678 buffer.append(controls[i]); 679 } 680 buffer.append('}'); 681 } 682 683 buffer.append(')'); 684 } 685 686 687 688 /** 689 * {@inheritDoc} 690 */ 691 @Override() 692 public void toCode(@NotNull final List<String> lineList, 693 @NotNull final String requestID, 694 final int indentSpaces, final boolean includeProcessing) 695 { 696 // Create the request variable. 697 final ArrayList<ToCodeArgHelper> constructorArgs = new ArrayList<>(3); 698 constructorArgs.add(ToCodeArgHelper.createString(oid, "Request OID")); 699 constructorArgs.add(ToCodeArgHelper.createASN1OctetString(value, 700 "Request Value")); 701 702 final Control[] controls = getControls(); 703 if (controls.length > 0) 704 { 705 constructorArgs.add(ToCodeArgHelper.createControlArray(controls, 706 "Request Controls")); 707 } 708 709 ToCodeHelper.generateMethodCall(lineList, indentSpaces, "ExtendedRequest", 710 requestID + "Request", "new ExtendedRequest", constructorArgs); 711 712 713 // Add lines for processing the request and obtaining the result. 714 if (includeProcessing) 715 { 716 // Generate a string with the appropriate indent. 717 final StringBuilder buffer = new StringBuilder(); 718 for (int i=0; i < indentSpaces; i++) 719 { 720 buffer.append(' '); 721 } 722 final String indent = buffer.toString(); 723 724 lineList.add(""); 725 lineList.add(indent + "try"); 726 lineList.add(indent + '{'); 727 lineList.add(indent + " ExtendedResult " + requestID + 728 "Result = connection.processExtendedOperation(" + requestID + 729 "Request);"); 730 lineList.add(indent + " // The extended operation was processed and " + 731 "we have a result."); 732 lineList.add(indent + " // This does not necessarily mean that the " + 733 "operation was successful."); 734 lineList.add(indent + " // Examine the result details for more " + 735 "information."); 736 lineList.add(indent + " ResultCode resultCode = " + requestID + 737 "Result.getResultCode();"); 738 lineList.add(indent + " String message = " + requestID + 739 "Result.getMessage();"); 740 lineList.add(indent + " String matchedDN = " + requestID + 741 "Result.getMatchedDN();"); 742 lineList.add(indent + " String[] referralURLs = " + requestID + 743 "Result.getReferralURLs();"); 744 lineList.add(indent + " String responseOID = " + requestID + 745 "Result.getOID();"); 746 lineList.add(indent + " ASN1OctetString responseValue = " + requestID + 747 "Result.getValue();"); 748 lineList.add(indent + " Control[] responseControls = " + requestID + 749 "Result.getResponseControls();"); 750 lineList.add(indent + '}'); 751 lineList.add(indent + "catch (LDAPException e)"); 752 lineList.add(indent + '{'); 753 lineList.add(indent + " // A problem was encountered while attempting " + 754 "to process the extended operation."); 755 lineList.add(indent + " // Maybe the following will help explain why."); 756 lineList.add(indent + " ResultCode resultCode = e.getResultCode();"); 757 lineList.add(indent + " String message = e.getMessage();"); 758 lineList.add(indent + " String matchedDN = e.getMatchedDN();"); 759 lineList.add(indent + " String[] referralURLs = e.getReferralURLs();"); 760 lineList.add(indent + " Control[] responseControls = " + 761 "e.getResponseControls();"); 762 lineList.add(indent + '}'); 763 } 764 } 765}