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