Skip to content

Commit 5803dfd

Browse files
authored
okhttp: use new APIs for configuring TLS whenever possible (Android Q+) (#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).
1 parent 56a410f commit 5803dfd

File tree

1 file changed

+154
-5
lines changed

1 file changed

+154
-5
lines changed

okhttp/src/main/java/io/grpc/okhttp/OkHttpProtocolNegotiator.java

+154-5
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,18 @@
2525
import io.grpc.okhttp.internal.Protocol;
2626
import io.grpc.okhttp.internal.Util;
2727
import java.io.IOException;
28+
import java.lang.reflect.Constructor;
29+
import java.lang.reflect.InvocationTargetException;
30+
import java.lang.reflect.Method;
2831
import java.net.Socket;
32+
import java.util.ArrayList;
33+
import java.util.Arrays;
34+
import java.util.Collections;
2935
import java.util.List;
3036
import java.util.logging.Level;
3137
import java.util.logging.Logger;
3238
import javax.annotation.Nullable;
39+
import javax.net.ssl.SSLParameters;
3340
import javax.net.ssl.SSLSocket;
3441

3542
/**
@@ -133,6 +140,69 @@ static final class AndroidNegotiator extends OkHttpProtocolNegotiator {
133140
private static final OptionalMethod<Socket> SET_NPN_PROTOCOLS =
134141
new OptionalMethod<>(null, "setNpnProtocols", byte[].class);
135142

143+
// Non-null on Android 10.0+.
144+
// SSLSockets.isSupportedSocket(SSLSocket)
145+
private static final Method SSL_SOCKETS_IS_SUPPORTED_SOCKET;
146+
// SSLSockets.setUseSessionTickets(SSLSocket, boolean)
147+
private static final Method SSL_SOCKETS_SET_USE_SESSION_TICKET;
148+
// SSLParameters.setApplicationProtocols(String[])
149+
private static final Method SET_APPLICATION_PROTOCOLS;
150+
// SSLParameters.getApplicationProtocols()
151+
private static final Method GET_APPLICATION_PROTOCOLS;
152+
// SSLSocket.getApplicationProtocol()
153+
private static final Method GET_APPLICATION_PROTOCOL;
154+
155+
// Non-null on Android 7.0+.
156+
// SSLParameters.setServerNames(List<SNIServerName>)
157+
private static final Method SET_SERVER_NAMES;
158+
// SNIHostName(String)
159+
private static final Constructor<?> SNI_HOST_NAME;
160+
161+
static {
162+
// Attempt to find Android 10.0+ APIs.
163+
Method setApplicationProtocolsMethod = null;
164+
Method getApplicationProtocolsMethod = null;
165+
Method getApplicationProtocolMethod = null;
166+
Method sslSocketsIsSupportedSocketMethod = null;
167+
Method sslSocketsSetUseSessionTicketsMethod = null;
168+
try {
169+
Class<?> sslParameters = SSLParameters.class;
170+
setApplicationProtocolsMethod =
171+
sslParameters.getMethod("setApplicationProtocols", String[].class);
172+
getApplicationProtocolsMethod = sslParameters.getMethod("getApplicationProtocols");
173+
getApplicationProtocolMethod = SSLSocket.class.getMethod("getApplicationProtocol");
174+
Class<?> sslSockets = Class.forName("android.net.ssl.SSLSockets");
175+
sslSocketsIsSupportedSocketMethod =
176+
sslSockets.getMethod("isSupportedSocket", SSLSocket.class);
177+
sslSocketsSetUseSessionTicketsMethod =
178+
sslSockets.getMethod("setUseSessionTickets", SSLSocket.class, boolean.class);
179+
} catch (ClassNotFoundException e) {
180+
logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e);
181+
} catch (NoSuchMethodException e) {
182+
logger.log(Level.FINER, "Failed to find Android 10.0+ APIs", e);
183+
}
184+
SET_APPLICATION_PROTOCOLS = setApplicationProtocolsMethod;
185+
GET_APPLICATION_PROTOCOLS = getApplicationProtocolsMethod;
186+
GET_APPLICATION_PROTOCOL = getApplicationProtocolMethod;
187+
SSL_SOCKETS_IS_SUPPORTED_SOCKET = sslSocketsIsSupportedSocketMethod;
188+
SSL_SOCKETS_SET_USE_SESSION_TICKET = sslSocketsSetUseSessionTicketsMethod;
189+
190+
// Attempt to find Android 7.0+ APIs.
191+
Method setServerNamesMethod = null;
192+
Constructor<?> sniHostNameConstructor = null;
193+
try {
194+
setServerNamesMethod = SSLParameters.class.getMethod("setServerNames", List.class);
195+
sniHostNameConstructor =
196+
Class.forName("javax.net.ssl.SNIHostName").getConstructor(String.class);
197+
} catch (ClassNotFoundException e) {
198+
logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e);
199+
} catch (NoSuchMethodException e) {
200+
logger.log(Level.FINER, "Failed to find Android 7.0+ APIs", e);
201+
}
202+
SET_SERVER_NAMES = setServerNamesMethod;
203+
SNI_HOST_NAME = sniHostNameConstructor;
204+
}
205+
136206
AndroidNegotiator(Platform platform) {
137207
super(platform);
138208
}
@@ -152,21 +222,75 @@ public String negotiate(SSLSocket sslSocket, String hostname, List<Protocol> pro
152222
/**
153223
* Override {@link Platform}'s configureTlsExtensions for Android older than 5.0, since OkHttp
154224
* (2.3+) only support such function for Android 5.0+.
225+
*
226+
* <p>Note: Prior to Android Q, the standard way of accessing some Conscrypt features was to
227+
* use reflection to call hidden APIs. Beginning in Q, there is public API for all of these
228+
* features. We attempt to use the public API where possible. Otherwise, fall back to use the
229+
* old reflective API.
155230
*/
156231
@Override
157232
protected void configureTlsExtensions(
158233
SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
159-
// Enable SNI and session tickets.
160-
if (hostname != null) {
161-
SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true);
162-
SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname);
234+
String[] protocolNames = protocolIds(protocols);
235+
SSLParameters sslParams = sslSocket.getSSLParameters();
236+
try {
237+
// Enable SNI and session tickets.
238+
if (hostname != null) {
239+
if (SSL_SOCKETS_IS_SUPPORTED_SOCKET != null
240+
&& (boolean) SSL_SOCKETS_IS_SUPPORTED_SOCKET.invoke(null, sslSocket)) {
241+
SSL_SOCKETS_SET_USE_SESSION_TICKET.invoke(null, sslSocket, true);
242+
} else {
243+
SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true);
244+
}
245+
if (SET_SERVER_NAMES != null && SNI_HOST_NAME != null) {
246+
SET_SERVER_NAMES
247+
.invoke(sslParams, Collections.singletonList(SNI_HOST_NAME.newInstance(hostname)));
248+
} else {
249+
SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname);
250+
}
251+
}
252+
boolean alpnEnabled = false;
253+
if (GET_APPLICATION_PROTOCOL != null) {
254+
try {
255+
// If calling SSLSocket.getApplicationProtocol() throws UnsupportedOperationException,
256+
// the underlying provider does not implement operations for enabling
257+
// ALPN in the fashion of SSLParameters.setApplicationProtocols(). Fall back to
258+
// use old hidden methods.
259+
GET_APPLICATION_PROTOCOL.invoke(sslSocket);
260+
SET_APPLICATION_PROTOCOLS.invoke(sslParams, (Object) protocolNames);
261+
alpnEnabled = true;
262+
} catch (InvocationTargetException e) {
263+
Throwable targetException = e.getTargetException();
264+
if (targetException instanceof UnsupportedOperationException) {
265+
logger.log(Level.FINER, "setApplicationProtocol unsupported, will try old methods");
266+
} else {
267+
throw e;
268+
}
269+
}
270+
}
271+
sslSocket.setSSLParameters(sslParams);
272+
// Check application protocols are configured correctly. If not, configure again with
273+
// old methods.
274+
// Workaround for Conscrypt bug: https://github.com/google/conscrypt/issues/832
275+
if (alpnEnabled && GET_APPLICATION_PROTOCOLS != null) {
276+
String[] configuredProtocols =
277+
(String[]) GET_APPLICATION_PROTOCOLS.invoke(sslSocket.getSSLParameters());
278+
if (Arrays.equals(protocolNames, configuredProtocols)) {
279+
return;
280+
}
281+
}
282+
} catch (IllegalAccessException e) {
283+
throw new RuntimeException(e);
284+
} catch (InvocationTargetException e) {
285+
throw new RuntimeException(e);
286+
} catch (InstantiationException e) {
287+
throw new RuntimeException(e);
163288
}
164289

