001    /*
002     * Copyright 2010-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2010-2016 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     */
021    package com.unboundid.ldap.listener;
022    
023    
024    
025    import java.io.Closeable;
026    import java.io.IOException;
027    import java.io.OutputStream;
028    import java.net.Socket;
029    import java.util.ArrayList;
030    import java.util.List;
031    import java.util.concurrent.CopyOnWriteArrayList;
032    import java.util.concurrent.atomic.AtomicBoolean;
033    import javax.net.ssl.SSLSocket;
034    import javax.net.ssl.SSLSocketFactory;
035    
036    import com.unboundid.asn1.ASN1Buffer;
037    import com.unboundid.asn1.ASN1StreamReader;
038    import com.unboundid.ldap.protocol.AddResponseProtocolOp;
039    import com.unboundid.ldap.protocol.BindResponseProtocolOp;
040    import com.unboundid.ldap.protocol.CompareResponseProtocolOp;
041    import com.unboundid.ldap.protocol.DeleteResponseProtocolOp;
042    import com.unboundid.ldap.protocol.ExtendedResponseProtocolOp;
043    import com.unboundid.ldap.protocol.IntermediateResponseProtocolOp;
044    import com.unboundid.ldap.protocol.LDAPMessage;
045    import com.unboundid.ldap.protocol.ModifyResponseProtocolOp;
046    import com.unboundid.ldap.protocol.ModifyDNResponseProtocolOp;
047    import com.unboundid.ldap.protocol.SearchResultDoneProtocolOp;
048    import com.unboundid.ldap.protocol.SearchResultEntryProtocolOp;
049    import com.unboundid.ldap.protocol.SearchResultReferenceProtocolOp;
050    import com.unboundid.ldap.sdk.Attribute;
051    import com.unboundid.ldap.sdk.Control;
052    import com.unboundid.ldap.sdk.Entry;
053    import com.unboundid.ldap.sdk.ExtendedResult;
054    import com.unboundid.ldap.sdk.LDAPException;
055    import com.unboundid.ldap.sdk.LDAPRuntimeException;
056    import com.unboundid.ldap.sdk.ResultCode;
057    import com.unboundid.ldap.sdk.extensions.NoticeOfDisconnectionExtendedResult;
058    import com.unboundid.util.Debug;
059    import com.unboundid.util.InternalUseOnly;
060    import com.unboundid.util.ObjectPair;
061    import com.unboundid.util.StaticUtils;
062    import com.unboundid.util.ThreadSafety;
063    import com.unboundid.util.ThreadSafetyLevel;
064    import com.unboundid.util.Validator;
065    
066    import 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)
079    public 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    }