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