371 lines
14 KiB
Java
371 lines
14 KiB
Java
/*
|
|
* Copyright (C) 2012 Square, Inc.
|
|
* Copyright (C) 2012 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package com.squareup.okhttp.internal;
|
|
|
|
import java.io.IOException;
|
|
import java.io.OutputStream;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.lang.reflect.Constructor;
|
|
import java.lang.reflect.InvocationHandler;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Proxy;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.Socket;
|
|
import java.net.SocketException;
|
|
import java.net.URI;
|
|
import java.net.URISyntaxException;
|
|
import java.net.URL;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
import java.util.zip.Deflater;
|
|
import java.util.zip.DeflaterOutputStream;
|
|
import javax.net.ssl.SSLSocket;
|
|
|
|
/**
|
|
* Access to Platform-specific features necessary for SPDY and advanced TLS.
|
|
*
|
|
* <h3>SPDY</h3>
|
|
* SPDY requires a TLS extension called NPN (Next Protocol Negotiation) that's
|
|
* available in Android 4.1+ and OpenJDK 7+ (with the npn-boot extension). It
|
|
* also requires a recent version of {@code DeflaterOutputStream} that is
|
|
* public API in Java 7 and callable via reflection in Android 4.1+.
|
|
*/
|
|
public class Platform {
|
|
private static final Platform PLATFORM = findPlatform();
|
|
|
|
private Constructor<DeflaterOutputStream> deflaterConstructor;
|
|
|
|
public static Platform get() {
|
|
return PLATFORM;
|
|
}
|
|
|
|
/** Prefix used on custom headers. */
|
|
public String getPrefix() {
|
|
return "OkHttp";
|
|
}
|
|
|
|
public void logW(String warning) {
|
|
System.out.println(warning);
|
|
}
|
|
|
|
public void tagSocket(Socket socket) throws SocketException {
|
|
}
|
|
|
|
public void untagSocket(Socket socket) throws SocketException {
|
|
}
|
|
|
|
public URI toUriLenient(URL url) throws URISyntaxException {
|
|
return url.toURI(); // this isn't as good as the built-in toUriLenient
|
|
}
|
|
|
|
/**
|
|
* Attempt a TLS connection with useful extensions enabled. This mode
|
|
* supports more features, but is less likely to be compatible with older
|
|
* HTTPS servers.
|
|
*/
|
|
public void enableTlsExtensions(SSLSocket socket, String uriHost) {
|
|
}
|
|
|
|
/**
|
|
* Attempt a secure connection with basic functionality to maximize
|
|
* compatibility. Currently this uses SSL 3.0.
|
|
*/
|
|
public void supportTlsIntolerantServer(SSLSocket socket) {
|
|
socket.setEnabledProtocols(new String[] {"SSLv3"});
|
|
}
|
|
|
|
/** Returns the negotiated protocol, or null if no protocol was negotiated. */
|
|
public byte[] getNpnSelectedProtocol(SSLSocket socket) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sets client-supported protocols on a socket to send to a server. The
|
|
* protocols are only sent if the socket implementation supports NPN.
|
|
*/
|
|
public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
|
|
}
|
|
|
|
public void connectSocket(Socket socket, InetSocketAddress address,
|
|
int connectTimeout) throws IOException {
|
|
socket.connect(address, connectTimeout);
|
|
}
|
|
|
|
/**
|
|
* Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
|
|
* value blocks. This throws an {@link UnsupportedOperationException} on
|
|
* Java 6 and earlier where there is no built-in API to do SYNC_FLUSH.
|
|
*/
|
|
public OutputStream newDeflaterOutputStream(OutputStream out, Deflater deflater,
|
|
boolean syncFlush) {
|
|
try {
|
|
Constructor<DeflaterOutputStream> constructor = deflaterConstructor;
|
|
if (constructor == null) {
|
|
constructor = deflaterConstructor = DeflaterOutputStream.class.getConstructor(
|
|
OutputStream.class, Deflater.class, boolean.class);
|
|
}
|
|
return constructor.newInstance(out, deflater, syncFlush);
|
|
} catch (NoSuchMethodException e) {
|
|
throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available");
|
|
} catch (InvocationTargetException e) {
|
|
throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause()
|
|
: new RuntimeException(e.getCause());
|
|
} catch (InstantiationException e) {
|
|
throw new RuntimeException(e);
|
|
} catch (IllegalAccessException e) {
|
|
throw new AssertionError();
|
|
}
|
|
}
|
|
|
|
/** Attempt to match the host runtime to a capable Platform implementation. */
|
|
private static Platform findPlatform() {
|
|
// Attempt to find Android 2.3+ APIs.
|
|
Class<?> openSslSocketClass;
|
|
Method setUseSessionTickets;
|
|
Method setHostname;
|
|
try {
|
|
try {
|
|
openSslSocketClass = Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl");
|
|
} catch (ClassNotFoundException ignored) {
|
|
// Older platform before being unbundled.
|
|
openSslSocketClass = Class.forName(
|
|
"org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
|
|
}
|
|
|
|
setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class);
|
|
setHostname = openSslSocketClass.getMethod("setHostname", String.class);
|
|
|
|
// Attempt to find Android 4.1+ APIs.
|
|
try {
|
|
Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
|
|
Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
|
|
return new Android41(openSslSocketClass, setUseSessionTickets, setHostname,
|
|
setNpnProtocols, getNpnSelectedProtocol);
|
|
} catch (NoSuchMethodException ignored) {
|
|
return new Android23(openSslSocketClass, setUseSessionTickets, setHostname);
|
|
}
|
|
} catch (ClassNotFoundException ignored) {
|
|
// This isn't an Android runtime.
|
|
} catch (NoSuchMethodException ignored) {
|
|
// This isn't Android 2.3 or better.
|
|
}
|
|
|
|
// Attempt to find the Jetty's NPN extension for OpenJDK.
|
|
try {
|
|
String npnClassName = "org.eclipse.jetty.npn.NextProtoNego";
|
|
Class<?> nextProtoNegoClass = Class.forName(npnClassName);
|
|
Class<?> providerClass = Class.forName(npnClassName + "$Provider");
|
|
Class<?> clientProviderClass = Class.forName(npnClassName + "$ClientProvider");
|
|
Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
|
|
Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
|
|
Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
|
|
return new JdkWithJettyNpnPlatform(
|
|
putMethod, getMethod, clientProviderClass, serverProviderClass);
|
|
} catch (ClassNotFoundException ignored) {
|
|
// NPN isn't on the classpath.
|
|
} catch (NoSuchMethodException ignored) {
|
|
// The NPN version isn't what we expect.
|
|
}
|
|
|
|
return new Platform();
|
|
}
|
|
|
|
/** Android version 2.3 and newer support TLS session tickets and server name indication (SNI). */
|
|
private static class Android23 extends Platform {
|
|
protected final Class<?> openSslSocketClass;
|
|
private final Method setUseSessionTickets;
|
|
private final Method setHostname;
|
|
|
|
private Android23(
|
|
Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname) {
|
|
this.openSslSocketClass = openSslSocketClass;
|
|
this.setUseSessionTickets = setUseSessionTickets;
|
|
this.setHostname = setHostname;
|
|
}
|
|
|
|
@Override public void connectSocket(Socket socket, InetSocketAddress address,
|
|
int connectTimeout) throws IOException {
|
|
try {
|
|
socket.connect(address, connectTimeout);
|
|
} catch (SecurityException se) {
|
|
// Before android 4.3, socket.connect could throw a SecurityException
|
|
// if opening a socket resulted in an EACCES error.
|
|
IOException ioException = new IOException("Exception in connect");
|
|
ioException.initCause(se);
|
|
throw ioException;
|
|
}
|
|
}
|
|
|
|
@Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
|
|
super.enableTlsExtensions(socket, uriHost);
|
|
if (openSslSocketClass.isInstance(socket)) {
|
|
// This is Android: use reflection on OpenSslSocketImpl.
|
|
try {
|
|
setUseSessionTickets.invoke(socket, true);
|
|
setHostname.invoke(socket, uriHost);
|
|
} catch (InvocationTargetException e) {
|
|
throw new RuntimeException(e);
|
|
} catch (IllegalAccessException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Android version 4.1 and newer support NPN. */
|
|
private static class Android41 extends Android23 {
|
|
private final Method setNpnProtocols;
|
|
private final Method getNpnSelectedProtocol;
|
|
|
|
private Android41(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
|
|
Method setNpnProtocols, Method getNpnSelectedProtocol) {
|
|
super(openSslSocketClass, setUseSessionTickets, setHostname);
|
|
this.setNpnProtocols = setNpnProtocols;
|
|
this.getNpnSelectedProtocol = getNpnSelectedProtocol;
|
|
}
|
|
|
|
@Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
|
|
if (!openSslSocketClass.isInstance(socket)) {
|
|
return;
|
|
}
|
|
try {
|
|
setNpnProtocols.invoke(socket, new Object[] {npnProtocols});
|
|
} catch (IllegalAccessException e) {
|
|
throw new AssertionError(e);
|
|
} catch (InvocationTargetException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
@Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
|
|
if (!openSslSocketClass.isInstance(socket)) {
|
|
return null;
|
|
}
|
|
try {
|
|
return (byte[]) getNpnSelectedProtocol.invoke(socket);
|
|
} catch (InvocationTargetException e) {
|
|
throw new RuntimeException(e);
|
|
} catch (IllegalAccessException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class path. */
|
|
private static class JdkWithJettyNpnPlatform extends Platform {
|
|
private final Method getMethod;
|
|
private final Method putMethod;
|
|
private final Class<?> clientProviderClass;
|
|
private final Class<?> serverProviderClass;
|
|
|
|
public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass,
|
|
Class<?> serverProviderClass) {
|
|
this.putMethod = putMethod;
|
|
this.getMethod = getMethod;
|
|
this.clientProviderClass = clientProviderClass;
|
|
this.serverProviderClass = serverProviderClass;
|
|
}
|
|
|
|
@Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
|
|
try {
|
|
List<String> strings = new ArrayList<String>();
|
|
for (int i = 0; i < npnProtocols.length; ) {
|
|
int length = npnProtocols[i++];
|
|
strings.add(new String(npnProtocols, i, length, "US-ASCII"));
|
|
i += length;
|
|
}
|
|
Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(),
|
|
new Class[] {clientProviderClass, serverProviderClass},
|
|
new JettyNpnProvider(strings));
|
|
putMethod.invoke(null, socket, provider);
|
|
} catch (UnsupportedEncodingException e) {
|
|
throw new AssertionError(e);
|
|
} catch (InvocationTargetException e) {
|
|
throw new AssertionError(e);
|
|
} catch (IllegalAccessException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
@Override public byte[] getNpnSelectedProtocol(SSLSocket socket) {
|
|
try {
|
|
JettyNpnProvider provider =
|
|
(JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
|
|
if (!provider.unsupported && provider.selected == null) {
|
|
Logger logger = Logger.getLogger("com.squareup.okhttp.OkHttpClient");
|
|
logger.log(Level.INFO,
|
|
"NPN callback dropped so SPDY is disabled. " + "Is npn-boot on the boot class path?");
|
|
return null;
|
|
}
|
|
return provider.unsupported ? null : provider.selected.getBytes("US-ASCII");
|
|
} catch (UnsupportedEncodingException e) {
|
|
throw new AssertionError();
|
|
} catch (InvocationTargetException e) {
|
|
throw new AssertionError();
|
|
} catch (IllegalAccessException e) {
|
|
throw new AssertionError();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle the methods of NextProtoNego's ClientProvider and ServerProvider
|
|
* without a compile-time dependency on those interfaces.
|
|
*/
|
|
private static class JettyNpnProvider implements InvocationHandler {
|
|
private final List<String> protocols;
|
|
private boolean unsupported;
|
|
private String selected;
|
|
|
|
public JettyNpnProvider(List<String> protocols) {
|
|
this.protocols = protocols;
|
|
}
|
|
|
|
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
|
String methodName = method.getName();
|
|
Class<?> returnType = method.getReturnType();
|
|
if (args == null) {
|
|
args = Util.EMPTY_STRING_ARRAY;
|
|
}
|
|
if (methodName.equals("supports") && boolean.class == returnType) {
|
|
return true;
|
|
} else if (methodName.equals("unsupported") && void.class == returnType) {
|
|
this.unsupported = true;
|
|
return null;
|
|
} else if (methodName.equals("protocols") && args.length == 0) {
|
|
return protocols;
|
|
} else if (methodName.equals("selectProtocol")
|
|
&& String.class == returnType
|
|
&& args.length == 1
|
|
&& (args[0] == null || args[0] instanceof List)) {
|
|
// TODO: use OpenSSL's algorithm which uses both lists
|
|
List<?> serverProtocols = (List) args[0];
|
|
this.selected = protocols.get(0);
|
|
return selected;
|
|
} else if (methodName.equals("protocolSelected") && args.length == 1) {
|
|
this.selected = (String) args[0];
|
|
return null;
|
|
} else {
|
|
return method.invoke(this, args);
|
|
}
|
|
}
|
|
}
|
|
}
|