io.grpc.okhttp.internal.Platform Maven / Gradle / Ivy
/*
* Copyright (C) 2012 Square, Inc.
* Copyright (C) 2012 The Android Open Source Project
*
* 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.
*/
/*
* Forked from OkHttp 2.5.0
*/
package io.grpc.okhttp.internal;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import okio.Buffer;
/**
* Access to platform-specific features.
*
* Server name indication (SNI)
*
* Supported on Android 2.3+.
*
* Session Tickets
*
* Supported on Android 2.3+.
*
* Android Traffic Stats (Socket Tagging)
*
* Supported on Android 4.0+.
*
* ALPN (Application Layer Protocol Negotiation)
*
* Supported on Android 5.0+. The APIs were present in Android 4.4, but that implementation was
* unstable.
*
* Supported on OpenJDK 9+.
*
*
Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library).
*/
public class Platform {
public static final Logger logger = Logger.getLogger(Platform.class.getName());
public enum TlsExtensionType {
ALPN_AND_NPN,
NPN,
NONE,
}
/**
* List of recognized security providers. The first recognized security provider according to the
* preference order returned by {@link Security#getProviders} will be selected.
*/
private static final String[] ANDROID_SECURITY_PROVIDERS =
new String[] {
// See https://developer.android.com/training/articles/security-gms-provider.html
"com.google.android.gms.org.conscrypt.OpenSSLProvider",
"org.conscrypt.OpenSSLProvider",
"com.android.org.conscrypt.OpenSSLProvider",
"org.apache.harmony.xnet.provider.jsse.OpenSSLProvider",
"com.google.android.libraries.stitch.sslguard.SslGuardProvider"
};
private static final Platform PLATFORM = findPlatform();
public static Platform get() {
return PLATFORM;
}
private final Provider sslProvider;
public Platform(Provider sslProvider) {
this.sslProvider = sslProvider;
}
/** Prefix used on custom headers. */
public String getPrefix() {
return "OkHttp";
}
public void logW(String warning) {
System.out.println(warning);
}
public void tagSocket(Socket socket) throws SocketException {
}
public void untagSocket(Socket socket) throws SocketException {
}
public Provider getProvider() {
return sslProvider;
}
/** Returns the TLS extension type available (ALPN and NPN, NPN, or None). */
public TlsExtensionType getTlsExtensionType() {
return TlsExtensionType.NONE;
}
/**
* Configure TLS extensions on {@code sslSocket} for {@code route}.
*
* @param hostname non-null for client-side handshakes; null for
* server-side handshakes.
*/
public void configureTlsExtensions(SSLSocket sslSocket, String hostname,
List protocols) {
}
/**
* Called after the TLS handshake to release resources allocated by {@link
* #configureTlsExtensions}.
*/
public void afterHandshake(SSLSocket sslSocket) {
}
/** Returns the negotiated protocol, or null if no protocol was negotiated. */
public String getSelectedProtocol(SSLSocket socket) {
return null;
}
public void connectSocket(Socket socket, InetSocketAddress address,
int connectTimeout) throws IOException {
socket.connect(address, connectTimeout);
}
/** Attempt to match the host runtime to a capable Platform implementation. */
private static Platform findPlatform() {
Provider androidOrAppEngineProvider = getAndroidSecurityProvider();
if (androidOrAppEngineProvider != null) {
// Attempt to find Android 2.3+ APIs.
OptionalMethod setUseSessionTickets
= new OptionalMethod<>(null, "setUseSessionTickets", boolean.class);
OptionalMethod setHostname
= new OptionalMethod<>(null, "setHostname", String.class);
Method trafficStatsTagSocket = null;
Method trafficStatsUntagSocket = null;
OptionalMethod getAlpnSelectedProtocol =
new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
OptionalMethod setAlpnProtocols =
new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
// Attempt to find Android 4.0+ APIs.
try {
Class> trafficStats = Class.forName("android.net.TrafficStats");
trafficStatsTagSocket = trafficStats.getMethod("tagSocket", Socket.class);
trafficStatsUntagSocket = trafficStats.getMethod("untagSocket", Socket.class);
} catch (ClassNotFoundException ignored) {
// On older Android
} catch (NoSuchMethodException ignored) {
// On older Android
}
TlsExtensionType tlsExtensionType;
if (androidOrAppEngineProvider.getName().equals("GmsCore_OpenSSL")
|| androidOrAppEngineProvider.getName().equals("Conscrypt")
|| androidOrAppEngineProvider.getName().equals("Ssl_Guard")) {
tlsExtensionType = TlsExtensionType.ALPN_AND_NPN;
} else if (isAtLeastAndroid5()) {
tlsExtensionType = TlsExtensionType.ALPN_AND_NPN;
} else if (isAtLeastAndroid41()) {
tlsExtensionType = TlsExtensionType.NPN;
} else {
tlsExtensionType = TlsExtensionType.NONE;
}
return new Android(
setUseSessionTickets,
setHostname,
trafficStatsTagSocket,
trafficStatsUntagSocket,
getAlpnSelectedProtocol,
setAlpnProtocols,
androidOrAppEngineProvider,
tlsExtensionType);
}
Provider sslProvider;
try {
sslProvider = SSLContext.getDefault().getProvider();
} catch (NoSuchAlgorithmException nsae) {
throw new RuntimeException(nsae);
}
// Find JDK9+ ALPN support
try {
// getApplicationProtocol() may throw UnsupportedOperationException, so first construct a
// dummy SSLEngine and verify the method does not throw.
SSLContext context = SSLContext.getInstance("TLS", sslProvider);
context.init(null, null, null);
SSLEngine engine = context.createSSLEngine();
Method getEngineApplicationProtocol = SSLEngine.class.getMethod("getApplicationProtocol");
getEngineApplicationProtocol.invoke(engine);
Method setApplicationProtocols =
SSLParameters.class.getMethod("setApplicationProtocols", String[].class);
Method getApplicationProtocol = SSLSocket.class.getMethod("getApplicationProtocol");
return new JdkAlpnPlatform(sslProvider, setApplicationProtocols, getApplicationProtocol);
} catch (NoSuchAlgorithmException ignored) {
// On older Java
} catch (KeyManagementException ignored) {
// On older Java
} catch (IllegalAccessException ignored) {
// On older Java
} catch (NoSuchMethodException ignored) {
// On older Java
} catch (InvocationTargetException ignored) {
// On older Java
}
// Find Jetty's ALPN extension for OpenJDK.
try {
String negoClassName = "org.eclipse.jetty.alpn.ALPN";
Class> negoClass = Class.forName(negoClassName);
Class> providerClass = Class.forName(negoClassName + "$Provider");
Class> clientProviderClass = Class.forName(negoClassName + "$ClientProvider");
Class> serverProviderClass = Class.forName(negoClassName + "$ServerProvider");
Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass);
Method getMethod = negoClass.getMethod("get", SSLSocket.class);
Method removeMethod = negoClass.getMethod("remove", SSLSocket.class);
return new JdkWithJettyBootPlatform(
putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass,
sslProvider);
} catch (ClassNotFoundException ignored) {
// No Jetty ALPN
} catch (NoSuchMethodException ignored) {
// Weird Jetty ALPN
}
// TODO(ericgribkoff) Return null here
return new Platform(sslProvider);
}
private static boolean isAtLeastAndroid5() {
try {
Platform.class
.getClassLoader()
.loadClass("android.net.Network"); // Arbitrary class added in Android 5.0.
return true;
} catch (ClassNotFoundException e) {
logger.log(Level.FINE, "Can't find class", e);
}
return false;
}
private static boolean isAtLeastAndroid41() {
try {
Platform.class
.getClassLoader()
.loadClass("android.app.ActivityOptions"); // Arbitrary class added in Android 4.1.
return true;
} catch (ClassNotFoundException e) {
logger.log(Level.FINE, "Can't find class", e);
}
return false;
}
/**
* Select the first recognized security provider according to the preference order returned by
* {@link Security#getProviders}. If a recognized provider is not found then warn but continue.
*/
private static Provider getAndroidSecurityProvider() {
Provider[] providers = Security.getProviders();
for (Provider availableProvider : providers) {
for (String providerClassName : ANDROID_SECURITY_PROVIDERS) {
if (providerClassName.equals(availableProvider.getClass().getName())) {
logger.log(Level.FINE, "Found registered provider {0}", providerClassName);
return availableProvider;
}
}
}
logger.log(Level.WARNING, "Unable to find Conscrypt");
return null;
}
/** Android 2.3 or better, or AppEngine with Conscrypt. */
private static class Android extends Platform {
private final OptionalMethod setUseSessionTickets;
private final OptionalMethod setHostname;
// Non-null on Android 4.0+.
private final Method trafficStatsTagSocket;
private final Method trafficStatsUntagSocket;
// Non-null on Android 5.0+.
private final OptionalMethod getAlpnSelectedProtocol;
private final OptionalMethod setAlpnProtocols;
private final TlsExtensionType tlsExtensionType;
public Android(
OptionalMethod setUseSessionTickets,
OptionalMethod setHostname,
Method trafficStatsTagSocket,
Method trafficStatsUntagSocket,
OptionalMethod getAlpnSelectedProtocol,
OptionalMethod setAlpnProtocols,
Provider provider,
TlsExtensionType tlsExtensionType) {
super(provider);
this.setUseSessionTickets = setUseSessionTickets;
this.setHostname = setHostname;
this.trafficStatsTagSocket = trafficStatsTagSocket;
this.trafficStatsUntagSocket = trafficStatsUntagSocket;
this.getAlpnSelectedProtocol = getAlpnSelectedProtocol;
this.setAlpnProtocols = setAlpnProtocols;
this.tlsExtensionType = tlsExtensionType;
}
@Override
public TlsExtensionType getTlsExtensionType() {
return tlsExtensionType;
}
@Override public void connectSocket(Socket socket, InetSocketAddress address,
int connectTimeout) throws IOException {
try {
socket.connect(address, connectTimeout);
} catch (SecurityException se) {
// Before android 4.3, socket.connect could throw a SecurityException
// if opening a socket resulted in an EACCES error.
IOException ioException = new IOException("Exception in connect");
ioException.initCause(se);
throw ioException;
}
}
@Override public void configureTlsExtensions(
SSLSocket sslSocket, String hostname, List protocols) {
// Enable SNI and session tickets.
if (hostname != null) {
setUseSessionTickets.invokeOptionalWithoutCheckedException(sslSocket, true);
setHostname.invokeOptionalWithoutCheckedException(sslSocket, hostname);
}
// Enable ALPN.
if (setAlpnProtocols.isSupported(sslSocket)) {
Object[] parameters = { concatLengthPrefixed(protocols) };
setAlpnProtocols.invokeWithoutCheckedException(sslSocket, parameters);
}
}
@Override public String getSelectedProtocol(SSLSocket socket) {
if (!getAlpnSelectedProtocol.isSupported(socket)) return null;
byte[] alpnResult = (byte[]) getAlpnSelectedProtocol.invokeWithoutCheckedException(socket);
return alpnResult != null ? new String(alpnResult, Util.UTF_8) : null;
}
@Override public void tagSocket(Socket socket) throws SocketException {
if (trafficStatsTagSocket == null) return;
try {
trafficStatsTagSocket.invoke(null, socket);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getCause());
}
}
@Override public void untagSocket(Socket socket) throws SocketException {
if (trafficStatsUntagSocket == null) return;
try {
trafficStatsUntagSocket.invoke(null, socket);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getCause());
}
}
}
/** OpenJDK 9+. */
private static class JdkAlpnPlatform extends Platform {
private final Method setApplicationProtocols;
private final Method getApplicationProtocol;
private JdkAlpnPlatform(
Provider provider, Method setApplicationProtocols, Method getApplicationProtocol) {
super(provider);
this.setApplicationProtocols = setApplicationProtocols;
this.getApplicationProtocol = getApplicationProtocol;
}
@Override
public TlsExtensionType getTlsExtensionType() {
return TlsExtensionType.ALPN_AND_NPN;
}
@Override
public void configureTlsExtensions(
SSLSocket sslSocket, String hostname, List protocols) {
SSLParameters parameters = sslSocket.getSSLParameters();
List names = new ArrayList<>(protocols.size());
for (Protocol protocol : protocols) {
if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for ALPN.
names.add(protocol.toString());
}
try {
setApplicationProtocols.invoke(
parameters, new Object[] {names.toArray(new String[names.size()])});
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
sslSocket.setSSLParameters(parameters);
}
/** Returns the negotiated protocol, or null if no protocol was negotiated. */
@Override
public String getSelectedProtocol(SSLSocket socket) {
try {
return (String) getApplicationProtocol.invoke(socket);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
/**
* OpenJDK 7+ with {@code org.mortbay.jetty.alpn/alpn-boot} in the boot class path.
*/
private static class JdkWithJettyBootPlatform extends Platform {
private final Method putMethod;
private final Method getMethod;
private final Method removeMethod;
private final Class> clientProviderClass;
private final Class> serverProviderClass;
public JdkWithJettyBootPlatform(Method putMethod, Method getMethod, Method removeMethod,
Class> clientProviderClass, Class> serverProviderClass, Provider provider) {
super(provider);
this.putMethod = putMethod;
this.getMethod = getMethod;
this.removeMethod = removeMethod;
this.clientProviderClass = clientProviderClass;
this.serverProviderClass = serverProviderClass;
}
@Override
public TlsExtensionType getTlsExtensionType() {
return TlsExtensionType.ALPN_AND_NPN;
}
@Override public void configureTlsExtensions(
SSLSocket sslSocket, String hostname, List protocols) {
List names = new ArrayList<>(protocols.size());
for (int i = 0, size = protocols.size(); i < size; i++) {
Protocol protocol = protocols.get(i);
if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for ALPN.
names.add(protocol.toString());
}
try {
Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(),
new Class>[] { clientProviderClass, serverProviderClass }, new JettyNegoProvider(names));
putMethod.invoke(null, sslSocket, provider);
} catch (InvocationTargetException e) {
throw new AssertionError(e);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
}
@Override public void afterHandshake(SSLSocket sslSocket) {
try {
removeMethod.invoke(null, sslSocket);
} catch (IllegalAccessException ignored) {
throw new AssertionError();
} catch (InvocationTargetException ex) {
// This would be very surprising and there's not much to do about it
logger.log(Level.FINE, "Failed to remove SSLSocket from Jetty ALPN", ex);
}
}
@Override public String getSelectedProtocol(SSLSocket socket) {
try {
JettyNegoProvider provider =
(JettyNegoProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
if (!provider.unsupported && provider.selected == null) {
logger.log(Level.INFO, "ALPN callback dropped: SPDY and HTTP/2 are disabled. "
+ "Is alpn-boot on the boot class path?");
return null;
}
return provider.unsupported ? null : provider.selected;
} catch (InvocationTargetException e) {
throw new AssertionError();
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
}
/**
* Handle the methods of ALPN's ClientProvider and ServerProvider
* without a compile-time dependency on those interfaces.
*/
private static class JettyNegoProvider implements InvocationHandler {
/** This peer's supported protocols. */
private final List protocols;
/** Set when remote peer notifies ALPN is unsupported. */
private boolean unsupported;
/** The protocol the server selected. */
private String selected;
public JettyNegoProvider(List protocols) {
this.protocols = protocols;
}
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class> returnType = method.getReturnType();
if (args == null) {
args = Util.EMPTY_STRING_ARRAY;
}
if (methodName.equals("supports") && boolean.class == returnType) {
return true; // ALPN is supported.
} else if (methodName.equals("unsupported") && void.class == returnType) {
this.unsupported = true; // Peer doesn't support ALPN.
return null;
} else if (methodName.equals("protocols") && args.length == 0) {
return protocols; // Client advertises these protocols.
} else if ((methodName.equals("selectProtocol") || methodName.equals("select"))
&& String.class == returnType && args.length == 1 && args[0] instanceof List) {
@SuppressWarnings("unchecked")
List peerProtocols = (List) args[0];
// Pick the first known protocol the peer advertises.
for (int i = 0, size = peerProtocols.size(); i < size; i++) {
if (protocols.contains(peerProtocols.get(i))) {
return selected = peerProtocols.get(i);
}
}
return selected = protocols.get(0); // On no intersection, try peer's first protocol.
} else if ((methodName.equals("protocolSelected") || methodName.equals("selected"))
&& args.length == 1) {
this.selected = (String) args[0]; // Server selected this protocol.
return null;
} else {
return method.invoke(this, args);
}
}
}
/**
* Returns the concatenation of 8-bit, length prefixed protocol names.
* http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4
*/
public static byte[] concatLengthPrefixed(List protocols) {
Buffer result = new Buffer();
for (int i = 0, size = protocols.size(); i < size; i++) {
Protocol protocol = protocols.get(i);
if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for ALPN.
result.writeByte(protocol.toString().length());
result.writeUtf8(protocol.toString());
}
return result.readByteArray();
}
}