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:
14
README.md
14
README.md
@@ -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
28
pom.xml
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user