NetHttpTransport.java

/*
 * Copyright (c) 2010 Google Inc.
 *
 * 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.
 */

package com.google.api.client.http.javanet;

import com.google.api.client.http.HttpMethods;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.util.Beta;
import com.google.api.client.util.Preconditions;
import com.google.api.client.util.SecurityUtils;
import com.google.api.client.util.SslUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.Provider;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

/**
 * Thread-safe HTTP low-level transport based on the {@code java.net} package.
 *
 * <p>Users should consider modifying the keep alive property on {@link NetHttpTransport} to control
 * whether the socket should be returned to a pool of connected sockets. More information is
 * available <a
 * href='http://docs.oracle.com/javase/7/docs/technotes/guides/net/http-keepalive.html'>here</a>.
 *
 * <p>We honor the default global caching behavior. To change the default behavior use {@link
 * HttpURLConnection#setDefaultUseCaches(boolean)}.
 *
 * <p>Implementation is thread-safe. For maximum efficiency, applications should use a single
 * globally-shared instance of the HTTP transport.
 *
 * @since 1.0
 * @author Yaniv Inbar
 */
public final class NetHttpTransport extends HttpTransport {
  private static Proxy defaultProxy() {
    return new Proxy(
        Proxy.Type.HTTP,
        new InetSocketAddress(
            System.getProperty("https.proxyHost"),
            Integer.parseInt(System.getProperty("https.proxyPort"))));
  }

  /**
   * All valid request methods as specified in {@link HttpURLConnection#setRequestMethod}, sorted in
   * ascending alphabetical order.
   */
  private static final String[] SUPPORTED_METHODS = {
    HttpMethods.DELETE,
    HttpMethods.GET,
    HttpMethods.HEAD,
    HttpMethods.OPTIONS,
    HttpMethods.POST,
    HttpMethods.PUT,
    HttpMethods.TRACE
  };

  static {
    Arrays.sort(SUPPORTED_METHODS);
  }

  private static final String SHOULD_USE_PROXY_FLAG = "com.google.api.client.should_use_proxy";

  private final ConnectionFactory connectionFactory;

  /** SSL socket factory or {@code null} for the default. */
  final SSLSocketFactory sslSocketFactory;

  /** Host name verifier or {@code null} for the default. */
  private final HostnameVerifier hostnameVerifier;

  /** Whether the transport is mTLS. Default value is {@code false}. */
  private final boolean isMtls;

  /**
   * Constructor with the default behavior.
   *
   * <p>Instead use {@link Builder} to modify behavior.
   */
  public NetHttpTransport() {
    this((ConnectionFactory) null, null, null, false);
  }

  /**
   * @param proxy HTTP proxy or {@code null} to use the proxy settings from <a
   *     href="http://docs.oracle.com/javase/7/docs/api/java/net/doc-files/net-properties.html">
   *     system properties</a>
   * @param sslSocketFactory SSL socket factory or {@code null} for the default
   * @param hostnameVerifier host name verifier or {@code null} for the default
   * @param isMtls Whether the transport is mTLS. Default value is {@code false}
   * @since 1.38
   */
  NetHttpTransport(
      Proxy proxy,
      SSLSocketFactory sslSocketFactory,
      HostnameVerifier hostnameVerifier,
      boolean isMtls) {
    this(new DefaultConnectionFactory(proxy), sslSocketFactory, hostnameVerifier, isMtls);
  }

  /**
   * @param connectionFactory factory to produce connections from {@link URL}s; if {@code null} then
   *     {@link DefaultConnectionFactory} is used
   * @param sslSocketFactory SSL socket factory or {@code null} for the default
   * @param hostnameVerifier host name verifier or {@code null} for the default
   * @param isMtls Whether the transport is mTLS. Default value is {@code false}
   * @since 1.38
   */
  NetHttpTransport(
      ConnectionFactory connectionFactory,
      SSLSocketFactory sslSocketFactory,
      HostnameVerifier hostnameVerifier,
      boolean isMtls) {
    this.connectionFactory = getConnectionFactory(connectionFactory);
    this.sslSocketFactory = sslSocketFactory;
    this.hostnameVerifier = hostnameVerifier;
    this.isMtls = isMtls;
  }

  private ConnectionFactory getConnectionFactory(ConnectionFactory connectionFactory) {
    if (connectionFactory == null) {
      if (System.getProperty(SHOULD_USE_PROXY_FLAG) != null) {
        return new DefaultConnectionFactory(defaultProxy());
      }
      return new DefaultConnectionFactory();
    }
    return connectionFactory;
  }

