All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.grpc.okhttp.OkHttpProtocolNegotiator Maven / Gradle / Ivy

There is a newer version: 1.68.1
Show newest version
/*
 * Copyright 2015 The gRPC Authors
 *
 * 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 io.grpc.okhttp;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.annotations.VisibleForTesting;
import io.grpc.internal.GrpcUtil;
import io.grpc.okhttp.internal.OptionalMethod;
import io.grpc.okhttp.internal.Platform;
import io.grpc.okhttp.internal.Platform.TlsExtensionType;
import io.grpc.okhttp.internal.Protocol;
import io.grpc.okhttp.internal.Util;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;

/**
 * A helper class located in package com.squareup.okhttp.internal for TLS negotiation.
 */
class OkHttpProtocolNegotiator {
  private static final Logger logger = Logger.getLogger(OkHttpProtocolNegotiator.class.getName());
  private static final Platform DEFAULT_PLATFORM = Platform.get();
  private static OkHttpProtocolNegotiator NEGOTIATOR =
      createNegotiator(OkHttpProtocolNegotiator.class.getClassLoader());

  protected final Platform platform;

  @VisibleForTesting
  OkHttpProtocolNegotiator(Platform platform) {
    this.platform = checkNotNull(platform, "platform");
  }

  public static OkHttpProtocolNegotiator get() {
    return NEGOTIATOR;
  }

