1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-27 04:22:07 +03:00

Merge pull request #87 from square/jwilson/mockspdyserver

Introduce MockSpdyServer.
This commit is contained in:
Jesse Wilson
2013-01-22 10:39:12 -08:00
5 changed files with 387 additions and 33 deletions

View File

@@ -23,11 +23,6 @@ You can also depend on the .jar through Maven:
Known Issues
------------
The SPDY implementation is incomplete:
* Settings frames are not honored. Flow control is not implemented.
* It assumes a well-behaved peer. If the peer sends an invalid frame, OkHttp's SPDY client will not respond with the required `RST` frame.
OkHttp uses the platform's [ProxySelector][2]. Prior to Android 4.0, `ProxySelector` didn't honor the `proxyHost` and `proxyPort` system properties for HTTPS connections. Work around this by specifying the `https.proxyHost` and `https.proxyPort` system properties when using a proxy with HTTPS.
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.
@@ -37,17 +32,10 @@ Building
--------
### On the Desktop
Run OkHttp tests on the desktop with Maven.
Run OkHttp tests on the desktop with Maven. Running SPDY tests on the desktop uses [Jetty-NPN](http://wiki.eclipse.org/Jetty/Feature/NPN) which requires OpenJDK 7+.
```
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.

28
pom.xml
View File

@@ -38,7 +38,7 @@
<!-- Compilation -->
<java.version>1.6</java.version>
<npn.version>8.1.2.v20120308</npn.version>
<mockwebserver.version>20120905</mockwebserver.version>
<mockwebserver.version>20130122</mockwebserver.version>
<bouncycastle.version>1.47</bouncycastle.version>
<!-- Test Dependencies -->
@@ -120,25 +120,15 @@
</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>
</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

@@ -21,7 +21,6 @@ import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;

View File

@@ -0,0 +1,267 @@
/*
* Copyright (C) 2013 Square, Inc.
* Copyright (C) 2011 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.mockspdyserver;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.QueueDispatcher;
import com.google.mockwebserver.RecordedRequest;
import com.squareup.okhttp.internal.spdy.IncomingStreamHandler;
import com.squareup.okhttp.internal.spdy.SpdyConnection;
import com.squareup.okhttp.internal.spdy.SpdyStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.eclipse.jetty.npn.NextProtoNego;
/**
* A scriptable spdy/3 + HTTP server.
*/
public final class MockSpdyServer {
private static final Logger logger = Logger.getLogger(MockSpdyServer.class.getName());
private SSLSocketFactory sslSocketFactory;
private QueueDispatcher dispatcher = new QueueDispatcher();
private ServerSocket serverSocket;
private final Set<Socket> openClientSockets
= Collections.newSetFromMap(new ConcurrentHashMap<Socket, Boolean>());
private int port = -1;
private final BlockingQueue<RecordedRequest> requestQueue
= new LinkedBlockingQueue<RecordedRequest>();
public MockSpdyServer(SSLSocketFactory sslSocketFactory) {
this.sslSocketFactory = sslSocketFactory;
}
public String getHostName() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
throw new AssertionError();
}
}
public int getPort() {
if (port == -1) {
throw new IllegalStateException("Cannot retrieve port before calling play()");
}
return port;
}
public URL getUrl(String path) {
try {
return new URL("https://" + getHostName() + ":" + getPort() + path);
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
}
/**
* Awaits the next HTTP request, removes it, and returns it. Callers should
* use this to verify the request sent was as intended.
*/
public RecordedRequest takeRequest() throws InterruptedException {
return requestQueue.take();
}
public void play() throws IOException {
serverSocket = new ServerSocket(8888);
serverSocket.setReuseAddress(true);
port = serverSocket.getLocalPort();
Thread acceptThread = new Thread("MockSpdyServer-accept-" + port) {
@Override public void run() {
int sequenceNumber = 0;
try {
acceptConnections(sequenceNumber);
} catch (Throwable e) {
logger.log(Level.WARNING, "MockWebServer connection failed", e);
}
/*
* This gnarly block of code will release all sockets and
* all thread, even if any close fails.
*/
try {
serverSocket.close();
} catch (Throwable e) {
logger.log(Level.WARNING, "MockWebServer server socket close failed", e);
}
for (Iterator<Socket> s = openClientSockets.iterator(); s.hasNext(); ) {
try {
s.next().close();
s.remove();
} catch (Throwable e) {
logger.log(Level.WARNING, "MockWebServer socket close failed", e);
}
}
}
};
acceptThread.start();
}
public void enqueue(MockResponse response) {
dispatcher.enqueueResponse(response);
}
private void acceptConnections(int sequenceNumber) throws Exception {
while (true) {
Socket socket;
try {
socket = serverSocket.accept();
} catch (SocketException e) {
return;
}
openClientSockets.add(socket);
new SocketHandler(sequenceNumber++, socket).serve();
}
}
public void shutdown() throws IOException {
if (serverSocket != null) {
serverSocket.close(); // should cause acceptConnections() to break out
}
}
private class SocketHandler implements IncomingStreamHandler {
private final int sequenceNumber;
private Socket socket;
private SocketHandler(int sequenceNumber, Socket socket) throws IOException {
this.socket = socket;
this.sequenceNumber = sequenceNumber;
}
public void serve() throws IOException {
if (sslSocketFactory != null) {
socket = doSsl(socket);
}
new SpdyConnection.Builder(false, socket).handler(this).build();
}
private Socket doSsl(Socket socket) throws IOException {
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket,
socket.getInetAddress().getHostAddress(), socket.getPort(), true);
sslSocket.setUseClientMode(false);
NextProtoNego.put(sslSocket, new NextProtoNego.ServerProvider() {
@Override public void unsupported() {
System.out.println("UNSUPPORTED");
}
@Override public List<String> protocols() {
return Arrays.asList("spdy/3");
}
@Override public void protocolSelected(String protocol) {
System.out.println("PROTOCOL SELECTED: " + protocol);
}
});
return sslSocket;
}
@Override public void receive(final SpdyStream stream) throws IOException {
RecordedRequest request = readRequest(stream);
requestQueue.add(request);
MockResponse response;
try {
response = dispatcher.dispatch(request);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
writeResponse(stream, response);
logger.info("Received request: " + request + " and responded: " + response);
}
private RecordedRequest readRequest(SpdyStream stream) throws IOException {
List<String> spdyHeaders = stream.getRequestHeaders();
List<String> httpHeaders = new ArrayList<String>();
String method = "<:method omitted>";
String path = "<:path omitted>";
String version = "<:version omitted>";
for (Iterator<String> i = spdyHeaders.iterator(); i.hasNext(); ) {
String name = i.next();
String value = i.next();
if (":method".equals(name)) {
method = value;
} else if (":path".equals(name)) {
path = value;
} else if (":version".equals(name)) {
version = value;
} else {
httpHeaders.add(name + ": " + value);
}
}
InputStream bodyIn = stream.getInputStream();
ByteArrayOutputStream bodyOut = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int count;
while ((count = bodyIn.read(buffer)) != -1) {
bodyOut.write(buffer, 0, count);
}
bodyIn.close();
String requestLine = method + ' ' + path + ' ' + version;
List<Integer> chunkSizes = Collections.emptyList(); // No chunked encoding for SPDY.
return new RecordedRequest(requestLine, httpHeaders, chunkSizes, bodyOut.size(),
bodyOut.toByteArray(), sequenceNumber, socket);
}
private void writeResponse(SpdyStream stream, MockResponse response) throws IOException {
List<String> spdyHeaders = new ArrayList<String>();
String[] statusParts = response.getStatus().split(" ", 2);
if (statusParts.length != 2) {
throw new AssertionError("Unexpected status: " + response.getStatus());
}
spdyHeaders.add(":status");
spdyHeaders.add(statusParts[1]);
spdyHeaders.add(":version");
spdyHeaders.add(statusParts[0]);
for (String header : response.getHeaders()) {
String[] headerParts = header.split(":", 2);
if (headerParts.length != 2) {
throw new AssertionError("Unexpected header: " + header);
}
spdyHeaders.add(headerParts[0].toLowerCase(Locale.US));
spdyHeaders.add(headerParts[1]);
}
byte[] body = response.getBody();
stream.reply(spdyHeaders, body.length > 0);
if (body.length > 0) {
stream.getOutputStream().write(body);
stream.getOutputStream().close();
}
}
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2013 Square, Inc.
*
* 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.spdy;
import com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.RecordedRequest;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.mockspdyserver.MockSpdyServer;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.util.Collection;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import org.junit.After;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;
/**
* Test how SPDY interacts with HTTP features.
*/
public final class HttpOverSpdyTest {
private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
private static final SSLContext sslContext;
static {
try {
sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
private final MockSpdyServer server = new MockSpdyServer(sslContext.getSocketFactory());
private final String hostName = server.getHostName();
private final OkHttpClient client = new OkHttpClient();
@Before public void setUp() throws Exception {
client.setSSLSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
}
@After public void tearDown() throws Exception {
server.shutdown();
}
@Test public void get() throws Exception {
MockResponse response = new MockResponse().setBody("ABCDE");
server.enqueue(response);
server.play();
HttpURLConnection connection = client.open(server.getUrl("/foo"));
assertContent("ABCDE", connection, Integer.MAX_VALUE);
RecordedRequest request = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
assertContains(request.getHeaders(), ":scheme: https");
assertContains(request.getHeaders(), ":host: " + hostName + ":" + server.getPort());
}
private <T> void assertContains(Collection<T> collection, T value) {
assertTrue(collection.toString(), collection.contains(value));
}
private void assertContent(String expected, URLConnection connection, int limit)
throws IOException {
connection.connect();
assertEquals(expected, readAscii(connection.getInputStream(), limit));
((HttpURLConnection) connection).disconnect();
}
private String readAscii(InputStream in, int count) throws IOException {
StringBuilder result = new StringBuilder();
for (int i = 0; i < count; i++) {
int value = in.read();
if (value == -1) {
in.close();
break;
}
result.append((char) value);
}
return result.toString();
}
}