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