  @Override
  public boolean supportsMethod(String method) {
    return Arrays.binarySearch(SUPPORTED_METHODS, method) >= 0;
  }

  @Override
  public boolean isMtls() {
    return this.isMtls;
  }

  @Override
  protected NetHttpRequest buildRequest(String method, String url) throws IOException {
    Preconditions.checkArgument(supportsMethod(method), "HTTP method %s not supported", method);
    // connection with proxy settings
    URL connUrl = new URL(url);
    HttpURLConnection connection = connectionFactory.openConnection(connUrl);
    connection.setRequestMethod(method);
    // SSL settings
    if (connection instanceof HttpsURLConnection) {
      HttpsURLConnection secureConnection = (HttpsURLConnection) connection;
      if (hostnameVerifier != null) {
        secureConnection.setHostnameVerifier(hostnameVerifier);
      }
      if (sslSocketFactory != null) {
        secureConnection.setSSLSocketFactory(sslSocketFactory);
      }
    }
    return new NetHttpRequest(connection);
  }

  /**
   * Builder for {@link NetHttpTransport}.
   *
   * <p>Implementation is not thread-safe.
   *
   * @since 1.13
   */
  public static final class Builder {

    /** SSL socket factory or {@code null} for the default. */
    private SSLSocketFactory sslSocketFactory;

    /** Security provider to use or {@code null} for default. */
    private Provider securityProvider;

    /** Custom SSLSocket configurator or {@code null} to disable callback configuration. */
    SslSocketConfigurator sslSocketConfigurator;

    /** Host name verifier or {@code null} for the default. */
    private HostnameVerifier hostnameVerifier;

    /**
     * HTTP proxy or {@code null} to use the proxy settings from <a
     * href="http://docs.oracle.com/javase/7/docs/api/java/net/doc-files/net-properties.html">system
     * properties</a>.
     */
    private Proxy proxy;

    /**
     * {@link ConnectionFactory} or {@code null} to use a DefaultConnectionFactory. This value is
     * only used if proxy is unset.
     */
    private ConnectionFactory connectionFactory;

    /** Whether the transport is mTLS. Default value is {@code false}. */
    private boolean isMtls;

    /**
     * Sets the HTTP proxy or {@code null} to use the proxy settings from <a
     * href="http://docs.oracle.com/javase/7/docs/api/java/net/doc-files/net-properties.html">system
     * properties</a>.
     *
     * <p>For example:
     *
     * <pre>
     * setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8080)))
     * </pre>
     */
    public Builder setProxy(Proxy proxy) {
      this.proxy = proxy;
      return this;
    }

    /**
     * Sets the {@link ConnectionFactory} or {@code null} to use a {@link DefaultConnectionFactory}.
     * <b>This value is ignored if the {@link #setProxy} has been called with a non-null value.</b>
     *
     * <p>If you wish to use a {@link Proxy}, it should be included in your {@link
     * ConnectionFactory} implementation.
     *
     * @since 1.20
     */
    public Builder setConnectionFactory(ConnectionFactory connectionFactory) {
      this.connectionFactory = connectionFactory;
      return this;
    }

    /**
     * Sets the SSL socket factory based on root certificates in a Java KeyStore.
     *
     * <p>Example usage:
     *
     * <pre>
     * trustCertificatesFromJavaKeyStore(new FileInputStream("certs.jks"), "password");
     * </pre>
     *
     * @param keyStoreStream input stream to the key store (closed at the end of this method in a
     *     finally block)
     * @param storePass password protecting the key store file
     * @since 1.14
     */
    public Builder trustCertificatesFromJavaKeyStore(InputStream keyStoreStream, String storePass)
        throws GeneralSecurityException, IOException {
      KeyStore trustStore = SecurityUtils.getJavaKeyStore();
      SecurityUtils.loadKeyStore(trustStore, keyStoreStream, storePass);
      return trustCertificates(trustStore);
    }

    /**
     * Sets the SSL socket factory based root certificates generated from the specified stream using
     * {@link CertificateFactory#generateCertificates(InputStream)}.
     *
     * <p>Example usage:
     *
     * <pre>
     * trustCertificatesFromStream(new FileInputStream("certs.pem"));
     * </pre>
     *
     * @param certificateStream certificate stream
     * @since 1.14
     */
    public Builder trustCertificatesFromStream(InputStream certificateStream)
        throws GeneralSecurityException, IOException {
      KeyStore trustStore = SecurityUtils.getJavaKeyStore();
      trustStore.load(null, null);
      SecurityUtils.loadKeyStoreFromCertificates(
          trustStore, SecurityUtils.getX509CertificateFactory(), certificateStream);
      return trustCertificates(trustStore);
    }