  /**
   * Creates corresponding negotiator according to whether on Android.
   */
  @VisibleForTesting
  static OkHttpProtocolNegotiator createNegotiator(ClassLoader loader) {
    boolean android = true;
    try {
      // Attempt to find Android 2.3+ APIs.
      loader.loadClass("com.android.org.conscrypt.OpenSSLSocketImpl");
    } catch (ClassNotFoundException e1) {
      logger.log(Level.FINE, "Unable to find Conscrypt. Skipping", e1);
      try {
        // Older platform before being unbundled.
        loader.loadClass("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
      } catch (ClassNotFoundException e2) {
        logger.log(Level.FINE, "Unable to find any OpenSSLSocketImpl. Skipping", e2);
        android = false;
      }
    }
    return android
        ? new AndroidNegotiator(DEFAULT_PLATFORM)
        : new OkHttpProtocolNegotiator(DEFAULT_PLATFORM);
  }

  /**
   * Start and wait until the negotiation is done, returns the negotiated protocol.
   *
   * @throws IOException if an IO error was encountered during the handshake.
   * @throws RuntimeException if the negotiation completed, but no protocol was selected.
   */
  public String negotiate(
      SSLSocket sslSocket, String hostname, @Nullable List protocols) throws IOException {
    if (protocols != null) {
      configureTlsExtensions(sslSocket, hostname, protocols);
    }
    try {
      // Force handshake.
      sslSocket.startHandshake();

      String negotiatedProtocol = getSelectedProtocol(sslSocket);
      if (negotiatedProtocol == null) {
        throw new RuntimeException("TLS ALPN negotiation failed with protocols: " + protocols);
      }
      return negotiatedProtocol;
    } finally {
      platform.afterHandshake(sslSocket);
    }
  }

  /** Configure TLS extensions. */
  protected void configureTlsExtensions(
      SSLSocket sslSocket, String hostname, List protocols) {
    platform.configureTlsExtensions(sslSocket, hostname, protocols);
  }

  /** Returns the negotiated protocol, or null if no protocol was negotiated. */
  public String getSelectedProtocol(SSLSocket socket) {
    return platform.getSelectedProtocol(socket);
  }

  @VisibleForTesting
  static final class AndroidNegotiator extends OkHttpProtocolNegotiator {
    // setUseSessionTickets(boolean)
    private static final OptionalMethod SET_USE_SESSION_TICKETS =
        new OptionalMethod<>(null, "setUseSessionTickets", Boolean.TYPE);
    // setHostname(String)
    private static final OptionalMethod SET_HOSTNAME =
        new OptionalMethod<>(null, "setHostname", String.class);
    // byte[] getAlpnSelectedProtocol()
    private static final OptionalMethod GET_ALPN_SELECTED_PROTOCOL =
        new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
    // setAlpnProtocol(byte[])
    private static final OptionalMethod SET_ALPN_PROTOCOLS =
        new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
    // byte[] getNpnSelectedProtocol()
    private static final OptionalMethod GET_NPN_SELECTED_PROTOCOL =
        new OptionalMethod<>(byte[].class, "getNpnSelectedProtocol");
    // setNpnProtocol(byte[])
    private static final OptionalMethod SET_NPN_PROTOCOLS =
        new OptionalMethod<>(null, "setNpnProtocols", byte[].class);

    // Non-null on Android 10.0+.
    // SSLSockets.isSupportedSocket(SSLSocket)
    private static final Method SSL_SOCKETS_IS_SUPPORTED_SOCKET;
    // SSLSockets.setUseSessionTickets(SSLSocket, boolean)
    private static final Method SSL_SOCKETS_SET_USE_SESSION_TICKET;
    // SSLParameters.setApplicationProtocols(String[])
    private static final Method SET_APPLICATION_PROTOCOLS;
    // SSLParameters.getApplicationProtocols()
    private static final Method GET_APPLICATION_PROTOCOLS;
    // SSLSocket.getApplicationProtocol()
    private static final Method GET_APPLICATION_PROTOCOL;

    // Non-null on Android 7.0+.
    // SSLParameters.setServerNames(List)
    private static final Method SET_SERVER_NAMES;
    // SNIHostName(String)
    private static final Constructor SNI_HOST_NAME;

    static {
      // Attempt to find Android 10.0+ APIs.
      Method setApplicationProtocolsMethod = null;
      Method getApplicationProtocolsMethod = null;
      Method getApplicationProtocolMethod = null;
      Method sslSocketsIsSupportedSocketMethod = null;
      Method sslSocketsSetUseSessionTicketsMethod = null;
      try {
        Class sslParameters = SSLParameters.class;
        setApplicationProtocolsMethod =
            sslParameters.getMethod("setApplicationProtocols", String[].class);
        getApplicationProtocolsMethod = sslParameters.getMethod("getApplicationProtocols");
        getApplicationProtocolMethod = SSLSocket.class.getMethod("getApplicationProtocol");
        Class sslSockets = Class.forName("android.net.ssl.SSLSockets");
        sslSocketsIsSupportedSocketMethod =
            sslSockets.getMethod("isSupportedSocket", SSLSocket.class);
        sslSocketsSetUseSessionTicketsMethod =
            sslSockets.getMethod("setUseSessionTickets", SSLSocket.class, boolean.class);
      } catch (ClassNotFoundException e) {
        logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e);
      } catch (NoSuchMethodException e) {
        logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e);
      }
      SET_APPLICATION_PROTOCOLS = setApplicationProtocolsMethod;
      GET_APPLICATION_PROTOCOLS = getApplicationProtocolsMethod;
      GET_APPLICATION_PROTOCOL = getApplicationProtocolMethod;
      SSL_SOCKETS_IS_SUPPORTED_SOCKET = sslSocketsIsSupportedSocketMethod;
      SSL_SOCKETS_SET_USE_SESSION_TICKET = sslSocketsSetUseSessionTicketsMethod;

      // Attempt to find Android 7.0+ APIs.
      Method setServerNamesMethod = null;
      Constructor sniHostNameConstructor = null;
      try {
        setServerNamesMethod = SSLParameters.class.getMethod("setServerNames", List.class);
        sniHostNameConstructor =
            Class.forName("javax.net.ssl.SNIHostName").getConstructor(String.class);
      } catch (ClassNotFoundException e) {
        logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e);
      } catch (NoSuchMethodException e) {
        logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e);
      }
      SET_SERVER_NAMES = setServerNamesMethod;
      SNI_HOST_NAME = sniHostNameConstructor;
    }

    AndroidNegotiator(Platform platform) {
      super(platform);
    }

    @Override
    public String negotiate(SSLSocket sslSocket, String hostname, List protocols)
        throws IOException {
      // First check if a protocol has already been selected, since it's possible that the user
      // provided SSLSocketFactory has already done the handshake when creates the SSLSocket.
      String negotiatedProtocol = getSelectedProtocol(sslSocket);
      if (negotiatedProtocol == null) {
        negotiatedProtocol = super.negotiate(sslSocket, hostname, protocols);
      }
      return negotiatedProtocol;
    }

    /**
     * Override {@link Platform}'s configureTlsExtensions for Android older than 5.0, since OkHttp
     * (2.3+) only support such function for Android 5.0+.
     *
     * 

Note: Prior to Android Q, the standard way of accessing some Conscrypt features was to * use reflection to call hidden APIs. Beginning in Q, there is public API for all of these * features. We attempt to use the public API where possible. Otherwise, fall back to use the * old reflective API. */ @Override protected void configureTlsExtensions( SSLSocket sslSocket, String hostname, List protocols) { String[] protocolNames = protocolIds(protocols); SSLParameters sslParams = sslSocket.getSSLParameters(); try { // Enable SNI and session tickets. // Hostname is normally validated in the builder (see checkAuthority) and it should // virtually always succeed. Check again here to avoid troubles (e.g., hostname with // underscore) enabling SNI, which works around cases where checkAuthority is disabled. // See b/154375837. if (hostname != null && isValidHostName(hostname)) { if (SSL_SOCKETS_IS_SUPPORTED_SOCKET != null && (boolean) SSL_SOCKETS_IS_SUPPORTED_SOCKET.invoke(null, sslSocket)) { SSL_SOCKETS_SET_USE_SESSION_TICKET.invoke(null, sslSocket, true); } else { SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true); } if (SET_SERVER_NAMES != null && SNI_HOST_NAME != null) { SET_SERVER_NAMES .invoke(sslParams, Collections.singletonList(SNI_HOST_NAME.newInstance(hostname))); } else { SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname); } } boolean alpnEnabled = false; if (GET_APPLICATION_PROTOCOL != null) { try { // If calling SSLSocket.getApplicationProtocol() throws UnsupportedOperationException, // the underlying provider does not implement operations for enabling // ALPN in the fashion of SSLParameters.setApplicationProtocols(). Fall back to // use old hidden methods. GET_APPLICATION_PROTOCOL.invoke(sslSocket); SET_APPLICATION_PROTOCOLS.invoke(sslParams, (Object) protocolNames); alpnEnabled = true; } catch (InvocationTargetException e) { Throwable targetException = e.getTargetException(); if (targetException instanceof UnsupportedOperationException) { logger.log(Level.FINER, "setApplicationProtocol unsupported, will try old methods"); } else { throw e; } } } sslSocket.setSSLParameters(sslParams); // Check application protocols are configured correctly. If not, configure again with // old methods. // Workaround for Conscrypt bug: https://github.com/google/conscrypt/issues/832 if (alpnEnabled && GET_APPLICATION_PROTOCOLS != null) { String[] configuredProtocols = (String[]) GET_APPLICATION_PROTOCOLS.invoke(sslSocket.getSSLParameters()); if (Arrays.equals(protocolNames, configuredProtocols)) { return; } } } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } Object[] parameters = {Platform.concatLengthPrefixed(protocols)}; if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) { SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters); } if (platform.getTlsExtensionType() != TlsExtensionType.NONE) { SET_NPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters); } else { throw new RuntimeException("We can not do TLS handshake on this Android version, please" + " install the Google Play Services Dynamic Security Provider to use TLS"); } } @Override public String getSelectedProtocol(SSLSocket socket) { if (GET_APPLICATION_PROTOCOL != null) { try { return (String) GET_APPLICATION_PROTOCOL.invoke(socket); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { Throwable targetException = e.getTargetException(); if (targetException instanceof UnsupportedOperationException) { logger.log( Level.FINER, "Socket unsupported for getApplicationProtocol, will try old methods"); } else { throw new RuntimeException(e); } } } if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) { try { byte[] alpnResult = (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket); if (alpnResult != null) { return new String(alpnResult, Util.UTF_8); } } catch (Exception e) { logger.log(Level.FINE, "Failed calling getAlpnSelectedProtocol()", e); // In some implementations, querying selected protocol before the handshake will fail with // exception. } } if (platform.getTlsExtensionType() != TlsExtensionType.NONE) { try { byte[] npnResult = (byte[]) GET_NPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket); if (npnResult != null) { return new String(npnResult, Util.UTF_8); } } catch (Exception e) { logger.log(Level.FINE, "Failed calling getNpnSelectedProtocol()", e); // In some implementations, querying selected protocol before the handshake will fail with // exception. } } return null; } } private static String[] protocolIds(List protocols) { List result = new ArrayList<>(); for (Protocol protocol : protocols) { result.add(protocol.toString()); } return result.toArray(new String[0]); } @VisibleForTesting static boolean isValidHostName(String name) { // GrpcUtil.checkAuthority() depends on URI implementation, while Android's URI implementation // allows underscore in hostname. Manually disallow hostname with underscore to avoid troubles. // See b/154375837. if (name.contains("_")) { return false; } try { GrpcUtil.checkAuthority(name); return true; } catch (IllegalArgumentException e) { return false; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy