/* * 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 com.squareup.okhttp.Protocol; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Constructor; 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.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import javax.net.ssl.SSLSocket; import okio.ByteString; /** * Access to Platform-specific features necessary for SPDY and advanced TLS. * * <h3>ALPN and NPN</h3> * This class uses TLS extensions ALPN and NPN to negotiate the upgrade from * HTTP/1.1 (the default protocol to use with TLS on port 443) to either SPDY * or HTTP/2. * * <p>NPN (Next Protocol Negotiation) was developed for SPDY. It is widely * available and we support it on both Android (4.1+) and OpenJDK 7 (via the * Jetty NPN-boot library). NPN is not yet available on Java 8. * * <p>ALPN (Application Layer Protocol Negotiation) is the successor to NPN. It * has some technical advantages over NPN. ALPN first arrived in Android 4.4, * but that release suffers a <a href="http://goo.gl/y5izPP">concurrency bug</a> * so we don't use it. ALPN will be supported in the future. * * <p>On platforms that support both extensions, OkHttp will use both, * preferring ALPN's result. Future versions of OkHttp will drop support for * NPN. * * <h3>Deflater Sync Flush</h3> * SPDY header compression requires a recent version of {@code * DeflaterOutputStream} that is public API in Java 7 and callable via * reflection in Android 4.1+. */ public class Platform { private static final Platform PLATFORM = findPlatform(); private Constructor<DeflaterOutputStream> deflaterConstructor; 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 URI toUriLenient(URL url) throws URISyntaxException { return url.toURI(); // this isn't as good as the built-in toUriLenient } /** * Attempt a TLS connection with useful extensions enabled. This mode * supports more features, but is less likely to be compatible with older * HTTPS servers. */ public void enableTlsExtensions(SSLSocket socket, String uriHost) { } /** * Attempt a secure connection with basic functionality to maximize * compatibility. Currently this uses SSL 3.0. */ public void supportTlsIntolerantServer(SSLSocket socket) { socket.setEnabledProtocols(new String[] {"SSLv3"}); } /** Returns the negotiated protocol, or null if no protocol was negotiated. */ public ByteString getNpnSelectedProtocol(SSLSocket socket) { return null; } /** * Sets client-supported protocols on a socket to send to a server. The * protocols are only sent if the socket implementation supports NPN. */ public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) { } public void connectSocket(Socket socket, InetSocketAddress address, int connectTimeout) throws IOException { socket.connect(address, connectTimeout); } /** * Returns a deflater output stream that supports SYNC_FLUSH for SPDY name * value blocks. This throws an {@link UnsupportedOperationException} on * Java 6 and earlier where there is no built-in API to do SYNC_FLUSH. */ public OutputStream newDeflaterOutputStream(OutputStream out, Deflater deflater, boolean syncFlush) { try { Constructor<DeflaterOutputStream> constructor = deflaterConstructor; if (constructor == null) { constructor = deflaterConstructor = DeflaterOutputStream.class.getConstructor( OutputStream.class, Deflater.class, boolean.class); } return constructor.newInstance(out, deflater, syncFlush); } catch (NoSuchMethodException e) { throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available"); } catch (InvocationTargetException e) { throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause() : new RuntimeException(e.getCause()); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new AssertionError(); } } /** Attempt to match the host runtime to a capable Platform implementation. */ private static Platform findPlatform() { // Attempt to find Android 2.3+ APIs. Class<?> openSslSocketClass; Method setUseSessionTickets; Method setHostname; try { try { openSslSocketClass = Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl"); } catch (ClassNotFoundException ignored) { // Older platform before being unbundled. openSslSocketClass = Class.forName( "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl"); } setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class); setHostname = openSslSocketClass.getMethod("setHostname", String.class); // Attempt to find Android 4.1+ APIs. Method setNpnProtocols = null; Method getNpnSelectedProtocol = null; try { setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class); getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol"); } catch (NoSuchMethodException ignored) { } return new Android(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols, getNpnSelectedProtocol); } catch (ClassNotFoundException ignored) { // This isn't an Android runtime. } catch (NoSuchMethodException ignored) { // This isn't Android 2.3 or better. } // Attempt to find the Jetty's NPN extension for OpenJDK. try { String npnClassName = "org.eclipse.jetty.npn.NextProtoNego"; Class<?> nextProtoNegoClass = Class.forName(npnClassName); Class<?> providerClass = Class.forName(npnClassName + "$Provider"); Class<?> clientProviderClass = Class.forName(npnClassName + "$ClientProvider"); Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider"); Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass); Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class); return new JdkWithJettyNpnPlatform( putMethod, getMethod, clientProviderClass, serverProviderClass); } catch (ClassNotFoundException ignored) { // NPN isn't on the classpath. } catch (NoSuchMethodException ignored) { // The NPN version isn't what we expect. } return new Platform(); } /** * Android 2.3 or better. Version 2.3 supports TLS session tickets and server * name indication (SNI). Versions 4.1 supports NPN. */ private static class Android extends Platform { // Non-null. protected final Class<?> openSslSocketClass; private final Method setUseSessionTickets; private final Method setHostname; // Non-null on Android 4.1+. private final Method setNpnProtocols; private final Method getNpnSelectedProtocol; private Android(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) { this.openSslSocketClass = openSslSocketClass; this.setUseSessionTickets = setUseSessionTickets; this.setHostname = setHostname; this.setNpnProtocols = setNpnProtocols; this.getNpnSelectedProtocol = getNpnSelectedProtocol; } @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 enableTlsExtensions(SSLSocket socket, String uriHost) { super.enableTlsExtensions(socket, uriHost); if (!openSslSocketClass.isInstance(socket)) return; try { setUseSessionTickets.invoke(socket, true); setHostname.invoke(socket, uriHost); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new AssertionError(e); } } @Override public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) { if (setNpnProtocols == null) return; if (!openSslSocketClass.isInstance(socket)) return; try { Object[] parameters = { concatLengthPrefixed(npnProtocols) }; setNpnProtocols.invoke(socket, parameters); } catch (IllegalAccessException e) { throw new AssertionError(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } } @Override public ByteString getNpnSelectedProtocol(SSLSocket socket) { if (getNpnSelectedProtocol == null) return null; if (!openSslSocketClass.isInstance(socket)) return null; try { byte[] npnResult = (byte[]) getNpnSelectedProtocol.invoke(socket); if (npnResult == null) return null; return ByteString.of(npnResult); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new AssertionError(e); } } } /** OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class path. */ private static class JdkWithJettyNpnPlatform extends Platform { private final Method getMethod; private final Method putMethod; private final Class<?> clientProviderClass; private final Class<?> serverProviderClass; public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass, Class<?> serverProviderClass) { this.putMethod = putMethod; this.getMethod = getMethod; this.clientProviderClass = clientProviderClass; this.serverProviderClass = serverProviderClass; } @Override public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) { try { List<String> names = new ArrayList<String>(npnProtocols.size()); for (int i = 0, size = npnProtocols.size(); i < size; i++) { names.add(npnProtocols.get(i).name.utf8()); } Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(), new Class[] { clientProviderClass, serverProviderClass }, new JettyNpnProvider(names)); putMethod.invoke(null, socket, provider); } catch (InvocationTargetException e) { throw new AssertionError(e); } catch (IllegalAccessException e) { throw new AssertionError(e); } } @Override public ByteString getNpnSelectedProtocol(SSLSocket socket) { try { JettyNpnProvider provider = (JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket)); if (!provider.unsupported && provider.selected == null) { Logger logger = Logger.getLogger("com.squareup.okhttp.OkHttpClient"); logger.log(Level.INFO, "NPN callback dropped so SPDY is disabled. Is npn-boot on the boot class path?"); return null; } return provider.unsupported ? null : ByteString.encodeUtf8(provider.selected); } catch (InvocationTargetException e) { throw new AssertionError(); } catch (IllegalAccessException e) { throw new AssertionError(); } } } /** * Handle the methods of NextProtoNego's ClientProvider and ServerProvider * without a compile-time dependency on those interfaces. */ private static class JettyNpnProvider implements InvocationHandler { /** This peer's supported protocols. */ private final List<String> protocols; /** Set when remote peer notifies NPN is unsupported. */ private boolean unsupported; /** The protocol the client selected. */ private String selected; public JettyNpnProvider(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; // Client supports NPN. } else if (methodName.equals("unsupported") && void.class == returnType) { this.unsupported = true; // Remote peer doesn't support NPN. return null; } else if (methodName.equals("protocols") && args.length == 0) { return protocols; // Server advertises these protocols. } else if (methodName.equals("selectProtocol") // Called when client. && String.class == returnType && args.length == 1 && (args[0] == null || args[0] instanceof List)) { List<String> serverProtocols = (List) args[0]; // Pick the first protocol the server advertises and client knows. for (int i = 0, size = serverProtocols.size(); i < size; i++) { if (protocols.contains(serverProtocols.get(i))) { return selected = serverProtocols.get(i); } } // On no intersection, try client's first protocol. return selected = protocols.get(0); } else if (methodName.equals("protocolSelected") && args.length == 1) { this.selected = (String) args[0]; // Client selected this protocol. return null; } else { return method.invoke(this, args); } } } /** * 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) { int size = 0; for (Protocol protocol : protocols) { size += protocol.name.size() + 1; // add a byte for 8-bit length prefix. } byte[] result = new byte[size]; int pos = 0; for (Protocol protocol : protocols) { int nameSize = protocol.name.size(); result[pos++] = (byte) nameSize; // toByteArray allocates an array, but this is only called on new connections. System.arraycopy(protocol.name.toByteArray(), 0, result, pos, nameSize); pos += nameSize; } return result; } }