/* * 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; } } }