001/* 002 * Copyright 2010-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2010-2023 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) 2010-2023 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.listener; 037 038 039 040import java.io.Closeable; 041import java.io.IOException; 042import java.io.OutputStream; 043import java.net.Socket; 044import java.util.ArrayList; 045import java.util.List; 046import java.util.concurrent.CopyOnWriteArrayList; 047import java.util.concurrent.atomic.AtomicBoolean; 048import javax.net.ssl.SSLSocket; 049import javax.net.ssl.SSLSocketFactory; 050 051import com.unboundid.asn1.ASN1Buffer; 052import com.unboundid.asn1.ASN1StreamReader; 053import com.unboundid.ldap.protocol.AddResponseProtocolOp; 054import com.unboundid.ldap.protocol.BindResponseProtocolOp; 055import com.unboundid.ldap.protocol.CompareResponseProtocolOp; 056import com.unboundid.ldap.protocol.DeleteResponseProtocolOp; 057import com.unboundid.ldap.protocol.ExtendedResponseProtocolOp; 058import com.unboundid.ldap.protocol.IntermediateResponseProtocolOp; 059import com.unboundid.ldap.protocol.LDAPMessage; 060import com.unboundid.ldap.protocol.ModifyResponseProtocolOp; 061import com.unboundid.ldap.protocol.ModifyDNResponseProtocolOp; 062import com.unboundid.ldap.protocol.SearchResultDoneProtocolOp; 063import com.unboundid.ldap.protocol.SearchResultEntryProtocolOp; 064import com.unboundid.ldap.protocol.SearchResultReferenceProtocolOp; 065import com.unboundid.ldap.sdk.Control; 066import com.unboundid.ldap.sdk.Entry; 067import com.unboundid.ldap.sdk.ExtendedResult; 068import com.unboundid.ldap.sdk.LDAPConnectionOptions; 069import com.unboundid.ldap.sdk.LDAPException; 070import com.unboundid.ldap.sdk.LDAPRuntimeException; 071import com.unboundid.ldap.sdk.ResultCode; 072import com.unboundid.ldap.sdk.extensions.NoticeOfDisconnectionExtendedResult; 073import com.unboundid.util.Debug; 074import com.unboundid.util.InternalUseOnly; 075import com.unboundid.util.NotNull; 076import com.unboundid.util.Nullable; 077import com.unboundid.util.ObjectPair; 078import com.unboundid.util.StaticUtils; 079import com.unboundid.util.ThreadSafety; 080import com.unboundid.util.ThreadSafetyLevel; 081import com.unboundid.util.Validator; 082 083import static com.unboundid.ldap.listener.ListenerMessages.*; 084 085 086 087/** 088 * This class provides an object which will be used to represent a connection to 089 * a client accepted by an {@link LDAPListener}, although connections may also 090 * be created independently if they were accepted in some other way. Each 091 * connection has its own thread that will be used to read requests from the 092 * client, and connections created outside of an {@code LDAPListener} instance, 093 * then the thread must be explicitly started. 094 */ 095@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 096public final class LDAPListenerClientConnection 097 extends Thread 098 implements Closeable 099{ 100 /** 101 * A pre-allocated empty array of controls. 102 */ 103 @NotNull private static final Control[] EMPTY_CONTROL_ARRAY = new Control[0]; 104 105 106 107 // The buffer used to hold responses to be sent to the client. 108 @NotNull private final ASN1Buffer asn1Buffer; 109 110 // The ASN.1 stream reader used to read requests from the client. 111 @NotNull private volatile ASN1StreamReader asn1Reader; 112 113 // Indicates whether to suppress the next call to sendMessage to send a 114 // response to the client. 115 @NotNull private final AtomicBoolean suppressNextResponse; 116 117 // The set of intermediate response transformers for this connection. 118 @NotNull private final CopyOnWriteArrayList<IntermediateResponseTransformer> 119 intermediateResponseTransformers; 120 121 // The set of search result entry transformers for this connection. 122 @NotNull private final CopyOnWriteArrayList<SearchEntryTransformer> 123 searchEntryTransformers; 124 125 // The set of search result reference transformers for this connection. 126 @NotNull private final CopyOnWriteArrayList<SearchReferenceTransformer> 127 searchReferenceTransformers; 128 129 // The listener that accepted this connection. 130 @Nullable private final LDAPListener listener; 131 132 // The exception handler to use for this connection, if any. 133 @Nullable private final LDAPListenerExceptionHandler exceptionHandler; 134 135 // The request handler to use for this connection. 136 @NotNull private final LDAPListenerRequestHandler requestHandler; 137 138 // The connection ID assigned to this connection. 139 private final long connectionID; 140 141 // The output stream used to write responses to the client. 142 @NotNull private volatile OutputStream outputStream; 143 144 // The socket used to communicate with the client. 145 @NotNull private volatile Socket socket; 146 147 148 149 /** 150 * Creates a new LDAP listener client connection that will communicate with 151 * the client using the provided socket. The {@link #start} method must be 152 * called to start listening for requests from the client. 153 * 154 * @param listener The listener that accepted this client 155 * connection. It may be {@code null} if this 156 * connection was not accepted by a listener. 157 * @param socket The socket that may be used to communicate with 158 * the client. It must not be {@code null}. 159 * @param requestHandler The request handler that will be used to process 160 * requests read from the client. The 161 * {@link LDAPListenerRequestHandler#newInstance} 162 * method will be called on the provided object to 163 * obtain a new instance to use for this connection. 164 * The provided request handler must not be 165 * {@code null}. 166 * @param exceptionHandler The disconnect handler to be notified when this 167 * connection is closed. It may be {@code null} if 168 * no disconnect handler should be used. 169 * 170 * @throws LDAPException If a problem occurs while preparing this client 171 * connection. for use. If this is thrown, then the 172 * provided socket will be closed. 173 */ 174 public LDAPListenerClientConnection(@Nullable final LDAPListener listener, 175 @NotNull final Socket socket, 176 @NotNull final LDAPListenerRequestHandler requestHandler, 177 @Nullable final LDAPListenerExceptionHandler exceptionHandler) 178 throws LDAPException 179 { 180 Validator.ensureNotNull(socket, requestHandler); 181 182 setName("LDAPListener client connection reader for connection from " + 183 socket.getInetAddress().getHostAddress() + ':' + 184 socket.getPort() + " to " + socket.getLocalAddress().getHostAddress() + 185 ':' + socket.getLocalPort()); 186 187 this.listener = listener; 188 this.socket = socket; 189 this.exceptionHandler = exceptionHandler; 190 191 asn1Buffer = new ASN1Buffer(); 192 suppressNextResponse = new AtomicBoolean(false); 193 194 intermediateResponseTransformers = new CopyOnWriteArrayList<>(); 195 searchEntryTransformers = new CopyOnWriteArrayList<>(); 196 searchReferenceTransformers = new CopyOnWriteArrayList<>(); 197 198 if (listener == null) 199 { 200 connectionID = -1L; 201 } 202 else 203 { 204 connectionID = listener.nextConnectionID(); 205 } 206 207 try 208 { 209 final LDAPListenerConfig config; 210 if (listener == null) 211 { 212 config = new LDAPListenerConfig(0, requestHandler); 213 } 214 else 215 { 216 config = listener.getConfig(); 217 } 218 219 socket.setKeepAlive(config.useKeepAlive()); 220 socket.setReuseAddress(config.useReuseAddress()); 221 socket.setSoLinger(config.useLinger(), config.getLingerTimeoutSeconds()); 222 socket.setTcpNoDelay(config.useTCPNoDelay()); 223 224 final int sendBufferSize = config.getSendBufferSize(); 225 if (sendBufferSize > 0) 226 { 227 socket.setSendBufferSize(sendBufferSize); 228 } 229 230 if (socket instanceof SSLSocket) 231 { 232 final SSLSocket sslSocket = (SSLSocket) socket; 233 if (config.requestClientCertificate()) 234 { 235 if (config.requireClientCertificate()) 236 { 237 sslSocket.setNeedClientAuth(true); 238 } 239 else 240 { 241 sslSocket.setWantClientAuth(true); 242 } 243 } 244 else 245 { 246 sslSocket.setWantClientAuth(false); 247 } 248 } 249 250 final int maxMessageSizeBytes; 251 if (listener == null) 252 { 253 asn1Reader = new ASN1StreamReader(socket.getInputStream()); 254 } 255 else 256 { 257 asn1Reader = new ASN1StreamReader(socket.getInputStream(), 258 listener.getConfig().getMaxMessageSizeBytes()); 259 } 260 261 } 262 catch (final IOException ioe) 263 { 264 Debug.debugException(ioe); 265 266 try 267 { 268 socket.close(); 269 } 270 catch (final Exception e) 271 { 272 Debug.debugException(e); 273 } 274 275 throw new LDAPException(ResultCode.CONNECT_ERROR, 276 ERR_CONN_CREATE_IO_EXCEPTION.get( 277 StaticUtils.getExceptionMessage(ioe)), 278 ioe); 279 } 280 281 try 282 { 283 outputStream = socket.getOutputStream(); 284 } 285 catch (final IOException ioe) 286 { 287 Debug.debugException(ioe); 288 289 try 290 { 291 asn1Reader.close(); 292 } 293 catch (final Exception e) 294 { 295 Debug.debugException(e); 296 } 297 298 try 299 { 300 socket.close(); 301 } 302 catch (final Exception e) 303 { 304 Debug.debugException(e); 305 } 306 307 throw new LDAPException(ResultCode.CONNECT_ERROR, 308 ERR_CONN_CREATE_IO_EXCEPTION.get( 309 StaticUtils.getExceptionMessage(ioe)), 310 ioe); 311 } 312 313 try 314 { 315 this.requestHandler = requestHandler.newInstance(this); 316 } 317 catch (final LDAPException le) 318 { 319 Debug.debugException(le); 320 321 try 322 { 323 asn1Reader.close(); 324 } 325 catch (final Exception e) 326 { 327 Debug.debugException(e); 328 } 329 330 try 331 { 332 outputStream.close(); 333 } 334 catch (final Exception e) 335 { 336 Debug.debugException(e); 337 } 338 339 try 340 { 341 socket.close(); 342 } 343 catch (final Exception e) 344 { 345 Debug.debugException(e); 346 } 347 348 throw le; 349 } 350 } 351 352 353 354 /** 355 * Closes the connection to the client. 356 * 357 * @throws IOException If a problem occurs while closing the socket. 358 */ 359 @Override() 360 public synchronized void close() 361 throws IOException 362 { 363 try 364 { 365 requestHandler.closeInstance(); 366 } 367 catch (final Exception e) 368 { 369 Debug.debugException(e); 370 } 371 372 try 373 { 374 asn1Reader.close(); 375 } 376 catch (final Exception e) 377 { 378 Debug.debugException(e); 379 } 380 381 try 382 { 383 outputStream.close(); 384 } 385 catch (final Exception e) 386 { 387 Debug.debugException(e); 388 } 389 390 socket.close(); 391 } 392 393 394 395 /** 396 * Closes the connection to the client as a result of an exception encountered 397 * during processing. Any associated exception handler will be notified 398 * prior to the connection closure. 399 * 400 * @param le The exception providing information about the reason that this 401 * connection will be terminated. 402 */ 403 void close(@NotNull final LDAPException le) 404 { 405 if (exceptionHandler == null) 406 { 407 Debug.debugException(le); 408 } 409 else 410 { 411 try 412 { 413 exceptionHandler.connectionTerminated(this, le); 414 } 415 catch (final Exception e) 416 { 417 Debug.debugException(e); 418 } 419 } 420 421 try 422 { 423 sendUnsolicitedNotification(new NoticeOfDisconnectionExtendedResult(le)); 424 } 425 catch (final Exception e) 426 { 427 Debug.debugException(e); 428 } 429 430 try 431 { 432 close(); 433 } 434 catch (final Exception e) 435 { 436 Debug.debugException(e); 437 } 438 } 439 440 441 442 /** 443 * Operates in a loop, waiting for a request to arrive from the client and 444 * handing it off to the request handler for processing. This method is for 445 * internal use only and must not be invoked by external callers. 446 */ 447 @InternalUseOnly() 448 @Override() 449 public void run() 450 { 451 try 452 { 453 while (true) 454 { 455 final LDAPMessage requestMessage; 456 try 457 { 458 requestMessage = LDAPMessage.readFrom(asn1Reader, false); 459 if (requestMessage == null) 460 { 461 // This indicates that the client has closed the connection without 462 // an unbind request. It's not all that nice, but it isn't an error 463 // so we won't notify the exception handler. 464 try 465 { 466 close(); 467 } 468 catch (final IOException ioe) 469 { 470 Debug.debugException(ioe); 471 } 472 473 return; 474 } 475 } 476 catch (final LDAPException le) 477 { 478 // This indicates that the client sent a malformed request. 479 Debug.debugException(le); 480 close(le); 481 return; 482 } 483 484 try 485 { 486 final int messageID = requestMessage.getMessageID(); 487 final List<Control> controls = requestMessage.getControls(); 488 489 LDAPMessage responseMessage; 490 switch (requestMessage.getProtocolOpType()) 491 { 492 case LDAPMessage.PROTOCOL_OP_TYPE_ABANDON_REQUEST: 493 requestHandler.processAbandonRequest(messageID, 494 requestMessage.getAbandonRequestProtocolOp(), controls); 495 responseMessage = null; 496 break; 497 498 case LDAPMessage.PROTOCOL_OP_TYPE_ADD_REQUEST: 499 try 500 { 501 responseMessage = requestHandler.processAddRequest(messageID, 502 requestMessage.getAddRequestProtocolOp(), controls); 503 } 504 catch (final Exception e) 505 { 506 Debug.debugException(e); 507 responseMessage = new LDAPMessage(messageID, 508 new AddResponseProtocolOp( 509 ResultCode.OTHER_INT_VALUE, null, 510 ERR_CONN_REQUEST_HANDLER_FAILURE.get( 511 StaticUtils.getExceptionMessage(e)), 512 null)); 513 } 514 break; 515 516 case LDAPMessage.PROTOCOL_OP_TYPE_BIND_REQUEST: 517 try 518 { 519 responseMessage = requestHandler.processBindRequest(messageID, 520 requestMessage.getBindRequestProtocolOp(), controls); 521 } 522 catch (final Exception e) 523 { 524 Debug.debugException(e); 525 responseMessage = new LDAPMessage(messageID, 526 new BindResponseProtocolOp( 527 ResultCode.OTHER_INT_VALUE, null, 528 ERR_CONN_REQUEST_HANDLER_FAILURE.get( 529 StaticUtils.getExceptionMessage(e)), 530 null, null)); 531 } 532 break; 533 534 case LDAPMessage.PROTOCOL_OP_TYPE_COMPARE_REQUEST: 535 try 536 { 537 responseMessage = requestHandler.processCompareRequest( 538 messageID, requestMessage.getCompareRequestProtocolOp(), 539 controls); 540 } 541 catch (final Exception e) 542 { 543 Debug.debugException(e); 544 responseMessage = new LDAPMessage(messageID, 545 new CompareResponseProtocolOp( 546 ResultCode.OTHER_INT_VALUE, null, 547 ERR_CONN_REQUEST_HANDLER_FAILURE.get( 548 StaticUtils.getExceptionMessage(e)), 549 null)); 550 } 551 break; 552 553 case LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST: 554 try 555 { 556 responseMessage = requestHandler.processDeleteRequest(messageID, 557 requestMessage.getDeleteRequestProtocolOp(), controls); 558 } 559 catch (final Exception e) 560 { 561 Debug.debugException(e); 562 responseMessage = new LDAPMessage(messageID, 563 new DeleteResponseProtocolOp( 564 ResultCode.OTHER_INT_VALUE, null, 565 ERR_CONN_REQUEST_HANDLER_FAILURE.get( 566 StaticUtils.getExceptionMessage(e)), 567 null)); 568 } 569 break; 570 571 case LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST: 572 try 573 { 574 responseMessage = requestHandler.processExtendedRequest( 575 messageID, requestMessage.getExtendedRequestProtocolOp(), 576 controls); 577 } 578 catch (final Exception e) 579 { 580 Debug.debugException(e); 581 responseMessage = new LDAPMessage(messageID, 582 new ExtendedResponseProtocolOp( 583 ResultCode.OTHER_INT_VALUE, null, 584 ERR_CONN_REQUEST_HANDLER_FAILURE.get( 585 StaticUtils.getExceptionMessage(e)), 586 null, null, null)); 587 } 588 break; 589 590 case LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST: 591 try 592 { 593 responseMessage = requestHandler.processModifyRequest(messageID, 594 requestMessage.getModifyRequestProtocolOp(), controls); 595 } 596 catch (final Exception e) 597 { 598 Debug.debugException(e); 599 responseMessage = new LDAPMessage(messageID, 600 new ModifyResponseProtocolOp( 601 ResultCode.OTHER_INT_VALUE, null, 602 ERR_CONN_REQUEST_HANDLER_FAILURE.get( 603 StaticUtils.getExceptionMessage(e)), 604 null)); 605 } 606 break; 607 608 case LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_DN_REQUEST: 609 try 610 { 611 responseMessage = requestHandler.processModifyDNRequest( 612 messageID, requestMessage.getModifyDNRequestProtocolOp(), 613 controls); 614 } 615 catch (final Exception e) 616 { 617 Debug.debugException(e); 618 responseMessage = new LDAPMessage(messageID, 619 new ModifyDNResponseProtocolOp( 620 ResultCode.OTHER_INT_VALUE, null, 621 ERR_CONN_REQUEST_HANDLER_FAILURE.get( 622 StaticUtils.getExceptionMessage(e)), 623 null)); 624 } 625 break; 626 627 case LDAPMessage.PROTOCOL_OP_TYPE_SEARCH_REQUEST: 628 try 629 { 630 responseMessage = requestHandler.processSearchRequest(messageID, 631 requestMessage.getSearchRequestProtocolOp(), controls); 632 } 633 catch (final Exception e) 634 { 635 Debug.debugException(e); 636 responseMessage = new LDAPMessage(messageID, 637 new SearchResultDoneProtocolOp( 638 ResultCode.OTHER_INT_VALUE, null, 639 ERR_CONN_REQUEST_HANDLER_FAILURE.get( 640 StaticUtils.getExceptionMessage(e)), 641 null)); 642 } 643 break; 644 645 case LDAPMessage.PROTOCOL_OP_TYPE_UNBIND_REQUEST: 646 requestHandler.processUnbindRequest(messageID, 647 requestMessage.getUnbindRequestProtocolOp(), controls); 648 close(); 649 return; 650 651 default: 652 close(new LDAPException(ResultCode.PROTOCOL_ERROR, 653 ERR_CONN_INVALID_PROTOCOL_OP_TYPE.get(StaticUtils.toHex( 654 requestMessage.getProtocolOpType())))); 655 return; 656 } 657 658 if (responseMessage != null) 659 { 660 try 661 { 662 sendMessage(responseMessage); 663 } 664 catch (final LDAPException le) 665 { 666 Debug.debugException(le); 667 close(le); 668 return; 669 } 670 } 671 } 672 catch (final Throwable t) 673 { 674 close(new LDAPException(ResultCode.LOCAL_ERROR, 675 ERR_CONN_EXCEPTION_IN_REQUEST_HANDLER.get( 676 String.valueOf(requestMessage), 677 StaticUtils.getExceptionMessage(t)))); 678 StaticUtils.throwErrorOrRuntimeException(t); 679 } 680 } 681 } 682 finally 683 { 684 if (listener != null) 685 { 686 listener.connectionClosed(this); 687 } 688 } 689 } 690 691 692 693 /** 694 * Sends the provided message to the client. 695 * 696 * @param message The message to be written to the client. 697 * 698 * @throws LDAPException If a problem occurs while attempting to send the 699 * response to the client. 700 */ 701 private synchronized void sendMessage(@NotNull final LDAPMessage message) 702 throws LDAPException 703 { 704 // If we should suppress this response (which will only be because the 705 // response has already been sent through some other means, for example as 706 // part of StartTLS processing), then do so. 707 if (suppressNextResponse.compareAndSet(true, false)) 708 { 709 return; 710 } 711 712 asn1Buffer.clear(); 713 714 try 715 { 716 message.writeTo(asn1Buffer); 717 } 718 catch (final LDAPRuntimeException lre) 719 { 720 Debug.debugException(lre); 721 lre.throwLDAPException(); 722 } 723 724 try 725 { 726 asn1Buffer.writeTo(outputStream); 727 } 728 catch (final IOException ioe) 729 { 730 Debug.debugException(ioe); 731 732 throw new LDAPException(ResultCode.LOCAL_ERROR, 733 ERR_CONN_SEND_MESSAGE_EXCEPTION.get( 734 StaticUtils.getExceptionMessage(ioe)), 735 ioe); 736 } 737 finally 738 { 739 if (asn1Buffer.zeroBufferOnClear()) 740 { 741 asn1Buffer.clear(); 742 } 743 } 744 } 745 746 747 748 /** 749 * Sends a search result entry message to the client with the provided 750 * information. 751 * 752 * @param messageID The message ID for the LDAP message to send to the 753 * client. It must match the message ID of the associated 754 * search request. 755 * @param protocolOp The search result entry protocol op to include in the 756 * LDAP message to send to the client. It must not be 757 * {@code null}. 758 * @param controls The set of controls to include in the response message. 759 * It may be empty or {@code null} if no controls should 760 * be included. 761 * 762 * @throws LDAPException If a problem occurs while attempting to send the 763 * provided response message. If an exception is 764 * thrown, then the client connection will have been 765 * terminated. 766 */ 767 public void sendSearchResultEntry(final int messageID, 768 @NotNull final SearchResultEntryProtocolOp protocolOp, 769 @Nullable final Control... controls) 770 throws LDAPException 771 { 772 if (searchEntryTransformers.isEmpty()) 773 { 774 sendMessage(new LDAPMessage(messageID, protocolOp, controls)); 775 } 776 else 777 { 778 Control[] c; 779 SearchResultEntryProtocolOp op = protocolOp; 780 if (controls == null) 781 { 782 c = EMPTY_CONTROL_ARRAY; 783 } 784 else 785 { 786 c = controls; 787 } 788 789 for (final SearchEntryTransformer t : searchEntryTransformers) 790 { 791 try 792 { 793 final ObjectPair<SearchResultEntryProtocolOp,Control[]> p = 794 t.transformEntry(messageID, op, c); 795 if (p == null) 796 { 797 return; 798 } 799 800 op = p.getFirst(); 801 c = p.getSecond(); 802 } 803 catch (final Exception e) 804 { 805 Debug.debugException(e); 806 sendMessage(new LDAPMessage(messageID, protocolOp, c)); 807 throw new LDAPException(ResultCode.LOCAL_ERROR, 808 ERR_CONN_SEARCH_ENTRY_TRANSFORMER_EXCEPTION.get( 809 t.getClass().getName(), String.valueOf(op), 810 StaticUtils.getExceptionMessage(e)), 811 e); 812 } 813 } 814 815 sendMessage(new LDAPMessage(messageID, op, c)); 816 } 817 } 818 819 820 821 /** 822 * Sends a search result entry message to the client with the provided 823 * information. 824 * 825 * @param messageID The message ID for the LDAP message to send to the 826 * client. It must match the message ID of the associated 827 * search request. 828 * @param entry The entry to return to the client. It must not be 829 * {@code null}. 830 * @param controls The set of controls to include in the response message. 831 * It may be empty or {@code null} if no controls should be 832 * included. 833 * 834 * @throws LDAPException If a problem occurs while attempting to send the 835 * provided response message. If an exception is 836 * thrown, then the client connection will have been 837 * terminated. 838 */ 839 public void sendSearchResultEntry(final int messageID, 840 @NotNull final Entry entry, 841 @Nullable final Control... controls) 842 throws LDAPException 843 { 844 sendSearchResultEntry(messageID, 845 new SearchResultEntryProtocolOp(entry.getDN(), 846 new ArrayList<>(entry.getAttributes())), 847 controls); 848 } 849 850 851 852 /** 853 * Sends a search result reference message to the client with the provided 854 * information. 855 * 856 * @param messageID The message ID for the LDAP message to send to the 857 * client. It must match the message ID of the associated 858 * search request. 859 * @param protocolOp The search result reference protocol op to include in 860 * the LDAP message to send to the client. 861 * @param controls The set of controls to include in the response message. 862 * It may be empty or {@code null} if no controls should 863 * be included. 864 * 865 * @throws LDAPException If a problem occurs while attempting to send the 866 * provided response message. If an exception is 867 * thrown, then the client connection will have been 868 * terminated. 869 */ 870 public void sendSearchResultReference(final int messageID, 871 @NotNull final SearchResultReferenceProtocolOp protocolOp, 872 @Nullable final Control... controls) 873 throws LDAPException 874 { 875 if (searchReferenceTransformers.isEmpty()) 876 { 877 sendMessage(new LDAPMessage(messageID, protocolOp, controls)); 878 } 879 else 880 { 881 Control[] c; 882 SearchResultReferenceProtocolOp op = protocolOp; 883 if (controls == null) 884 { 885 c = EMPTY_CONTROL_ARRAY; 886 } 887 else 888 { 889 c = controls; 890 } 891 892 for (final SearchReferenceTransformer t : searchReferenceTransformers) 893 { 894 try 895 { 896 final ObjectPair<SearchResultReferenceProtocolOp,Control[]> p = 897 t.transformReference(messageID, op, c); 898 if (p == null) 899 { 900 return; 901 } 902 903 op = p.getFirst(); 904 c = p.getSecond(); 905 } 906 catch (final Exception e) 907 { 908 Debug.debugException(e); 909 sendMessage(new LDAPMessage(messageID, protocolOp, c)); 910 throw new LDAPException(ResultCode.LOCAL_ERROR, 911 ERR_CONN_SEARCH_REFERENCE_TRANSFORMER_EXCEPTION.get( 912 t.getClass().getName(), String.valueOf(op), 913 StaticUtils.getExceptionMessage(e)), 914 e); 915 } 916 } 917 918 sendMessage(new LDAPMessage(messageID, op, c)); 919 } 920 } 921 922 923 924 /** 925 * Sends an intermediate response message to the client with the provided 926 * information. 927 * 928 * @param messageID The message ID for the LDAP message to send to the 929 * client. It must match the message ID of the associated 930 * search request. 931 * @param protocolOp The intermediate response protocol op to include in the 932 * LDAP message to send to the client. 933 * @param controls The set of controls to include in the response message. 934 * It may be empty or {@code null} if no controls should 935 * be included. 936 * 937 * @throws LDAPException If a problem occurs while attempting to send the 938 * provided response message. If an exception is 939 * thrown, then the client connection will have been 940 * terminated. 941 */ 942 public void sendIntermediateResponse(final int messageID, 943 @NotNull final IntermediateResponseProtocolOp protocolOp, 944 @Nullable final Control... controls) 945 throws LDAPException 946 { 947 if (intermediateResponseTransformers.isEmpty()) 948 { 949 sendMessage(new LDAPMessage(messageID, protocolOp, controls)); 950 } 951 else 952 { 953 Control[] c; 954 IntermediateResponseProtocolOp op = protocolOp; 955 if (controls == null) 956 { 957 c = EMPTY_CONTROL_ARRAY; 958 } 959 else 960 { 961 c = controls; 962 } 963 964 for (final IntermediateResponseTransformer t : 965 intermediateResponseTransformers) 966 { 967 try 968 { 969 final ObjectPair<IntermediateResponseProtocolOp,Control[]> p = 970 t.transformIntermediateResponse(messageID, op, c); 971 if (p == null) 972 { 973 return; 974 } 975 976 op = p.getFirst(); 977 c = p.getSecond(); 978 } 979 catch (final Exception e) 980 { 981 Debug.debugException(e); 982 sendMessage(new LDAPMessage(messageID, protocolOp, c)); 983 throw new LDAPException(ResultCode.LOCAL_ERROR, 984 ERR_CONN_INTERMEDIATE_RESPONSE_TRANSFORMER_EXCEPTION.get( 985 t.getClass().getName(), String.valueOf(op), 986 StaticUtils.getExceptionMessage(e)), 987 e); 988 } 989 } 990 991 sendMessage(new LDAPMessage(messageID, op, c)); 992 } 993 } 994 995 996 997 /** 998 * Sends an unsolicited notification message to the client with the provided 999 * extended result. 1000 * 1001 * @param result The extended result to use for the unsolicited 1002 * notification. 1003 * 1004 * @throws LDAPException If a problem occurs while attempting to send the 1005 * unsolicited notification. If an exception is 1006 * thrown, then the client connection will have been 1007 * terminated. 1008 */ 1009 public void sendUnsolicitedNotification(@NotNull final ExtendedResult result) 1010 throws LDAPException 1011 { 1012 sendUnsolicitedNotification( 1013 new ExtendedResponseProtocolOp(result.getResultCode().intValue(), 1014 result.getMatchedDN(), result.getDiagnosticMessage(), 1015 StaticUtils.toList(result.getReferralURLs()), result.getOID(), 1016 result.getValue()), 1017 result.getResponseControls() 1018 ); 1019 } 1020 1021 1022 1023 /** 1024 * Sends an unsolicited notification message to the client with the provided 1025 * information. 1026 * 1027 * @param extendedResponse The extended response to use for the unsolicited 1028 * notification. 1029 * @param controls The set of controls to include with the 1030 * unsolicited notification. It may be empty or 1031 * {@code null} if no controls should be included. 1032 * 1033 * @throws LDAPException If a problem occurs while attempting to send the 1034 * unsolicited notification. If an exception is 1035 * thrown, then the client connection will have been 1036 * terminated. 1037 */ 1038 public void sendUnsolicitedNotification( 1039 @NotNull final ExtendedResponseProtocolOp extendedResponse, 1040 @Nullable final Control... controls) 1041 throws LDAPException 1042 { 1043 sendMessage(new LDAPMessage(0, extendedResponse, controls)); 1044 } 1045 1046 1047 1048 /** 1049 * Retrieves the socket used to communicate with the client. 1050 * 1051 * @return The socket used to communicate with the client. 1052 */ 1053 @NotNull() 1054 public synchronized Socket getSocket() 1055 { 1056 return socket; 1057 } 1058 1059 1060 1061 /** 1062 * Attempts to convert this unencrypted connection to one that uses TLS 1063 * encryption, as would be used during the course of invoking the StartTLS 1064 * extended operation. If this is called, then the response that would have 1065 * been returned from the associated request will be suppressed, so the 1066 * returned output stream must be used to send the appropriate response to 1067 * the client. 1068 * 1069 * @param sslSocketFactory The SSL socket factory that will be used to 1070 * convert the existing {@code Socket} to an 1071 * {@code SSLSocket}. 1072 * 1073 * @return An output stream that can be used to send a clear-text message to 1074 * the client (e.g., the StartTLS response message). 1075 * 1076 * @throws LDAPException If a problem is encountered while trying to convert 1077 * the existing socket to an SSL socket. If this is 1078 * thrown, then the connection will have been closed. 1079 */ 1080 @NotNull() 1081 public synchronized OutputStream convertToTLS( 1082 @NotNull final SSLSocketFactory sslSocketFactory) 1083 throws LDAPException 1084 { 1085 return convertToTLS(sslSocketFactory, false, false); 1086 } 1087 1088 1089 1090 /** 1091 * Attempts to convert this unencrypted connection to one that uses TLS 1092 * encryption, as would be used during the course of invoking the StartTLS 1093 * extended operation. If this is called, then the response that would have 1094 * been returned from the associated request will be suppressed, so the 1095 * returned output stream must be used to send the appropriate response to 1096 * the client. 1097 * 1098 * @param sslSocketFactory The SSL socket factory that will be used 1099 * to convert the existing {@code Socket} to 1100 * an {@code SSLSocket}. 1101 * @param requestClientCertificate Indicates whether the listener should 1102 * request that the client present its own 1103 * certificate chain during TLS negotiation. 1104 * This will be ignored for non-TLS-based 1105 * connections. 1106 * @param requireClientCertificate Indicates whether the listener should 1107 * require that the client present its own 1108 * certificate chain during TLS negotiation, 1109 * and should fail negotiation if the client 1110 * does not present one. This will be 1111 * ignored for non-TLS-based connections or 1112 * if {@code requestClientCertificate} is 1113 * {@code false}. 1114 * 1115 * @return An output stream that can be used to send a clear-text message to 1116 * the client (e.g., the StartTLS response message). 1117 * 1118 * @throws LDAPException If a problem is encountered while trying to convert 1119 * the existing socket to an SSL socket. If this is 1120 * thrown, then the connection will have been closed. 1121 */ 1122 @NotNull() 1123 public synchronized OutputStream convertToTLS( 1124 @NotNull final SSLSocketFactory sslSocketFactory, 1125 final boolean requestClientCertificate, 1126 final boolean requireClientCertificate) 1127 throws LDAPException 1128 { 1129 final OutputStream clearOutputStream = outputStream; 1130 1131 final Socket origSocket = socket; 1132 final String hostname = LDAPConnectionOptions.DEFAULT_NAME_RESOLVER. 1133 getHostName(origSocket.getInetAddress()); 1134 final int port = origSocket.getPort(); 1135 1136 try 1137 { 1138 synchronized (sslSocketFactory) 1139 { 1140 socket = sslSocketFactory.createSocket(socket, hostname, port, true); 1141 } 1142 1143 final SSLSocket sslSocket = (SSLSocket) socket; 1144 sslSocket.setUseClientMode(false); 1145 1146 if (requestClientCertificate) 1147 { 1148 if (requireClientCertificate) 1149 { 1150 sslSocket.setNeedClientAuth(true); 1151 } 1152 else 1153 { 1154 sslSocket.setWantClientAuth(true); 1155 } 1156 } 1157 else 1158 { 1159 sslSocket.setWantClientAuth(false); 1160 } 1161 1162 1163 outputStream = socket.getOutputStream(); 1164 asn1Reader = new ASN1StreamReader(socket.getInputStream()); 1165 suppressNextResponse.set(true); 1166 return clearOutputStream; 1167 } 1168 catch (final Exception e) 1169 { 1170 Debug.debugException(e); 1171 1172 final LDAPException le = new LDAPException(ResultCode.LOCAL_ERROR, 1173 ERR_CONN_CONVERT_TO_TLS_FAILURE.get( 1174 StaticUtils.getExceptionMessage(e)), 1175 e); 1176 1177 close(le); 1178 1179 throw le; 1180 } 1181 } 1182 1183 1184 1185 /** 1186 * Retrieves the connection ID that has been assigned to this connection by 1187 * the associated listener. 1188 * 1189 * @return The connection ID that has been assigned to this connection by 1190 * the associated listener, or -1 if it is not associated with a 1191 * listener. 1192 */ 1193 public long getConnectionID() 1194 { 1195 return connectionID; 1196 } 1197 1198 1199 1200 /** 1201 * Adds the provided search entry transformer to this client connection. 1202 * 1203 * @param t A search entry transformer to be used to intercept and/or alter 1204 * search result entries before they are returned to the client. 1205 */ 1206 public void addSearchEntryTransformer( 1207 @NotNull final SearchEntryTransformer t) 1208 { 1209 searchEntryTransformers.add(t); 1210 } 1211 1212 1213 1214 /** 1215 * Removes the provided search entry transformer from this client connection. 1216 * 1217 * @param t The search entry transformer to be removed. 1218 */ 1219 public void removeSearchEntryTransformer( 1220 @NotNull final SearchEntryTransformer t) 1221 { 1222 searchEntryTransformers.remove(t); 1223 } 1224 1225 1226 1227 /** 1228 * Adds the provided search reference transformer to this client connection. 1229 * 1230 * @param t A search reference transformer to be used to intercept and/or 1231 * alter search result references before they are returned to the 1232 * client. 1233 */ 1234 public void addSearchReferenceTransformer( 1235 @NotNull final SearchReferenceTransformer t) 1236 { 1237 searchReferenceTransformers.add(t); 1238 } 1239 1240 1241 1242 /** 1243 * Removes the provided search reference transformer from this client 1244 * connection. 1245 * 1246 * @param t The search reference transformer to be removed. 1247 */ 1248 public void removeSearchReferenceTransformer( 1249 @NotNull final SearchReferenceTransformer t) 1250 { 1251 searchReferenceTransformers.remove(t); 1252 } 1253 1254 1255 1256 /** 1257 * Adds the provided intermediate response transformer to this client 1258 * connection. 1259 * 1260 * @param t An intermediate response transformer to be used to intercept 1261 * and/or alter intermediate responses before they are returned to 1262 * the client. 1263 */ 1264 public void addIntermediateResponseTransformer( 1265 @NotNull final IntermediateResponseTransformer t) 1266 { 1267 intermediateResponseTransformers.add(t); 1268 } 1269 1270 1271 1272 /** 1273 * Removes the provided intermediate response transformer from this client 1274 * connection. 1275 * 1276 * @param t The intermediate response transformer to be removed. 1277 */ 1278 public void removeIntermediateResponseTransformer( 1279 @NotNull final IntermediateResponseTransformer t) 1280 { 1281 intermediateResponseTransformers.remove(t); 1282 } 1283}