diff --git a/docs/release-notes.html b/docs/release-notes.html index 54b238a1b..16ea836a2 100644 --- a/docs/release-notes.html +++ b/docs/release-notes.html @@ -35,11 +35,13 @@

Version 6.0.9

  • - Added a new SOCKSProxySocketFactory class that can be used to allow establishing - LDAP and LDAPS connections through a SOCKSv4 or SOCKSv5 proxy server. - Communication with the SOCKS proxy server itself must be unencrypted (although - communication with the target LDAP server may optionally be end-to-end encrypted - with TLS), and the SOCKS proxy server must not require authentication. + Added a new HTTPProxySocketFactory class that can be used to allow establishing + LDAP and LDAPS connections through an HTTP proxy server, and added a new + SOCKSProxySocketFactory class that can be used to allow establishing connections + through a SOCKSv4 or SOCKSv5 proxy server. Communication with the proxy server + itself must be unencrypted (although communication with the target LDAP server + may optionally be end-to-end encrypted with TLS), and the proxy server must not + require authentication.

  • diff --git a/messages/unboundid-ldapsdk-util.properties b/messages/unboundid-ldapsdk-util.properties index 47759c6f5..14f691efe 100644 --- a/messages/unboundid-ldapsdk-util.properties +++ b/messages/unboundid-ldapsdk-util.properties @@ -1250,6 +1250,6 @@ ERR_GET_NON_BC_FIPS_CLASS_LOADER_ERROR_FINDING_JARS=Unable to create a class \ ERR_GET_NON_FIPS_BC_CLASS_LOADER_NO_JARS_FOUND=Unable to create a class \ loader for accessing non-FIPS-compliant Bouncy Castle functionality because \ the expected Bouncy Castle library was not found in the ''{0}'' directory. -ERR_SOCKS_PROXY_SF_CANNOT_CREATE_UNCONNECTED_SOCKET=Unable to create an \ - unconnected socket for communication through a SOCKS proxy server when an \ - SSLSocketFactory has been configured. +ERR_PROXY_SF_CANNOT_CREATE_UNCONNECTED_SOCKET=Unable to create an unconnected \ + socket for communication through a proxy server when an SSLSocketFactory \ + has been configured. diff --git a/src/com/unboundid/util/HTTPProxySocketFactory.java b/src/com/unboundid/util/HTTPProxySocketFactory.java new file mode 100644 index 000000000..e4d397372 --- /dev/null +++ b/src/com/unboundid/util/HTTPProxySocketFactory.java @@ -0,0 +1,160 @@ +/* + * Copyright 2023 Ping Identity Corporation + * All Rights Reserved. + */ +/* + * Copyright 2023 Ping Identity Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * Copyright (C) 2023 Ping Identity Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License (GPLv2 only) + * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ +package com.unboundid.util; + + + +import java.net.InetSocketAddress; +import java.net.Proxy; +import javax.net.ssl.SSLSocketFactory; + + + +/** + * This class provides an implementation of a socket factory that can be used + * to forward traffic through an HTTP proxy server. Because of limitations in + * the Java support for HTTP proxy servers, the following constraints will be + * imposed: + * + *

    + *

    Example

    + * The following example demonstrates the process for establishing an LDAPS + * connection through a HTTP proxy server: + *
    + *   final String httpProxyServerAddress = "http-proxy.example.com";
    + *   final int httpProxyServerPort = 3128;
    + *   final int connectTimeoutMillis = 10_000;
    + *
    + *   final SSLUtil sslUtil =
    + *        new SSLUtil(new TrustStoreTrustManager("/path/to/trust/store"));
    + *   final SSLSocketFactory ldapsSocketFactory =
    + *        sslUtil.createSSLSocketFactory();
    + *
    + *   final HTTPProxySocketFactory httpProxySocketFactory =
    + *        new HTTPProxySocketFactory(httpProxyServerAddress,
    + *             httpProxyServerPort, connectTimeoutMillis,
    + *             ldapsSocketFactory);
    + *
    + *   final String ldapsServerAddress = "ds.example.com";
    + *   final int ldapsServerPort = 636;
    + *
    + *   try (LDAPConnection conn = new LDAPConnection(httpProxySocketFactory,
    + *        ldapsServerAddress, ldapsServerPort))
    + *   {
    + *     // Do something with the connection here.
    + *   }
    + * 
    + */ +@NotMutable() +@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) +public final class HTTPProxySocketFactory + extends ProxySocketFactory +{ + /** + * Creates a new instance of this HTTP socket factory with the provided + * settings. The resulting socket factory will provide support for + * unencrypted LDAP communication. + * + * @param httpProxyHost The address of the HTTP proxy server. It + * must not be {@code null}. + * @param httpProxyPort The port on which the HTTP proxy is + * listening for new connections. + * @param connectTimeoutMillis The maximum length of time in milliseconds to + * wait for a connection to be established. A + * value that is less than or equal to zero + * indicates that no explicit timeout will be + * imposed. + */ + public HTTPProxySocketFactory(@NotNull final String httpProxyHost, + final int httpProxyPort, + final int connectTimeoutMillis) + { + this(httpProxyHost, httpProxyPort, connectTimeoutMillis, null); + } + + + + /** + * Creates a new instance of this HTTP socket factory with the provided + * settings. The resulting socket factory may provide support for either + * unencrypted LDAP communication (if the provided {@code sslSocketFactory} + * value is {@code null}) or encrypted LDAPS communication (if the provided + * {@code sslSocketFactory} value is non-{@code null}). + * + * @param httpProxyHost The address of the HTTP proxy server. It + * must not be {@code null}. + * @param httpProxyPort The port on which the HTTP proxy is + * listening for new connections. + * @param connectTimeoutMillis The maximum length of time in milliseconds to + * wait for a connection to be established. A + * value that is less than or equal to zero + * indicates that no explicit timeout will be + * imposed. + * @param sslSocketFactory An SSL socket factory that should be used if + * communication with the target LDAP server + * should be encrypted with TLS. It must be + * {@code null} if communication should not be + * encrypted, and it must not be {@code null} if + * communication should be encrypted with TLS. + */ + public HTTPProxySocketFactory(@NotNull final String httpProxyHost, + final int httpProxyPort, + final int connectTimeoutMillis, + @Nullable final SSLSocketFactory sslSocketFactory) + { + super(new Proxy(Proxy.Type.HTTP, + new InetSocketAddress(httpProxyHost, httpProxyPort)), + connectTimeoutMillis, sslSocketFactory); + } +} diff --git a/src/com/unboundid/util/ProxySocketFactory.java b/src/com/unboundid/util/ProxySocketFactory.java new file mode 100644 index 000000000..cd74acf92 --- /dev/null +++ b/src/com/unboundid/util/ProxySocketFactory.java @@ -0,0 +1,295 @@ +/* + * Copyright 2023 Ping Identity Corporation + * All Rights Reserved. + */ +/* + * Copyright 2023 Ping Identity Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* + * Copyright (C) 2023 Ping Identity Corporation + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License (GPLv2 only) + * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ +package com.unboundid.util; + + + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; + +import static com.unboundid.util.UtilityMessages.*; + + + +/** + * This class provides a base API for creating sockets that tunnel communication + * through a proxy server. Subclasses may implement support for specific types + * of proxy servers (e.g., HTTP or SOCKS). + */ +@NotExtensible() +@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_THREADSAFE) +abstract class ProxySocketFactory + extends SocketFactory +{ + // The maximum length of time in milliseconds to wait for a connection to be + // established. + private final int connectTimeoutMillis; + + // The Proxy instance that will be used to communicate with the proxy server. + @NotNull private final Proxy proxy; + + // An optional SSLSocketFactory instance that can be used to secure + // communication through the proxy server. + @Nullable private final SSLSocketFactory sslSocketFactory; + + + + /** + * Creates a new instance of this socket factory with the provided settings. + * The resulting socket factory may provide support for either unencrypted + * LDAP communication (if the provided {@code sslSocketFactory} value is + * {@code null}) or encrypted LDAPS communication (if the provided + * {@code sslSocketFactory} value is non-{@code null}). + * + * @param proxy A preconfigured {@code Proxy} instance to use + * to communicate with the proxy server. It + * must not be {@code null}. + * @param connectTimeoutMillis The maximum length of time in milliseconds to + * wait for a connection to be established. A + * value that is less than or equal to zero + * indicates that no explicit timeout will be + * imposed. + * @param sslSocketFactory An SSL socket factory that should be used if + * communication with the target LDAP server + * should be encrypted with TLS. It must be + * {@code null} if communication should not be + * encrypted, and it must not be {@code null} if + * communication should be encrypted with TLS. + */ + protected ProxySocketFactory(@NotNull final Proxy proxy, + final int connectTimeoutMillis, + @Nullable final SSLSocketFactory sslSocketFactory) + { + this.proxy = proxy; + this.connectTimeoutMillis = Math.max(connectTimeoutMillis, 0); + this.sslSocketFactory = sslSocketFactory; + } + + + + /** + * Creates an unconnected socket that will use the configured proxy server for + * communication. Note that this method can only be used when communication + * through the proxy server will not be encrypted. + * + * @throws UnsupportedOperationException If an {@code SSLSocketFactory} + * has been configured to secure + * communication with end servers. + */ + @Override() + @NotNull() + public final Socket createSocket() + throws UnsupportedOperationException + { + if (sslSocketFactory == null) + { + return new Socket(proxy); + } + else + { + throw new UnsupportedOperationException( + ERR_PROXY_SF_CANNOT_CREATE_UNCONNECTED_SOCKET.get()); + } + } + + + + /** + * Creates a new socket that is connected to the specified system through the + * proxy server. + * + * @param host The address of the server to which the socket should be + * established. It must not be {@code null}. + * @param port The port of the server to which the socket should be + * established. + * + * @throws IOException If a problem is encountered while attempting to + * establish the connection. + */ + @Override() + @NotNull() + public final Socket createSocket(@NotNull final String host, final int port) + throws IOException + { + final Socket socket = new Socket(proxy); + socket.connect(new InetSocketAddress(host, port), connectTimeoutMillis); + return secureSocket(socket, host, port); + } + + + + /** + * Creates a new socket that is connected to the specified system through the + * proxy server. + * + * @param host The address of the server to which the socket should be + * established. It must not be {@code null}. + * @param port The port of the server to which the socket should be + * established. + * @param localHost The local address to which the socket should be bound. + * It may optionally be {@code null} it may be bound to + * any local address. + * @param localPort The local port to which the socket should be bound. + * + * @throws IOException If a problem is encountered while attempting to + * establish the connection. + */ + @Override() + @NotNull() + public final Socket createSocket(@NotNull final String host, final int port, + @Nullable final InetAddress localHost, + final int localPort) + throws IOException + { + final Socket socket = new Socket(proxy); + socket.bind(new InetSocketAddress(localHost, localPort)); + socket.connect(new InetSocketAddress(host, port), connectTimeoutMillis); + return secureSocket(socket, host, port); + } + + + + /** + * Creates a new socket that is connected to the specified system through the + * proxy server. + * + * @param host The address of the server to which the socket should be + * established. It must not be {@code null}. + * @param port The port of the server to which the socket should be + * established. + * + * @throws IOException If a problem is encountered while attempting to + * establish the connection. + */ + @Override() + @NotNull() + public final Socket createSocket(@NotNull final InetAddress host, + final int port) + throws IOException + { + final Socket socket = new Socket(proxy); + socket.connect(new InetSocketAddress(host, port), connectTimeoutMillis); + return secureSocket(socket, host.getHostName(), port); + } + + + + /** + * Creates a new socket that is connected to the specified system through the + * proxy server. + * + * @param host The address of the server to which the socket should be + * established. It must not be {@code null}. + * @param port The port of the server to which the socket should be + * established. + * @param localHost The local address to which the socket should be bound. + * It may optionally be {@code null} if it may be bound to + * any local address. + * @param localPort The local port to which the socket should be bound. + * + * @throws IOException If a problem is encountered while attempting to + * establish the connection. + */ + @Override() + @NotNull() + public final Socket createSocket(@NotNull final InetAddress host, + final int port, + @Nullable final InetAddress localHost, + final int localPort) + throws IOException + { + final Socket socket = new Socket(proxy); + socket.bind(new InetSocketAddress(localHost, localPort)); + socket.connect(new InetSocketAddress(host, port), connectTimeoutMillis); + return secureSocket(socket, host.getHostName(), port); + } + + + + /** + * Adds TLS security to the provided socket, if appropriate. + * + * @param socket The socket to be optionally secured. + * @param host The address of the server to which the socket is + * established. + * @param port The port of the server to which the socket is established. + * + * @return An {@code SSLSocket} that wraps the provided socket if the + * communication should be secured, or the provided socket if no + * additional security is needed. + * + * @throws IOException If a problem is encountered while attempting to + * secure communication with the target server. If an + * exception is thrown, then the socket will have been + * closed + */ + @NotNull() + private Socket secureSocket(@NotNull final Socket socket, + @NotNull final String host, + final int port) + throws IOException + { + if (sslSocketFactory == null) + { + return socket; + } + + try + { + return sslSocketFactory.createSocket(socket, host, port, true); + } + catch (final IOException e) + { + Debug.debugException(e); + + try + { + socket.close(); + } + catch (final Exception e2) + { + Debug.debugException(e2); + } + + throw e; + } + } +} diff --git a/src/com/unboundid/util/SOCKSProxySocketFactory.java b/src/com/unboundid/util/SOCKSProxySocketFactory.java index 0d3b99e94..f9b2f8212 100644 --- a/src/com/unboundid/util/SOCKSProxySocketFactory.java +++ b/src/com/unboundid/util/SOCKSProxySocketFactory.java @@ -37,16 +37,10 @@ -import java.io.IOException; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; -import java.net.Socket; -import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; -import static com.unboundid.util.UtilityMessages.*; - /** @@ -78,14 +72,25 @@ * The following example demonstrates the process for establishing an LDAPS * connection through a SOCKS proxy server: *
    - *   final SOCKSProxySocketFactory socksSocketFactory =
    + *   final String socksProxyServerAddress = "socks-proxy.example.com";
    + *   final int socksProxyServerPort = 1080;
    + *   final int connectTimeoutMillis = 10_000;
    + *
    + *   final SSLUtil sslUtil =
    + *        new SSLUtil(new TrustStoreTrustManager("/path/to/trust/store"));
    + *   final SSLSocketFactory ldapsSocketFactory =
    + *        sslUtil.createSSLSocketFactory();
    + *
    + *   final SOCKSProxySocketFactory socksProxySocketFactory =
      *        new SOCKSProxySocketFactory(socksProxyServerAddress,
    - *             socksProxyServerPort,
    - *             proxyConnectTimeoutMillis,
    - *             ldapsSSLSocketFactory);
    + *             socksProxyServerPort, connectTimeoutMillis,
    + *             ldapsSocketFactory);
      *
    - *   try (LDAPConnection conn = new LDAPConnection(socksSocketFactory,
    - *        ldapsServerAdderess, ldapsServerPort))
    + *   final String ldapsServerAddress = "ds.example.com";
    + *   final int ldapsServerPort = 636;
    + *
    + *   try (LDAPConnection conn = new LDAPConnection(socksProxySocketFactory,
    + *        ldapsServerAddress, ldapsServerPort))
      *   {
      *     // Do something with the connection here.
      *   }
    @@ -94,21 +99,8 @@
     @NotMutable()
     @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
     public final class SOCKSProxySocketFactory
    -       extends SocketFactory
    +       extends ProxySocketFactory
     {
    -  // The maximum length of time in milliseconds to wait for a connection to be
    -  // established.
    -  private final int connectTimeoutMillis;
    -
    -  // The Proxy instance that will be used to communicate with the proxy server.
    -  @NotNull private final Proxy proxy;
    -
    -  // An optional SSLSocketFactory instance that can be used to secure
    -  // communication through the proxy server.
    -  @Nullable private final SSLSocketFactory sslSocketFactory;
    -
    -
    -
       /**
        * Creates a new instance of this SOCKS socket factory with the provided
        * settings.  The resulting socket factory will provide support for
    @@ -161,229 +153,8 @@ public SOCKSProxySocketFactory(@NotNull final String socksProxyHost,
                   final int connectTimeoutMillis,
                   @Nullable final SSLSocketFactory sslSocketFactory)
       {
    -    this(new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(socksProxyHost,
    -              socksProxyPort)),
    +    super(new Proxy(Proxy.Type.SOCKS,
    +                    new InetSocketAddress(socksProxyHost, socksProxyPort)),
              connectTimeoutMillis, sslSocketFactory);
       }
    -
    -
    -
    -  /**
    -   * Creates a new instance of this SOCKS socket factory with the provided
    -   * settings.  The resulting socket factory may provide support for either
    -   * unencrypted LDAP communication (if the provided {@code sslSocketFactory}
    -   * value is {@code null}) or encrypted LDAPS communication (if the provided
    -   * {@code sslSocketFactory} value is non-{@code null}).
    -   *
    -   * @param  proxy                 A preconfigured {@code Proxy} instance to use
    -   *                               to communicate with the proxy server.  It
    -   *                               must not be {@code null}.
    -   * @param  connectTimeoutMillis  The maximum length of time in milliseconds to
    -   *                               wait for a connection to be established.  A
    -   *                               value that is less than or equal to zero
    -   *                               indicates that no explicit timeout will be
    -   *                               imposed.
    -   * @param  sslSocketFactory      An SSL socket factory that should be used if
    -   *                               communication with the target LDAP server
    -   *                               should be encrypted with TLS.  It must be
    -   *                               {@code null} if communication should not be
    -   *                               encrypted, and it must not be {@code null} if
    -   *                               communication should be encrypted with TLS.
    -   */
    -  public SOCKSProxySocketFactory(@NotNull final Proxy proxy,
    -              final int connectTimeoutMillis,
    -              @Nullable final SSLSocketFactory sslSocketFactory)
    -  {
    -    this.proxy = proxy;
    -    this.connectTimeoutMillis = Math.max(connectTimeoutMillis, 0);
    -    this.sslSocketFactory = sslSocketFactory;
    -  }
    -
    -
    -
    -  /**
    -   * Creates an unconnected socket that will use the configured SOCKS proxy
    -   * server for communication.  Note that this method can only be used when
    -   * communication through the proxy server will not be encrypted.
    -   *
    -   * @throws  UnsupportedOperationException  If an {@code SSLSocketFactory}
    -   *                                         has been configured to secure
    -   *                                         communication with end servers.
    -   */
    -  @Override()
    -  @NotNull()
    -  public Socket createSocket()
    -         throws UnsupportedOperationException
    -  {
    -    if (sslSocketFactory == null)
    -    {
    -      return new Socket(proxy);
    -    }
    -    else
    -    {
    -      throw new UnsupportedOperationException(
    -           ERR_SOCKS_PROXY_SF_CANNOT_CREATE_UNCONNECTED_SOCKET.get());
    -    }
    -  }
    -
    -
    -
    -  /**
    -   * Creates a new socket that is connected to the specified system through the
    -   * proxy server.
    -   *
    -   * @param  host  The address of the server to which the socket should be
    -   *               established.  It must not be {@code null}.
    -   * @param  port  The port of the server to which the socket should be
    -   *               established.
    -   *
    -   * @throws  IOException  If a problem is encountered while attempting to
    -   *                       establish the connection.
    -   */
    -  @Override()
    -  @NotNull()
    -  public Socket createSocket(@NotNull final String host, final int port)
    -         throws IOException
    -  {
    -    final Socket socket = new Socket(proxy);
    -    socket.connect(new InetSocketAddress(host, port), connectTimeoutMillis);
    -    return secureSocket(socket, host, port);
    -  }
    -
    -
    -
    -  /**
    -   * Creates a new socket that is connected to the specified system through the
    -   * proxy server.
    -   *
    -   * @param  host       The address of the server to which the socket should be
    -   *                    established.  It must not be {@code null}.
    -   * @param  port       The port of the server to which the socket should be
    -   *                    established.
    -   * @param  localHost  The local address to which the socket should be bound.
    -   *                    It may optionally be {@code null} it may be bound to
    -   *                    any local address.
    -   * @param  localPort  The local port to which the socket should be bound.
    -   *
    -   * @throws  IOException  If a problem is encountered while attempting to
    -   *                       establish the connection.
    -   */
    -  @Override()
    -  @NotNull()
    -  public Socket createSocket(@NotNull final String host, final int port,
    -                             @Nullable final InetAddress localHost,
    -                             final int localPort)
    -         throws IOException
    -  {
    -    final Socket socket = new Socket(proxy);
    -    socket.bind(new InetSocketAddress(localHost, localPort));
    -    socket.connect(new InetSocketAddress(host, port), connectTimeoutMillis);
    -    return secureSocket(socket, host, port);
    -  }
    -
    -
    -
    -  /**
    -   * Creates a new socket that is connected to the specified system through the
    -   * proxy server.
    -   *
    -   * @param  host  The address of the server to which the socket should be
    -   *               established.  It must not be {@code null}.
    -   * @param  port  The port of the server to which the socket should be
    -   *               established.
    -   *
    -   * @throws  IOException  If a problem is encountered while attempting to
    -   *                       establish the connection.
    -   */
    -  @Override()
    -  @NotNull()
    -  public Socket createSocket(@NotNull final InetAddress host, final int port)
    -         throws IOException
    -  {
    -    final Socket socket = new Socket(proxy);
    -    socket.connect(new InetSocketAddress(host, port), connectTimeoutMillis);
    -    return secureSocket(socket, host.getHostName(), port);
    -  }
    -
    -
    -
    -  /**
    -   * Creates a new socket that is connected to the specified system through the
    -   * proxy server.
    -   *
    -   * @param  host       The address of the server to which the socket should be
    -   *                    established.  It must not be {@code null}.
    -   * @param  port       The port of the server to which the socket should be
    -   *                    established.
    -   * @param  localHost  The local address to which the socket should be bound.
    -   *                    It may optionally be {@code null} if it may be bound to
    -   *                    any local address.
    -   * @param  localPort  The local port to which the socket should be bound.
    -   *
    -   * @throws  IOException  If a problem is encountered while attempting to
    -   *                       establish the connection.
    -   */
    -  @Override()
    -  @NotNull()
    -  public Socket createSocket(@NotNull final InetAddress host, final int port,
    -                             @Nullable final InetAddress localHost,
    -                             final int localPort)
    -         throws IOException
    -  {
    -    final Socket socket = new Socket(proxy);
    -    socket.bind(new InetSocketAddress(localHost, localPort));
    -    socket.connect(new InetSocketAddress(host, port), connectTimeoutMillis);
    -    return secureSocket(socket, host.getHostName(), port);
    -  }
    -
    -
    -
    -  /**
    -   * Adds TLS security to the provided socket, if appropriate.
    -   *
    -   * @param  socket  The socket to be optionally secured.
    -   * @param  host    The address of the server to which the socket is
    -   *                 established.
    -   * @param  port    The port of the server to which the socket is established.
    -   *
    -   * @return  An {@code SSLSocket} that wraps the provided socket if the
    -   *          communication should be secured, or the provided socket if no
    -   *          additional security is needed.
    -   *
    -   * @throws  IOException  If a problem is encountered while attempting to
    -   *                       secure communication with the target server.  If an
    -   *                       exception is thrown, then the socket will have been
    -   *                       closed
    -   */
    -  @NotNull()
    -  private Socket secureSocket(@NotNull final Socket socket,
    -                              @NotNull final String host,
    -                              final int port)
    -          throws IOException
    -  {
    -    if (sslSocketFactory == null)
    -    {
    -      return socket;
    -    }
    -
    -    try
    -    {
    -      return sslSocketFactory.createSocket(socket, host, port, true);
    -    }
    -    catch (final IOException e)
    -    {
    -      Debug.debugException(e);
    -
    -      try
    -      {
    -        socket.close();
    -      }
    -      catch (final Exception e2)
    -      {
    -        Debug.debugException(e2);
    -      }
    -
    -      throw e;
    -    }
    -  }
     }
    diff --git a/tests/unit/src/com/unboundid/util/HTTPProxySocketFactoryTestCase.java b/tests/unit/src/com/unboundid/util/HTTPProxySocketFactoryTestCase.java
    new file mode 100644
    index 000000000..5fbbb6a7d
    --- /dev/null
    +++ b/tests/unit/src/com/unboundid/util/HTTPProxySocketFactoryTestCase.java
    @@ -0,0 +1,303 @@
    +/*
    + * Copyright 2023 Ping Identity Corporation
    + * All Rights Reserved.
    + */
    +/*
    + * Copyright 2023 Ping Identity Corporation
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *    http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +/*
    + * Copyright (C) 2023 Ping Identity Corporation
    + *
    + * This program is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU General Public License (GPLv2 only)
    + * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
    + * as published by the Free Software Foundation.
    + *
    + * This program is distributed in the hope that it will be useful,
    + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    + * GNU General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this program; if not, see .
    + */
    +package com.unboundid.util;
    +
    +
    +
    +import java.net.InetAddress;
    +import java.net.Socket;
    +
    +import org.testng.annotations.BeforeClass;
    +import org.testng.annotations.Test;
    +
    +import com.unboundid.ldap.listener.InMemoryDirectoryServer;
    +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
    +import com.unboundid.ldap.sdk.LDAPSDKTestCase;
    +import com.unboundid.util.ssl.SSLUtil;
    +import com.unboundid.util.ssl.TrustAllTrustManager;
    +
    +
    +
    +/**
    + * This class provides test coverage for the {@code HTTPProxySocketFactory}
    + * class.  Note, however, that since we can't guarantee the existence of an HTTP
    + * proxy during unit tests, this class will not actually guarantee communication
    + * with a server through a proxy.  Instead, it will merely attempt to get code
    + * coverage.
    + */
    +public final class HTTPProxySocketFactoryTestCase
    +       extends LDAPSDKTestCase
    +{
    +  // The port to use when trying to communicate with an HTTP proxy server.  This
    +  // port will not be in use on the local system, so communication attempts
    +  // should fail instantly.
    +  private int httpProxyPort;
    +
    +
    +
    +  /**
    +   * Identifies a free port on the system that will not be in use so that
    +   * attempts to connect to it will fail immediately.
    +   *
    +   * @throws  Exception  If an unexpected problem occurs.
    +   */
    +  @BeforeClass()
    +  public void setUp()
    +         throws Exception
    +  {
    +    // Start an in-memory directory server instance, figure out which port it's
    +    // listening on, and then shut it down.  That will ensure that attempts to
    +    // connect to that port will fail instantly.
    +    final InMemoryDirectoryServerConfig dsCfg =
    +         new InMemoryDirectoryServerConfig("dc=example,dc=com");
    +    final InMemoryDirectoryServer ds = new InMemoryDirectoryServer(dsCfg);
    +    ds.startListening();
    +    httpProxyPort = ds.getListenPort();
    +    ds.shutDown(true);
    +  }
    +
    +
    +
    +  /**
    +   * Tests the behavior when using a socket factory instance that was created
    +   * with the constructor that does not use an SSL socket factory.
    +   *
    +   * @throws  Exception  If an unexpected problem occurs.
    +   */
    +  @Test()
    +  public void testWithoutSSLSocketFactory()
    +         throws Exception
    +  {
    +    final HTTPProxySocketFactory socketFactory = new HTTPProxySocketFactory(
    +         "127.0.0.1", httpProxyPort, 123);
    +
    +    try
    +    {
    +      final Socket socket = socketFactory.createSocket();
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +
    +    try
    +    {
    +      final Socket socket = socketFactory.createSocket("127.0.0.1", 389);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +
    +    try
    +    {
    +      final Socket socket =
    +           socketFactory.createSocket("127.0.0.1", 389, null, 0);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +
    +    try
    +    {
    +      final Socket socket =
    +           socketFactory.createSocket(InetAddress.getByName("127.0.0.1"), 389);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +
    +    try
    +    {
    +      final Socket socket =
    +           socketFactory.createSocket(InetAddress.getByName("127.0.0.1"), 389,
    +                null, 0);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +  }
    +
    +
    +
    +  /**
    +   * Tests the behavior when using a socket factory instance that was created
    +   * with a {@code null} {@code SSLSocketFactory} instance.
    +   *
    +   * @throws  Exception  If an unexpected problem occurs.
    +   */
    +  @Test()
    +  public void testWithNullSSLSocketFactory()
    +         throws Exception
    +  {
    +    final HTTPProxySocketFactory socketFactory = new HTTPProxySocketFactory(
    +         "127.0.0.1", httpProxyPort, 123, null);
    +
    +    try
    +    {
    +      final Socket socket = socketFactory.createSocket();
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +
    +    try
    +    {
    +      final Socket socket = socketFactory.createSocket("127.0.0.1", 636);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +
    +    try
    +    {
    +      final Socket socket =
    +           socketFactory.createSocket("127.0.0.1", 636, null, 0);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +
    +    try
    +    {
    +      final Socket socket =
    +           socketFactory.createSocket(InetAddress.getByName("127.0.0.1"), 636);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +
    +    try
    +    {
    +      final Socket socket =
    +           socketFactory.createSocket(InetAddress.getByName("127.0.0.1"), 636,
    +                null, 0);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +  }
    +
    +
    +
    +  /**
    +   * Tests the behavior when using a socket factory instance that was created
    +   * with a non-{@code null} {@code SSLSocketFactory} instance.
    +   *
    +   * @throws  Exception  If an unexpected problem occurs.
    +   */
    +  @Test()
    +  public void testWithNonNullSSLSocketFactory()
    +         throws Exception
    +  {
    +    final SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
    +    final HTTPProxySocketFactory socketFactory = new HTTPProxySocketFactory(
    +         "127.0.0.1", httpProxyPort, 123, sslUtil.createSSLSocketFactory());
    +
    +    try
    +    {
    +      final Socket socket = socketFactory.createSocket();
    +      fail("Should have gotten an UnsupportedOperationException");
    +    }
    +    catch (final UnsupportedOperationException e)
    +    {
    +      // This is expected when attempting to create an unconnected socket with
    +      // an SSLSocketFactory.
    +    }
    +
    +    try
    +    {
    +      final Socket socket = socketFactory.createSocket("127.0.0.1", 636);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +
    +    try
    +    {
    +      final Socket socket =
    +           socketFactory.createSocket("127.0.0.1", 636, null, 0);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +
    +    try
    +    {
    +      final Socket socket =
    +           socketFactory.createSocket(InetAddress.getByName("127.0.0.1"), 636);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +
    +    try
    +    {
    +      final Socket socket =
    +           socketFactory.createSocket(InetAddress.getByName("127.0.0.1"), 636,
    +                null, 0);
    +      socket.close();
    +    }
    +    catch (final Exception e)
    +    {
    +      // This is expected when no HTTP proxy is actually available.
    +    }
    +  }
    +}