Platform.java revision b32e86a30ec0fa455a37939ec63cb5a69fd3b9ce
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 */
17package com.squareup.okhttp.internal;
18
19import com.squareup.okhttp.Protocol;
20import java.io.IOException;
21import java.io.OutputStream;
22import java.lang.reflect.Constructor;
23import java.lang.reflect.InvocationHandler;
24import java.lang.reflect.InvocationTargetException;
25import java.lang.reflect.Method;
26import java.lang.reflect.Proxy;
27import java.net.InetSocketAddress;
28import java.net.Socket;
29import java.net.SocketException;
30import java.net.URI;
31import java.net.URISyntaxException;
32import java.net.URL;
33import java.util.ArrayList;
34import java.util.List;
35import java.util.logging.Level;
36import java.util.logging.Logger;
37import java.util.zip.Deflater;
38import java.util.zip.DeflaterOutputStream;
39import javax.net.ssl.SSLSocket;
40import 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 */
68public 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