From e1f9c597ff67b34bac3ea4e6c22cbc25e5b5fdef Mon Sep 17 00:00:00 2001 From: Neil Wilson Date: Mon, 1 May 2023 11:17:44 -0500 Subject: [PATCH] Add support for HTTP proxy servers Refactored the support for SOCKS proxy servers to create a new ProxySocketFactory superclass, and added an HTTPProxySocketFactory subclass that can be used to tunnel LDAP connections through an HTTP proxy server. As with the SOCKSProxySocketFactory, the HTTP proxy server must not require authentication, and communication with the proxy server itself will be unencrypted, but the LDAP communication can optionally be end-to-end encrypted with the target LDAP server. --- docs/release-notes.html | 12 +- messages/unboundid-ldapsdk-util.properties | 6 +- .../util/HTTPProxySocketFactory.java | 160 +++++++++ .../unboundid/util/ProxySocketFactory.java | 295 +++++++++++++++++ .../util/SOCKSProxySocketFactory.java | 269 ++-------------- .../util/HTTPProxySocketFactoryTestCase.java | 303 ++++++++++++++++++ 6 files changed, 788 insertions(+), 257 deletions(-) create mode 100644 src/com/unboundid/util/HTTPProxySocketFactory.java create mode 100644 src/com/unboundid/util/ProxySocketFactory.java create mode 100644 tests/unit/src/com/unboundid/util/HTTPProxySocketFactoryTestCase.java 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.
    +    }
    +  }
    +}