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