1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-17 08:42:25 +03:00

New API to break platform-specific dependencies.

In particular:
 - Don't require OkHttp to depend on Jetty's non-boot package for
   Android. This was causing ugly, confidence-smashing dalvik
   errors in Android apps.
 - Don't prevent HTTP on Java6 just because SPDY isn't available.

This uses gross reflection and dynamic proxies! That's sad, but
it's encapsulated so users won't have to know it exists.
This commit is contained in:
Jesse Wilson
2012-11-13 00:09:16 -08:00
parent 6991363c38
commit 9a1da9b420
6 changed files with 372 additions and 188 deletions

View File

@@ -33,6 +33,36 @@ OkHttp uses the platform's [ProxySelector][2]. Prior to Android 4.0, `ProxySelec
OkHttp's test suite creates an in-process HTTPS server. Prior to Android 2.3, SSL server sockets were broken, and so HTTPS tests will time out when run on such devices.
Building
--------
### On the Desktop
Run OkHttp tests on the desktop with Maven.
```
mvn clean test
```
SPDY support uses a Deflater API that wasn't available in Java 6. For this reason SPDY tests will fail with this error: `Cannot SPDY; no SYNC_FLUSH available`. All other tests should run fine.
### On the Desktop with NPN
Using NPN on the desktop uses [Jetty-NPN](http://wiki.eclipse.org/Jetty/Feature/NPN) which requires OpenJDK 7+.
```
mvn clean test -Pspdy-tls
```
### On a Device
Test on a USB-attached Android using [Vogar](https://code.google.com/p/vogar/). Unfortunately `dx` requires that you build with Java 6, otherwise the test class will be silently omitted from the `.dex` file.
```
mvn clean
mvn package -DskipTests
vogar \
--classpath ~/.m2/repository/org/bouncycastle/bcprov-jdk15on/1.47/bcprov-jdk15on-1.47.jar \
--classpath ~/.m2/repository/com/google/mockwebserver/mockwebserver/20121111/mockwebserver-20121111.jar \
--classpath target/okhttp-0.8-SNAPSHOT.jar \
./src/test/java/libcore/net/http/URLConnectionTest.java
```
Because the OkHttp uses `jarjar` to repackage classes in `libcore`, OkHttp tests that use those classes directly cannot be run on a device.
License
-------

58
pom.xml
View File

@@ -68,6 +68,7 @@
<groupId>org.mortbay.jetty.npn</groupId>
<artifactId>npn-boot</artifactId>
<version>${npn.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.mockwebserver</groupId>
@@ -125,45 +126,6 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.9</version>
<configuration>
<argLine>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</argLine>
</configuration>
</plugin>
<plugin>
<!--
OkHttp requires with javac >= 1.7 for syncFlush on DeflaterOutputStream.
Its language version must be <= 1.6 for dx.
Running this code on Java 6 or earlier will fail at runtime due to the missing
syncFlush API.
Dalvik's core library includes syncFlush, but with an @hide tag so that it doesn't
show up in the documentation or the android.jar stubs. This code works fine on
Dalvik.
-->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>1.1</version>
<executions>
<execution>
<id>enforce-java</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireJavaVersion>
<version>[1.7.0,)</version>
</requireJavaVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
@@ -185,5 +147,23 @@
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>spdy-tls</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.9</version>
<configuration>
<argLine>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</argLine>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,315 @@
/*
* 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 libcore;
import com.squareup.okhttp.OkHttpsConnection;
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.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;
}
public void makeTlsTolerant(SSLSocket socket, String uriHost, boolean tlsTolerant) {
if (!tlsTolerant) {
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) {
}
/**
* 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 {
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");
Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass);
} catch (ClassNotFoundException ignored) {
return new Platform(); // NPN isn't on the classpath.
} catch (NoSuchMethodException ignored) {
return new Platform(); // The NPN version isn't what we expect.
}
}
/**
* 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 makeTlsTolerant(
SSLSocket socket, String uriHost, boolean tlsTolerant) {
super.makeTlsTolerant(socket, uriHost, tlsTolerant);
if (tlsTolerant && 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;
public JdkWithJettyNpnPlatform(
Method putMethod, Method getMethod, Class<?> clientProviderClass) {
this.putMethod = putMethod;
this.getMethod = getMethod;
this.clientProviderClass = clientProviderClass;
}
@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 }, 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(OkHttpsConnection.class.getName());
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> clientProtocols;
private boolean unsupported;
private String selected;
public JettyNpnProvider(List<String> clientProtocols) {
this.clientProtocols = clientProtocols;
}
@Override public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String methodName = method.getName();
Class<?> returnType = method.getReturnType();
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("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];
System.out.println("CLIENT PROTOCOLS: " + clientProtocols + " SERVER PROTOCOLS: " + serverProtocols);
this.selected = clientProtocols.get(0);
return selected;
} else {
return method.invoke(this, args);
}
}
}
}

View File

@@ -33,6 +33,7 @@ import java.util.Arrays;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import libcore.Platform;
import libcore.io.IoUtils;
import libcore.net.spdy.SpdyConnection;
import libcore.util.Libcore;
@@ -131,6 +132,8 @@ final class HttpConnection {
* validation.
*/
private void upgradeToTls(TunnelConfig tunnelConfig) throws IOException {
Platform platform = Platform.get();
// Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
if (requiresTunnel()) {
makeTunnel(tunnelConfig);
@@ -140,10 +143,10 @@ final class HttpConnection {
socket = address.sslSocketFactory.createSocket(
socket, address.uriHost, address.uriPort, true /* autoClose */);
SSLSocket sslSocket = (SSLSocket) socket;
Libcore.makeTlsTolerant(sslSocket, address.uriHost, tlsMode == TLS_MODE_AGGRESSIVE);
platform.makeTlsTolerant(sslSocket, address.uriHost, tlsMode == TLS_MODE_AGGRESSIVE);
if (tlsMode == TLS_MODE_AGGRESSIVE) {
Libcore.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
}
// Force handshake. This can throw!
@@ -159,7 +162,7 @@ final class HttpConnection {
byte[] selectedProtocol;
if (tlsMode == TLS_MODE_AGGRESSIVE
&& (selectedProtocol = Libcore.getNpnSelectedProtocol(sslSocket)) != null) {
&& (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
if (Arrays.equals(selectedProtocol, SPDY2)) {
spdyConnection = new SpdyConnection.Builder(true, in, out).build();
HttpConnectionPool.INSTANCE.share(this);

View File

@@ -22,7 +22,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import libcore.Platform;
/**
* Write version 2 SPDY frames.
@@ -39,7 +39,7 @@ final class SpdyWriter {
deflater.setDictionary(SpdyReader.DICTIONARY);
nameValueBlockBuffer = new ByteArrayOutputStream();
nameValueBlockOut = new DataOutputStream(
new DeflaterOutputStream(nameValueBlockBuffer, deflater, true));
Platform.get().newDeflaterOutputStream(nameValueBlockBuffer, deflater, true));
}
public void synStream(int flags, int streamId, int associatedStreamId, int priority,

View File

@@ -18,19 +18,12 @@ package libcore.util;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLSocket;
import org.eclipse.jetty.npn.NextProtoNego;
/**
* APIs for interacting with Android's core library. This mostly emulates the
@@ -41,143 +34,6 @@ public final class Libcore {
private Libcore() {
}
private static boolean useAndroidTlsApis;
private static Class<?> openSslSocketClass;
private static Method setUseSessionTickets;
private static Method setHostname;
private static boolean android23TlsOptionsAvailable;
private static Method setNpnProtocols;
private static Method getNpnSelectedProtocol;
private static boolean android41TlsOptionsAvailable;
static {
try {
openSslSocketClass = Class.forName(
"org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
useAndroidTlsApis = true;
setUseSessionTickets = openSslSocketClass.getMethod(
"setUseSessionTickets", boolean.class);
setHostname = openSslSocketClass.getMethod("setHostname", String.class);
android23TlsOptionsAvailable = true;
setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
android41TlsOptionsAvailable = true;
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
} catch (NoSuchMethodException ignored) {
// This Android runtime is missing some optional TLS options.
}
}
public static void makeTlsTolerant(SSLSocket socket, String uriHost, boolean tlsTolerant) {
if (!tlsTolerant) {
socket.setEnabledProtocols(new String[] {"SSLv3"});
return;
}
if (android23TlsOptionsAvailable && 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);
}
}
}
/**
* Returns the negotiated protocol, or null if no protocol was negotiated.
*/
public static byte[] getNpnSelectedProtocol(SSLSocket socket) {
if (useAndroidTlsApis) {
// This is Android: use reflection on OpenSslSocketImpl.
if (android41TlsOptionsAvailable && openSslSocketClass.isInstance(socket)) {
try {
return (byte[]) getNpnSelectedProtocol.invoke(socket);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
}
return null;
} else {
// This is OpenJDK: use JettyNpnProvider.
JettyNpnProvider provider = (JettyNpnProvider) NextProtoNego.get(socket);
if (!provider.unsupported && provider.selected == null) {
throw new IllegalStateException(
"No callback received. Is NPN configured properly?");
}
try {
return provider.unsupported
? null
: provider.selected.getBytes("US-ASCII");
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
}
public static void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
if (useAndroidTlsApis) {
// This is Android: use reflection on OpenSslSocketImpl.
if (android41TlsOptionsAvailable && openSslSocketClass.isInstance(socket)) {
try {
setNpnProtocols.invoke(socket, new Object[] {npnProtocols});
} catch (IllegalAccessException e) {
throw new AssertionError(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
} else {
// This is OpenJDK: use JettyNpnProvider.
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;
}
JettyNpnProvider provider = new JettyNpnProvider();
provider.protocols = strings;
NextProtoNego.put(socket, provider);
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
}
private static class JettyNpnProvider
implements NextProtoNego.ClientProvider, NextProtoNego.ServerProvider {
List<String> protocols;
boolean unsupported;
String selected;
@Override public boolean supports() {
return true;
}
@Override public List<String> protocols() {
return protocols;
}
@Override public void unsupported() {
this.unsupported = true;
}
@Override public void protocolSelected(String selected) {
this.selected = selected;
}
@Override public String selectProtocol(List<String> strings) {
// TODO: use OpenSSL's algorithm which uses 2 lists
System.out.println("CLIENT PROTOCOLS: " + protocols + " SERVER PROTOCOLS: " + strings);
String selected = protocols.get(0);
protocolSelected(selected);
return selected;
}
}
public static void deleteIfExists(File file) throws IOException {
// okhttp-changed: was Libcore.os.remove() in a try/catch block
file.delete();