Home | History | Annotate | Download | only in internal
      1 /*
      2  * Copyright (C) 2012 Square, Inc.
      3  * Copyright (C) 2012 The Android Open Source Project
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 package com.squareup.okhttp.internal;
     18 
     19 import com.squareup.okhttp.Protocol;
     20 import java.io.IOException;
     21 import java.io.OutputStream;
     22 import java.lang.reflect.Constructor;
     23 import java.lang.reflect.InvocationHandler;
     24 import java.lang.reflect.InvocationTargetException;
     25 import java.lang.reflect.Method;
     26 import java.lang.reflect.Proxy;
     27 import java.net.InetSocketAddress;
     28 import java.net.Socket;
     29 import java.net.SocketException;
     30 import java.net.URI;
     31 import java.net.URISyntaxException;
     32 import java.net.URL;
     33 import java.util.ArrayList;
     34 import java.util.List;
     35 import java.util.logging.Level;
     36 import java.util.logging.Logger;
     37 import java.util.zip.Deflater;
     38 import java.util.zip.DeflaterOutputStream;
     39 import javax.net.ssl.SSLSocket;
     40 import okio.ByteString;
     41 
     42 /**
     43  * Access to Platform-specific features necessary for SPDY and advanced TLS.
     44  *
     45  * <h3>ALPN and NPN</h3>
     46  * This class uses TLS extensions ALPN and NPN to negotiate the upgrade from
     47  * HTTP/1.1 (the default protocol to use with TLS on port 443) to either SPDY
     48  * or HTTP/2.
     49  *
     50  * <p>NPN (Next Protocol Negotiation) was developed for SPDY. It is widely
     51  * available and we support it on both Android (4.1+) and OpenJDK 7 (via the
     52  * Jetty NPN-boot library). NPN is not yet available on Java 8.
     53  *
     54  * <p>ALPN (Application Layer Protocol Negotiation) is the successor to NPN. It
     55  * has some technical advantages over NPN. ALPN first arrived in Android 4.4,
     56  * but that release suffers a <a href="http://goo.gl/y5izPP">concurrency bug</a>
     57  * so we don't use it. ALPN will be supported in the future.
     58  *
     59  * <p>On platforms that support both extensions, OkHttp will use both,
     60  * preferring ALPN's result. Future versions of OkHttp will drop support for
     61  * NPN.
     62  *
     63  * <h3>Deflater Sync Flush</h3>
     64  * SPDY header compression requires a recent version of {@code
     65  * DeflaterOutputStream} that is public API in Java 7 and callable via
     66  * reflection in Android 4.1+.
     67  */
     68 public class Platform {
     69   private static final Platform PLATFORM = findPlatform();
     70 
     71   private Constructor<DeflaterOutputStream> deflaterConstructor;
     72 
     73   public static Platform get() {
     74     return PLATFORM;
     75   }
     76 
     77   /** Prefix used on custom headers. */
     78   public String getPrefix() {
     79     return "OkHttp";
     80   }
     81 
     82   public void logW(String warning) {
     83     System.out.println(warning);
     84   }
     85 
     86   public void tagSocket(Socket socket) throws SocketException {
     87   }
     88 
     89   public void untagSocket(Socket socket) throws SocketException {
     90   }
     91 
     92   public URI toUriLenient(URL url) throws URISyntaxException {
     93     return url.toURI(); // this isn't as good as the built-in toUriLenient
     94   }
     95 
     96   /**
     97    * Attempt a TLS connection with useful extensions enabled. This mode
     98    * supports more features, but is less likely to be compatible with older
     99    * HTTPS servers.
    100    */
    101   public void enableTlsExtensions(SSLSocket socket, String uriHost) {
    102   }
    103 
    104   /**
    105    * Attempt a secure connection with basic functionality to maximize
    106    * compatibility. Currently this uses SSL 3.0.
    107    */
    108   public void supportTlsIntolerantServer(SSLSocket socket) {
    109     socket.setEnabledProtocols(new String[] {"SSLv3"});
    110   }
    111 
    112   /** Returns the negotiated protocol, or null if no protocol was negotiated. */
    113   public ByteString getNpnSelectedProtocol(SSLSocket socket) {
    114     return null;
    115   }
    116 
    117   /**
    118    * Sets client-supported protocols on a socket to send to a server. The
    119    * protocols are only sent if the socket implementation supports NPN.
    120    */
    121   public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
    122   }
    123 
    124   public void connectSocket(Socket socket, InetSocketAddress address,
    125       int connectTimeout) throws IOException {
    126     socket.connect(address, connectTimeout);
    127   }
    128 
    129   /**
    130    * Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
    131    * value blocks. This throws an {@link UnsupportedOperationException} on
    132    * Java 6 and earlier where there is no built-in API to do SYNC_FLUSH.
    133    */
    134   public OutputStream newDeflaterOutputStream(OutputStream out, Deflater deflater,
    135       boolean syncFlush) {
    136     try {
    137       Constructor<DeflaterOutputStream> constructor = deflaterConstructor;
    138       if (constructor == null) {
    139         constructor = deflaterConstructor = DeflaterOutputStream.class.getConstructor(
    140             OutputStream.class, Deflater.class, boolean.class);
    141       }
    142       return constructor.newInstance(out, deflater, syncFlush);
    143     } catch (NoSuchMethodException e) {
    144       throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available");
    145     } catch (InvocationTargetException e) {
    146       throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause()
    147           : new RuntimeException(e.getCause());
    148     } catch (InstantiationException e) {
    149       throw new RuntimeException(e);
    150     } catch (IllegalAccessException e) {
    151       throw new AssertionError();
    152     }
    153   }
    154 
    155   /** Attempt to match the host runtime to a capable Platform implementation. */
    156   private static Platform findPlatform() {
    157     // Attempt to find Android 2.3+ APIs.
    158     Class<?> openSslSocketClass;
    159     Method setUseSessionTickets;
    160     Method setHostname;
    161     try {
    162       try {
    163         openSslSocketClass = Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl");
    164       } catch (ClassNotFoundException ignored) {
    165         // Older platform before being unbundled.
    166         openSslSocketClass = Class.forName(
    167             "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
    168       }
    169 
    170       setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class);
    171       setHostname = openSslSocketClass.getMethod("setHostname", String.class);
    172 
    173       // Attempt to find Android 4.1+ APIs.
    174       Method setNpnProtocols = null;
    175       Method getNpnSelectedProtocol = null;
    176       try {
    177         setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
    178         getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
    179       } catch (NoSuchMethodException ignored) {
    180       }
    181 
    182       return new Android(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols,
    183           getNpnSelectedProtocol);
    184     } catch (ClassNotFoundException ignored) {
    185       // This isn't an Android runtime.
    186     } catch (NoSuchMethodException ignored) {
    187       // This isn't Android 2.3 or better.
    188     }
    189 
    190     // Attempt to find the Jetty's NPN extension for OpenJDK.
    191     try {
    192       String npnClassName = "org.eclipse.jetty.npn.NextProtoNego";
    193       Class<?> nextProtoNegoClass = Class.forName(npnClassName);
    194       Class<?> providerClass = Class.forName(npnClassName + "$Provider");
    195       Class<?> clientProviderClass = Class.forName(npnClassName + "$ClientProvider");
    196       Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
    197       Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
    198       Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
    199       return new JdkWithJettyNpnPlatform(
    200           putMethod, getMethod, clientProviderClass, serverProviderClass);
    201     } catch (ClassNotFoundException ignored) {
    202       // NPN isn't on the classpath.
    203     } catch (NoSuchMethodException ignored) {
    204       // The NPN version isn't what we expect.
    205     }
    206 
    207     return new Platform();
    208   }
    209 
    210   /**
    211    * Android 2.3 or better. Version 2.3 supports TLS session tickets and server
    212    * name indication (SNI). Versions 4.1 supports NPN.
    213    */
    214   private static class Android extends Platform {
    215     // Non-null.
    216     protected final Class<?> openSslSocketClass;
    217     private final Method setUseSessionTickets;
    218     private final Method setHostname;
    219 
    220     // Non-null on Android 4.1+.
    221     private final Method setNpnProtocols;
    222     private final Method getNpnSelectedProtocol;
    223 
    224     private Android(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
    225         Method setNpnProtocols, Method getNpnSelectedProtocol) {
    226       this.openSslSocketClass = openSslSocketClass;
    227       this.setUseSessionTickets = setUseSessionTickets;
    228       this.setHostname = setHostname;
    229       this.setNpnProtocols = setNpnProtocols;
    230       this.getNpnSelectedProtocol = getNpnSelectedProtocol;
    231     }
    232 
    233     @Override public void connectSocket(Socket socket, InetSocketAddress address,
    234         int connectTimeout) throws IOException {
    235       try {
    236         socket.connect(address, connectTimeout);
    237       } catch (SecurityException se) {
    238         // Before android 4.3, socket.connect could throw a SecurityException
    239         // if opening a socket resulted in an EACCES error.
    240         IOException ioException = new IOException("Exception in connect");
    241         ioException.initCause(se);
    242         throw ioException;
    243       }
    244     }
    245 
    246     @Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
    247       super.enableTlsExtensions(socket, uriHost);
    248       if (!openSslSocketClass.isInstance(socket)) return;
    249       try {
    250         setUseSessionTickets.invoke(socket, true);
    251         setHostname.invoke(socket, uriHost);
    252       } catch (InvocationTargetException e) {
    253         throw new RuntimeException(e);
    254       } catch (IllegalAccessException e) {
    255         throw new AssertionError(e);
    256       }
    257     }
    258 
    259     @Override public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
    260       if (setNpnProtocols == null) return;
    261       if (!openSslSocketClass.isInstance(socket)) return;
    262       try {
    263         Object[] parameters = { concatLengthPrefixed(npnProtocols) };
    264         setNpnProtocols.invoke(socket, parameters);
    265       } catch (IllegalAccessException e) {
    266         throw new AssertionError(e);
    267       } catch (InvocationTargetException e) {
    268         throw new RuntimeException(e);
    269       }
    270     }
    271 
    272     @Override public ByteString getNpnSelectedProtocol(SSLSocket socket) {
    273       if (getNpnSelectedProtocol == null) return null;
    274       if (!openSslSocketClass.isInstance(socket)) return null;
    275       try {
    276         byte[] npnResult = (byte[]) getNpnSelectedProtocol.invoke(socket);
    277         if (npnResult == null) return null;
    278         return ByteString.of(npnResult);
    279       } catch (InvocationTargetException e) {
    280         throw new RuntimeException(e);
    281       } catch (IllegalAccessException e) {
    282         throw new AssertionError(e);
    283       }
    284     }
    285   }
    286 
    287   /** OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class path. */
    288   private static class JdkWithJettyNpnPlatform extends Platform {
    289     private final Method getMethod;
    290     private final Method putMethod;
    291     private final Class<?> clientProviderClass;
    292     private final Class<?> serverProviderClass;
    293 
    294     public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass,
    295         Class<?> serverProviderClass) {
    296       this.putMethod = putMethod;
    297       this.getMethod = getMethod;
    298       this.clientProviderClass = clientProviderClass;
    299       this.serverProviderClass = serverProviderClass;
    300     }
    301 
    302     @Override public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
    303       try {
    304         List<String> names = new ArrayList<String>(npnProtocols.size());
    305         for (int i = 0, size = npnProtocols.size(); i < size; i++) {
    306           names.add(npnProtocols.get(i).name.utf8());
    307         }
    308         Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(),
    309             new Class[] { clientProviderClass, serverProviderClass }, new JettyNpnProvider(names));
    310         putMethod.invoke(null, socket, provider);
    311       } catch (InvocationTargetException e) {
    312         throw new AssertionError(e);
    313       } catch (IllegalAccessException e) {
    314         throw new AssertionError(e);
    315       }
    316     }
    317 
    318     @Override public ByteString getNpnSelectedProtocol(SSLSocket socket) {
    319       try {
    320         JettyNpnProvider provider =
    321             (JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
    322         if (!provider.unsupported && provider.selected == null) {
    323           Logger logger = Logger.getLogger("com.squareup.okhttp.OkHttpClient");
    324           logger.log(Level.INFO,
    325               "NPN callback dropped so SPDY is disabled. Is npn-boot on the boot class path?");
    326           return null;
    327         }
    328         return provider.unsupported ? null : ByteString.encodeUtf8(provider.selected);
    329       } catch (InvocationTargetException e) {
    330         throw new AssertionError();
    331       } catch (IllegalAccessException e) {
    332         throw new AssertionError();
    333       }
    334     }
    335   }
    336 
    337   /**
    338    * Handle the methods of NextProtoNego's ClientProvider and ServerProvider
    339    * without a compile-time dependency on those interfaces.
    340    */
    341   private static class JettyNpnProvider implements InvocationHandler {
    342     /** This peer's supported protocols. */
    343     private final List<String> protocols;
    344     /** Set when remote peer notifies NPN is unsupported. */
    345     private boolean unsupported;
    346     /** The protocol the client selected. */
    347     private String selected;
    348 
    349     public JettyNpnProvider(List<String> protocols) {
    350       this.protocols = protocols;
    351     }
    352 
    353     @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    354       String methodName = method.getName();
    355       Class<?> returnType = method.getReturnType();
    356       if (args == null) {
    357         args = Util.EMPTY_STRING_ARRAY;
    358       }
    359       if (methodName.equals("supports") && boolean.class == returnType) {
    360         return true; // Client supports NPN.
    361       } else if (methodName.equals("unsupported") && void.class == returnType) {
    362         this.unsupported = true; // Remote peer doesn't support NPN.
    363         return null;
    364       } else if (methodName.equals("protocols") && args.length == 0) {
    365         return protocols; // Server advertises these protocols.
    366       } else if (methodName.equals("selectProtocol") // Called when client.
    367           && String.class == returnType
    368           && args.length == 1
    369           && (args[0] == null || args[0] instanceof List)) {
    370         List<String> serverProtocols = (List) args[0];
    371         // Pick the first protocol the server advertises and client knows.
    372         for (int i = 0, size = serverProtocols.size(); i < size; i++) {
    373           if (protocols.contains(serverProtocols.get(i))) {
    374             return selected = serverProtocols.get(i);
    375           }
    376         }
    377         // On no intersection, try client's first protocol.
    378         return selected = protocols.get(0);
    379       } else if (methodName.equals("protocolSelected") && args.length == 1) {
    380         this.selected = (String) args[0]; // Client selected this protocol.
    381         return null;
    382       } else {
    383         return method.invoke(this, args);
    384       }
    385     }
    386   }
    387 
    388   /**
    389    * Concatenation of 8-bit, length prefixed protocol names.
    390    *
    391    * http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4
    392    */
    393   static byte[] concatLengthPrefixed(List<Protocol> protocols) {
    394     int size = 0;
    395     for (Protocol protocol : protocols) {
    396       size += protocol.name.size() + 1; // add a byte for 8-bit length prefix.
    397     }
    398     byte[] result = new byte[size];
    399     int pos = 0;
    400     for (Protocol protocol : protocols) {
    401       int nameSize = protocol.name.size();
    402       result[pos++] = (byte) nameSize;
    403       // toByteArray allocates an array, but this is only called on new connections.
    404       System.arraycopy(protocol.name.toByteArray(), 0, result, pos, nameSize);
    405       pos += nameSize;
    406     }
    407     return result;
    408   }
    409 }
    410