165290
Object[] parameters = {Platform.concatLengthPrefixed(protocols)};
166291
if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) {
167292
SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters);
168293
}
169-
170294
if (platform.getTlsExtensionType() != TlsExtensionType.NONE) {
171295
SET_NPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters);
172296
} else {
@@ -177,6 +301,23 @@ protected void configureTlsExtensions(
177301

178302
@Override
179303
public String getSelectedProtocol(SSLSocket socket) {
304+
if (GET_APPLICATION_PROTOCOL != null) {
305+
try {
306+
return (String) GET_APPLICATION_PROTOCOL.invoke(socket);
307+
} catch (IllegalAccessException e) {
308+
throw new RuntimeException(e);
309+
} catch (InvocationTargetException e) {
310+
Throwable targetException = e.getTargetException();
311+
if (targetException instanceof UnsupportedOperationException) {
312+
logger.log(
313+
Level.FINER,
314+
"Socket unsupported for getApplicationProtocol, will try old methods");
315+
} else {
316+
throw new RuntimeException(e);
317+
}
318+
}
319+
}
320+
180321
if (platform.getTlsExtensionType() == TlsExtensionType.ALPN_AND_NPN) {
181322
try {
182323
byte[] alpnResult =
@@ -207,4 +348,12 @@ public String getSelectedProtocol(SSLSocket socket) {
207348
return null;
208349
}
209350
}
351+
352+
private static String[] protocolIds(List<Protocol> protocols) {
353+
List<String> result = new ArrayList<>();
354+
for (Protocol protocol : protocols) {
355+
result.add(protocol.toString());
356+
}
357+
return result.toArray(new String[0]);
358+
}
210359
}

0 commit comments

Comments
 (0)