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