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