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