| /* |
| * 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. |
| */ |
| package com.squareup.okhttp.internal; |
| |
| import android.util.Log; |
| import com.squareup.okhttp.Protocol; |
| import com.squareup.okhttp.internal.tls.AndroidTrustRootIndex; |
| import com.squareup.okhttp.internal.tls.RealTrustRootIndex; |
| import com.squareup.okhttp.internal.tls.TrustRootIndex; |
| import java.io.IOException; |
| import java.lang.reflect.Field; |
| 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.util.ArrayList; |
| import java.util.List; |
| import java.util.logging.Level; |
| import javax.net.ssl.SSLSocket; |
| import javax.net.ssl.SSLSocketFactory; |
| import javax.net.ssl.X509TrustManager; |
| import okio.Buffer; |
| |
| import static com.squareup.okhttp.internal.Internal.logger; |
| |
| /** |
| * Access to platform-specific features. |
| * |
| * <h3>Server name indication (SNI)</h3> |
| * Supported on Android 2.3+. |
| * |
| * <h3>Session Tickets</h3> |
| * Supported on Android 2.3+. |
| * |
| * <h3>Android Traffic Stats (Socket Tagging)</h3> |
| * Supported on Android 4.0+. |
| * |
| * <h3>ALPN (Application Layer Protocol Negotiation)</h3> |
| * Supported on Android 5.0+. The APIs were present in Android 4.4, but that implementation was |
| * unstable. |
| * |
| * Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library). |
| * |
| * <h3>Trust Manager Extraction</h3> |
| * |
| * <p>Supported on Android 2.3+ and OpenJDK 7+. There are no public APIs to recover the trust |
| * manager that was used to create an {@link SSLSocketFactory}. |
| */ |
| public class Platform { |
| private static final Platform PLATFORM = findPlatform(); |
| |
| public static Platform get() { |
| return PLATFORM; |
| } |
| |
| /** 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 X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) { |
| return null; |
| } |
| |
| public TrustRootIndex trustRootIndex(X509TrustManager trustManager) { |
| return new RealTrustRootIndex(trustManager.getAcceptedIssuers()); |
| } |
| |
| /** |
| * 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<Protocol> 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); |
| } |
| |
| public void log(String message) { |
| System.out.println(message); |
| } |
| |
| /** Attempt to match the host runtime to a capable Platform implementation. */ |
| private static Platform findPlatform() { |
| // Attempt to find Android 2.3+ APIs. |
| try { |
| Class<?> sslParametersClass; |
| try { |
| sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl"); |
| } catch (ClassNotFoundException e) { |
| // Older platform before being unbundled. |
| sslParametersClass = Class.forName( |
| "org.apache.harmony.xnet.provider.jsse.SSLParametersImpl"); |
| } |
| |
| OptionalMethod<Socket> setUseSessionTickets |
| = new OptionalMethod<>(null, "setUseSessionTickets", boolean.class); |
| OptionalMethod<Socket> setHostname |
| = new OptionalMethod<>(null, "setHostname", String.class); |
| Method trafficStatsTagSocket = null; |
| Method trafficStatsUntagSocket = null; |
| OptionalMethod<Socket> getAlpnSelectedProtocol = null; |
| OptionalMethod<Socket> setAlpnProtocols = null; |
| |
| // 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); |
| |
| // Attempt to find Android 5.0+ APIs. |
| try { |
| Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0. |
| getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol"); |
| setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class); |
| } catch (ClassNotFoundException ignored) { |
| } |
| } catch (ClassNotFoundException | NoSuchMethodException ignored) { |
| } |
| |
| return new Android(sslParametersClass, setUseSessionTickets, setHostname, |
| trafficStatsTagSocket, trafficStatsUntagSocket, getAlpnSelectedProtocol, |
| setAlpnProtocols); |
| } catch (ClassNotFoundException ignored) { |
| // This isn't an Android runtime. |
| } |
| |
| // Find an Oracle JDK. |
| try { |
| Class<?> sslContextClass = Class.forName("sun.security.ssl.SSLContextImpl"); |
| |
| // 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(sslContextClass, |
| putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass); |
| } catch (ClassNotFoundException | NoSuchMethodException ignored) { |
| } |
| |
| return new JdkPlatform(sslContextClass); |
| } catch (ClassNotFoundException ignored) { |
| } |
| |
| return new Platform(); |
| } |
| |
| /** Android 2.3 or better. */ |
| private static class Android extends Platform { |
| private static final int MAX_LOG_LENGTH = 4000; |
| |
| private final Class<?> sslParametersClass; |
| private final OptionalMethod<Socket> setUseSessionTickets; |
| private final OptionalMethod<Socket> setHostname; |
| |
| // Non-null on Android 4.0+. |
| private final Method trafficStatsTagSocket; |
| private final Method trafficStatsUntagSocket; |
| |
| // Non-null on Android 5.0+. |
| private final OptionalMethod<Socket> getAlpnSelectedProtocol; |
| private final OptionalMethod<Socket> setAlpnProtocols; |
| |
| public Android(Class<?> sslParametersClass, OptionalMethod<Socket> setUseSessionTickets, |
| OptionalMethod<Socket> setHostname, Method trafficStatsTagSocket, |
| Method trafficStatsUntagSocket, OptionalMethod<Socket> getAlpnSelectedProtocol, |
| OptionalMethod<Socket> setAlpnProtocols) { |
| this.sslParametersClass = sslParametersClass; |
| this.setUseSessionTickets = setUseSessionTickets; |
| this.setHostname = setHostname; |
| this.trafficStatsTagSocket = trafficStatsTagSocket; |
| this.trafficStatsUntagSocket = trafficStatsUntagSocket; |
| this.getAlpnSelectedProtocol = getAlpnSelectedProtocol; |
| this.setAlpnProtocols = setAlpnProtocols; |
| } |
| |
| @Override public void connectSocket(Socket socket, InetSocketAddress address, |
| int connectTimeout) throws IOException { |
| try { |
| socket.connect(address, connectTimeout); |
| } catch (AssertionError e) { |
| if (Util.isAndroidGetsocknameError(e)) throw new IOException(e); |
| throw e; |
| } catch (SecurityException e) { |
| // 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(e); |
| throw ioException; |
| } |
| } |
| |
| @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) { |
| Object context = readFieldOrNull(sslSocketFactory, sslParametersClass, "sslParameters"); |
| if (context == null) { |
| // If that didn't work, try the Google Play Services SSL provider before giving up. This |
| // must be loaded by the SSLSocketFactory's class loader. |
| try { |
| Class<?> gmsSslParametersClass = Class.forName( |
| "com.google.android.gms.org.conscrypt.SSLParametersImpl", false, |
| sslSocketFactory.getClass().getClassLoader()); |
| context = readFieldOrNull(sslSocketFactory, gmsSslParametersClass, "sslParameters"); |
| } catch (ClassNotFoundException e) { |
| return null; |
| } |
| } |
| |
| X509TrustManager x509TrustManager = readFieldOrNull( |
| context, X509TrustManager.class, "x509TrustManager"); |
| if (x509TrustManager != null) return x509TrustManager; |
| |
| return readFieldOrNull(context, X509TrustManager.class, "trustManager"); |
| } |
| |
| @Override public TrustRootIndex trustRootIndex(X509TrustManager trustManager) { |
| TrustRootIndex result = AndroidTrustRootIndex.get(trustManager); |
| if (result != null) return result; |
| return super.trustRootIndex(trustManager); |
| } |
| |
| @Override public void configureTlsExtensions( |
| SSLSocket sslSocket, String hostname, List<Protocol> protocols) { |
| // Enable SNI and session tickets. |
| if (hostname != null) { |
| setUseSessionTickets.invokeOptionalWithoutCheckedException(sslSocket, true); |
| setHostname.invokeOptionalWithoutCheckedException(sslSocket, hostname); |
| } |
| |
| // Enable ALPN. |
| if (setAlpnProtocols != null && setAlpnProtocols.isSupported(sslSocket)) { |
| Object[] parameters = { concatLengthPrefixed(protocols) }; |
| setAlpnProtocols.invokeWithoutCheckedException(sslSocket, parameters); |
| } |
| } |
| |
| @Override public String getSelectedProtocol(SSLSocket socket) { |
| if (getAlpnSelectedProtocol == null) return null; |
| 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()); |
| } |
| } |
| |
| @Override public void log(String message) { |
| // Split by line, then ensure each line can fit into Log's maximum length. |
| for (int i = 0, length = message.length(); i < length; i++) { |
| int newline = message.indexOf('\n', i); |
| newline = newline != -1 ? newline : length; |
| do { |
| int end = Math.min(newline, i + MAX_LOG_LENGTH); |
| Log.d("OkHttp", message.substring(i, end)); |
| i = end; |
| } while (i < newline); |
| } |
| } |
| } |
| |
| /** JDK 1.7 or better. */ |
| private static class JdkPlatform extends Platform { |
| private final Class<?> sslContextClass; |
| |
| public JdkPlatform(Class<?> sslContextClass) { |
| this.sslContextClass = sslContextClass; |
| } |
| |
| @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) { |
| Object context = readFieldOrNull(sslSocketFactory, sslContextClass, "context"); |
| if (context == null) return null; |
| return readFieldOrNull(context, X509TrustManager.class, "trustManager"); |
| } |
| } |
| |
| /** |
| * OpenJDK 7+ with {@code org.mortbay.jetty.alpn/alpn-boot} in the boot class path. |
| */ |
| private static class JdkWithJettyBootPlatform extends JdkPlatform { |
| private final Method putMethod; |
| private final Method getMethod; |
| private final Method removeMethod; |
| private final Class<?> clientProviderClass; |
| private final Class<?> serverProviderClass; |
| |
| public JdkWithJettyBootPlatform(Class<?> sslContextClass, Method putMethod, Method getMethod, |
| Method removeMethod, Class<?> clientProviderClass, Class<?> serverProviderClass) { |
| super(sslContextClass); |
| this.putMethod = putMethod; |
| this.getMethod = getMethod; |
| this.removeMethod = removeMethod; |
| this.clientProviderClass = clientProviderClass; |
| this.serverProviderClass = serverProviderClass; |
| } |
| |
| @Override public void configureTlsExtensions( |
| SSLSocket sslSocket, String hostname, List<Protocol> protocols) { |
| List<String> 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 | IllegalAccessException e) { |
| throw new AssertionError(e); |
| } |
| } |
| |
| @Override public void afterHandshake(SSLSocket sslSocket) { |
| try { |
| removeMethod.invoke(null, sslSocket); |
| } catch (IllegalAccessException | InvocationTargetException ignored) { |
| throw new AssertionError(); |
| } |
| } |
| |
| @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 | 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<String> protocols; |
| /** Set when remote peer notifies ALPN is unsupported. */ |
| private boolean unsupported; |
| /** The protocol the server selected. */ |
| private String selected; |
| |
| public JettyNegoProvider(List<String> 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) { |
| List<String> 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 |
| */ |
| static byte[] concatLengthPrefixed(List<Protocol> 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(); |
| } |
| |
| static <T> T readFieldOrNull(Object instance, Class<T> fieldType, String fieldName) { |
| for (Class<?> c = instance.getClass(); c != Object.class; c = c.getSuperclass()) { |
| try { |
| Field field = c.getDeclaredField(fieldName); |
| field.setAccessible(true); |
| Object value = field.get(instance); |
| if (value == null || !fieldType.isInstance(value)) return null; |
| return fieldType.cast(value); |
| } catch (NoSuchFieldException ignored) { |
| } catch (IllegalAccessException e) { |
| throw new AssertionError(); |
| } |
| } |
| |
| // Didn't find the field we wanted. As a last gasp attempt, try to find the value on a delegate. |
| if (!fieldName.equals("delegate")) { |
| Object delegate = readFieldOrNull(instance, Object.class, "delegate"); |
| if (delegate != null) return readFieldOrNull(delegate, fieldType, fieldName); |
| } |
| |
| return null; |
| } |
| } |