001/*
002 * Copyright 2007-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-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) 2007-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.sdk.extensions;
037
038
039
040import javax.net.ssl.SSLContext;
041import javax.net.ssl.SSLSocketFactory;
042
043import com.unboundid.ldap.sdk.Control;
044import com.unboundid.ldap.sdk.ExtendedRequest;
045import com.unboundid.ldap.sdk.ExtendedResult;
046import com.unboundid.ldap.sdk.InternalSDKHelper;
047import com.unboundid.ldap.sdk.LDAPConnection;
048import com.unboundid.ldap.sdk.LDAPException;
049import com.unboundid.ldap.sdk.LDAPExtendedOperationException;
050import com.unboundid.ldap.sdk.ResultCode;
051import com.unboundid.util.CryptoHelper;
052import com.unboundid.util.Debug;
053import com.unboundid.util.NotMutable;
054import com.unboundid.util.NotNull;
055import com.unboundid.util.Nullable;
056import com.unboundid.util.ThreadSafety;
057import com.unboundid.util.ThreadSafetyLevel;
058import com.unboundid.util.ssl.SSLUtil;
059
060import static com.unboundid.ldap.sdk.extensions.ExtOpMessages.*;
061
062
063
064/**
065 * This class provides an implementation of the LDAP StartTLS extended request
066 * as defined in <A HREF="http://www.ietf.org/rfc/rfc4511.txt">RFC 4511</A>
067 * section 4.14.  It may be used to establish a secure communication channel
068 * over an otherwise unencrypted connection.
069 * <BR><BR>
070 * Note that when using the StartTLS extended operation, you should establish
071 * a connection to the server's unencrypted LDAP port rather than its secure
072 * port.  Then, you can use the StartTLS extended request in order to secure
073 * that connection.
074 * <BR><BR>
075 * <H2>Example</H2>
076 * The following example attempts to use the StartTLS extended request in order
077 * to secure communication on a previously insecure connection.  In this case,
078 * it will use the {@link SSLUtil} class in conjunction with the
079 * {@link com.unboundid.util.ssl.TrustStoreTrustManager} class to ensure that
080 * only certificates from trusted authorities will be accepted.
081 * <PRE>
082 * // Create an SSLContext that will be used to perform the cryptographic
083 * // processing.
084 * SSLUtil sslUtil = new SSLUtil(new TrustStoreTrustManager(trustStorePath));
085 * SSLContext sslContext = sslUtil.createSSLContext();
086 *
087 *  // Create and process the extended request to secure a connection.
088 * StartTLSExtendedRequest startTLSRequest =
089 *      new StartTLSExtendedRequest(sslContext);
090 * ExtendedResult startTLSResult;
091 * try
092 * {
093 *   startTLSResult = connection.processExtendedOperation(startTLSRequest);
094 *   // This doesn't necessarily mean that the operation was successful, since
095 *   // some kinds of extended operations return non-success results under
096 *   // normal conditions.
097 * }
098 * catch (LDAPException le)
099 * {
100 *   // For an extended operation, this generally means that a problem was
101 *   // encountered while trying to send the request or read the result.
102 *   startTLSResult = new ExtendedResult(le);
103 * }
104 *
105 * // Make sure that we can use the connection to interact with the server.
106 * RootDSE rootDSE = connection.getRootDSE();
107 * </PRE>
108 */
109@NotMutable()
110@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
111public final class StartTLSExtendedRequest
112       extends ExtendedRequest
113{
114  /**
115   * The OID (1.3.6.1.4.1.1466.20037) for the StartTLS extended request.
116   */
117  @NotNull public static final String STARTTLS_REQUEST_OID =
118       "1.3.6.1.4.1.1466.20037";
119
120
121
122  /**
123   * The serial version UID for this serializable class.
124   */
125  private static final long serialVersionUID = -3234194603452821233L;
126
127
128
129  // The SSL socket factory used to perform the negotiation.
130  @Nullable private final SSLSocketFactory sslSocketFactory;
131
132
133
134  /**
135   * Creates a new StartTLS extended request using a default SSL context.
136   *
137   * @throws  LDAPException  If a problem occurs while trying to initialize a
138   *                         default SSL context.
139   */
140  public StartTLSExtendedRequest()
141         throws LDAPException
142  {
143    this((SSLSocketFactory) null, null);
144  }
145
146
147
148  /**
149   * Creates a new StartTLS extended request using a default SSL context.
150   *
151   * @param  controls  The set of controls to include in the request.
152   *
153   * @throws  LDAPException  If a problem occurs while trying to initialize a
154   *                         default SSL context.
155   */
156  public StartTLSExtendedRequest(@Nullable final Control[] controls)
157         throws LDAPException
158  {
159    this((SSLSocketFactory) null, controls);
160  }
161
162
163
164  /**
165   * Creates a new StartTLS extended request using the provided SSL context.
166   *
167   * @param  sslContext  The SSL context to use to perform the negotiation.  It
168   *                     may be {@code null} to indicate that a default SSL
169   *                     context should be used.  If an SSL context is provided,
170   *                     then it must already be initialized.
171   *
172   * @throws  LDAPException  If a problem occurs while trying to initialize a
173   *                         default SSL context.
174   */
175  public StartTLSExtendedRequest(@Nullable final SSLContext sslContext)
176         throws LDAPException
177  {
178    this(sslContext, null);
179  }
180
181
182
183  /**
184   * Creates a new StartTLS extended request using the provided SSL socket
185   * factory.
186   *
187   * @param  sslSocketFactory  The SSL socket factory to use to convert an
188   *                           insecure connection into a secure connection.  It
189   *                           may be {@code null} to indicate that a default
190   *                           SSL socket factory should be used.
191   *
192   * @throws  LDAPException  If a problem occurs while trying to initialize a
193   *                         default SSL socket factory.
194   */
195  public StartTLSExtendedRequest(
196              @Nullable final SSLSocketFactory sslSocketFactory)
197         throws LDAPException
198  {
199    this(sslSocketFactory, null);
200  }
201
202
203
204  /**
205   * Creates a new StartTLS extended request.
206   *
207   * @param  sslContext  The SSL context to use to perform the negotiation.  It
208   *                     may be {@code null} to indicate that a default SSL
209   *                     context should be used.  If an SSL context is provided,
210   *                     then it must already be initialized.
211   * @param  controls    The set of controls to include in the request.
212   *
213   * @throws  LDAPException  If a problem occurs while trying to initialize a
214   *                         default SSL context.
215   */
216  public StartTLSExtendedRequest(@Nullable final SSLContext sslContext,
217                                 @Nullable final Control[] controls)
218         throws LDAPException
219  {
220    super(STARTTLS_REQUEST_OID, controls);
221
222    if (sslContext == null)
223    {
224      try
225      {
226        final SSLContext ctx =
227             CryptoHelper.getSSLContext(SSLUtil.getDefaultSSLProtocol());
228        ctx.init(null, null, null);
229        sslSocketFactory = ctx.getSocketFactory();
230      }
231      catch (final Exception e)
232      {
233        Debug.debugException(e);
234        throw new LDAPException(ResultCode.LOCAL_ERROR,
235             ERR_STARTTLS_REQUEST_CANNOT_CREATE_DEFAULT_CONTEXT.get(e), e);
236      }
237    }
238    else
239    {
240      sslSocketFactory = sslContext.getSocketFactory();
241    }
242  }
243
244
245
246  /**
247   * Creates a new StartTLS extended request.
248   *
249   * @param  sslSocketFactory  The SSL socket factory to use to convert an
250   *                           insecure connection into a secure connection.  It
251   *                           may be {@code null} to indicate that a default
252   *                           SSL socket factory should be used.
253   * @param  controls          The set of controls to include in the request.
254   *
255   * @throws  LDAPException  If a problem occurs while trying to initialize a
256   *                         default SSL context.
257   */
258  public StartTLSExtendedRequest(
259              @Nullable final SSLSocketFactory sslSocketFactory,
260              @Nullable final Control[] controls)
261         throws LDAPException
262  {
263    super(STARTTLS_REQUEST_OID, controls);
264
265    if (sslSocketFactory == null)
266    {
267      try
268      {
269        final SSLContext ctx =
270             CryptoHelper.getSSLContext(SSLUtil.getDefaultSSLProtocol());
271        ctx.init(null, null, null);
272        this.sslSocketFactory = ctx.getSocketFactory();
273      }
274      catch (final Exception e)
275      {
276        Debug.debugException(e);
277        throw new LDAPException(ResultCode.LOCAL_ERROR,
278             ERR_STARTTLS_REQUEST_CANNOT_CREATE_DEFAULT_CONTEXT.get(e), e);
279      }
280    }
281    else
282    {
283      this.sslSocketFactory = sslSocketFactory;
284    }
285  }
286
287
288
289  /**
290   * Creates a new StartTLS extended request from the provided generic extended
291   * request.
292   *
293   * @param  extendedRequest  The generic extended request to use to create this
294   *                          StartTLS extended request.
295   *
296   * @throws  LDAPException  If a problem occurs while decoding the request.
297   */
298  public StartTLSExtendedRequest(@NotNull final ExtendedRequest extendedRequest)
299         throws LDAPException
300  {
301    this(extendedRequest.getControls());
302
303    if (extendedRequest.hasValue())
304    {
305      throw new LDAPException(ResultCode.DECODING_ERROR,
306                              ERR_STARTTLS_REQUEST_HAS_VALUE.get());
307    }
308  }
309
310
311
312  /**
313   * Retrieves the SSL socket factory that this extended request will use for
314   * performing TLS negotiation.
315   *
316   * @return  The SSL socket factor that this extended request will use for
317   *          performing TLS negotiation.
318   */
319  @NotNull()
320  public SSLSocketFactory getSSLSocketFactory()
321  {
322    return sslSocketFactory;
323  }
324
325
326
327  /**
328   * Sends this StartTLS request to the server and performs the necessary
329   * client-side security processing if the operation is processed successfully.
330   * That this method is guaranteed to throw an {@code LDAPException} if the
331   * server returns a non-success result.
332   *
333   * @param  connection  The connection to use to communicate with the directory
334   *                     server.
335   * @param  depth       The current referral depth for this request.  It should
336   *                     always be zero for the initial request, and should only
337   *                     be incremented when following referrals.
338   *
339   * @return The extended result received from the server if StartTLS processing
340   *         was completed successfully.
341   *
342   * @throws  LDAPException  If the server returned a non-success result, or if
343   *                         a problem was encountered while performing
344   *                         client-side security processing.
345   */
346  @Override()
347  @NotNull()
348  public ExtendedResult process(@NotNull final LDAPConnection connection,
349                                final int depth)
350         throws LDAPException
351  {
352    // Set an SO_TIMEOUT on the connection if it's not operating in synchronous
353    // mode to make it more responsive during the negotiation phase.
354    InternalSDKHelper.setSoTimeout(connection, 50);
355
356    final ExtendedResult result = super.process(connection, depth);
357    if (result.getResultCode() == ResultCode.SUCCESS)
358    {
359      InternalSDKHelper.convertToTLS(connection, sslSocketFactory);
360    }
361    else
362    {
363      throw new LDAPExtendedOperationException(result);
364    }
365
366    return result;
367  }
368
369
370
371  /**
372   * {@inheritDoc}
373   */
374  @Override()
375  @NotNull()
376  public StartTLSExtendedRequest duplicate()
377  {
378    return duplicate(getControls());
379  }
380
381
382
383  /**
384   * {@inheritDoc}
385   */
386  @Override()
387  @NotNull()
388  public StartTLSExtendedRequest duplicate(@Nullable final Control[] controls)
389  {
390    try
391    {
392      final StartTLSExtendedRequest r =
393           new StartTLSExtendedRequest(sslSocketFactory, controls);
394      r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
395      r.setIntermediateResponseListener(getIntermediateResponseListener());
396      r.setReferralDepth(getReferralDepth());
397      r.setReferralConnector(getReferralConnectorInternal());
398      return r;
399    }
400    catch (final Exception e)
401    {
402      // This should never happen, since an exception should only be thrown if
403      // there is no SSL context, but this instance already has a context.
404      Debug.debugException(e);
405      throw new RuntimeException(e);
406    }
407  }
408
409
410
411  /**
412   * {@inheritDoc}
413   */
414  @Override()
415  @NotNull()
416  public String getExtendedRequestName()
417  {
418    return INFO_EXTENDED_REQUEST_NAME_START_TLS.get();
419  }
420
421
422
423  /**
424   * {@inheritDoc}
425   */
426  @Override()
427  public void toString(@NotNull final StringBuilder buffer)
428  {
429    buffer.append("StartTLSExtendedRequest(");
430
431    final Control[] controls = getControls();
432    if (controls.length > 0)
433    {
434      buffer.append("controls={");
435      for (int i=0; i < controls.length; i++)
436      {
437        if (i > 0)
438        {
439          buffer.append(", ");
440        }
441
442        buffer.append(controls[i]);
443      }
444      buffer.append('}');
445    }
446
447    buffer.append(')');
448  }
449}