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