Skip to content

Commit

Permalink
okhttp: use new APIs for configuring TLS whenever possible (Android Q…
Browse files Browse the repository at this point in the history
…+) (grpc#6912)

Use new APIs for configuring TLS in Android environment. Starting from Android 29, there is a new set of public APIs for configuring ALPN (and starting from Android 24, there is API for enabling SNI). This change migrates to use these new APIs whenever possible. Only fallback to call the old hidden APIs if new ones do not exist (or do not work).
  • Loading branch information
voidzcy authored and dfawley committed Jan 15, 2021
1 parent ba06a09 commit 9617eb1
Showing 1 changed file with 154 additions and 5 deletions.
159 changes: 154 additions & 5 deletions okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java
Expand Up @@ -25,11 +25,18 @@
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;

/**
Expand Down Expand Up @@ -133,6 +140,69 @@ static final class AndroidNegotiator extends OkHttpProtocolNegotiator {
private static final OptionalMethod<Socket> 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<SNIServerName>)
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);
}
Expand All @@ -152,21 +222,75 @@ public String negotiate(SSLSocket sslSocket, String hostname, List<Protocol> pro
/**
* Override {@link Platform}'s configureTlsExtensions for Android older than 5.0, since OkHttp
* (2.3+) only support such function for Android 5.0+.
*
* <p>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<Protocol> protocols) {
// Enable SNI and session tickets.
if (hostname != null) {
SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true);
SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname);
String[] protocolNames = protocolIds(protocols);
SSLParameters sslParams = sslSocket.getSSLParameters();
try {
// Enable SNI and session tickets.
if (hostname != null) {
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 {
Expand All @@ -177,6 +301,23 @@ protected void configureTlsExtensions(

@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 =
Expand Down Expand Up @@ -207,4 +348,12 @@ public String getSelectedProtocol(SSLSocket socket) {
return null;
}
}

private static String[] protocolIds(List<Protocol> protocols) {
List<String> result = new ArrayList<>();
for (Protocol protocol : protocols) {
result.add(protocol.toString());
}
return result.toArray(new String[0]);
}
}

0 comments on commit 9617eb1

Please sign in to comment.