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.OkHttpClient;
20import java.io.OutputStream;
21import java.io.UnsupportedEncodingException;
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.Socket;
28import java.net.SocketException;
29import java.net.URI;
30import java.net.URISyntaxException;
31import java.net.URL;
32import java.util.ArrayList;
33import java.util.List;
34import java.util.logging.Level;
35import java.util.logging.Logger;
36import java.util.zip.Deflater;
37import java.util.zip.DeflaterOutputStream;
38import javax.net.ssl.SSLSocket;
39
40/**
41 * Access to Platform-specific features necessary for SPDY and advanced TLS.
42 *
43 * <h3>SPDY</h3>
44 * SPDY requires a TLS extension called NPN (Next Protocol Negotiation) that's
45 * available in Android 4.1+ and OpenJDK 7+ (with the npn-boot extension). It
46 * also requires a recent version of {@code DeflaterOutputStream} that is
47 * public API in Java 7 and callable via reflection in Android 4.1+.
48 */
49public class Platform {
50  private static final Platform PLATFORM = findPlatform();
51
52  private Constructor<DeflaterOutputStream> deflaterConstructor;
53
54  public static Platform get() {
55    return PLATFORM;
56  }
57
58  public void logW(String warning) {
59    System.out.println(warning);
60  }
61
62  public void tagSocket(Socket socket) throws SocketException {
63  }
64
65  public void untagSocket(Socket socket) throws SocketException {
66  }
67
68  public URI toUriLenient(URL url) throws URISyntaxException {
69    return url.toURI(); // this isn't as good as the built-in toUriLenient
70  }
71
72  /**
73   * Attempt a TLS connection with useful extensions enabled. This mode
74   * supports more features, but is less likely to be compatible with older
75   * HTTPS servers.
76   */
77  public void enableTlsExtensions(SSLSocket socket, String uriHost) {
78  }
79
80  /**
81   * Attempt a secure connection with basic functionality to maximize
82   * compatibility. Currently this uses SSL 3.0.
83   */
84  public void supportTlsIntolerantServer(SSLSocket socket) {
85    socket.setEnabledProtocols(new String[] {"SSLv3"});
86  }
87
88  /** Returns the negotiated protocol, or null if no protocol was negotiated. */
89  public byte[] getNpnSelectedProtocol(SSLSocket socket) {
90    return null;
91  }
92
93  /**
94   * Sets client-supported protocols on a socket to send to a server. The
95   * protocols are only sent if the socket implementation supports NPN.
96   */
97  public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
98  }
99
100  /**
101   * Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
102   * value blocks. This throws an {@link UnsupportedOperationException} on
103   * Java 6 and earlier where there is no built-in API to do SYNC_FLUSH.
104   */
105  public OutputStream newDeflaterOutputStream(OutputStream out, Deflater deflater,
106      boolean syncFlush) {
107    try {
108      Constructor<DeflaterOutputStream> constructor = deflaterConstructor;
109      if (constructor == null) {
110        constructor = deflaterConstructor = DeflaterOutputStream.class.getConstructor(
111            OutputStream.class, Deflater.class, boolean.class);
112      }
113      return constructor.newInstance(out, deflater, syncFlush);
114    } catch (NoSuchMethodException e) {
115      throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available");
116    } catch (InvocationTargetException e) {
117      throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause()
118          : new RuntimeException(e.getCause());
119    } catch (InstantiationException e) {
120      throw new RuntimeException(e);
121    } catch (IllegalAccessException e) {
122      throw new AssertionError();
123    }
124  }
125
126  /** Attempt to match the host runtime to a capable Platform implementation. */
127  private static Platform findPlatform() {
128    // Attempt to find Android 2.3+ APIs.
129    Class<?> openSslSocketClass;
130    Method setUseSessionTickets;
131    Method setHostname;
132    try {
133      openSslSocketClass = Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
134      setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class);
135      setHostname = openSslSocketClass.getMethod("setHostname", String.class);
136
137      // Attempt to find Android 4.1+ APIs.
138      try {
139        Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
140        Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
141        return new Android41(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols,
142            getNpnSelectedProtocol);
143      } catch (NoSuchMethodException ignored) {
144        return new Android23(openSslSocketClass, setUseSessionTickets, setHostname);
145      }
146    } catch (ClassNotFoundException ignored) {
147      // This isn't an Android runtime.
148    } catch (NoSuchMethodException ignored) {
149      // This isn't Android 2.3 or better.
150    }
151
152    // Attempt to find the Jetty's NPN extension for OpenJDK.
153    try {
154      String npnClassName = "org.eclipse.jetty.npn.NextProtoNego";
155      Class<?> nextProtoNegoClass = Class.forName(npnClassName);
156      Class<?> providerClass = Class.forName(npnClassName + "$Provider");
157      Class<?> clientProviderClass = Class.forName(npnClassName + "$ClientProvider");
158      Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
159      Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
160      Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
161      return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass,
162          serverProviderClass);
163    } catch (ClassNotFoundException ignored) {
164      return new Platform(); // NPN isn't on the classpath.
165    } catch (NoSuchMethodException ignored) {
166      return new Platform(); // The NPN version isn't what we expect.
167    }
168  }
169
170  /**
171   * Android version 2.3 and newer support TLS session tickets and server name
172   * indication (SNI).
173   */
174  private static class Android23 extends Platform {
175    protected final Class<?> openSslSocketClass;
176    private final Method setUseSessionTickets;
177    private final Method setHostname;
178
179    private Android23(Class<?> openSslSocketClass, Method setUseSessionTickets,
180        Method setHostname) {
181      this.openSslSocketClass = openSslSocketClass;
182      this.setUseSessionTickets = setUseSessionTickets;
183      this.setHostname = setHostname;
184    }
185
186    @Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
187      super.enableTlsExtensions(socket, uriHost);
188      if (openSslSocketClass.isInstance(socket)) {
189        // This is Android: use reflection on OpenSslSocketImpl.
190        try {
191          setUseSessionTickets.invoke(socket, true);
192          setHostname.invoke(socket, uriHost);
193        } catch (InvocationTargetException e) {
194          throw new RuntimeException(e);
195        } catch (IllegalAccessException e) {
196          throw new AssertionError(e);
197        }
198      }
199    }
200  }
201
202  /** Android version 4.1 and newer support NPN. */
203  private static class Android41 extends Android23 {
204    private final Method setNpnProtocols;
205    private final Method getNpnSelectedProtocol;
206
207    private Android41(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
208        Method setNpnProtocols, Method getNpnSelectedProtocol) {
209      super(openSslSocketClass, setUseSessionTickets, setHostname);
210      this.setNpnProtocols = setNpnProtocols;
211      this.getNpnSelectedProtocol = getNpnSelectedProtocol;
212    }
213
214    @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
215      if (!openSslSocketClass.isInstance(socket)) {
216        return;
217      }
218      try {
219        setNpnProtocols.invoke(socket, new Object[] {npnProtocols});
220      } catch (IllegalAccessException e) {
221        throw new AssertionError(e);
222      } catch (InvocationTargetException e) {
223        throw new RuntimeException(e);
224      }
225    }
226
227    @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
228      if (!openSslSocketClass.isInstance(socket)) {
229        return null;
230      }
231      try {
232        return (byte[]) getNpnSelectedProtocol.invoke(socket);
233      } catch (InvocationTargetException e) {
234        throw new RuntimeException(e);
235      } catch (IllegalAccessException e) {
236        throw new AssertionError(e);
237      }
238    }
239  }
240
241  /**
242   * OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class
243   * path.
244   */
245  private static class JdkWithJettyNpnPlatform extends Platform {
246    private final Method getMethod;
247    private final Method putMethod;
248    private final Class<?> clientProviderClass;
249    private final Class<?> serverProviderClass;
250
251    public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass,
252        Class<?> serverProviderClass) {
253      this.putMethod = putMethod;
254      this.getMethod = getMethod;
255      this.clientProviderClass = clientProviderClass;
256      this.serverProviderClass = serverProviderClass;
257    }
258
259    @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
260      try {
261        List<String> strings = new ArrayList<String>();
262        for (int i = 0; i < npnProtocols.length; ) {
263          int length = npnProtocols[i++];
264          strings.add(new String(npnProtocols, i, length, "US-ASCII"));
265          i += length;
266        }
267        Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(),
268            new Class[] {clientProviderClass, serverProviderClass},
269            new JettyNpnProvider(strings));
270        putMethod.invoke(null, socket, provider);
271      } catch (UnsupportedEncodingException e) {
272        throw new AssertionError(e);
273      } catch (InvocationTargetException e) {
274        throw new AssertionError(e);
275      } catch (IllegalAccessException e) {
276        throw new AssertionError(e);
277      }
278    }
279
280    @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
281      try {
282        JettyNpnProvider provider =
283            (JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
284        if (!provider.unsupported && provider.selected == null) {
285          Logger logger = Logger.getLogger(OkHttpClient.class.getName());
286          logger.log(Level.INFO,
287              "NPN callback dropped so SPDY is disabled. " + "Is npn-boot on the boot class path?");
288          return null;
289        }
290        return provider.unsupported ? null : provider.selected.getBytes("US-ASCII");
291      } catch (UnsupportedEncodingException e) {
292        throw new AssertionError();
293      } catch (InvocationTargetException e) {
294        throw new AssertionError();
295      } catch (IllegalAccessException e) {
296        throw new AssertionError();
297      }
298    }
299  }
300
301  /**
302   * Handle the methods of NextProtoNego's ClientProvider and ServerProvider
303   * without a compile-time dependency on those interfaces.
304   */
305  private static class JettyNpnProvider implements InvocationHandler {
306    private final List<String> protocols;
307    private boolean unsupported;
308    private String selected;
309
310    public JettyNpnProvider(List<String> protocols) {
311      this.protocols = protocols;
312    }
313
314    @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
315      String methodName = method.getName();
316      Class<?> returnType = method.getReturnType();
317      if (args == null) {
318        args = Util.EMPTY_STRING_ARRAY;
319      }
320      if (methodName.equals("supports") && boolean.class == returnType) {
321        return true;
322      } else if (methodName.equals("unsupported") && void.class == returnType) {
323        this.unsupported = true;
324        return null;
325      } else if (methodName.equals("protocols") && args.length == 0) {
326        return protocols;
327      } else if (methodName.equals("selectProtocol")
328          && String.class == returnType
329          && args.length == 1
330          && (args[0] == null || args[0] instanceof List)) {
331        // TODO: use OpenSSL's algorithm which uses both lists
332        List<?> serverProtocols = (List) args[0];
333        this.selected = protocols.get(0);
334        return selected;
335      } else if (methodName.equals("protocolSelected") && args.length == 1) {
336        this.selected = (String) args[0];
337        return null;
338      } else {
339        return method.invoke(this, args);
340      }
341    }
342  }
343}
344