    /**
     * Sets the SSL socket factory based on a root certificate trust store.
     *
     * @param trustStore certificate trust store (use for example {@link SecurityUtils#loadKeyStore}
     *     or {@link SecurityUtils#loadKeyStoreFromCertificates})
     * @since 1.14
     */
    public Builder trustCertificates(KeyStore trustStore) throws GeneralSecurityException {
      SSLContext sslContext = SslUtils.getTlsSslContext(securityProvider);
      SslUtils.initSslContext(
          sslContext, trustStore, SslUtils.getPkixTrustManagerFactory(securityProvider));
      return setSslSocketFactory(sslContext.getSocketFactory());
    }

    /**
     * {@link Beta} <br>
     * Sets the SSL socket factory based on a root certificate trust store and a client certificate
     * key store. The client certificate key store will be used to establish mutual TLS.
     *
     * @param trustStore certificate trust store (use for example {@link SecurityUtils#loadKeyStore}
     *     or {@link SecurityUtils#loadKeyStoreFromCertificates})
     * @param mtlsKeyStore key store for client certificate and key to establish mutual TLS. (use
     *     for example {@link SecurityUtils#createMtlsKeyStore(InputStream)})
     * @param mtlsKeyStorePassword password for mtlsKeyStore parameter
     * @since 1.38
     */
    @Beta
    public Builder trustCertificates(
        KeyStore trustStore, KeyStore mtlsKeyStore, String mtlsKeyStorePassword)
        throws GeneralSecurityException {
      if (mtlsKeyStore != null && mtlsKeyStore.size() > 0) {
        this.isMtls = true;
      }
      SSLContext sslContext = SslUtils.getTlsSslContext(securityProvider);
      SslUtils.initSslContext(
          sslContext,
          trustStore,
          SslUtils.getPkixTrustManagerFactory(securityProvider),
          mtlsKeyStore,
          mtlsKeyStorePassword,
          SslUtils.getDefaultKeyManagerFactory(securityProvider));
      return setSslSocketFactory(sslContext.getSocketFactory());
    }

    /**
     * {@link Beta} <br>
     * Disables validating server SSL certificates by setting the SSL socket factory using {@link
     * SslUtils#trustAllSSLContext()} for the SSL context and {@link
     * SslUtils#trustAllHostnameVerifier()} for the host name verifier.
     *
     * <p>Be careful! Disabling certificate validation is dangerous and should only be done in
     * testing environments.
     */
    @Beta
    public Builder doNotValidateCertificate() throws GeneralSecurityException {
      hostnameVerifier = SslUtils.trustAllHostnameVerifier();
      sslSocketFactory = SslUtils.trustAllSSLContext().getSocketFactory();
      return this;
    }

    /** Returns the SSL socket factory. */
    public SSLSocketFactory getSslSocketFactory() {
      return sslSocketFactory;
    }

    /**
     * Sets the SSL socket factory or {@code null} for the default.
     *
     * <p>Note: If a custom {@link SslSocketConfigurator} is also provided, it will wrap and apply
     * its configuration callback to all sockets created by this factory.
     */
    public Builder setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
      this.sslSocketFactory = sslSocketFactory;
      return this;
    }

    /**
     * Sets the custom security provider or {@code null} to use the default JRE provider.
     *
     * <p>When enabling Post-Quantum Cryptography (PQC) transport:
     *
     * <ul>
     *   <li>On JDK 8-19: A custom JCA provider (such as Conscrypt or BouncyCastle) must be
     *       configured via this method, in addition to configuring a custom {@link
     *       SslSocketConfigurator} callback to select the hybrid/PQC curves using provider-specific
     *       APIs.
     *   <li>On JDK 20-26: A custom provider (like Conscrypt) is recommended, but a custom {@link
     *       SslSocketConfigurator} invoking {@code SSLParameters.setNamedGroups(String[])} directly
     *       can be used natively without a custom JCA provider if standard JSSE supports the
     *       curves.
     *   <li>On JDK 27+: Neither a custom provider nor a configurator is required as PQC algorithms
     *       are negotiated natively by default.
     * </ul>
     *
     * @param securityProvider provider to use
     */
    public Builder setSecurityProvider(Provider securityProvider) {
      this.securityProvider = securityProvider;
      return this;
    }

