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          // This indicates that the client sent a malformed request.
435          Debug.debugException(le);
436          close(le);
437          return;
438        }
439
440        try
441        {
442          final int messageID = requestMessage.getMessageID();
443          final List<Control> controls = requestMessage.getControls();
444
445          LDAPMessage responseMessage;
446          switch (requestMessage.getProtocolOpType())
447          {
448            case LDAPMessage.PROTOCOL_OP_TYPE_ABANDON_REQUEST:
449              requestHandler.processAbandonRequest(messageID,
450                   requestMessage.getAbandonRequestProtocolOp(), controls);
451              responseMessage = null;
452              break;
453
454            case LDAPMessage.PROTOCOL_OP_TYPE_ADD_REQUEST:
455              try
456              {
457                responseMessage = requestHandler.processAddRequest(messageID,
458                     requestMessage.getAddRequestProtocolOp(), controls);
459              }
460              catch (final Exception e)
461              {
462                Debug.debugException(e);
463                responseMessage = new LDAPMessage(messageID,
464                     new AddResponseProtocolOp(
465                          ResultCode.OTHER_INT_VALUE, null,
466                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
467                               StaticUtils.getExceptionMessage(e)),
468                          null));
469              }
470              break;
471
472            case LDAPMessage.PROTOCOL_OP_TYPE_BIND_REQUEST:
473              try
474              {
475                responseMessage = requestHandler.processBindRequest(messageID,
476                     requestMessage.getBindRequestProtocolOp(), controls);
477              }
478              catch (final Exception e)
479              {
480                Debug.debugException(e);
481                responseMessage = new LDAPMessage(messageID,
482                     new BindResponseProtocolOp(
483                          ResultCode.OTHER_INT_VALUE, null,
484                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
485                               StaticUtils.getExceptionMessage(e)),
486                          null, null));
487              }
488              break;
489
490            case LDAPMessage.PROTOCOL_OP_TYPE_COMPARE_REQUEST:
491              try
492              {
493                responseMessage = requestHandler.processCompareRequest(
494                     messageID, requestMessage.getCompareRequestProtocolOp(),
495                     controls);
496              }
497              catch (final Exception e)
498              {
499                Debug.debugException(e);
500                responseMessage = new LDAPMessage(messageID,
501                     new CompareResponseProtocolOp(
502                          ResultCode.OTHER_INT_VALUE, null,
503                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
504                               StaticUtils.getExceptionMessage(e)),
505                          null));
506              }
507              break;
508
509            case LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST:
510              try
511              {
512                responseMessage = requestHandler.processDeleteRequest(messageID,
513                     requestMessage.getDeleteRequestProtocolOp(), controls);
514              }
515              catch (final Exception e)
516              {
517                Debug.debugException(e);
518                responseMessage = new LDAPMessage(messageID,
519                     new DeleteResponseProtocolOp(
520                          ResultCode.OTHER_INT_VALUE, null,
521                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
522                               StaticUtils.getExceptionMessage(e)),
523                          null));
524              }
525              break;
526
527            case LDAPMessage.PROTOCOL_OP_TYPE_EXTENDED_REQUEST:
528              try
529              {
530                responseMessage = requestHandler.processExtendedRequest(
531                     messageID, requestMessage.getExtendedRequestProtocolOp(),
532                     controls);
533              }
534              catch (final Exception e)
535              {
536                Debug.debugException(e);
537                responseMessage = new LDAPMessage(messageID,
538                     new ExtendedResponseProtocolOp(
539                          ResultCode.OTHER_INT_VALUE, null,
540                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
541                               StaticUtils.getExceptionMessage(e)),
542                          null, null, null));
543              }
544              break;
545
546            case LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST:
547              try
548              {
549                responseMessage = requestHandler.processModifyRequest(messageID,
550                     requestMessage.getModifyRequestProtocolOp(), controls);
551              }
552              catch (final Exception e)
553              {
554                Debug.debugException(e);
555                responseMessage = new LDAPMessage(messageID,
556                     new ModifyResponseProtocolOp(
557                          ResultCode.OTHER_INT_VALUE, null,
558                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
559                               StaticUtils.getExceptionMessage(e)),
560                          null));
561              }
562              break;
563
564            case LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_DN_REQUEST:
565              try
566              {
567                responseMessage = requestHandler.processModifyDNRequest(
568                     messageID, requestMessage.getModifyDNRequestProtocolOp(),
569                     controls);
570              }
571              catch (final Exception e)
572              {
573                Debug.debugException(e);
574                responseMessage = new LDAPMessage(messageID,
575                     new ModifyDNResponseProtocolOp(
576                          ResultCode.OTHER_INT_VALUE, null,
577                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
578                               StaticUtils.getExceptionMessage(e)),
579                          null));
580              }
581              break;
582
583            case LDAPMessage.PROTOCOL_OP_TYPE_SEARCH_REQUEST:
584              try
585              {
586                responseMessage = requestHandler.processSearchRequest(messageID,
587                     requestMessage.getSearchRequestProtocolOp(), controls);
588              }
589              catch (final Exception e)
590              {
591                Debug.debugException(e);
592                responseMessage = new LDAPMessage(messageID,
593                     new SearchResultDoneProtocolOp(
594                          ResultCode.OTHER_INT_VALUE, null,
595                          ERR_CONN_REQUEST_HANDLER_FAILURE.get(
596                               StaticUtils.getExceptionMessage(e)),
597                          null));
598              }
599              break;
600
601            case LDAPMessage.PROTOCOL_OP_TYPE_UNBIND_REQUEST:
602              requestHandler.processUnbindRequest(messageID,
603                   requestMessage.getUnbindRequestProtocolOp(), controls);
604              close();
605              return;
606
607            default:
608              close(new LDAPException(ResultCode.PROTOCOL_ERROR,
609                   ERR_CONN_INVALID_PROTOCOL_OP_TYPE.get(StaticUtils.toHex(
610                        requestMessage.getProtocolOpType()))));
611              return;
612          }
613
614          if (responseMessage != null)
615          {
616            try
617            {
618              sendMessage(responseMessage);
619            }
620            catch (final LDAPException le)
621            {
622              Debug.debugException(le);
623              close(le);
624              return;
625            }
626          }
627        }
628        catch (final Throwable t)
629        {
630          close(new LDAPException(ResultCode.LOCAL_ERROR,
631               ERR_CONN_EXCEPTION_IN_REQUEST_HANDLER.get(
632                    String.valueOf(requestMessage),
633                    StaticUtils.getExceptionMessage(t))));
634          return;
635        }
636      }
637    }
638    finally
639    {
640      if (listener != null)
641      {
642        listener.connectionClosed(this);
643      }
644    }
645  }
646
647
648
649  /**
650   * Sends the provided message to the client.
651   *
652   * @param  message  The message to be written to the client.
653   *
654   * @throws  LDAPException  If a problem occurs while attempting to send the
655   *                         response to the client.
656   */
657  private synchronized void sendMessage(final LDAPMessage message)
658          throws LDAPException
659  {
660    // If we should suppress this response (which will only be because the
661    // response has already been sent through some other means, for example as
662    // part of StartTLS processing), then do so.
663    if (suppressNextResponse.compareAndSet(true, false))
664    {
665      return;
666    }
667
668    asn1Buffer.clear();
669
670    try
671    {
672      message.writeTo(asn1Buffer);
673    }
674    catch (final LDAPRuntimeException lre)
675    {
676      Debug.debugException(lre);
677      lre.throwLDAPException();
678    }
679
680    try
681    {
682      asn1Buffer.writeTo(outputStream);
683    }
684    catch (final IOException ioe)
685    {
686      Debug.debugException(ioe);
687
688      throw new LDAPException(ResultCode.LOCAL_ERROR,
689           ERR_CONN_SEND_MESSAGE_EXCEPTION.get(
690                StaticUtils.getExceptionMessage(ioe)),
691           ioe);
692    }
693    finally
694    {
695      if (asn1Buffer.zeroBufferOnClear())
696      {
697        asn1Buffer.clear();
698      }
699    }
700  }
701
702
703
704  /**
705   * Sends a search result entry message to the client with the provided
706   * information.
707   *
708   * @param  messageID   The message ID for the LDAP message to send to the
709   *                     client.  It must match the message ID of the associated
710   *                     search request.
711   * @param  protocolOp  The search result entry protocol op to include in the
712   *                     LDAP message to send to the client.  It must not be
713   *                     {@code null}.
714   * @param  controls    The set of controls to include in the response message.
715   *                     It may be empty or {@code null} if no controls should
716   *                     be included.
717   *
718   * @throws  LDAPException  If a problem occurs while attempting to send the
719   *                         provided response message.  If an exception is
720   *                         thrown, then the client connection will have been
721   *                         terminated.
722   */
723  public void sendSearchResultEntry(final int messageID,
724                   final SearchResultEntryProtocolOp protocolOp,
725                   final Control... controls)
726         throws LDAPException
727  {
728    if (searchEntryTransformers.isEmpty())
729    {
730      sendMessage(new LDAPMessage(messageID, protocolOp, controls));
731    }
732    else
733    {
734      Control[] c;
735      SearchResultEntryProtocolOp op = protocolOp;
736      if (controls == null)
737      {
738        c = EMPTY_CONTROL_ARRAY;
739      }
740      else
741      {
742        c = controls;
743      }
744
745      for (final SearchEntryTransformer t : searchEntryTransformers)
746      {
747        try
748        {
749          final ObjectPair<SearchResultEntryProtocolOp,Control[]> p =
750               t.transformEntry(messageID, op, c);
751          if (p == null)
752          {
753            return;
754          }
755
756          op = p.getFirst();
757          c  = p.getSecond();
758        }
759        catch (final Exception e)
760        {
761          Debug.debugException(e);
762          sendMessage(new LDAPMessage(messageID, protocolOp, c));
763          throw new LDAPException(ResultCode.LOCAL_ERROR,
764               ERR_CONN_SEARCH_ENTRY_TRANSFORMER_EXCEPTION.get(
765                    t.getClass().getName(), String.valueOf(op),
766                    StaticUtils.getExceptionMessage(e)),
767               e);
768        }
769      }
770
771      sendMessage(new LDAPMessage(messageID, op, c));
772    }
773  }
774
775
776
777  /**
778   * Sends a search result entry message to the client with the provided
779   * information.
780   *
781   * @param  messageID  The message ID for the LDAP message to send to the
782   *                    client.  It must match the message ID of the associated
783   *                    search request.
784   * @param  entry      The entry to return to the client.  It must not be
785   *                    {@code null}.
786   * @param  controls   The set of controls to include in the response message.
787   *                    It may be empty or {@code null} if no controls should be
788   *                    included.
789   *
790   * @throws  LDAPException  If a problem occurs while attempting to send the
791   *                         provided response message.  If an exception is
792   *                         thrown, then the client connection will have been
793   *                         terminated.
794   */
795  public void sendSearchResultEntry(final int messageID, final Entry entry,
796                                    final Control... controls)
797         throws LDAPException
798  {
799    sendSearchResultEntry(messageID,
800         new SearchResultEntryProtocolOp(entry.getDN(),
801              new ArrayList<Attribute>(entry.getAttributes())),
802         controls);
803  }
804
805
806
807  /**
808   * Sends a search result reference message to the client with the provided
809   * information.
810   *
811   * @param  messageID   The message ID for the LDAP message to send to the
812   *                     client.  It must match the message ID of the associated
813   *                     search request.
814   * @param  protocolOp  The search result reference protocol op to include in
815   *                     the LDAP message to send to the client.
816   * @param  controls    The set of controls to include in the response message.
817   *                     It may be empty or {@code null} if no controls should
818   *                     be included.
819   *
820   * @throws  LDAPException  If a problem occurs while attempting to send the
821   *                         provided response message.  If an exception is
822   *                         thrown, then the client connection will have been
823   *                         terminated.
824   */
825  public void sendSearchResultReference(final int messageID,
826                   final SearchResultReferenceProtocolOp protocolOp,
827                   final Control... controls)
828         throws LDAPException
829  {
830    if (searchReferenceTransformers.isEmpty())
831    {
832      sendMessage(new LDAPMessage(messageID, protocolOp, controls));
833    }
834    else
835    {
836      Control[] c;
837      SearchResultReferenceProtocolOp op = protocolOp;
838      if (controls == null)
839      {
840        c = EMPTY_CONTROL_ARRAY;
841      }
842      else
843      {
844        c = controls;
845      }
846
847      for (final SearchReferenceTransformer t : searchReferenceTransformers)
848      {
849        try
850        {
851          final ObjectPair<SearchResultReferenceProtocolOp,Control[]> p =
852               t.transformReference(messageID, op, c);
853          if (p == null)
854          {
855            return;
856          }
857
858          op = p.getFirst();
859          c  = p.getSecond();
860        }
861        catch (final Exception e)
862        {
863          Debug.debugException(e);
864          sendMessage(new LDAPMessage(messageID, protocolOp, c));
865          throw new LDAPException(ResultCode.LOCAL_ERROR,
866               ERR_CONN_SEARCH_REFERENCE_TRANSFORMER_EXCEPTION.get(
867                    t.getClass().getName(), String.valueOf(op),
868                    StaticUtils.getExceptionMessage(e)),
869               e);
870        }
871      }
872
873      sendMessage(new LDAPMessage(messageID, op, c));
874    }
875  }
876
877
878
879  /**
880   * Sends an intermediate response message to the client with the provided
881   * information.
882   *
883   * @param  messageID   The message ID for the LDAP message to send to the
884   *                     client.  It must match the message ID of the associated
885   *                     search request.
886   * @param  protocolOp  The intermediate response protocol op to include in the
887   *                     LDAP message to send to the client.
888   * @param  controls    The set of controls to include in the response message.
889   *                     It may be empty or {@code null} if no controls should
890   *                     be included.
891   *
892   * @throws  LDAPException  If a problem occurs while attempting to send the
893   *                         provided response message.  If an exception is
894   *                         thrown, then the client connection will have been
895   *                         terminated.
896   */
897  public void sendIntermediateResponse(final int messageID,
898                   final IntermediateResponseProtocolOp protocolOp,
899                   final Control... controls)
900         throws LDAPException
901  {
902    if (intermediateResponseTransformers.isEmpty())
903    {
904      sendMessage(new LDAPMessage(messageID, protocolOp, controls));
905    }
906    else
907    {
908      Control[] c;
909      IntermediateResponseProtocolOp op = protocolOp;
910      if (controls == null)
911      {
912        c = EMPTY_CONTROL_ARRAY;
913      }
914      else
915      {
916        c = controls;
917      }
918
919      for (final IntermediateResponseTransformer t :
920           intermediateResponseTransformers)
921      {
922        try
923        {
924          final ObjectPair<IntermediateResponseProtocolOp,Control[]> p =
925               t.transformIntermediateResponse(messageID, op, c);
926          if (p == null)
927          {
928            return;
929          }
930
931          op = p.getFirst();
932          c  = p.getSecond();
933        }
934        catch (final Exception e)
935        {
936          Debug.debugException(e);
937          sendMessage(new LDAPMessage(messageID, protocolOp, c));
938          throw new LDAPException(ResultCode.LOCAL_ERROR,
939               ERR_CONN_INTERMEDIATE_RESPONSE_TRANSFORMER_EXCEPTION.get(
940                    t.getClass().getName(), String.valueOf(op),
941                    StaticUtils.getExceptionMessage(e)),
942               e);
943        }
944      }
945
946      sendMessage(new LDAPMessage(messageID, op, c));
947    }
948  }
949
950
951
952  /**
953   * Sends an unsolicited notification message to the client with the provided
954   * extended result.
955   *
956   * @param  result  The extended result to use for the unsolicited
957   *                 notification.
958   *
959   * @throws  LDAPException  If a problem occurs while attempting to send the
960   *                         unsolicited notification.  If an exception is
961   *                         thrown, then the client connection will have been
962   *                         terminated.
963   */
964  public void sendUnsolicitedNotification(final ExtendedResult result)
965         throws LDAPException
966  {
967    sendUnsolicitedNotification(
968         new ExtendedResponseProtocolOp(result.getResultCode().intValue(),
969              result.getMatchedDN(), result.getDiagnosticMessage(),
970              StaticUtils.toList(result.getReferralURLs()), result.getOID(),
971              result.getValue()),
972         result.getResponseControls()
973    );
974  }
975
976
977
978  /**
979   * Sends an unsolicited notification message to the client with the provided
980   * information.
981   *
982   * @param  extendedResponse  The extended response to use for the unsolicited
983   *                           notification.
984   * @param  controls          The set of controls to include with the
985   *                           unsolicited notification.  It may be empty or
986   *                           {@code null} if no controls should be included.
987   *
988   * @throws  LDAPException  If a problem occurs while attempting to send the
989   *                         unsolicited notification.  If an exception is
990   *                         thrown, then the client connection will have been
991   *                         terminated.
992   */
993  public void sendUnsolicitedNotification(
994                   final ExtendedResponseProtocolOp extendedResponse,
995                   final Control... controls)
996         throws LDAPException
997  {
998    sendMessage(new LDAPMessage(0, extendedResponse, controls));
999  }
1000
1001
1002
1003  /**
1004   * Retrieves the socket used to communicate with the client.
1005   *
1006   * @return  The socket used to communicate with the client.
1007   */
1008  public synchronized Socket getSocket()
1009  {
1010    return socket;
1011  }
1012
1013
1014
1015  /**
1016   * Attempts to convert this unencrypted connection to one that uses TLS
1017   * encryption, as would be used during the course of invoking the StartTLS
1018   * extended operation.  If this is called, then the response that would have
1019   * been returned from the associated request will be suppressed, so the
1020   * returned output stream must be used to send the appropriate response to
1021   * the client.
1022   *
1023   * @param  f  The SSL socket factory that will be used to convert the existing
1024   *            {@code Socket} to an {@code SSLSocket}.
1025   *
1026   * @return  An output stream that can be used to send a clear-text message to
1027   *          the client (e.g., the StartTLS response message).
1028   *
1029   * @throws  LDAPException  If a problem is encountered while trying to convert
1030   *                         the existing socket to an SSL socket.  If this is
1031   *                         thrown, then the connection will have been closed.
1032   */
1033  public synchronized OutputStream convertToTLS(final SSLSocketFactory f)
1034         throws LDAPException
1035  {
1036    final OutputStream clearOutputStream = outputStream;
1037
1038    final Socket origSocket = socket;
1039    final String hostname   = origSocket.getInetAddress().getHostName();
1040    final int port          = origSocket.getPort();
1041
1042    try
1043    {
1044      synchronized (f)
1045      {
1046        socket = f.createSocket(socket, hostname, port, true);
1047      }
1048      ((SSLSocket) socket).setUseClientMode(false);
1049      outputStream = socket.getOutputStream();
1050      asn1Reader = new ASN1StreamReader(socket.getInputStream());
1051      suppressNextResponse.set(true);
1052      return clearOutputStream;
1053    }
1054    catch (final Exception e)
1055    {
1056      Debug.debugException(e);
1057
1058      final LDAPException le = new LDAPException(ResultCode.LOCAL_ERROR,
1059           ERR_CONN_CONVERT_TO_TLS_FAILURE.get(
1060                StaticUtils.getExceptionMessage(e)),
1061           e);
1062
1063      close(le);
1064
1065      throw le;
1066    }
1067  }
1068
1069
1070
1071  /**
1072   * Retrieves the connection ID that has been assigned to this connection by
1073   * the associated listener.
1074   *
1075   * @return  The connection ID that has been assigned to this connection by
1076   *          the associated listener, or -1 if it is not associated with a
1077   *          listener.
1078   */
1079  public long getConnectionID()
1080  {
1081    return connectionID;
1082  }
1083
1084
1085
1086  /**
1087   * Adds the provided search entry transformer to this client connection.
1088   *
1089   * @param  t  A search entry transformer to be used to intercept and/or alter
1090   *            search result entries before they are returned to the client.
1091   */
1092  public void addSearchEntryTransformer(final SearchEntryTransformer t)
1093  {
1094    searchEntryTransformers.add(t);
1095  }
1096
1097
1098
1099  /**
1100   * Removes the provided search entry transformer from this client connection.
1101   *
1102   * @param  t  The search entry transformer to be removed.
1103   */
1104  public void removeSearchEntryTransformer(final SearchEntryTransformer t)
1105  {
1106    searchEntryTransformers.remove(t);
1107  }
1108
1109
1110
1111  /**
1112   * Adds the provided search reference transformer to this client connection.
1113   *
1114   * @param  t  A search reference transformer to be used to intercept and/or
1115   *            alter search result references before they are returned to the
1116   *            client.
1117   */
1118  public void addSearchReferenceTransformer(final SearchReferenceTransformer t)
1119  {
1120    searchReferenceTransformers.add(t);
1121  }
1122
1123
1124
1125  /**
1126   * Removes the provided search reference transformer from this client
1127   * connection.
1128   *
1129   * @param  t  The search reference transformer to be removed.
1130   */
1131  public void removeSearchReferenceTransformer(
1132                   final SearchReferenceTransformer t)
1133  {
1134    searchReferenceTransformers.remove(t);
1135  }
1136
1137
1138
1139  /**
1140   * Adds the provided intermediate response transformer to this client
1141   * connection.
1142   *
1143   * @param  t  An intermediate response transformer to be used to intercept
1144   *            and/or alter intermediate responses before they are returned to
1145   *            the client.
1146   */
1147  public void addIntermediateResponseTransformer(
1148                   final IntermediateResponseTransformer t)
1149  {
1150    intermediateResponseTransformers.add(t);
1151  }
1152
1153
1154
1155  /**
1156   * Removes the provided intermediate response transformer from this client
1157   * connection.
1158   *
1159   * @param  t  The intermediate response transformer to be removed.
1160   */
1161  public void removeIntermediateResponseTransformer(
1162                   final IntermediateResponseTransformer t)
1163  {
1164    intermediateResponseTransformers.remove(t);
1165  }
1166}