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