    /**
     * Sets the custom {@link SslSocketConfigurator} callback to configure active SSLSockets.
     *
     * <p>If both a custom {@link SSLSocketFactory} (via {@link
     * #setSslSocketFactory(SSLSocketFactory)}) and a custom configurator are set, the configurator
     * callback will be applied to all sockets created by the custom socket factory. If no custom
     * factory is provided, the configurator will wrap and apply to sockets created by the default
     * resolved socket factory.
     *
     * <p>When enabling Post-Quantum Cryptography (PQC) transport:
     *
     * <ul>
     *   <li>On JDK 20-26: Callers can configure a custom {@link SslSocketConfigurator} that sets
     *       the named groups (such as {@code X25519MLKEM768}) directly on {@code SSLParameters}.
     *   <li>On JDK 8-19: Callers must provide a custom {@link SslSocketConfigurator} implementation
     *       to inspect the socket types and invoke provider-specific API extensions (e.g. Conscrypt
     *       JNI interfaces) to configure the curves.
     * </ul>
     *
     * @param configurator the callback configurator
     * @since 2.1.2
     */
    public Builder setSslSocketConfigurator(SslSocketConfigurator configurator) {
      this.sslSocketConfigurator = configurator;
      return this;
    }

    /** Returns the host name verifier or {@code null} for the default. */
    public HostnameVerifier getHostnameVerifier() {
      return hostnameVerifier;
    }

    /** Sets the host name verifier or {@code null} for the default. */
    public Builder setHostnameVerifier(HostnameVerifier hostnameVerifier) {
      this.hostnameVerifier = hostnameVerifier;
      return this;
    }

    /**
     * Resolves the {@link SSLSocketFactory} to be used by the transport.
     *
     * <p>If a custom factory has been set via {@link #setSslSocketFactory(SSLSocketFactory)}, it
     * will be returned. Otherwise, a default SSL socket factory will be constructed via {@link
     * #createDefaultSslSocketFactory()}.
     *
     * @return the resolved {@link SSLSocketFactory}
     */
    SSLSocketFactory resolveSslSocketFactory() {
      if (securityProvider == null && sslSocketConfigurator == null) {
        return sslSocketFactory;
      }
      SSLSocketFactory factory =
          sslSocketFactory != null ? sslSocketFactory : createDefaultSslSocketFactory();
      if (sslSocketConfigurator != null) {
        return new ConfigurableSSLSocketFactory(factory, sslSocketConfigurator);
      }
      return factory;
    }

    /**
     * Constructs a default {@link SSLSocketFactory} configured with the specified {@link Provider}.
     *
     * <p>This method initializes an {@link SSLContext} and resolves its {@link TrustManagerFactory}
     * using the same security provider (if provided), ensuring compatibility for TLS handshakes
     * when using custom providers (such as Conscrypt).
     *
     * @return the initialized default {@link SSLSocketFactory}
     */
    SSLSocketFactory createDefaultSslSocketFactory() {
      try {
        SSLContext sslContext = SslUtils.getTlsSslContext(securityProvider);
        TrustManager[] trustManagers = null;
        if (securityProvider != null) {
          TrustManagerFactory tmf = SslUtils.getDefaultTrustManagerFactory(securityProvider);
          tmf.init((KeyStore) null);
          trustManagers = tmf.getTrustManagers();
        }
        sslContext.init(null, trustManagers, null);
        return sslContext.getSocketFactory();
      } catch (GeneralSecurityException e) {
        // Halt execution because the SSLContext cannot be initialized with the requested
        // configuration.
        throw new IllegalStateException("Failed to initialize SSLSocketFactory.", e);
      }
    }

    /** Returns a new instance of {@link NetHttpTransport} based on the options. */
    public NetHttpTransport build() {
      if (System.getProperty(SHOULD_USE_PROXY_FLAG) != null) {
        setProxy(defaultProxy());
      }
      SSLSocketFactory resolvedFactory = resolveSslSocketFactory();
      return this.proxy == null
          ? new NetHttpTransport(connectionFactory, resolvedFactory, hostnameVerifier, isMtls)
          : new NetHttpTransport(this.proxy, resolvedFactory, hostnameVerifier, isMtls);
    }
  }
}