blob: 5bffcfef5d15906534429195aae130c742b238dc [file] [log] [blame]
/*
* Copyright (C) 2009 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.java.net;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Authenticator;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.ResponseCache;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import libcore.java.security.TestKeyStore;
import libcore.javax.net.ssl.TestSSLContext;
import tests.http.DefaultResponseCache;
import tests.http.MockResponse;
import tests.http.MockWebServer;
import tests.http.RecordedRequest;
public class URLConnectionTest extends junit.framework.TestCase {
private static final Authenticator SIMPLE_AUTHENTICATOR = new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("username", "password".toCharArray());
}
};
private MockWebServer server = new MockWebServer();
private String hostname;
@Override protected void setUp() throws Exception {
super.setUp();
hostname = InetAddress.getLocalHost().getHostName();
}
@Override protected void tearDown() throws Exception {
ResponseCache.setDefault(null);
Authenticator.setDefault(null);
System.clearProperty("proxyHost");
System.clearProperty("proxyPort");
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
server.shutdown();
super.tearDown();
}
public void testRequestHeaders() throws IOException, InterruptedException {
server.enqueue(new MockResponse());
server.play();
HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
urlConnection.addRequestProperty("D", "e");
urlConnection.addRequestProperty("D", "f");
Map<String, List<String>> requestHeaders = urlConnection.getRequestProperties();
assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("D")));
try {
requestHeaders.put("G", Arrays.asList("h"));
fail("Modified an unmodifiable view.");
} catch (UnsupportedOperationException expected) {
}
try {
requestHeaders.get("D").add("i");
fail("Modified an unmodifiable view.");
} catch (UnsupportedOperationException expected) {
}
try {
urlConnection.setRequestProperty(null, "j");
fail();
} catch (NullPointerException expected) {
}
try {
urlConnection.addRequestProperty(null, "k");
fail();
} catch (NullPointerException expected) {
}
urlConnection.setRequestProperty("NullValue", null); // should fail silently!
urlConnection.addRequestProperty("AnotherNullValue", null); // should fail silently!
urlConnection.getResponseCode();
RecordedRequest request = server.takeRequest();
assertContains(request.getHeaders(), "D: e");
assertContains(request.getHeaders(), "D: f");
assertContainsNoneMatching(request.getHeaders(), "NullValue.*");
assertContainsNoneMatching(request.getHeaders(), "AnotherNullValue.*");
assertContainsNoneMatching(request.getHeaders(), "G:.*");
assertContainsNoneMatching(request.getHeaders(), "null:.*");
try {
urlConnection.addRequestProperty("N", "o");
fail("Set header after connect");
} catch (IllegalStateException expected) {
}
try {
urlConnection.setRequestProperty("P", "q");
fail("Set header after connect");
} catch (IllegalStateException expected) {
}
}
public void testResponseHeaders() throws IOException, InterruptedException {
server.enqueue(new MockResponse()
.setStatus("HTTP/1.0 200 Fantastic")
.addHeader("A: b")
.addHeader("A: c")
.setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8));
server.play();
HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
assertEquals(200, urlConnection.getResponseCode());
assertEquals("Fantastic", urlConnection.getResponseMessage());
Map<String, List<String>> responseHeaders = urlConnection.getHeaderFields();
assertEquals(newSet("b", "c"), new HashSet<String>(responseHeaders.get("A")));
try {
responseHeaders.put("N", Arrays.asList("o"));
fail("Modified an unmodifiable view.");
} catch (UnsupportedOperationException expected) {
}
try {
responseHeaders.get("A").add("d");
fail("Modified an unmodifiable view.");
} catch (UnsupportedOperationException expected) {
}
}
// Check that if we don't read to the end of a response, the next request on the
// recycled connection doesn't get the unread tail of the first request's response.
// http://code.google.com/p/android/issues/detail?id=2939
public void test_2939() throws Exception {
MockResponse response = new MockResponse().setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8);
server.enqueue(response);
server.enqueue(response);
server.play();
assertContent("ABCDE", server.getUrl("/").openConnection(), 5);
assertContent("ABCDE", server.getUrl("/").openConnection(), 5);
}
// Check that we recognize a few basic mime types by extension.
// http://code.google.com/p/android/issues/detail?id=10100
public void test_10100() throws Exception {
assertEquals("image/jpeg", URLConnection.guessContentTypeFromName("someFile.jpg"));
assertEquals("application/pdf", URLConnection.guessContentTypeFromName("stuff.pdf"));
}
public void testConnectionsArePooled() throws Exception {
MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR");
server.enqueue(response);
server.enqueue(response);
server.enqueue(response);
server.play();
assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/foo").openConnection());
assertEquals(0, server.takeRequest().getSequenceNumber());
assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/bar?baz=quux").openConnection());
assertEquals(1, server.takeRequest().getSequenceNumber());
assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/z").openConnection());
assertEquals(2, server.takeRequest().getSequenceNumber());
}
public void testChunkedConnectionsArePooled() throws Exception {
MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5);
server.enqueue(response);
server.enqueue(response);
server.enqueue(response);
server.play();
assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/foo").openConnection());
assertEquals(0, server.takeRequest().getSequenceNumber());
assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/bar?baz=quux").openConnection());
assertEquals(1, server.takeRequest().getSequenceNumber());
assertContent("ABCDEFGHIJKLMNOPQR", server.getUrl("/z").openConnection());
assertEquals(2, server.takeRequest().getSequenceNumber());
}
enum WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS }
public void test_chunkedUpload_byteByByte() throws Exception {
doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE);
}
public void test_chunkedUpload_smallBuffers() throws Exception {
doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS);
}
public void test_chunkedUpload_largeBuffers() throws Exception {
doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS);
}
public void test_fixedLengthUpload_byteByByte() throws Exception {
doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE);
}
public void test_fixedLengthUpload_smallBuffers() throws Exception {
doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS);
}
public void test_fixedLengthUpload_largeBuffers() throws Exception {
doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS);
}
private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception {
int n = 512*1024;
server.setBodyLimit(0);
server.enqueue(new MockResponse());
server.play();
HttpURLConnection conn = (HttpURLConnection) server.getUrl("/").openConnection();
conn.setDoOutput(true);
conn.setRequestMethod("POST");
if (uploadKind == TransferKind.CHUNKED) {
conn.setChunkedStreamingMode(-1);
} else {
conn.setFixedLengthStreamingMode(n);
}
OutputStream out = conn.getOutputStream();
if (writeKind == WriteKind.BYTE_BY_BYTE) {
for (int i = 0; i < n; ++i) {
out.write('x');
}
} else {
byte[] buf = new byte[writeKind == WriteKind.SMALL_BUFFERS ? 256 : 64*1024];
Arrays.fill(buf, (byte) 'x');
for (int i = 0; i < n; i += buf.length) {
out.write(buf, 0, Math.min(buf.length, n - i));
}
}
out.close();
assertEquals(200, conn.getResponseCode());
RecordedRequest request = server.takeRequest();
assertEquals(n, request.getBodySize());
if (uploadKind == TransferKind.CHUNKED) {
assertTrue(request.getChunkSizes().size() > 0);
} else {
assertTrue(request.getChunkSizes().isEmpty());
}
}
/**
* Test that response caching is consistent with the RI and the spec.
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
*/
public void test_responseCaching() throws Exception {
// Test each documented HTTP/1.1 code, plus the first unused value in each range.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
// We can't test 100 because it's not really a response.
// assertCached(false, 100);
assertCached(false, 101);
assertCached(false, 102);
assertCached(true, 200);
assertCached(false, 201);
assertCached(false, 202);
assertCached(true, 203);
assertCached(false, 204);
assertCached(false, 205);
assertCached(true, 206);
assertCached(false, 207);
// (See test_responseCaching_300.)
assertCached(true, 301);
for (int i = 302; i <= 308; ++i) {
assertCached(false, i);
}
for (int i = 400; i <= 406; ++i) {
assertCached(false, i);
}
// (See test_responseCaching_407.)
assertCached(false, 408);
assertCached(false, 409);
// (See test_responseCaching_410.)
for (int i = 411; i <= 418; ++i) {
assertCached(false, i);
}
for (int i = 500; i <= 506; ++i) {
assertCached(false, i);
}
}
public void test_responseCaching_300() throws Exception {
// TODO: fix this for android
assertCached(false, 300);
}
/**
* Response code 407 should only come from proxy servers. Android's client
* throws if it is sent by an origin server.
*/
public void testOriginServerSends407() throws Exception {
server.enqueue(new MockResponse().setResponseCode(407));
server.play();
URL url = server.getUrl("/");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
try {
conn.getResponseCode();
fail();
} catch (IOException expected) {
}
}
public void test_responseCaching_410() throws Exception {
// the HTTP spec permits caching 410s, but the RI doesn't.
assertCached(false, 410);
}
private void assertCached(boolean shouldPut, int responseCode) throws Exception {
server = new MockWebServer();
server.enqueue(new MockResponse()
.setResponseCode(responseCode)
.setBody("ABCDE")
.addHeader("WWW-Authenticate: challenge"));
server.play();
DefaultResponseCache responseCache = new DefaultResponseCache();
ResponseCache.setDefault(responseCache);
URL url = server.getUrl("/");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
assertEquals(responseCode, conn.getResponseCode());
// exhaust the content stream
try {
// TODO: remove special case once testUnauthorizedResponseHandling() is fixed
if (responseCode != 401) {
readAscii(conn.getInputStream(), Integer.MAX_VALUE);
}
} catch (IOException ignored) {
}
Set<URI> expectedCachedUris = shouldPut
? Collections.singleton(url.toURI())
: Collections.<URI>emptySet();
assertEquals(Integer.toString(responseCode),
expectedCachedUris, responseCache.getContents().keySet());
server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers
}
public void testConnectViaHttps() throws IOException, InterruptedException {
TestSSLContext testSSLContext = TestSSLContext.create();
server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
server.play();
HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection();
connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
assertContent("this response comes via HTTPS", connection);
RecordedRequest request = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
}
public void testConnectViaHttpsReusingConnections() throws IOException, InterruptedException {
TestSSLContext testSSLContext = TestSSLContext.create();
server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
server.enqueue(new MockResponse().setBody("another response via HTTPS"));
server.play();
HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
assertContent("this response comes via HTTPS", connection);
connection = (HttpsURLConnection) server.getUrl("/").openConnection();
connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
assertContent("another response via HTTPS", connection);
assertEquals(0, server.takeRequest().getSequenceNumber());
assertEquals(1, server.takeRequest().getSequenceNumber());
}
public void testConnectViaHttpsReusingConnectionsDifferentFactories()
throws IOException, InterruptedException {
TestSSLContext testSSLContext = TestSSLContext.create();
server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
server.enqueue(new MockResponse().setBody("another response via HTTPS"));
server.play();
// install a custom SSL socket factory so the server can be authorized
HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
assertContent("this response comes via HTTPS", connection);
connection = (HttpsURLConnection) server.getUrl("/").openConnection();
try {
readAscii(connection.getInputStream(), Integer.MAX_VALUE);
fail("without an SSL socket factory, the connection should fail");
} catch (SSLException expected) {
}
}
public void testConnectViaHttpsWithSSLFallback() throws IOException, InterruptedException {
TestSSLContext testSSLContext = TestSSLContext.create();
server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setDisconnectAtStart(true));
server.enqueue(new MockResponse().setBody("this response comes via SSL"));
server.play();
HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection();
connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
assertContent("this response comes via SSL", connection);
RecordedRequest request = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
}
/**
* Verify that we don't retry connections on certificate verification errors.
*
* http://code.google.com/p/android/issues/detail?id=13178
*/
public void testConnectViaHttpsToUntrustedServer() throws IOException, InterruptedException {
TestSSLContext testSSLContext = TestSSLContext.create(TestKeyStore.getClientCA2(),
TestKeyStore.getServer());
server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
server.enqueue(new MockResponse()); // unused
server.play();
HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection();
connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
try {
connection.getInputStream();
fail();
} catch (SSLHandshakeException expected) {
assertTrue(expected.getCause() instanceof CertificateException);
}
assertEquals(0, server.getRequestCount());
}
public void testConnectViaProxyUsingProxyArg() throws Exception {
testConnectViaProxy(ProxyConfig.CREATE_ARG);
}
public void testConnectViaProxyUsingProxySystemProperty() throws Exception {
testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY);
}
public void testConnectViaProxyUsingHttpProxySystemProperty() throws Exception {
testConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
}
private void testConnectViaProxy(ProxyConfig proxyConfig) throws Exception {
MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy");
server.enqueue(mockResponse);
server.play();
URL url = new URL("http://android.com/foo");
HttpURLConnection connection = proxyConfig.connect(server, url);
assertContent("this response comes via a proxy", connection);
RecordedRequest request = server.takeRequest();
assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine());
assertContains(request.getHeaders(), "Host: android.com");
}
public void testContentDisagreesWithContentLengthHeader() throws IOException {
server.enqueue(new MockResponse()
.setBody("abc\r\nYOU SHOULD NOT SEE THIS")
.clearHeaders()
.addHeader("Content-Length: 3"));
server.play();
assertContent("abc", server.getUrl("/").openConnection());
}
public void testContentDisagreesWithChunkedHeader() throws IOException {
MockResponse mockResponse = new MockResponse();
mockResponse.setChunkedBody("abc", 3);
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
bytesOut.write(mockResponse.getBody());
bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes());
mockResponse.setBody(bytesOut.toByteArray());
mockResponse.clearHeaders();
mockResponse.addHeader("Transfer-encoding: chunked");
server.enqueue(mockResponse);
server.play();
assertContent("abc", server.getUrl("/").openConnection());
}
public void testConnectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() throws Exception {
testConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY);
}
public void testConnectViaHttpProxyToHttpsUsingHttpProxySystemProperty() throws Exception {
// https should not use http proxy
testConnectViaDirectProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
}
private void testConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception {
TestSSLContext testSSLContext = TestSSLContext.create();
server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
server.play();
URL url = server.getUrl("/foo");
HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, url);
connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
assertContent("this response comes via HTTPS", connection);
RecordedRequest request = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
}
public void testConnectViaHttpProxyToHttpsUsingProxyArg() throws Exception {
testConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG);
}
public void testConnectViaHttpProxyToHttpsUsingHttpsProxySystemProperty() throws Exception {
testConnectViaHttpProxyToHttps(ProxyConfig.HTTPS_PROXY_SYSTEM_PROPERTY);
}
/**
* We were verifying the wrong hostname when connecting to an HTTPS site
* through a proxy. http://b/3097277
*/
private void testConnectViaHttpProxyToHttps(ProxyConfig proxyConfig) throws Exception {
TestSSLContext testSSLContext = TestSSLContext.create();
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
server.useHttps(testSSLContext.serverContext.getSocketFactory(), true);
server.enqueue(new MockResponse().clearHeaders()); // for CONNECT
server.enqueue(new MockResponse().setBody("this response comes via a secure proxy"));
server.play();
URL url = new URL("https://android.com/foo");
HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, url);
connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
connection.setHostnameVerifier(hostnameVerifier);
assertContent("this response comes via a secure proxy", connection);
RecordedRequest connect = server.takeRequest();
assertEquals("Connect line failure on proxy",
"CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
assertContains(connect.getHeaders(), "Host: android.com");
RecordedRequest get = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
assertContains(get.getHeaders(), "Host: android.com");
assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
}
/**
* Test which headers are sent unencrypted to the HTTP proxy.
*/
public void testProxyConnectIncludesProxyHeadersOnly()
throws IOException, InterruptedException {
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
TestSSLContext testSSLContext = TestSSLContext.create();
server.useHttps(testSSLContext.serverContext.getSocketFactory(), true);
server.enqueue(new MockResponse().clearHeaders()); // for CONNECT
server.enqueue(new MockResponse().setBody("encrypted response from the origin server"));
server.play();
URL url = new URL("https://android.com/foo");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(
server.toProxyAddress());
connection.addRequestProperty("Private", "Secret");
connection.addRequestProperty("Proxy-Authorization", "bar");
connection.addRequestProperty("User-Agent", "baz");
connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
connection.setHostnameVerifier(hostnameVerifier);
assertContent("encrypted response from the origin server", connection);
RecordedRequest connect = server.takeRequest();
assertContainsNoneMatching(connect.getHeaders(), "Private.*");
assertContains(connect.getHeaders(), "Proxy-Authorization: bar");
assertContains(connect.getHeaders(), "User-Agent: baz");
assertContains(connect.getHeaders(), "Host: android.com");
assertContains(connect.getHeaders(), "Proxy-Connection: Keep-Alive");
RecordedRequest get = server.takeRequest();
assertContains(get.getHeaders(), "Private: Secret");
assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
}
public void testDisconnectedConnection() throws IOException {
server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
server.play();
HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
InputStream in = connection.getInputStream();
assertEquals('A', (char) in.read());
connection.disconnect();
try {
in.read();
fail("Expected a connection closed exception");
} catch (IOException expected) {
}
}
public void testResponseCachingAndInputStreamSkipWithFixedLength() throws IOException {
testResponseCaching(TransferKind.FIXED_LENGTH);
}
public void testResponseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException {
testResponseCaching(TransferKind.CHUNKED);
}
public void testResponseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException {
testResponseCaching(TransferKind.END_OF_STREAM);
}
/**
* HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption
* http://code.google.com/p/android/issues/detail?id=8175
*/
private void testResponseCaching(TransferKind transferKind) throws IOException {
MockResponse response = new MockResponse();
transferKind.setBody(response, "I love puppies but hate spiders", 1);
server.enqueue(response);
server.play();
DefaultResponseCache cache = new DefaultResponseCache();
ResponseCache.setDefault(cache);
// Make sure that calling skip() doesn't omit bytes from the cache.
URLConnection urlConnection = server.getUrl("/").openConnection();
InputStream in = urlConnection.getInputStream();
assertEquals("I love ", readAscii(in, "I love ".length()));
reliableSkip(in, "puppies but hate ".length());
assertEquals("spiders", readAscii(in, "spiders".length()));
assertEquals(-1, in.read());
in.close();
assertEquals(1, cache.getSuccessCount());
assertEquals(0, cache.getAbortCount());
urlConnection = server.getUrl("/").openConnection(); // this response is cached!
in = urlConnection.getInputStream();
assertEquals("I love puppies but hate spiders",
readAscii(in, "I love puppies but hate spiders".length()));
assertEquals(-1, in.read());
assertEquals(1, cache.getMissCount());
assertEquals(1, cache.getHitCount());
assertEquals(1, cache.getSuccessCount());
assertEquals(0, cache.getAbortCount());
}
public void testResponseCacheRequestHeaders() throws IOException, URISyntaxException {
server.enqueue(new MockResponse().setBody("ABC"));
server.play();
final AtomicReference<Map<String, List<String>>> requestHeadersRef
= new AtomicReference<Map<String, List<String>>>();
ResponseCache.setDefault(new ResponseCache() {
@Override public CacheResponse get(URI uri, String requestMethod,
Map<String, List<String>> requestHeaders) throws IOException {
requestHeadersRef.set(requestHeaders);
return null;
}
@Override public CacheRequest put(URI uri, URLConnection conn) throws IOException {
return null;
}
});
URL url = server.getUrl("/");
URLConnection urlConnection = url.openConnection();
urlConnection.addRequestProperty("A", "android");
readAscii(urlConnection.getInputStream(), Integer.MAX_VALUE);
assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A"));
}
private void reliableSkip(InputStream in, int length) throws IOException {
while (length > 0) {
length -= in.skip(length);
}
}
/**
* Reads {@code count} characters from the stream. If the stream is
* exhausted before {@code count} characters can be read, the remaining
* characters are returned and the stream is closed.
*/
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();
}
public void testServerDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
}
public void testServerDisconnectsPrematurelyWithChunkedEncoding() throws IOException {
testServerPrematureDisconnect(TransferKind.CHUNKED);
}
public void testServerDisconnectsPrematurelyWithNoLengthHeaders() throws IOException {
/*
* Intentionally empty. This case doesn't make sense because there's no
* such thing as a premature disconnect when the disconnect itself
* indicates the end of the data stream.
*/
}
private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException {
MockResponse response = new MockResponse();
transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16);
server.enqueue(truncateViolently(response, 16));
server.enqueue(new MockResponse().setBody("Request #2"));
server.play();
DefaultResponseCache cache = new DefaultResponseCache();
ResponseCache.setDefault(cache);
BufferedReader reader = new BufferedReader(new InputStreamReader(
server.getUrl("/").openConnection().getInputStream()));
assertEquals("ABCDE", reader.readLine());
try {
reader.readLine();
fail("This implementation silently ignored a truncated HTTP body.");
} catch (IOException expected) {
}
assertEquals(1, cache.getAbortCount());
assertEquals(0, cache.getSuccessCount());
assertContent("Request #2", server.getUrl("/").openConnection());
assertEquals(1, cache.getAbortCount());
assertEquals(1, cache.getSuccessCount());
}
public void testClientPrematureDisconnectWithContentLengthHeader() throws IOException {
testClientPrematureDisconnect(TransferKind.FIXED_LENGTH);
}
public void testClientPrematureDisconnectWithChunkedEncoding() throws IOException {
testClientPrematureDisconnect(TransferKind.CHUNKED);
}
public void testClientPrematureDisconnectWithNoLengthHeaders() throws IOException {
testClientPrematureDisconnect(TransferKind.END_OF_STREAM);
}
private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException {
MockResponse response = new MockResponse();
transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
server.enqueue(response);
server.enqueue(new MockResponse().setBody("Request #2"));
server.play();
DefaultResponseCache cache = new DefaultResponseCache();
ResponseCache.setDefault(cache);
InputStream in = server.getUrl("/").openConnection().getInputStream();
assertEquals("ABCDE", readAscii(in, 5));
in.close();
try {
in.read();
fail("Expected an IOException because the stream is closed.");
} catch (IOException expected) {
}
assertEquals(1, cache.getAbortCount());
assertEquals(0, cache.getSuccessCount());
assertContent("Request #2", server.getUrl("/").openConnection());
assertEquals(1, cache.getAbortCount());
assertEquals(1, cache.getSuccessCount());
}
/**
* Shortens the body of {@code response} but not the corresponding headers.
* Only useful to test how clients respond to the premature conclusion of
* the HTTP body.
*/
private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) {
response.setDisconnectAtEnd(true);
List<String> headers = new ArrayList<String>(response.getHeaders());
response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep));
response.getHeaders().clear();
response.getHeaders().addAll(headers);
return response;
}
public void testMarkAndResetWithContentLengthHeader() throws IOException {
testMarkAndReset(TransferKind.FIXED_LENGTH);
}
public void testMarkAndResetWithChunkedEncoding() throws IOException {
testMarkAndReset(TransferKind.CHUNKED);
}
public void testMarkAndResetWithNoLengthHeaders() throws IOException {
testMarkAndReset(TransferKind.END_OF_STREAM);
}
public void testMarkAndReset(TransferKind transferKind) throws IOException {
MockResponse response = new MockResponse();
transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024);
server.enqueue(response);
server.play();
DefaultResponseCache cache = new DefaultResponseCache();
ResponseCache.setDefault(cache);
InputStream in = server.getUrl("/").openConnection().getInputStream();
assertFalse("This implementation claims to support mark().", in.markSupported());
in.mark(5);
assertEquals("ABCDE", readAscii(in, 5));
try {
in.reset();
fail();
} catch (IOException expected) {
}
assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE));
assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", server.getUrl("/").openConnection());
assertEquals(1, cache.getSuccessCount());
assertEquals(1, cache.getHitCount());
}
/**
* We've had a bug where we forget the HTTP response when we see response
* code 401. This causes a new HTTP request to be issued for every call into
* the URLConnection.
*/
public void testUnauthorizedResponseHandling() throws IOException {
MockResponse response = new MockResponse()
.addHeader("WWW-Authenticate: challenge")
.setResponseCode(401) // UNAUTHORIZED
.setBody("Unauthorized");
server.enqueue(response);
server.enqueue(response);
server.enqueue(response);
server.play();
URL url = server.getUrl("/");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
assertEquals(401, conn.getResponseCode());
assertEquals(401, conn.getResponseCode());
assertEquals(401, conn.getResponseCode());
assertEquals(1, server.getRequestCount());
}
public void testNonHexChunkSize() throws IOException {
server.enqueue(new MockResponse()
.setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n")
.clearHeaders()
.addHeader("Transfer-encoding: chunked"));
server.play();
URLConnection connection = server.getUrl("/").openConnection();
try {
readAscii(connection.getInputStream(), Integer.MAX_VALUE);
fail();
} catch (IOException e) {
}
}
public void testMissingChunkBody() throws IOException {
server.enqueue(new MockResponse()
.setBody("5")
.clearHeaders()
.addHeader("Transfer-encoding: chunked")
.setDisconnectAtEnd(true));
server.play();
URLConnection connection = server.getUrl("/").openConnection();
try {
readAscii(connection.getInputStream(), Integer.MAX_VALUE);
fail();
} catch (IOException e) {
}
}
/**
* This test checks whether connections are gzipped by default. This
* behavior in not required by the API, so a failure of this test does not
* imply a bug in the implementation.
*/
public void testGzipEncodingEnabledByDefault() throws IOException, InterruptedException {
server.enqueue(new MockResponse()
.setBody(gzip("ABCABCABC".getBytes("UTF-8")))
.addHeader("Content-Encoding: gzip"));
server.play();
URLConnection connection = server.getUrl("/").openConnection();
assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
assertNull(connection.getContentEncoding());
RecordedRequest request = server.takeRequest();
assertContains(request.getHeaders(), "Accept-Encoding: gzip");
}
public void testClientConfiguredGzipContentEncoding() throws Exception {
server.enqueue(new MockResponse()
.setBody(gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8")))
.addHeader("Content-Encoding: gzip"));
server.play();
URLConnection connection = server.getUrl("/").openConnection();
connection.addRequestProperty("Accept-Encoding", "gzip");
InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE));
RecordedRequest request = server.takeRequest();
assertContains(request.getHeaders(), "Accept-Encoding: gzip");
}
public void testGzipAndConnectionReuseWithFixedLength() throws Exception {
testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH);
}
public void testGzipAndConnectionReuseWithChunkedEncoding() throws Exception {
testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED);
}
public void testClientConfiguredCustomContentEncoding() throws Exception {
server.enqueue(new MockResponse()
.setBody("ABCDE")
.addHeader("Content-Encoding: custom"));
server.play();
URLConnection connection = server.getUrl("/").openConnection();
connection.addRequestProperty("Accept-Encoding", "custom");
assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
RecordedRequest request = server.takeRequest();
assertContains(request.getHeaders(), "Accept-Encoding: custom");
}
/**
* Test a bug where gzip input streams weren't exhausting the input stream,
* which corrupted the request that followed.
* http://code.google.com/p/android/issues/detail?id=7059
*/
private void testClientConfiguredGzipContentEncodingAndConnectionReuse(
TransferKind transferKind) throws Exception {
MockResponse responseOne = new MockResponse();
responseOne.addHeader("Content-Encoding: gzip");
transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5);
server.enqueue(responseOne);
MockResponse responseTwo = new MockResponse();
transferKind.setBody(responseTwo, "two (identity)", 5);
server.enqueue(responseTwo);
server.play();
URLConnection connection = server.getUrl("/").openConnection();
connection.addRequestProperty("Accept-Encoding", "gzip");
InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE));
assertEquals(0, server.takeRequest().getSequenceNumber());
connection = server.getUrl("/").openConnection();
assertEquals("two (identity)", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
assertEquals(1, server.takeRequest().getSequenceNumber());
}
/**
* Obnoxiously test that the chunk sizes transmitted exactly equal the
* requested data+chunk header size. Although setChunkedStreamingMode()
* isn't specific about whether the size applies to the data or the
* complete chunk, the RI interprets it as a complete chunk.
*/
public void testSetChunkedStreamingMode() throws IOException, InterruptedException {
server.enqueue(new MockResponse());
server.play();
HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
urlConnection.setChunkedStreamingMode(8);
urlConnection.setDoOutput(true);
OutputStream outputStream = urlConnection.getOutputStream();
outputStream.write("ABCDEFGHIJKLMNOPQ".getBytes("US-ASCII"));
assertEquals(200, urlConnection.getResponseCode());
RecordedRequest request = server.takeRequest();
assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII"));
assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes());
}
public void testAuthenticateWithFixedLengthStreaming() throws Exception {
testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH);
}
public void testAuthenticateWithChunkedStreaming() throws Exception {
testAuthenticateWithStreamingPost(StreamingMode.CHUNKED);
}
private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception {
MockResponse pleaseAuthenticate = new MockResponse()
.setResponseCode(401)
.addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
.setBody("Please authenticate.");
server.enqueue(pleaseAuthenticate);
server.play();
Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
connection.setDoOutput(true);
byte[] requestBody = { 'A', 'B', 'C', 'D' };
if (streamingMode == StreamingMode.FIXED_LENGTH) {
connection.setFixedLengthStreamingMode(requestBody.length);
} else if (streamingMode == StreamingMode.CHUNKED) {
connection.setChunkedStreamingMode(0);
}
OutputStream outputStream = connection.getOutputStream();
outputStream.write(requestBody);
outputStream.close();
try {
connection.getInputStream();
fail();
} catch (HttpRetryException expected) {
}
// no authorization header for the request...
RecordedRequest request = server.takeRequest();
assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
}
enum StreamingMode {
FIXED_LENGTH, CHUNKED
}
public void testAuthenticateWithPost() throws Exception {
MockResponse pleaseAuthenticate = new MockResponse()
.setResponseCode(401)
.addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
.setBody("Please authenticate.");
// fail auth three times...
server.enqueue(pleaseAuthenticate);
server.enqueue(pleaseAuthenticate);
server.enqueue(pleaseAuthenticate);
// ...then succeed the fourth time
server.enqueue(new MockResponse().setBody("Successful auth!"));
server.play();
Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
connection.setDoOutput(true);
byte[] requestBody = { 'A', 'B', 'C', 'D' };
OutputStream outputStream = connection.getOutputStream();
outputStream.write(requestBody);
outputStream.close();
assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
// no authorization header for the first request...
RecordedRequest request = server.takeRequest();
assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
// ...but the three requests that follow include an authorization header
for (int i = 0; i < 3; i++) {
request = server.takeRequest();
assertEquals("POST / HTTP/1.1", request.getRequestLine());
assertContains(request.getHeaders(), "Authorization: Basic "
+ "dXNlcm5hbWU6cGFzc3dvcmQ="); // "dXNl..." == base64("username:password")
assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
}
}
public void testAuthenticateWithGet() throws Exception {
MockResponse pleaseAuthenticate = new MockResponse()
.setResponseCode(401)
.addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
.setBody("Please authenticate.");
// fail auth three times...
server.enqueue(pleaseAuthenticate);
server.enqueue(pleaseAuthenticate);
server.enqueue(pleaseAuthenticate);
// ...then succeed the fourth time
server.enqueue(new MockResponse().setBody("Successful auth!"));
server.play();
Authenticator.setDefault(SIMPLE_AUTHENTICATOR);
HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
// no authorization header for the first request...
RecordedRequest request = server.takeRequest();
assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
// ...but the three requests that follow requests include an authorization header
for (int i = 0; i < 3; i++) {
request = server.takeRequest();
assertEquals("GET / HTTP/1.1", request.getRequestLine());
assertContains(request.getHeaders(), "Authorization: Basic "
+ "dXNlcm5hbWU6cGFzc3dvcmQ="); // "dXNl..." == base64("username:password")
}
}
public void testRedirectedWithChunkedEncoding() throws Exception {
testRedirected(TransferKind.CHUNKED, true);
}
public void testRedirectedWithContentLengthHeader() throws Exception {
testRedirected(TransferKind.FIXED_LENGTH, true);
}
public void testRedirectedWithNoLengthHeaders() throws Exception {
testRedirected(TransferKind.END_OF_STREAM, false);
}
private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception {
MockResponse response = new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: /foo");
transferKind.setBody(response, "This page has moved!", 10);
server.enqueue(response);
server.enqueue(new MockResponse().setBody("This is the new location!"));
server.play();
URLConnection connection = server.getUrl("/").openConnection();
assertEquals("This is the new location!",
readAscii(connection.getInputStream(), Integer.MAX_VALUE));
RecordedRequest first = server.takeRequest();
assertEquals("GET / HTTP/1.1", first.getRequestLine());
RecordedRequest retry = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
if (reuse) {
assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
}
}
public void testRedirectedOnHttps() throws IOException, InterruptedException {
TestSSLContext testSSLContext = TestSSLContext.create();
server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: /foo")
.setBody("This page has moved!"));
server.enqueue(new MockResponse().setBody("This is the new location!"));
server.play();
HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
assertEquals("This is the new location!",
readAscii(connection.getInputStream(), Integer.MAX_VALUE));
RecordedRequest first = server.takeRequest();
assertEquals("GET / HTTP/1.1", first.getRequestLine());
RecordedRequest retry = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", retry.getRequestLine());
assertEquals("Expected connection reuse", 1, retry.getSequenceNumber());
}
public void testNotRedirectedFromHttpsToHttp() throws IOException, InterruptedException {
TestSSLContext testSSLContext = TestSSLContext.create();
server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: http://anyhost/foo")
.setBody("This page has moved!"));
server.play();
HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection();
connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory());
assertEquals("This page has moved!",
readAscii(connection.getInputStream(), Integer.MAX_VALUE));
}
public void testNotRedirectedFromHttpToHttps() throws IOException, InterruptedException {
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: https://anyhost/foo")
.setBody("This page has moved!"));
server.play();
HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
assertEquals("This page has moved!",
readAscii(connection.getInputStream(), Integer.MAX_VALUE));
}
public void testRedirectToAnotherOriginServer() throws Exception {
MockWebServer server2 = new MockWebServer();
server2.enqueue(new MockResponse().setBody("This is the 2nd server!"));
server2.play();
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: " + server2.getUrl("/").toString())
.setBody("This page has moved!"));
server.enqueue(new MockResponse().setBody("This is the first server again!"));
server.play();
URLConnection connection = server.getUrl("/").openConnection();
assertEquals("This is the 2nd server!",
readAscii(connection.getInputStream(), Integer.MAX_VALUE));
assertEquals(server2.getUrl("/"), connection.getURL());
// make sure the first server was careful to recycle the connection
assertEquals("This is the first server again!",
readAscii(server.getUrl("/").openStream(), Integer.MAX_VALUE));
RecordedRequest first = server.takeRequest();
assertContains(first.getHeaders(), "Host: " + hostname + ":" + server.getPort());
RecordedRequest second = server2.takeRequest();
assertContains(second.getHeaders(), "Host: " + hostname + ":" + server2.getPort());
RecordedRequest third = server.takeRequest();
assertEquals("Expected connection reuse", 1, third.getSequenceNumber());
server2.shutdown();
}
public void testHttpsWithCustomTrustManager() throws Exception {
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
RecordingTrustManager trustManager = new RecordingTrustManager();
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
SSLSocketFactory defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
try {
TestSSLContext testSSLContext = TestSSLContext.create();
server.useHttps(testSSLContext.serverContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("ABC"));
server.enqueue(new MockResponse().setBody("DEF"));
server.enqueue(new MockResponse().setBody("GHI"));
server.play();
URL url = server.getUrl("/");
assertEquals("ABC", readAscii(url.openStream(), Integer.MAX_VALUE));
assertEquals("DEF", readAscii(url.openStream(), Integer.MAX_VALUE));
assertEquals("GHI", readAscii(url.openStream(), Integer.MAX_VALUE));
assertEquals(Arrays.asList("verify " + hostname), hostnameVerifier.calls);
assertEquals(Arrays.asList("checkServerTrusted ["
+ "CN=" + hostname + " 1, "
+ "CN=Test Intermediate Certificate Authority 1, "
+ "CN=Test Root Certificate Authority 1"
+ "] RSA"),
trustManager.calls);
} finally {
HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier);
HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
}
}
public void testConnectTimeouts() throws IOException {
// Set a backlog and use it up so that we can expect the
// URLConnection to properly timeout. According to Steven's
// 4.5 "listen function", linux adds 3 to the specified
// backlog, so we need to connect 4 times before it will hang.
ServerSocket serverSocket = new ServerSocket(0, 1);
int serverPort = serverSocket.getLocalPort();
Socket[] sockets = new Socket[4];
for (int i = 0; i < sockets.length; i++) {
sockets[i] = new Socket("localhost", serverPort);
}
URLConnection urlConnection = new URL("http://localhost:" + serverPort).openConnection();
urlConnection.setConnectTimeout(1000);
try {
urlConnection.getInputStream();
fail();
} catch (SocketTimeoutException expected) {
}
for (Socket s : sockets) {
s.close();
}
}
public void testReadTimeouts() throws IOException {
/*
* This relies on the fact that MockWebServer doesn't close the
* connection after a response has been sent. This causes the client to
* try to read more bytes than are sent, which results in a timeout.
*/
MockResponse timeout = new MockResponse()
.setBody("ABC")
.clearHeaders()
.addHeader("Content-Length: 4");
server.enqueue(timeout);
server.play();
URLConnection urlConnection = server.getUrl("/").openConnection();
urlConnection.setReadTimeout(1000);
InputStream in = urlConnection.getInputStream();
assertEquals('A', in.read());
assertEquals('B', in.read());
assertEquals('C', in.read());
try {
in.read(); // if Content-Length was accurate, this would return -1 immediately
fail();
} catch (SocketTimeoutException expected) {
}
}
public void testSetChunkedEncodingAsRequestProperty() throws IOException, InterruptedException {
server.enqueue(new MockResponse());
server.play();
HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection();
urlConnection.setRequestProperty("Transfer-encoding", "chunked");
urlConnection.setDoOutput(true);
urlConnection.getOutputStream().write("ABC".getBytes("UTF-8"));
assertEquals(200, urlConnection.getResponseCode());
RecordedRequest request = server.takeRequest();
assertEquals("ABC", new String(request.getBody(), "UTF-8"));
}
public void testConnectionCloseInRequest() throws IOException, InterruptedException {
server.enqueue(new MockResponse()); // server doesn't honor the connection: close header!
server.enqueue(new MockResponse());
server.play();
HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection();
a.setRequestProperty("Connection", "close");
assertEquals(200, a.getResponseCode());
HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection();
assertEquals(200, b.getResponseCode());
assertEquals(0, server.takeRequest().getSequenceNumber());
assertEquals("When connection: close is used, each request should get its own connection",
0, server.takeRequest().getSequenceNumber());
}
public void testConnectionCloseInResponse() throws IOException, InterruptedException {
server.enqueue(new MockResponse().addHeader("Connection: close"));
server.enqueue(new MockResponse());
server.play();
HttpURLConnection a = (HttpURLConnection) server.getUrl("/").openConnection();
assertEquals(200, a.getResponseCode());
HttpURLConnection b = (HttpURLConnection) server.getUrl("/").openConnection();
assertEquals(200, b.getResponseCode());
assertEquals(0, server.takeRequest().getSequenceNumber());
assertEquals("When connection: close is used, each request should get its own connection",
0, server.takeRequest().getSequenceNumber());
}
public void testConnectionCloseWithRedirect() throws IOException, InterruptedException {
MockResponse response = new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: /foo")
.addHeader("Connection: close");
server.enqueue(response);
server.enqueue(new MockResponse().setBody("This is the new location!"));
server.play();
URLConnection connection = server.getUrl("/").openConnection();
assertEquals("This is the new location!",
readAscii(connection.getInputStream(), Integer.MAX_VALUE));
assertEquals(0, server.takeRequest().getSequenceNumber());
assertEquals("When connection: close is used, each request should get its own connection",
0, server.takeRequest().getSequenceNumber());
}
public void testResponseCodeDisagreesWithHeaders() throws IOException, InterruptedException {
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NO_CONTENT)
.setBody("This body is not allowed!"));
server.play();
URLConnection connection = server.getUrl("/").openConnection();
assertEquals("This body is not allowed!",
readAscii(connection.getInputStream(), Integer.MAX_VALUE));
}
public void testSingleByteReadIsSigned() throws IOException {
server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 }));
server.play();
URLConnection connection = server.getUrl("/").openConnection();
InputStream in = connection.getInputStream();
assertEquals(254, in.read());
assertEquals(255, in.read());
assertEquals(-1, in.read());
}
public void testFlushAfterStreamTransmittedWithChunkedEncoding() throws IOException {
testFlushAfterStreamTransmitted(TransferKind.CHUNKED);
}
public void testFlushAfterStreamTransmittedWithFixedLength() throws IOException {
testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH);
}
public void testFlushAfterStreamTransmittedWithNoLengthHeaders() throws IOException {
testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM);
}
/**
* We explicitly permit apps to close the upload stream even after it has
* been transmitted. We also permit flush so that buffered streams can
* do a no-op flush when they are closed. http://b/3038470
*/
private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException {
server.enqueue(new MockResponse().setBody("abc"));
server.play();
HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
connection.setDoOutput(true);
byte[] upload = "def".getBytes("UTF-8");
if (transferKind == TransferKind.CHUNKED) {
connection.setChunkedStreamingMode(0);
} else if (transferKind == TransferKind.FIXED_LENGTH) {
connection.setFixedLengthStreamingMode(upload.length);
}
OutputStream out = connection.getOutputStream();
out.write(upload);
assertEquals("abc", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
out.flush(); // dubious but permitted
try {
out.write("ghi".getBytes("UTF-8"));
fail();
} catch (IOException expected) {
}
}
public void testGetHeadersThrows() throws IOException {
server.enqueue(new MockResponse().setDisconnectAtStart(true));
server.play();
HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
try {
connection.getInputStream();
fail();
} catch (IOException expected) {
}
try {
connection.getInputStream();
fail();
} catch (IOException expected) {
}
}
/**
* http://code.google.com/p/android/issues/detail?id=14562
*/
public void testReadAfterLastByte() throws Exception {
server.enqueue(new MockResponse()
.setBody("ABC")
.clearHeaders()
.addHeader("Connection: close")
.setDisconnectAtEnd(true));
server.play();
HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
InputStream in = connection.getInputStream();
assertEquals("ABC", readAscii(in, 3));
assertEquals(-1, in.read());
assertEquals(-1, in.read()); // throws IOException in Gingerbread
}
/**
* Encodes the response body using GZIP and adds the corresponding header.
*/
public byte[] gzip(byte[] bytes) throws IOException {
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
gzippedOut.write(bytes);
gzippedOut.close();
return bytesOut.toByteArray();
}
/**
* Reads at most {@code limit} characters from {@code in} and asserts that
* content equals {@code expected}.
*/
private void assertContent(String expected, URLConnection connection, int limit)
throws IOException {
connection.connect();
assertEquals(expected, readAscii(connection.getInputStream(), limit));
((HttpURLConnection) connection).disconnect();
}
private void assertContent(String expected, URLConnection connection) throws IOException {
assertContent(expected, connection, Integer.MAX_VALUE);
}
private void assertContains(List<String> headers, String header) {
assertTrue(headers.toString(), headers.contains(header));
}
private void assertContainsNoneMatching(List<String> headers, String pattern) {
for (String header : headers) {
if (header.matches(pattern)) {
fail("Header " + header + " matches " + pattern);
}
}
}
private Set<String> newSet(String... elements) {
return new HashSet<String>(Arrays.asList(elements));
}
enum TransferKind {
CHUNKED() {
@Override void setBody(MockResponse response, byte[] content, int chunkSize)
throws IOException {
response.setChunkedBody(content, chunkSize);
}
},
FIXED_LENGTH() {
@Override void setBody(MockResponse response, byte[] content, int chunkSize) {
response.setBody(content);
}
},
END_OF_STREAM() {
@Override void setBody(MockResponse response, byte[] content, int chunkSize) {
response.setBody(content);
response.setDisconnectAtEnd(true);
for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
if (h.next().startsWith("Content-Length:")) {
h.remove();
break;
}
}
}
};
abstract void setBody(MockResponse response, byte[] content, int chunkSize)
throws IOException;
void setBody(MockResponse response, String content, int chunkSize) throws IOException {
setBody(response, content.getBytes("UTF-8"), chunkSize);
}
}
enum ProxyConfig {
NO_PROXY() {
@Override public HttpURLConnection connect(MockWebServer server, URL url)
throws IOException {
return (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
}
},
CREATE_ARG() {
@Override public HttpURLConnection connect(MockWebServer server, URL url)
throws IOException {
return (HttpURLConnection) url.openConnection(server.toProxyAddress());
}
},
PROXY_SYSTEM_PROPERTY() {
@Override public HttpURLConnection connect(MockWebServer server, URL url)
throws IOException {
System.setProperty("proxyHost", "localhost");
System.setProperty("proxyPort", Integer.toString(server.getPort()));
return (HttpURLConnection) url.openConnection();
}
},
HTTP_PROXY_SYSTEM_PROPERTY() {
@Override public HttpURLConnection connect(MockWebServer server, URL url)
throws IOException {
System.setProperty("http.proxyHost", "localhost");
System.setProperty("http.proxyPort", Integer.toString(server.getPort()));
return (HttpURLConnection) url.openConnection();
}
},
HTTPS_PROXY_SYSTEM_PROPERTY() {
@Override public HttpURLConnection connect(MockWebServer server, URL url)
throws IOException {
System.setProperty("https.proxyHost", "localhost");
System.setProperty("https.proxyPort", Integer.toString(server.getPort()));
return (HttpURLConnection) url.openConnection();
}
};
public abstract HttpURLConnection connect(MockWebServer server, URL url) throws IOException;
}
private static class RecordingTrustManager implements X509TrustManager {
private final List<String> calls = new ArrayList<String>();
public X509Certificate[] getAcceptedIssuers() {
calls.add("getAcceptedIssuers");
return new X509Certificate[] {};
}
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
calls.add("checkClientTrusted " + certificatesToString(chain) + " " + authType);
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
calls.add("checkServerTrusted " + certificatesToString(chain) + " " + authType);
}
private String certificatesToString(X509Certificate[] certificates) {
List<String> result = new ArrayList<String>();
for (X509Certificate certificate : certificates) {
result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber());
}
return result.toString();
}
}
private static class RecordingHostnameVerifier implements HostnameVerifier {
private final List<String> calls = new ArrayList<String>();
public boolean verify(String hostname, SSLSession session) {
calls.add("verify " + hostname);
return true;
}
}
}