/* * 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 com.google.mockwebserver.Dispatcher; import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; import com.google.mockwebserver.RecordedRequest; import com.google.mockwebserver.SocketPolicy; import com.android.okhttp.AndroidShimResponseCache; import com.android.okhttp.internal.Platform; import com.android.okhttp.internal.tls.TrustRootIndex; import junit.framework.TestCase; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Authenticator; import java.net.CacheRequest; import java.net.CacheResponse; import java.net.CookieHandler; import java.net.CookieManager; import java.net.HttpRetryException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.PasswordAuthentication; import java.net.ProtocolException; import java.net.Proxy; import java.net.ProxySelector; import java.net.ResponseCache; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; import java.nio.channels.SocketChannel; 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.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import javax.net.SocketFactory; import javax.net.ssl.HandshakeCompletedListener; 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.SSLParameters; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; 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.net.DelegatingSocketFactory; import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START; import static com.google.mockwebserver.SocketPolicy.FAIL_HANDSHAKE; import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END; import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; public final class URLConnectionTest extends TestCase { private MockWebServer server; private AndroidShimResponseCache cache; private String hostName; private List testSSLContextsToClose; @Override protected void setUp() throws Exception { super.setUp(); server = new MockWebServer(); hostName = server.getHostName(); testSSLContextsToClose = new ArrayList<>(); } @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(); server = null; if (cache != null) { cache.delete(); cache = null; } for (TestSSLContext testSSLContext : testSSLContextsToClose) { testSSLContext.close(); } super.tearDown(); } public void testRequestHeaderValidation() throws Exception { // Android became more strict after M about which characters were allowed in request header // names and values: previously almost anything was allowed if it didn't contain \0. assertForbiddenRequestHeaderName(null); assertForbiddenRequestHeaderName(""); assertForbiddenRequestHeaderName("\n"); assertForbiddenRequestHeaderName("a\nb"); assertForbiddenRequestHeaderName("\u0000"); assertForbiddenRequestHeaderName("\r"); assertForbiddenRequestHeaderName("\t"); assertForbiddenRequestHeaderName("\u001f"); assertForbiddenRequestHeaderName("\u007f"); assertForbiddenRequestHeaderName("\u0080"); assertForbiddenRequestHeaderName("\ud83c\udf69"); assertEquals(null, setAndReturnRequestHeaderValue(null)); assertEquals("", setAndReturnRequestHeaderValue("")); assertForbiddenRequestHeaderValue("\u0000"); // Workaround for http://b/26422335 , http://b/26889631 , http://b/27606665 : // allow (but strip) trailing \n, \r and \r\n // assertForbiddenRequestHeaderValue("\r"); // End of workaround // '\t' in header values can either be (a) forbidden or (b) allowed. // The original version of Android N (API 23) implemented behavior // (a), but OEMs can backport a fix that changes the behavior to (b). // Therefore, this test has been relaxed for Android N CTS to allow // either behavior. It is planned that future versions of Android only // allow behavior (b). try { // throws IAE in case (a), passes in case (b) assertEquals("a valid\tvalue", setAndReturnRequestHeaderValue("a valid\tvalue")); } catch (IllegalArgumentException tolerated) { // verify case (a) assertForbiddenRequestHeaderValue("\t"); } assertForbiddenRequestHeaderValue("\u001f"); assertForbiddenRequestHeaderValue("\u007f"); // For http://b/28867041 the allowed character range was changed. // assertForbiddenRequestHeaderValue("\u0080"); // assertForbiddenRequestHeaderValue("\ud83c\udf69"); assertEquals("\u0080", setAndReturnRequestHeaderValue("\u0080")); assertEquals("\ud83c\udf69", setAndReturnRequestHeaderValue("\ud83c\udf69")); // Workaround for http://b/26422335 , http://b/26889631 , http://b/27606665 : // allow (but strip) trailing \n, \r and \r\n assertEquals("", setAndReturnRequestHeaderValue("\n")); assertEquals("a", setAndReturnRequestHeaderValue("a\n")); assertEquals("", setAndReturnRequestHeaderValue("\r")); assertEquals("a", setAndReturnRequestHeaderValue("a\r")); assertEquals("", setAndReturnRequestHeaderValue("\r\n")); assertEquals("a", setAndReturnRequestHeaderValue("a\r\n")); assertForbiddenRequestHeaderValue("a\nb"); assertForbiddenRequestHeaderValue("\nb"); assertForbiddenRequestHeaderValue("a\rb"); assertForbiddenRequestHeaderValue("\rb"); assertForbiddenRequestHeaderValue("a\r\nb"); assertForbiddenRequestHeaderValue("\r\nb"); // End of workaround } private static void assertForbiddenRequestHeaderName(String name) throws Exception { URL url = new URL("http://www.google.com/"); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); try { urlConnection.addRequestProperty(name, "value"); fail("Expected exception"); } catch (IllegalArgumentException expected) { } catch (NullPointerException expectedIfNull) { assertTrue(name == null); } } private static void assertForbiddenRequestHeaderValue(String value) throws Exception { URL url = new URL("http://www.google.com/"); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); try { urlConnection.addRequestProperty("key", value); fail("Expected exception"); } catch (IllegalArgumentException expected) { } } private static String setAndReturnRequestHeaderValue(String value) throws Exception { URL url = new URL("http://www.google.com/"); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.addRequestProperty("key", value); return urlConnection.getRequestProperty("key"); } 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"); assertEquals("f", urlConnection.getRequestProperty("D")); assertEquals("f", urlConnection.getRequestProperty("d")); Map> requestHeaders = urlConnection.getRequestProperties(); assertEquals(newSet("e", "f"), new HashSet(requestHeaders.get("D"))); assertEquals(newSet("e", "f"), new HashSet(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! assertNull(urlConnection.getRequestProperty("NullValue")); urlConnection.addRequestProperty("AnotherNullValue", null); // should fail silently! assertNull(urlConnection.getRequestProperty("AnotherNullValue")); 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) { } try { urlConnection.getRequestProperties(); fail(); } catch (IllegalStateException expected) { } } public void testGetRequestPropertyReturnsLastValue() throws Exception { server.play(); HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); urlConnection.addRequestProperty("A", "value1"); urlConnection.addRequestProperty("A", "value2"); assertEquals("value2", urlConnection.getRequestProperty("A")); } public void testResponseHeaders() throws IOException, InterruptedException { server.enqueue(new MockResponse() .setStatus("HTTP/1.0 200 Fantastic") .addHeader("A: c") .addHeader("B: d") .addHeader("A: e") .setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8)); server.play(); HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals(200, urlConnection.getResponseCode()); assertEquals("Fantastic", urlConnection.getResponseMessage()); assertEquals("HTTP/1.0 200 Fantastic", urlConnection.getHeaderField(null)); Map> responseHeaders = urlConnection.getHeaderFields(); assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null)); assertEquals(newSet("c", "e"), new HashSet(responseHeaders.get("A"))); assertEquals(newSet("c", "e"), new HashSet(responseHeaders.get("a"))); try { responseHeaders.put("N", Arrays.asList("o")); fail("Modified an unmodifiable view."); } catch (UnsupportedOperationException expected) { } try { responseHeaders.get("A").add("f"); fail("Modified an unmodifiable view."); } catch (UnsupportedOperationException expected) { } assertEquals("A", urlConnection.getHeaderFieldKey(0)); assertEquals("c", urlConnection.getHeaderField(0)); assertEquals("B", urlConnection.getHeaderFieldKey(1)); assertEquals("d", urlConnection.getHeaderField(1)); assertEquals("A", urlConnection.getHeaderFieldKey(2)); assertEquals("e", urlConnection.getHeaderField(2)); } public void testGetErrorStreamOnSuccessfulRequest() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertNull(connection.getErrorStream()); } public void testGetErrorStreamOnUnsuccessfulRequest() throws Exception { server.enqueue(new MockResponse().setResponseCode(404).setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection.getErrorStream(), Integer.MAX_VALUE)); } // 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()); } /** * Test that connections are added to the pool as soon as the response has * been consumed. */ public void testConnectionsArePooledWithoutExplicitDisconnect() throws Exception { server.enqueue(new MockResponse().setBody("ABC")); server.enqueue(new MockResponse().setBody("DEF")); server.play(); URLConnection connection1 = server.getUrl("/").openConnection(); assertEquals("ABC", readAscii(connection1.getInputStream(), Integer.MAX_VALUE)); assertEquals(0, server.takeRequest().getSequenceNumber()); URLConnection connection2 = server.getUrl("/").openConnection(); assertEquals("DEF", readAscii(connection2.getInputStream(), Integer.MAX_VALUE)); assertEquals(1, server.takeRequest().getSequenceNumber()); } public void testServerClosesSocket() throws Exception { testServerClosesSocket(DISCONNECT_AT_END); } public void testServerShutdownInput() throws Exception { testServerClosesSocket(SHUTDOWN_INPUT_AT_END); } private void testServerClosesSocket(SocketPolicy socketPolicy) throws Exception { server.enqueue(new MockResponse() .setBody("This connection won't pool properly") .setSocketPolicy(socketPolicy)); server.enqueue(new MockResponse().setBody("This comes after a busted connection")); server.play(); assertContent("This connection won't pool properly", server.getUrl("/a").openConnection()); assertEquals(0, server.takeRequest().getSequenceNumber()); assertContent("This comes after a busted connection", server.getUrl("/b").openConnection()); // sequence number 0 means the HTTP socket connection was not reused assertEquals(0, server.takeRequest().getSequenceNumber()); } public void testServerShutdownOutput() throws Exception { // This test causes MockWebServer to log a "connection failed" stack trace // Setting the server workerThreads to 1 ensures the responses are generated in the order // the requests are accepted by the server. Without this the second and third requests made // by the client (the request for "/b" and the retry for "/b" when the bad socket is // detected) can be handled by the server out of order leading to test failure. server.setWorkerThreads(1); server.enqueue(new MockResponse() .setBody("Output shutdown after this response") .setSocketPolicy(SHUTDOWN_OUTPUT_AT_END)); server.enqueue(new MockResponse().setBody("This response will fail to write")); server.enqueue(new MockResponse().setBody("This comes after a busted connection")); server.play(); assertContent("Output shutdown after this response", server.getUrl("/a").openConnection()); assertEquals(0, server.takeRequest().getSequenceNumber()); assertContent("This comes after a busted connection", server.getUrl("/b").openConnection()); assertEquals(1, server.takeRequest().getSequenceNumber()); assertEquals(0, 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()); } } public void testGetResponseCodeNoResponseBody() throws Exception { server.enqueue(new MockResponse() .addHeader("abc: def")); server.play(); URL url = server.getUrl("/"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setDoInput(false); assertEquals("def", conn.getHeaderField("abc")); assertEquals(200, conn.getResponseCode()); try { conn.getInputStream(); fail(); } catch (ProtocolException expected) { } } public void testConnectViaHttps() throws IOException, InterruptedException { TestSSLContext testSSLContext = createDefaultTestSSLContext(); 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()); assertEquals("TLSv1.2", request.getSslProtocol()); } public void testConnectViaHttpsReusingConnections() throws IOException, InterruptedException { TestSSLContext testSSLContext = createDefaultTestSSLContext(); SSLSocketFactory clientSocketFactory = testSSLContext.clientContext.getSocketFactory(); 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(clientSocketFactory); assertContent("this response comes via HTTPS", connection); connection = (HttpsURLConnection) server.getUrl("/").openConnection(); connection.setSSLSocketFactory(clientSocketFactory); assertContent("another response via HTTPS", connection); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } public void testConnectViaHttpsReusingConnectionsDifferentFactories() throws IOException, InterruptedException { TestSSLContext testSSLContext = createDefaultTestSSLContext(); 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) { } } /** * 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()); testSSLContextsToClose.add(testSSLContext); 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 testConnectViaProxy_emptyPath() throws Exception { // expected normalization http://android -> http://android/ per b/30107354 checkConnectViaProxy( ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY, "http://android.com", "http://android.com/", "android.com"); } public void testConnectViaProxy_complexUrlWithNoPath() throws Exception { checkConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY, "http://android.com:8080?height=100&width=42", "http://android.com:8080/?height=100&width=42", "android.com:8080"); } public void testConnectViaProxyUsingProxyArg() throws Exception { checkConnectViaProxy(ProxyConfig.CREATE_ARG); } public void testConnectViaProxyUsingProxySystemProperty() throws Exception { checkConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY); } public void testConnectViaProxyUsingHttpProxySystemProperty() throws Exception { checkConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY); } private void checkConnectViaProxy(ProxyConfig proxyConfig) throws Exception { checkConnectViaProxy(proxyConfig, "http://android.com/foo", "http://android.com/foo", "android.com"); } private void checkConnectViaProxy(ProxyConfig proxyConfig, String urlString, String expectedUrlInRequestLine, String expectedHost) throws Exception { MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy"); server.enqueue(mockResponse); server.play(); URL url = new URL(urlString); HttpURLConnection connection = proxyConfig.connect(server, url); assertContent("this response comes via a proxy", connection); RecordedRequest request = server.takeRequest(); assertEquals("GET " + expectedUrlInRequestLine + " HTTP/1.1", request.getRequestLine()); assertContains(request.getHeaders(), "Host: " + expectedHost); } 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 = createDefaultTestSSLContext(); 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); } /** * We weren't honoring all of the appropriate proxy system properties when * connecting via HTTPS. http://b/3097518 */ public void testConnectViaHttpProxyToHttpsUsingProxySystemProperty() throws Exception { testConnectViaHttpProxyToHttps(ProxyConfig.PROXY_SYSTEM_PROPERTY); } 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 = createDefaultTestSSLContext(); RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), true); server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) .clearHeaders()); 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:443"); 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); } /** * Tolerate bad https proxy response when using HttpResponseCache. http://b/6754912 */ public void testConnectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache() throws Exception { TestSSLContext testSSLContext = createDefaultTestSSLContext(); initResponseCache(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), true); // The inclusion of a body in the response to the CONNECT is key to reproducing b/6754912. MockResponse badProxyResponse = new MockResponse() .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) .clearHeaders() .setBody("bogus proxy connect response content"); server.enqueue(badProxyResponse); server.enqueue(new MockResponse().setBody("response")); server.play(); URL url = new URL("https://android.com/foo"); ProxyConfig proxyConfig = ProxyConfig.PROXY_SYSTEM_PROPERTY; HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, url); connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); connection.setHostnameVerifier(new RecordingHostnameVerifier()); assertContent("response", connection); RecordedRequest connect = server.takeRequest(); assertEquals("CONNECT android.com:443 HTTP/1.1", connect.getRequestLine()); assertContains(connect.getHeaders(), "Host: android.com:443"); } private void initResponseCache() throws IOException { String tmp = System.getProperty("java.io.tmpdir"); File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID()); cache = AndroidShimResponseCache.create(cacheDir, Integer.MAX_VALUE); ResponseCache.setDefault(cache); } /** * Test Etag headers are returned correctly when a client-side cache is not installed. * https://code.google.com/p/android/issues/detail?id=108949 */ public void testEtagHeaders_uncached() throws Exception { final String etagValue1 = "686897696a7c876b7e"; final String body1 = "Response with etag 1"; final String etagValue2 = "686897696a7c876b7f"; final String body2 = "Response with etag 2"; server.enqueue( new MockResponse() .setBody(body1) .setHeader("Content-Type", "text/plain") .setHeader("Etag", etagValue1)); server.enqueue( new MockResponse() .setBody(body2) .setHeader("Content-Type", "text/plain") .setHeader("Etag", etagValue2)); server.play(); URL url = server.getUrl("/"); HttpURLConnection connection1 = (HttpURLConnection) url.openConnection(); assertEquals(etagValue1, connection1.getHeaderField("Etag")); assertContent(body1, connection1); connection1.disconnect(); // Discard the server-side record of the request made. server.takeRequest(); HttpURLConnection connection2 = (HttpURLConnection) url.openConnection(); assertEquals(etagValue2, connection2.getHeaderField("Etag")); assertContent(body2, connection2); connection2.disconnect(); // Check the client did not cache. RecordedRequest request = server.takeRequest(); assertNull(request.getHeader("If-None-Match")); } /** * Test Etag headers are returned correctly when a client-side cache is installed and the server * data is unchanged. * https://code.google.com/p/android/issues/detail?id=108949 */ public void testEtagHeaders_cachedWithServerHit() throws Exception { final String etagValue = "686897696a7c876b7e"; final String body = "Response with etag"; server.enqueue( new MockResponse() .setBody(body) .setResponseCode(HttpURLConnection.HTTP_OK) .setHeader("Content-Type", "text/plain") .setHeader("Etag", etagValue)); server.enqueue( new MockResponse() .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.play(); initResponseCache(); URL url = server.getUrl("/"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); assertEquals(etagValue, connection.getHeaderField("Etag")); assertContent(body, connection); connection.disconnect(); // Discard the server-side record of the request made. server.takeRequest(); // Confirm the cached body is returned along with the original etag header. HttpURLConnection cachedConnection = (HttpURLConnection) url.openConnection(); assertEquals(etagValue, cachedConnection.getHeaderField("Etag")); assertContent(body, cachedConnection); cachedConnection.disconnect(); // Check the client formatted the request correctly. RecordedRequest request = server.takeRequest(); assertEquals(etagValue, request.getHeader("If-None-Match")); } /** * Test Etag headers are returned correctly when a client-side cache is installed and the server * data has changed. * https://code.google.com/p/android/issues/detail?id=108949 */ public void testEtagHeaders_cachedWithServerMiss() throws Exception { final String etagValue1 = "686897696a7c876b7e"; final String body1 = "Response with etag 1"; final String etagValue2 = "686897696a7c876b7f"; final String body2 = "Response with etag 2"; server.enqueue( new MockResponse() .setBody(body1) .setResponseCode(HttpURLConnection.HTTP_OK) .setHeader("Content-Type", "text/plain") .setHeader("Etag", etagValue1)); server.enqueue( new MockResponse() .setBody(body2) .setResponseCode(HttpURLConnection.HTTP_OK) .setHeader("Content-Type", "text/plain") .setHeader("Etag", etagValue2)); server.play(); initResponseCache(); URL url = server.getUrl("/"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); assertEquals(etagValue1, connection.getHeaderField("Etag")); assertContent(body1, connection); connection.disconnect(); // Discard the server-side record of the request made. server.takeRequest(); // Confirm the new body is returned along with the new etag header. HttpURLConnection cachedConnection = (HttpURLConnection) url.openConnection(); assertEquals(etagValue2, cachedConnection.getHeaderField("Etag")); assertContent(body2, cachedConnection); cachedConnection.disconnect(); // Check the client formatted the request correctly. RecordedRequest request = server.takeRequest(); assertEquals(etagValue1, request.getHeader("If-None-Match")); } /** * Test which headers are sent unencrypted to the HTTP proxy. */ public void testProxyConnectIncludesProxyHeadersOnly() throws IOException, InterruptedException { Authenticator.setDefault(new SimpleAuthenticator()); RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); TestSSLContext testSSLContext = createDefaultTestSSLContext(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), true); server.enqueue(new MockResponse() .setResponseCode(407) .addHeader("Proxy-Authenticate: Basic realm=\"localhost\"")); server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) .clearHeaders()); 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("User-Agent", "baz"); connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); connection.setHostnameVerifier(hostnameVerifier); assertContent("encrypted response from the origin server", connection); // connect1 and connect2 are tunnel requests which potentially tunnel multiple requests; // Thus we can't expect its headers to exactly match those of the wrapped request. // See https://github.com/square/okhttp/commit/457fb428a729c50c562822571ea9b13e689648f3 { RecordedRequest connect1 = server.takeRequest(); List headers = connect1.getHeaders(); assertContainsNoneMatching(headers, "Private.*"); assertContainsNoneMatching(headers, "Proxy\\-Authorization.*"); assertHeaderPresent(connect1, "User-Agent"); assertContains(headers, "Host: android.com:443"); assertContains(headers, "Proxy-Connection: Keep-Alive"); } { RecordedRequest connect2 = server.takeRequest(); List headers = connect2.getHeaders(); assertContainsNoneMatching(headers, "Private.*"); assertHeaderPresent(connect2, "Proxy-Authorization"); assertHeaderPresent(connect2, "User-Agent"); assertContains(headers, "Host: android.com:443"); assertContains(headers, "Proxy-Connection: Keep-Alive"); } RecordedRequest get = server.takeRequest(); assertContains(get.getHeaders(), "Private: Secret"); assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls); } public void testProxyAuthenticateOnConnect() throws Exception { Authenticator.setDefault(new SimpleAuthenticator()); TestSSLContext testSSLContext = createDefaultTestSSLContext(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), true); server.enqueue(new MockResponse() .setResponseCode(407) .addHeader("Proxy-Authenticate: Basic realm=\"localhost\"")); server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) .clearHeaders()); server.enqueue(new MockResponse().setBody("A")); server.play(); URL url = new URL("https://android.com/foo"); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection( server.toProxyAddress()); connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); connection.setHostnameVerifier(new RecordingHostnameVerifier()); assertContent("A", connection); RecordedRequest connect1 = server.takeRequest(); assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine()); assertContainsNoneMatching(connect1.getHeaders(), "Proxy\\-Authorization.*"); RecordedRequest connect2 = server.takeRequest(); assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine()); assertContains(connect2.getHeaders(), "Proxy-Authorization: Basic " + SimpleAuthenticator.BASE_64_CREDENTIALS); RecordedRequest get = server.takeRequest(); assertEquals("GET /foo HTTP/1.1", get.getRequestLine()); assertContainsNoneMatching(get.getHeaders(), "Proxy\\-Authorization.*"); } // Don't disconnect after building a tunnel with CONNECT // http://code.google.com/p/android/issues/detail?id=37221 public void testProxyWithConnectionClose() throws IOException { TestSSLContext testSSLContext = createDefaultTestSSLContext(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), true); server.enqueue(new MockResponse() .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) .clearHeaders()); server.enqueue(new MockResponse().setBody("this response comes via a proxy")); server.play(); URL url = new URL("https://android.com/foo"); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection( server.toProxyAddress()); connection.setRequestProperty("Connection", "close"); connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); connection.setHostnameVerifier(new RecordingHostnameVerifier()); assertContent("this response comes via a proxy", connection); } public void testDisconnectedConnection() throws IOException { server.enqueue(new MockResponse() .throttleBody(2, 100, TimeUnit.MILLISECONDS) .setBody("ABCD")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); InputStream in = connection.getInputStream(); assertEquals('A', (char) in.read()); connection.disconnect(); try { // Reading 'B' may succeed if it's buffered. in.read(); // But 'C' shouldn't be buffered (the response is throttled) and this should fail. in.read(); fail("Expected a connection closed exception"); } catch (IOException expected) { } } // http://b/33763156 public void testDisconnectDuringConnect_getInputStream() throws IOException { checkDisconnectDuringConnect(HttpURLConnection::getInputStream); } // http://b/33763156 public void testDisconnectDuringConnect_getOutputStream() throws IOException { checkDisconnectDuringConnect(HttpURLConnection::getOutputStream); } // http://b/33763156 public void testDisconnectDuringConnect_getResponseCode() throws IOException { checkDisconnectDuringConnect(HttpURLConnection::getResponseCode); } // http://b/33763156 public void testDisconnectDuringConnect_getResponseMessage() throws IOException { checkDisconnectDuringConnect(HttpURLConnection::getResponseMessage); } interface ConnectStrategy { /** * Causes the given {@code connection}, which was previously disconnected, * to initiate the connection. */ void connect(HttpURLConnection connection) throws IOException; } // http://b/33763156 private void checkDisconnectDuringConnect(ConnectStrategy connectStrategy) throws IOException { server.enqueue(new MockResponse().setBody("This should never be sent")); server.play(); final AtomicReference connectionHolder = new AtomicReference<>(); class DisconnectingCookieHandler extends CookieManager { @Override public Map> get(URI uri, Map> map) throws IOException { Map> result = super.get(uri, map); connectionHolder.get().disconnect(); return result; } } CookieHandler defaultCookieHandler = CookieHandler.getDefault(); try { CookieHandler.setDefault(new DisconnectingCookieHandler()); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connectionHolder.set(connection); try { connectStrategy.connect(connection); fail(); } catch (IOException expected) { assertEquals("Canceled", expected.getMessage()); } finally { connection.disconnect(); } } finally { CookieHandler.setDefault(defaultCookieHandler); } } public void testDisconnectBeforeConnect() throws IOException { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.disconnect(); assertContent("A", connection); assertEquals(200, connection.getResponseCode()); } public void testDisconnectAfterOnlyResponseCodeCausesNoCloseGuardWarning() throws IOException { server.enqueue(new MockResponse() .setBody(gzip("ABCABCABC".getBytes("UTF-8"))) .addHeader("Content-Encoding: gzip")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); try { assertEquals(200, connection.getResponseCode()); } finally { connection.disconnect(); } } public void testDefaultRequestProperty() throws Exception { URLConnection.setDefaultRequestProperty("X-testSetDefaultRequestProperty", "A"); assertNull(URLConnection.getDefaultRequestProperty("X-setDefaultRequestProperty")); } /** * 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 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); } private void testMarkAndReset(TransferKind transferKind) throws IOException { MockResponse response = new MockResponse(); transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024); server.enqueue(response); server.enqueue(response); server.play(); 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()); } /** * 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: Basic realm=\"protected area\"") .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") .setSocketPolicy(DISCONNECT_AT_END)); 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()); assertEquals(-1, connection.getContentLength()); RecordedRequest request = server.takeRequest(); assertContains(request.getHeaders(), "Accept-Encoding: gzip"); } public void testClientConfiguredGzipContentEncoding() throws Exception { byte[] bodyBytes = gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8")); server.enqueue(new MockResponse() .setBody(bodyBytes) .addHeader("Content-Encoding: gzip") .addHeader("Content-Length: " + bodyBytes.length)); 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)); assertEquals(bodyBytes.length, connection.getContentLength()); RecordedRequest request = server.takeRequest(); assertContains(request.getHeaders(), "Accept-Encoding: gzip"); assertEquals("gzip", connection.getContentEncoding()); } 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()); } /** * Test that HEAD requests don't have a body regardless of the response * headers. http://code.google.com/p/android/issues/detail?id=24672 */ public void testHeadAndContentLength() throws Exception { server.enqueue(new MockResponse() .clearHeaders() .addHeader("Content-Length: 100")); server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection1 = (HttpURLConnection) server.getUrl("/").openConnection(); connection1.setRequestMethod("HEAD"); assertEquals("100", connection1.getHeaderField("Content-Length")); assertContent("", connection1); HttpURLConnection connection2 = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection2.getInputStream(), Integer.MAX_VALUE)); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(1, server.takeRequest().getSequenceNumber()); } /** * Test that request body chunking works. This test has been relaxed from treating * the {@link java.net.HttpURLConnection#setChunkedStreamingMode(int)} * chunk length as being fixed because OkHttp no longer guarantees * the fixed chunk size. Instead, we check that chunking takes place * and we force the chunk size with flushes. */ public void testSetChunkedStreamingMode() throws IOException, InterruptedException { server.enqueue(new MockResponse()); server.play(); HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); // Later releases of Android ignore the value for chunkLength if it is > 0 and default to // a fixed chunkLength. During the change-over period while the chunkLength indicates the // chunk buffer size (inc. header) the chunkLength has to be >= 8. This enables the flush() // to dictate the size of the chunks. urlConnection.setChunkedStreamingMode(50 /* chunkLength */); urlConnection.setDoOutput(true); OutputStream outputStream = urlConnection.getOutputStream(); String outputString = "ABCDEFGH"; byte[] outputBytes = outputString.getBytes("US-ASCII"); int targetChunkSize = 3; for (int i = 0; i < outputBytes.length; i += targetChunkSize) { int count = i + targetChunkSize < outputBytes.length ? 3 : outputBytes.length - i; outputStream.write(outputBytes, i, count); outputStream.flush(); } assertEquals(200, urlConnection.getResponseCode()); RecordedRequest request = server.takeRequest(); assertEquals(outputString, new String(request.getBody(), "US-ASCII")); assertEquals(Arrays.asList(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(new SimpleAuthenticator()); 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())); } public void testSetValidRequestMethod() throws Exception { server.play(); assertValidRequestMethod("GET"); assertValidRequestMethod("DELETE"); assertValidRequestMethod("HEAD"); assertValidRequestMethod("OPTIONS"); assertValidRequestMethod("POST"); assertValidRequestMethod("PUT"); assertValidRequestMethod("TRACE"); } private void assertValidRequestMethod(String requestMethod) throws Exception { HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setRequestMethod(requestMethod); assertEquals(requestMethod, connection.getRequestMethod()); } public void testSetInvalidRequestMethodLowercase() throws Exception { server.play(); assertInvalidRequestMethod("get"); } public void testSetInvalidRequestMethodConnect() throws Exception { server.play(); assertInvalidRequestMethod("CONNECT"); } private void assertInvalidRequestMethod(String requestMethod) throws Exception { HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); try { connection.setRequestMethod(requestMethod); fail(); } catch (ProtocolException expected) { } } public void testCannotSetNegativeFixedLengthStreamingMode() throws Exception { server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); try { connection.setFixedLengthStreamingMode(-2); fail(); } catch (IllegalArgumentException expected) { } } public void testCanSetNegativeChunkedStreamingMode() throws Exception { server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setChunkedStreamingMode(-2); } public void testCannotSetFixedLengthStreamingModeAfterConnect() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); try { connection.setFixedLengthStreamingMode(1); fail(); } catch (IllegalStateException expected) { } } public void testCannotSetChunkedStreamingModeAfterConnect() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); try { connection.setChunkedStreamingMode(1); fail(); } catch (IllegalStateException expected) { } } public void testCannotSetFixedLengthStreamingModeAfterChunkedStreamingMode() throws Exception { server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setChunkedStreamingMode(1); try { connection.setFixedLengthStreamingMode(1); fail(); } catch (IllegalStateException expected) { } } public void testCannotSetChunkedStreamingModeAfterFixedLengthStreamingMode() throws Exception { server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setFixedLengthStreamingMode(1); try { connection.setChunkedStreamingMode(1); fail(); } catch (IllegalStateException expected) { } } public void testSecureFixedLengthStreaming() throws Exception { testSecureStreamingPost(StreamingMode.FIXED_LENGTH); } public void testSecureChunkedStreaming() throws Exception { testSecureStreamingPost(StreamingMode.CHUNKED); } /** * Users have reported problems using HTTPS with streaming request bodies. * http://code.google.com/p/android/issues/detail?id=12860 */ private void testSecureStreamingPost(StreamingMode streamingMode) throws Exception { TestSSLContext testSSLContext = createDefaultTestSSLContext(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); server.enqueue(new MockResponse().setBody("Success!")); server.play(); HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); 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(); assertEquals("Success!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); RecordedRequest request = server.takeRequest(); assertEquals("POST / HTTP/1.1", request.getRequestLine()); if (streamingMode == StreamingMode.FIXED_LENGTH) { assertEquals(Collections.emptyList(), request.getChunkSizes()); } else if (streamingMode == StreamingMode.CHUNKED) { assertEquals(Arrays.asList(4), request.getChunkSizes()); } 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(new SimpleAuthenticator()); 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: .*"); // ...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 " + SimpleAuthenticator.BASE_64_CREDENTIALS); 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(); SimpleAuthenticator authenticator = new SimpleAuthenticator(); Authenticator.setDefault(authenticator); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); assertEquals(Authenticator.RequestorType.SERVER, authenticator.requestorType); assertEquals(server.getPort(), authenticator.requestingPort); assertEquals(InetAddress.getByName(server.getHostName()), authenticator.requestingSite); assertEquals("protected area", authenticator.requestingPrompt); assertEquals("http", authenticator.requestingProtocol); assertEquals("Basic", authenticator.requestingScheme); // no authorization header for the first request... RecordedRequest request = server.takeRequest(); assertContainsNoneMatching(request.getHeaders(), "Authorization: .*"); // ...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 " + SimpleAuthenticator.BASE_64_CREDENTIALS); } } // bug 11473660 public void testAuthenticateWithLowerCaseHeadersAndScheme() 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(); SimpleAuthenticator authenticator = new SimpleAuthenticator(); Authenticator.setDefault(authenticator); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); assertEquals(Authenticator.RequestorType.SERVER, authenticator.requestorType); assertEquals(server.getPort(), authenticator.requestingPort); assertEquals(InetAddress.getByName(server.getHostName()), authenticator.requestingSite); assertEquals("protected area", authenticator.requestingPrompt); assertEquals("http", authenticator.requestingProtocol); assertEquals("basic", authenticator.requestingScheme); } // http://code.google.com/p/android/issues/detail?id=19081 public void testAuthenticateWithCommaSeparatedAuthenticationMethods() throws Exception { server.enqueue(new MockResponse() .setResponseCode(401) .addHeader("WWW-Authenticate: Scheme1 realm=\"a\", Basic realm=\"b\", " + "Scheme3 realm=\"c\"") .setBody("Please authenticate.")); server.enqueue(new MockResponse().setBody("Successful auth!")); server.play(); SimpleAuthenticator authenticator = new SimpleAuthenticator(); authenticator.expectedPrompt = "b"; Authenticator.setDefault(authenticator); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); assertContainsNoneMatching(server.takeRequest().getHeaders(), "Authorization: .*"); assertContains(server.takeRequest().getHeaders(), "Authorization: Basic " + SimpleAuthenticator.BASE_64_CREDENTIALS); assertEquals("Basic", authenticator.requestingScheme); } public void testAuthenticateWithMultipleAuthenticationHeaders() throws Exception { server.enqueue(new MockResponse() .setResponseCode(401) .addHeader("WWW-Authenticate: Scheme1 realm=\"a\"") .addHeader("WWW-Authenticate: Basic realm=\"b\"") .addHeader("WWW-Authenticate: Scheme3 realm=\"c\"") .setBody("Please authenticate.")); server.enqueue(new MockResponse().setBody("Successful auth!")); server.play(); SimpleAuthenticator authenticator = new SimpleAuthenticator(); authenticator.expectedPrompt = "b"; Authenticator.setDefault(authenticator); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); assertContainsNoneMatching(server.takeRequest().getHeaders(), "Authorization: .*"); assertContains(server.takeRequest().getHeaders(), "Authorization: Basic " + SimpleAuthenticator.BASE_64_CREDENTIALS); assertEquals("Basic", authenticator.requestingScheme); } 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 = createDefaultTestSSLContext(); 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 = createDefaultTestSSLContext(); 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(); } // http://b/27590872 - assert we do not throw a runtime exception if a server responds with // a location that cannot be represented directly by URI. public void testRedirectWithInvalidRedirectUrl() throws Exception { // The first server hosts a redirect to a second. We need two so that the ProxySelector // installed is used for the redirect. Otherwise the second request will be handled via the // existing keep-alive connection. server.play(); MockWebServer server2 = new MockWebServer(); server2.play(); String targetPath = "/target"; // The "%0" in the suffix is invalid without a second digit. String invalidSuffix = "?foo=%0&bar=%00"; String redirectPath = server2.getUrl(targetPath).toString(); String invalidRedirectUri = redirectPath + invalidSuffix; // Redirect to the invalid URI. server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: " + invalidRedirectUri)); server2.enqueue(new MockResponse().setBody("Target")); // Assert the target URI is actually invalid. try { new URI(invalidRedirectUri); fail("Target URL is expected to be invalid"); } catch (URISyntaxException expected) {} // The ProxySelector requires a URI object, which forces the HttpURLConnectionImpl to create // a URI object containing a string based on the redirect address, regardless of what it is // using internally to hold the target address. ProxySelector originalSelector = ProxySelector.getDefault(); final List proxySelectorUris = new ArrayList<>(); ProxySelector.setDefault(new ProxySelector() { @Override public List select(URI uri) { if (uri.getScheme().equals("http")) { // Ignore socks proxy lookups. proxySelectorUris.add(uri); } return Collections.singletonList(Proxy.NO_PROXY); } @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { // no-op } }); try { HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("Target", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); // Inspect the redirect request to see what request was actually made. RecordedRequest actualRequest = server2.takeRequest(); assertEquals(targetPath + invalidSuffix, actualRequest.getPath()); // The first URI will be the initial request. We want to inspect the redirect. URI uri = proxySelectorUris.get(1); // The proxy is selected by Address alone (not the whole target URI). // In OkHttp, HttpEngine.createAddress() converts to an Address and the // RouteSelector converts back to address.url(). assertEquals(server2.getUrl("/").toString(), uri.toString()); } finally { ProxySelector.setDefault(originalSelector); server2.shutdown(); } } public void testInstanceFollowsRedirects() throws Exception { testInstanceFollowsRedirects("http://www.google.com/"); testInstanceFollowsRedirects("https://www.google.com/"); } private void testInstanceFollowsRedirects(String spec) throws Exception { URL url = new URL(spec); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.setInstanceFollowRedirects(true); assertTrue(urlConnection.getInstanceFollowRedirects()); urlConnection.setInstanceFollowRedirects(false); assertFalse(urlConnection.getInstanceFollowRedirects()); } public void testFollowRedirects() throws Exception { testFollowRedirects("http://www.google.com/"); testFollowRedirects("https://www.google.com/"); } private void testFollowRedirects(String spec) throws Exception { URL url = new URL(spec); boolean originalValue = HttpURLConnection.getFollowRedirects(); try { HttpURLConnection.setFollowRedirects(false); assertFalse(HttpURLConnection.getFollowRedirects()); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); assertFalse(connection.getInstanceFollowRedirects()); HttpURLConnection.setFollowRedirects(true); assertTrue(HttpURLConnection.getFollowRedirects()); HttpURLConnection connection2 = (HttpURLConnection) url.openConnection(); assertTrue(connection2.getInstanceFollowRedirects()); } finally { HttpURLConnection.setFollowRedirects(originalValue); } } public void testResponse300MultipleChoiceWithPost() throws Exception { // Chrome doesn't follow the redirect, but Firefox and the RI both do testResponseRedirectedWithPost(HttpURLConnection.HTTP_MULT_CHOICE); } public void testResponse301MovedPermanentlyWithPost() throws Exception { testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_PERM); } public void testResponse302MovedTemporarilyWithPost() throws Exception { testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP); } public void testResponse303SeeOtherWithPost() throws Exception { testResponseRedirectedWithPost(HttpURLConnection.HTTP_SEE_OTHER); } private void testResponseRedirectedWithPost(int redirectCode) throws Exception { server.enqueue(new MockResponse() .setResponseCode(redirectCode) .addHeader("Location: /page2") .setBody("This page has moved!")); server.enqueue(new MockResponse().setBody("Page 2")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/page1").openConnection(); connection.setDoOutput(true); byte[] requestBody = { 'A', 'B', 'C', 'D' }; OutputStream outputStream = connection.getOutputStream(); outputStream.write(requestBody); outputStream.close(); assertEquals("Page 2", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); assertTrue(connection.getDoOutput()); RecordedRequest page1 = server.takeRequest(); assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine()); assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody())); RecordedRequest page2 = server.takeRequest(); assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine()); } public void testResponse305UseProxy() throws Exception { server.play(); server.enqueue(new MockResponse() .setResponseCode(HttpURLConnection.HTTP_USE_PROXY) .addHeader("Location: " + server.getUrl("/")) .setBody("This page has moved!")); server.enqueue(new MockResponse().setBody("Proxy Response")); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/foo").openConnection(); // Fails on the RI, which gets "Proxy Response" assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); RecordedRequest page1 = server.takeRequest(); assertEquals("GET /foo HTTP/1.1", page1.getRequestLine()); assertEquals(1, server.getRequestCount()); } 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 = createDefaultTestSSLContext(); 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=Local Host 3, " + "CN=Test Intermediate Certificate Authority 2, " + "CN=Test Root Certificate Authority 1" + "] ECDHE_RSA"), trustManager.calls); } finally { HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier); HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory); } } public void testSetDefaultSSLSocketFactory_null() { try { HttpsURLConnection.setDefaultSSLSocketFactory(null); fail(); } catch (IllegalArgumentException expected) { } } /** * Test that the timeout period is honored. The connect timeout is applied to each socket * connection attempt. If a hostname resolves to multiple IPs HttpURLConnection will wait the * full timeout for each. */ public void testConnectTimeouts() throws IOException { // During CTS tests we are limited in what host names we can depend on and unfortunately // DNS lookups are not pluggable through standard APIs. During manual testing you should be // able to change this to any name that can be resolved to multiple IPs and it should still // work. String hostName = "localhost"; int expectedConnectionAttempts = InetAddress.getAllByName(hostName).length; int perSocketTimeout = 1000; final int[] socketCreationCount = new int[1]; final int[] socketConnectTimeouts = new int[expectedConnectionAttempts]; // It is difficult to force socket timeouts reliably so we replace the default SocketFactory // and have it create Sockets that always time out when connect(SocketAddress, int) is // called. SocketFactory originalDefault = SocketFactory.getDefault(); try { // Override the default SocketFactory so we can intercept socket creation. SocketFactory.setDefault(new DelegatingSocketFactory(originalDefault) { @Override protected Socket configureSocket(Socket socket) throws IOException { final int attemptNumber = socketCreationCount[0]++; Socket socketWrapper = new DelegatingSocket(socket) { @Override public void connect(SocketAddress endpoint, int timeout) throws IOException { socketConnectTimeouts[attemptNumber] = timeout; throw new SocketTimeoutException("Simulated timeout after " + timeout); } }; return socketWrapper; } }); URLConnection urlConnection = new URL("http://" + hostName + "/").openConnection(); urlConnection.setConnectTimeout(perSocketTimeout); urlConnection.getInputStream(); fail(); } catch (SocketTimeoutException e) { assertEquals(expectedConnectionAttempts, socketCreationCount[0]); for (int i = 0; i < expectedConnectionAttempts; i++) { assertEquals(perSocketTimeout, socketConnectTimeouts[i]); } } finally { SocketFactory.setDefault(originalDefault); } } 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.enqueue(new MockResponse().setBody("unused")); // to keep the server alive 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().setSocketPolicy(DISCONNECT_AT_START)); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); try { connection.getInputStream(); fail(); } catch (IOException expected) { } try { connection.getInputStream(); fail(); } catch (IOException expected) { } } public void testReadTimeoutsOnRecycledConnections() throws Exception { server.enqueue(new MockResponse().setBody("ABC")); server.play(); // The request should work once and then fail URLConnection connection = server.getUrl("").openConnection(); // Read timeout of a day, sure to cause the test to timeout and fail. connection.setReadTimeout(24 * 3600 * 1000); InputStream input = connection.getInputStream(); assertEquals("ABC", readAscii(input, Integer.MAX_VALUE)); input.close(); try { connection = server.getUrl("").openConnection(); // Set the read timeout back to 100ms, this request will time out // because we've only enqueued one response. connection.setReadTimeout(100); connection.getInputStream(); fail(); } catch (IOException expected) { } } /** * This test goes through the exhaustive set of interesting ASCII characters * because most of those characters are interesting in some way according to * RFC 2396 and RFC 2732. http://b/1158780 * After M, Android's HttpURLConnection started canonicalizing hostnames to lower case, IDN * encoding and being more strict about invalid characters. */ public void testUrlCharacterMapping() throws Exception { server.setDispatcher(new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { return new MockResponse(); } }); server.play(); // alphanum testUrlToUriMapping("abzABZ09", "abzabz09", "abzABZ09", "abzABZ09", "abzABZ09"); testUrlToRequestMapping("abzABZ09", "abzABZ09", "abzABZ09"); // control characters // On JB-MR2 and below, we would allow a host containing \u0000 // and then generate a request with a Host header that violated RFC2616. // We now reject such hosts. // // The ideal behaviour here is to be "lenient" about the host and rewrite // it, but attempting to do so introduces a new range of incompatible // behaviours. testUrlToUriMapping("\u0000", null, "%00", "%00", "%00"); // RI fails this testUrlToRequestMapping("\u0000", "%00", "%00"); testUrlToUriMapping("\u0001", null, "%01", "%01", "%01"); testUrlToRequestMapping("\u0001", "%01", "%01"); testUrlToUriMapping("\u001f", null, "%1F", "%1F", "%1F"); testUrlToRequestMapping("\u001f", "%1F", "%1F"); // ascii characters testUrlToUriMapping("%20", null, "%20", "%20", "%20"); testUrlToRequestMapping("%20", "%20", "%20"); testUrlToUriMapping(" ", null, "%20", "%20", "%20"); testUrlToRequestMapping(" ", "%20", "%20"); testUrlToUriMapping("!", "!", "!", "!", "!"); testUrlToRequestMapping("!", "!", "!"); testUrlToUriMapping("\"", null, "%22", "%22", "%22"); testUrlToRequestMapping("\"", "%22", "%22"); testUrlToUriMapping("#", null, null, null, "%23"); testUrlToRequestMapping("#", null, null); testUrlToUriMapping("$", "$", "$", "$", "$"); testUrlToRequestMapping("$", "$", "$"); testUrlToUriMapping("&", "&", "&", "&", "&"); testUrlToRequestMapping("&", "&", "&"); // http://b/30405333 - upstream OkHttp encodes single quote (') as %27 in query parameters // but this breaks iTunes remote apps: iTunes currently does not accept %27 so we have a // local patch to retain the historic Android behavior of not encoding single quote. testUrlToUriMapping("'", "'", "'", "'", "'"); testUrlToRequestMapping("'", "'", "'"); testUrlToUriMapping("(", "(", "(", "(", "("); testUrlToRequestMapping("(", "(", "("); testUrlToUriMapping(")", ")", ")", ")", ")"); testUrlToRequestMapping(")", ")", ")"); testUrlToUriMapping("*", "*", "*", "*", "*"); testUrlToRequestMapping("*", "*", "*"); testUrlToUriMapping("+", "+", "+", "+", "+"); testUrlToRequestMapping("+", "+", "+"); testUrlToUriMapping(",", ",", ",", ",", ","); testUrlToRequestMapping(",", ",", ","); testUrlToUriMapping("-", "-", "-", "-", "-"); testUrlToRequestMapping("-", "-", "-"); testUrlToUriMapping(".", null, ".", ".", "."); testUrlToRequestMapping(".", ".", "."); testUrlToUriMapping(".foo", ".foo", ".foo", ".foo", ".foo"); testUrlToRequestMapping(".foo", ".foo", ".foo"); testUrlToUriMapping("/", null, "/", "/", "/"); testUrlToRequestMapping("/", "/", "/"); testUrlToUriMapping(":", null, ":", ":", ":"); testUrlToRequestMapping(":", ":", ":"); testUrlToUriMapping(";", ";", ";", ";", ";"); testUrlToRequestMapping(";", ";", ";"); testUrlToUriMapping("<", null, "%3C", "%3C", "%3C"); testUrlToRequestMapping("<", "%3C", "%3C"); testUrlToUriMapping("=", "=", "=", "=", "="); testUrlToRequestMapping("=", "=", "="); testUrlToUriMapping(">", null, "%3E", "%3E", "%3E"); testUrlToRequestMapping(">", "%3E", "%3E"); testUrlToUriMapping("?", null, null, "?", "?"); testUrlToRequestMapping("?", null, "?"); testUrlToUriMapping("@", null, "@", "@", "@"); testUrlToRequestMapping("@", "@", "@"); testUrlToUriMapping("[", null, null, null, "%5B"); testUrlToRequestMapping("[", null, null); testUrlToUriMapping("\\", null, null, null, "%5C"); testUrlToRequestMapping("\\", null, null); testUrlToUriMapping("]", null, null, null, "%5D"); testUrlToRequestMapping("]", null, null); testUrlToUriMapping("^", null, "%5E", null, "%5E"); testUrlToRequestMapping("^", "%5E", null); testUrlToUriMapping("_", "_", "_", "_", "_"); testUrlToRequestMapping("_", "_", "_"); testUrlToUriMapping("`", null, "%60", null, "%60"); testUrlToRequestMapping("`", "%60", null); testUrlToUriMapping("{", null, "%7B", null, "%7B"); testUrlToRequestMapping("{", "%7B", null); testUrlToUriMapping("|", null, "%7C", null, "%7C"); testUrlToRequestMapping("|", "%7C", null); testUrlToUriMapping("}", null, "%7D", null, "%7D"); testUrlToRequestMapping("}", "%7D", null); testUrlToUriMapping("~", "~", "~", "~", "~"); testUrlToRequestMapping("~", "~", "~"); testUrlToUriMapping("\u007f", null, "%7F", "%7F", "%7F"); testUrlToRequestMapping("\u007f", "%7F", "%7F"); // beyond ASCII // 0x80 is the code point for the Euro sign in CP1252 (but not 8859-15 or Unicode). // Unicode code point 0x80 is a control character and maps to {0xC2, 0x80} in UTF-8. // 0x80 is outside of the ASCII range and is not supported by IDN in hostnames. testUrlToUriMapping("\u0080", null, "%C2%80", "%C2%80", "%C2%80"); testUrlToRequestMapping("\u0080", "%C2%80", "%C2%80"); // More complicated transformations for the authorities below. // 0x20AC is the code point for the Euro sign in Unicode. // Unicode code point 0x20AC maps to {0xE2, 0x82, 0xAC} in UTF-8 // 0x20AC is not supported by all registrars but there are some legacy domains that // use it and Android currently supports IDN conversion for it. testUrlToUriMapping("\u20ac", null /* skip */, "%E2%82%AC", "%E2%82%AC", "%E2%82%AC"); testUrlToUriMappingAuthority("http://host\u20ac.tld/", "http://xn--host-yv7a.tld/"); testUrlToRequestMapping("\u20ac", "%E2%82%AC", "%E2%82%AC"); // UTF-16 {0xD842, 0xDF9F} -> Unicode 0x20B9F (a Kanji character) // Unicode code point 0x20B9F maps to {0xF0, 0xA0, 0xAE, 0x9F} in UTF-8 // IDN can deal with this code point. testUrlToUriMapping("\ud842\udf9f", null /* skip */, "%F0%A0%AE%9F", "%F0%A0%AE%9F", "%F0%A0%AE%9F"); testUrlToUriMappingAuthority("http://host\uD842\uDF9F.tld/", "http://xn--host-ov06c.tld/"); testUrlToRequestMapping("\ud842\udf9f", "%F0%A0%AE%9F", "%F0%A0%AE%9F"); } private void testUrlToUriMappingAuthority(String urlString, String expectedUriString) throws Exception { URI authorityUri = backdoorUrlToUri(new URL(urlString)); assertEquals(expectedUriString, authorityUri.toString()); } /** * Exercises HttpURLConnection to convert URL to a URI. Unlike URL#toURI, * HttpURLConnection recovers from URLs with unescaped but unsupported URI * characters like '{' and '|' by escaping these characters. */ private URI backdoorUrlToUri(URL url) throws Exception { final AtomicReference uriReference = new AtomicReference(); ResponseCache.setDefault(new ResponseCache() { @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { return null; } @Override public CacheResponse get(URI uri, String requestMethod, Map> requestHeaders) throws IOException { uriReference.set(uri); throw new UnsupportedOperationException(); } }); try { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.getResponseCode(); } catch (Exception expected) { } return uriReference.get(); } /* * Test the request that would be made by making an actual request a MockWebServer and capturing * the request made. * * Any "as" values that are null are not tested. */ private void testUrlToRequestMapping( String string, String asFile, String asQuery) throws Exception { if (asFile != null) { URL fileUrl = server.getUrl("/file" + string + "/#discarded"); HttpURLConnection urlConnection = (HttpURLConnection) fileUrl.openConnection(); // Bypass the cache. urlConnection.setUseCaches(false); assertEquals(200, urlConnection.getResponseCode()); assertEquals("/file" + asFile + "/", server.takeRequest().getPath()); } if (asQuery != null) { URL queryUrl = server.getUrl("/file?q" + string + "=x#discarded"); HttpURLConnection urlConnection = (HttpURLConnection) queryUrl.openConnection(); // Bypass the cache. urlConnection.setUseCaches(false); assertEquals(200, urlConnection.getResponseCode()); assertEquals("/file?q" + asQuery + "=x", server.takeRequest().getPath()); } } /* * Test the request that would be made by looking at the URI presented to the cache. This * includes the likely host name that would be used if a request were made. The cache throws an * exception so no request is actually made. * * Any "as" values that are null are not tested. */ private void testUrlToUriMapping(String string, String asAuthority, String asFile, String asQuery, String asFragment) throws Exception { if (asAuthority != null) { URI authorityUri = backdoorUrlToUri(new URL("http://host" + string + ".tld/")); assertEquals("http://host" + asAuthority + ".tld/", authorityUri.toString()); } if (asFile != null) { URI fileUri = backdoorUrlToUri(new URL("http://host.tld/file" + string + "/")); assertEquals("http://host.tld/file" + asFile + "/", fileUri.toString()); } if (asQuery != null) { URI queryUri = backdoorUrlToUri(new URL("http://host.tld/file?q" + string + "=x")); assertEquals("http://host.tld/file?q" + asQuery + "=x", queryUri.toString()); } assertEquals("http://host.tld/file#" + asFragment + "-x", backdoorUrlToUri(new URL("http://host.tld/file#" + asFragment + "-x")).toString()); } public void testHostWithNul() throws Exception { URL url = new URL("http://host\u0000/"); try { url.openStream(); fail(); } catch (UnknownHostException expected) {} } /** * Don't explode if the cache returns a null body. http://b/3373699 */ public void testResponseCacheReturnsNullOutputStream() throws Exception { final AtomicBoolean aborted = new AtomicBoolean(); ResponseCache.setDefault(new ResponseCache() { @Override public CacheResponse get(URI uri, String requestMethod, Map> requestHeaders) throws IOException { return null; } @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { return new CacheRequest() { @Override public void abort() { aborted.set(true); } @Override public OutputStream getBody() throws IOException { return null; } }; } }); server.enqueue(new MockResponse().setBody("abcdef")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); InputStream in = connection.getInputStream(); assertEquals("abc", readAscii(in, 3)); in.close(); assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here } /** * 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") .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END)); 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 } public void testGetContent() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); InputStream in = (InputStream) connection.getContent(); assertEquals("A", readAscii(in, Integer.MAX_VALUE)); } public void testGetContentOfType() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); try { connection.getContent(null); fail(); } catch (NullPointerException expected) { } try { connection.getContent(new Class[] { null }); fail(); } catch (NullPointerException expected) { } assertNull(connection.getContent(new Class[] { getClass() })); connection.disconnect(); } public void testGetOutputStreamOnGetFails() throws Exception { server.enqueue(new MockResponse()); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); try { connection.getOutputStream(); fail(); } catch (ProtocolException expected) { } } public void testGetOutputAfterGetInputStreamFails() throws Exception { server.enqueue(new MockResponse()); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setDoOutput(true); try { connection.getInputStream(); connection.getOutputStream(); fail(); } catch (ProtocolException expected) { } } public void testSetDoOutputOrDoInputAfterConnectFails() throws Exception { server.enqueue(new MockResponse()); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.connect(); try { connection.setDoOutput(true); fail(); } catch (IllegalStateException expected) { } try { connection.setDoInput(true); fail(); } catch (IllegalStateException expected) { } connection.disconnect(); } public void testLastModified() throws Exception { server.enqueue(new MockResponse() .addHeader("Last-Modified", "Wed, 27 Nov 2013 11:26:00 GMT") .setBody("Hello")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.connect(); assertEquals(1385551560000L, connection.getLastModified()); assertEquals(1385551560000L, connection.getHeaderFieldDate("Last-Modified", -1)); } public void testClientSendsContentLength() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); connection.setDoOutput(true); OutputStream out = connection.getOutputStream(); out.write(new byte[] { 'A', 'B', 'C' }); out.close(); assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); RecordedRequest request = server.takeRequest(); assertContains(request.getHeaders(), "Content-Length: 3"); } public void testGetContentLengthConnects() throws Exception { server.enqueue(new MockResponse().setBody("ABC")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals(3, connection.getContentLength()); connection.disconnect(); } public void testGetContentTypeConnects() throws Exception { server.enqueue(new MockResponse() .addHeader("Content-Type: text/plain") .setBody("ABC")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("text/plain", connection.getContentType()); connection.disconnect(); } public void testGetContentEncodingConnects() throws Exception { server.enqueue(new MockResponse() .addHeader("Content-Encoding: identity") .setBody("ABC")); server.play(); HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals("identity", connection.getContentEncoding()); connection.disconnect(); } // http://b/4361656 public void testUrlContainsQueryButNoPath() throws Exception { server.enqueue(new MockResponse().setBody("A")); server.play(); URL url = new URL("http", server.getHostName(), server.getPort(), "?query"); assertEquals("A", readAscii(url.openConnection().getInputStream(), Integer.MAX_VALUE)); RecordedRequest request = server.takeRequest(); assertEquals("GET /?query HTTP/1.1", request.getRequestLine()); } // http://code.google.com/p/android/issues/detail?id=20442 public void testInputStreamAvailableWithChunkedEncoding() throws Exception { testInputStreamAvailable(TransferKind.CHUNKED); } public void testInputStreamAvailableWithContentLengthHeader() throws Exception { testInputStreamAvailable(TransferKind.FIXED_LENGTH); } public void testInputStreamAvailableWithNoLengthHeaders() throws Exception { testInputStreamAvailable(TransferKind.END_OF_STREAM); } private void testInputStreamAvailable(TransferKind transferKind) throws IOException { String body = "ABCDEFGH"; MockResponse response = new MockResponse(); transferKind.setBody(response, body, 4); server.enqueue(response); server.play(); URLConnection connection = server.getUrl("/").openConnection(); InputStream in = connection.getInputStream(); for (int i = 0; i < body.length(); i++) { assertTrue(in.available() >= 0); assertEquals(body.charAt(i), in.read()); } assertEquals(0, in.available()); assertEquals(-1, in.read()); } // http://code.google.com/p/android/issues/detail?id=28095 public void testInvalidIpv4Address() throws Exception { try { URI uri = new URI("http://1111.111.111.111/index.html"); uri.toURL().openConnection().connect(); fail(); } catch (UnknownHostException expected) { } } public void testConnectIpv6() throws Exception { server.enqueue(new MockResponse().setBody("testConnectIpv6 body")); server.play(); URL url = new URL("http://[::1]:" + server.getPort() + "/"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); assertContent("testConnectIpv6 body", connection); } // http://code.google.com/p/android/issues/detail?id=16895 public void testUrlWithSpaceInHost() throws Exception { URLConnection urlConnection = new URL("http://and roid.com/").openConnection(); try { urlConnection.getInputStream(); fail(); } catch (UnknownHostException expected) { } } // http://code.google.com/p/android/issues/detail?id=16895 public void testUrlWithSpaceInHostViaHttpProxy() throws Exception { server.enqueue(new MockResponse()); server.play(); URLConnection urlConnection = new URL("http://and roid.com/") .openConnection(server.toProxyAddress()); try { // This test is to check that a NullPointerException is not thrown. urlConnection.getInputStream(); fail(); // the RI makes a bogus proxy request for "GET http://and roid.com/ HTTP/1.1" } catch (UnknownHostException expected) { } } /** Checks that if the first TLS handshake fails, no fallback is attempted. */ private void checkNoFallbackOnFailedHandshake(SSLSocketFactory clientSocketFactory, SSLSocketFactory serverSocketFactory, String... expectedProtocols) throws Exception { server.useHttps(serverSocketFactory, false); server.enqueue(new MockResponse().setSocketPolicy(FAIL_HANDSHAKE)); server.enqueue(new MockResponse().setBody("This required fallbacks")); server.play(); HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // Keeps track of the client sockets created so that we can interrogate them. final boolean disableFallbackScsv = true; FallbackTestClientSocketFactory fallbackTestClientSocketFactory = new FallbackTestClientSocketFactory(clientSocketFactory, disableFallbackScsv); connection.setSSLSocketFactory(fallbackTestClientSocketFactory); try { connection.getInputStream().read(); fail(); } catch (SSLHandshakeException expected) { } List createdSockets = fallbackTestClientSocketFactory.getCreatedSockets(); assertEquals(1, createdSockets.size()); assertSslSocket((TlsFallbackDisabledScsvSSLSocket) createdSockets.get(0), false /* expectedWasFallbackScsvSet */, expectedProtocols); } public void testNoSslFallback_specifiedProtocols() throws Exception { String[] enabledProtocols = { "TLSv1.2", "TLSv1.1" }; TestSSLContext testSSLContext = createDefaultTestSSLContext(); SSLSocketFactory serverSocketFactory = new LimitedProtocolsSocketFactory( testSSLContext.serverContext.getSocketFactory(), enabledProtocols); SSLSocketFactory clientSocketFactory = new LimitedProtocolsSocketFactory( testSSLContext.clientContext.getSocketFactory(), enabledProtocols); checkNoFallbackOnFailedHandshake(clientSocketFactory, serverSocketFactory, enabledProtocols); } public void testNoSslFallback_defaultProtocols() throws Exception { // Will need to be updated if the enabled protocols in Android's SSLSocketFactory change String[] expectedEnabledProtocols = { "TLSv1.2", "TLSv1.1", "TLSv1" }; TestSSLContext testSSLContext = createDefaultTestSSLContext(); SSLSocketFactory serverSocketFactory = testSSLContext.serverContext.getSocketFactory(); SSLSocketFactory clientSocketFactory = testSSLContext.clientContext.getSocketFactory(); checkNoFallbackOnFailedHandshake(clientSocketFactory, serverSocketFactory, expectedEnabledProtocols); } private static void assertSslSocket(TlsFallbackDisabledScsvSSLSocket socket, boolean expectedWasFallbackScsvSet, String... expectedEnabledProtocols) { Set enabledProtocols = new HashSet(Arrays.asList(socket.getEnabledProtocols())); Set expectedProtocolsSet = new HashSet(Arrays.asList(expectedEnabledProtocols)); assertEquals(expectedProtocolsSet, enabledProtocols); assertEquals(expectedWasFallbackScsvSet, socket.wasTlsFallbackScsvSet()); } public void testInspectSslBeforeConnect() throws Exception { TestSSLContext testSSLContext = createDefaultTestSSLContext(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); server.enqueue(new MockResponse()); server.play(); HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); assertNotNull(connection.getHostnameVerifier()); try { connection.getLocalCertificates(); fail(); } catch (IllegalStateException expected) { } try { connection.getServerCertificates(); fail(); } catch (IllegalStateException expected) { } try { connection.getCipherSuite(); fail(); } catch (IllegalStateException expected) { } try { connection.getPeerPrincipal(); fail(); } catch (IllegalStateException expected) { } } /** * Test that we can inspect the SSL session after connect(). * http://code.google.com/p/android/issues/detail?id=24431 */ public void testInspectSslAfterConnect() throws Exception { TestSSLContext testSSLContext = createDefaultTestSSLContext(); server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); server.enqueue(new MockResponse()); server.play(); HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); connection.connect(); try { assertNotNull(connection.getHostnameVerifier()); assertNull(connection.getLocalCertificates()); assertNotNull(connection.getServerCertificates()); assertNotNull(connection.getCipherSuite()); assertNotNull(connection.getPeerPrincipal()); } finally { connection.disconnect(); } } /** * Checks that OkHttp's certificate pinning logic is not used for the common case * of HttpsUrlConnections. * *

OkHttp 2.7 introduced logic for Certificate Pinning. We deliberately don't * expose any API surface that would interact with OkHttp's implementation because * Android has its own API / implementation for certificate pinning. We can't * easily test that there is *no* code path that would invoke OkHttp's certificate * pinning logic, so this test only covers the *common* code path of a * HttpsURLConnection as a sanity check. * *

To check whether OkHttp performs certificate pinning under the hood, this * test disables two {@link Platform} methods. In OkHttp 2.7.5, these two methods * are exclusively used in relation to certificate pinning. Android only provides * the minimal implementation of these methods to get OkHttp's tests to pass, so * they should never be invoked outside of OkHttp's tests. */ public void testTrustManagerAndTrustRootIndex_unusedForHttpsConnection() throws Exception { Platform platform = Platform.getAndSetForTest(new PlatformWithoutTrustManager()); try { testConnectViaHttps(); } finally { Platform.getAndSetForTest(platform); } } /** * Similar to {@link #testTrustManagerAndTrustRootIndex_unusedForHttpsConnection()}, * but for the HTTP case. In the HTTP case, no certificate or trust management * related logic should ever be involved at all, so some pretty basic things must * be going wrong in order for this test to (unexpectedly) invoke the corresponding * Platform methods. */ public void testTrustManagerAndTrustRootIndex_unusedForHttpConnection() throws Exception { Platform platform = Platform.getAndSetForTest(new PlatformWithoutTrustManager()); try { server.enqueue(new MockResponse().setBody("response").setResponseCode(200)); server.play(); HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals(200, urlConnection.getResponseCode()); } finally { Platform.getAndSetForTest(platform); } } /** * A {@link Platform} that doesn't support two methods that, in OkHttp 2.7.5, * are exclusively used to provide custom CertificatePinning. */ static class PlatformWithoutTrustManager extends Platform { @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) { throw new AssertionError("Unexpected call"); } @Override public TrustRootIndex trustRootIndex(X509TrustManager trustManager) { throw new AssertionError("Unexpected call"); } } /** * Returns a gzipped copy of {@code bytes}. */ 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 static void assertHeaderPresent(RecordedRequest request, String headerName) { assertNotNull(headerName + " missing: " + request.getHeaders(), request.getHeader(headerName)); } private void assertContains(List list, String value) { assertTrue(list.toString(), list.contains(value)); } private void assertContainsNoneMatching(List list, String pattern) { for (String header : list) { if (header.matches(pattern)) { fail("Header " + header + " matches " + pattern); } } } private Set newSet(String... elements) { return new HashSet(Arrays.asList(elements)); } private TestSSLContext createDefaultTestSSLContext() { TestSSLContext result = TestSSLContext.create(); testSSLContextsToClose.add(result); return result; } 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.setSocketPolicy(DISCONNECT_AT_END); for (Iterator 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 calls = new ArrayList(); 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 result = new ArrayList(); for (X509Certificate certificate : certificates) { result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber()); } return result.toString(); } } private static class RecordingHostnameVerifier implements HostnameVerifier { private final List calls = new ArrayList(); public boolean verify(String hostname, SSLSession session) { calls.add("verify " + hostname); return true; } } private static class SimpleAuthenticator extends Authenticator { /** base64("username:password") */ private static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ="; private String expectedPrompt; private RequestorType requestorType; private int requestingPort; private InetAddress requestingSite; private String requestingPrompt; private String requestingProtocol; private String requestingScheme; protected PasswordAuthentication getPasswordAuthentication() { requestorType = getRequestorType(); requestingPort = getRequestingPort(); requestingSite = getRequestingSite(); requestingPrompt = getRequestingPrompt(); requestingProtocol = getRequestingProtocol(); requestingScheme = getRequestingScheme(); return (expectedPrompt == null || expectedPrompt.equals(requestingPrompt)) ? new PasswordAuthentication("username", "password".toCharArray()) : null; } } /** * An SSLSocketFactory that delegates all calls. */ private static class DelegatingSSLSocketFactory extends SSLSocketFactory { protected final SSLSocketFactory delegate; public DelegatingSSLSocketFactory(SSLSocketFactory delegate) { this.delegate = delegate; } @Override public String[] getDefaultCipherSuites() { return delegate.getDefaultCipherSuites(); } @Override public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); } @Override public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { return (SSLSocket) delegate.createSocket(s, host, port, autoClose); } @Override public SSLSocket createSocket() throws IOException { return (SSLSocket) delegate.createSocket(); } @Override public SSLSocket createSocket(String host, int port) throws IOException, UnknownHostException { return (SSLSocket) delegate.createSocket(host, port); } @Override public SSLSocket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { return (SSLSocket) delegate.createSocket(host, port, localHost, localPort); } @Override public SSLSocket createSocket(InetAddress host, int port) throws IOException { return (SSLSocket) delegate.createSocket(host, port); } @Override public SSLSocket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return (SSLSocket) delegate.createSocket(address, port, localAddress, localPort); } } /** * An SSLSocketFactory that delegates calls but limits the enabled protocols for any created * sockets. */ private static class LimitedProtocolsSocketFactory extends DelegatingSSLSocketFactory { private final String[] protocols; private LimitedProtocolsSocketFactory(SSLSocketFactory delegate, String... protocols) { super(delegate); this.protocols = protocols; } @Override public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { SSLSocket socket = (SSLSocket) delegate.createSocket(s, host, port, autoClose); socket.setEnabledProtocols(protocols); return socket; } @Override public SSLSocket createSocket() throws IOException { SSLSocket socket = (SSLSocket) delegate.createSocket(); socket.setEnabledProtocols(protocols); return socket; } @Override public SSLSocket createSocket(String host, int port) throws IOException, UnknownHostException { SSLSocket socket = (SSLSocket) delegate.createSocket(host, port); socket.setEnabledProtocols(protocols); return socket; } @Override public SSLSocket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { SSLSocket socket = (SSLSocket) delegate.createSocket(host, port, localHost, localPort); socket.setEnabledProtocols(protocols); return socket; } @Override public SSLSocket createSocket(InetAddress host, int port) throws IOException { SSLSocket socket = (SSLSocket) delegate.createSocket(host, port); socket.setEnabledProtocols(protocols); return socket; } @Override public SSLSocket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { SSLSocket socket = (SSLSocket) delegate.createSocket(address, port, localAddress, localPort); socket.setEnabledProtocols(protocols); return socket; } } /** * A Socket that forwards all calls to public or protected methods, except for those * that Socket inherits from Object, to a delegate. */ private static abstract class DelegatingSocket extends Socket { private final Socket delegate; public DelegatingSocket(Socket delegate) { if (delegate == null) { throw new NullPointerException(); } this.delegate = delegate; } @Override public void bind(SocketAddress bindpoint) throws IOException { delegate.bind(bindpoint); } @Override public void close() throws IOException { delegate.close(); } @Override public void connect(SocketAddress endpoint) throws IOException { delegate.connect(endpoint); } @Override public void connect(SocketAddress endpoint, int timeout) throws IOException { delegate.connect(endpoint, timeout); } @Override public SocketChannel getChannel() { return delegate.getChannel(); } @Override public FileDescriptor getFileDescriptor$() { return delegate.getFileDescriptor$(); } @Override public InetAddress getInetAddress() { return delegate.getInetAddress(); } @Override public InputStream getInputStream() throws IOException { return delegate.getInputStream(); } @Override public boolean getKeepAlive() throws SocketException { return delegate.getKeepAlive(); } @Override public InetAddress getLocalAddress() { return delegate.getLocalAddress(); } @Override public int getLocalPort() { return delegate.getLocalPort(); } @Override public SocketAddress getLocalSocketAddress() { return delegate.getLocalSocketAddress(); } @Override public boolean getOOBInline() throws SocketException { return delegate.getOOBInline(); } @Override public OutputStream getOutputStream() throws IOException { return delegate.getOutputStream(); } @Override public int getPort() { return delegate.getPort(); } @Override public int getReceiveBufferSize() throws SocketException { return delegate.getReceiveBufferSize(); } @Override public SocketAddress getRemoteSocketAddress() { return delegate.getRemoteSocketAddress(); } @Override public boolean getReuseAddress() throws SocketException { return delegate.getReuseAddress(); } @Override public int getSendBufferSize() throws SocketException { return delegate.getSendBufferSize(); } @Override public int getSoLinger() throws SocketException { return delegate.getSoLinger(); } @Override public int getSoTimeout() throws SocketException { return delegate.getSoTimeout(); } @Override public boolean getTcpNoDelay() throws SocketException { return delegate.getTcpNoDelay(); } @Override public int getTrafficClass() throws SocketException { return delegate.getTrafficClass(); } @Override public boolean isBound() { return delegate.isBound(); } @Override public boolean isClosed() { return delegate.isClosed(); } @Override public boolean isConnected() { return delegate.isConnected(); } @Override public boolean isInputShutdown() { return delegate.isInputShutdown(); } @Override public boolean isOutputShutdown() { return delegate.isOutputShutdown(); } @Override public void sendUrgentData(int data) throws IOException { delegate.sendUrgentData(data); } @Override public void setKeepAlive(boolean on) throws SocketException { delegate.setKeepAlive(on); } @Override public void setOOBInline(boolean on) throws SocketException { delegate.setOOBInline(on); } @Override public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { delegate.setPerformancePreferences(connectionTime, latency, bandwidth); } @Override public void setReceiveBufferSize(int size) throws SocketException { delegate.setReceiveBufferSize(size); } @Override public void setReuseAddress(boolean on) throws SocketException { delegate.setReuseAddress(on); } @Override public void setSendBufferSize(int size) throws SocketException { delegate.setSendBufferSize(size); } @Override public void setSoLinger(boolean on, int linger) throws SocketException { delegate.setSoLinger(on, linger); } @Override public void setSoTimeout(int timeout) throws SocketException { delegate.setSoTimeout(timeout); } @Override public void setTcpNoDelay(boolean on) throws SocketException { delegate.setTcpNoDelay(on); } @Override public void setTrafficClass(int tc) throws SocketException { delegate.setTrafficClass(tc); } @Override public void shutdownInput() throws IOException { delegate.shutdownInput(); } @Override public void shutdownOutput() throws IOException { delegate.shutdownOutput(); } @Override public String toString() { return delegate.toString(); } } /** * An {@link javax.net.ssl.SSLSocket} that delegates all calls. */ private static abstract class DelegatingSSLSocket extends SSLSocket { protected final SSLSocket delegate; public DelegatingSSLSocket(SSLSocket delegate) { this.delegate = delegate; } @Override public void shutdownInput() throws IOException { delegate.shutdownInput(); } @Override public void shutdownOutput() throws IOException { delegate.shutdownOutput(); } @Override public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); } @Override public String[] getEnabledCipherSuites() { return delegate.getEnabledCipherSuites(); } @Override public void setEnabledCipherSuites(String[] suites) { delegate.setEnabledCipherSuites(suites); } @Override public String[] getSupportedProtocols() { return delegate.getSupportedProtocols(); } @Override public String[] getEnabledProtocols() { return delegate.getEnabledProtocols(); } @Override public void setEnabledProtocols(String[] protocols) { delegate.setEnabledProtocols(protocols); } @Override public SSLSession getSession() { return delegate.getSession(); } @Override public void addHandshakeCompletedListener(HandshakeCompletedListener listener) { delegate.addHandshakeCompletedListener(listener); } @Override public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) { delegate.removeHandshakeCompletedListener(listener); } @Override public void startHandshake() throws IOException { delegate.startHandshake(); } @Override public void setUseClientMode(boolean mode) { delegate.setUseClientMode(mode); } @Override public boolean getUseClientMode() { return delegate.getUseClientMode(); } @Override public void setNeedClientAuth(boolean need) { delegate.setNeedClientAuth(need); } @Override public void setWantClientAuth(boolean want) { delegate.setWantClientAuth(want); } @Override public boolean getNeedClientAuth() { return delegate.getNeedClientAuth(); } @Override public boolean getWantClientAuth() { return delegate.getWantClientAuth(); } @Override public void setEnableSessionCreation(boolean flag) { delegate.setEnableSessionCreation(flag); } @Override public boolean getEnableSessionCreation() { return delegate.getEnableSessionCreation(); } @Override public SSLParameters getSSLParameters() { return delegate.getSSLParameters(); } @Override public void setSSLParameters(SSLParameters p) { delegate.setSSLParameters(p); } @Override public void close() throws IOException { delegate.close(); } @Override public InetAddress getInetAddress() { return delegate.getInetAddress(); } @Override public InputStream getInputStream() throws IOException { return delegate.getInputStream(); } @Override public boolean getKeepAlive() throws SocketException { return delegate.getKeepAlive(); } @Override public InetAddress getLocalAddress() { return delegate.getLocalAddress(); } @Override public int getLocalPort() { return delegate.getLocalPort(); } @Override public OutputStream getOutputStream() throws IOException { return delegate.getOutputStream(); } @Override public int getPort() { return delegate.getPort(); } @Override public int getSoLinger() throws SocketException { return delegate.getSoLinger(); } @Override public int getReceiveBufferSize() throws SocketException { return delegate.getReceiveBufferSize(); } @Override public int getSendBufferSize() throws SocketException { return delegate.getSendBufferSize(); } @Override public int getSoTimeout() throws SocketException { return delegate.getSoTimeout(); } @Override public boolean getTcpNoDelay() throws SocketException { return delegate.getTcpNoDelay(); } @Override public void setKeepAlive(boolean keepAlive) throws SocketException { delegate.setKeepAlive(keepAlive); } @Override public void setSendBufferSize(int size) throws SocketException { delegate.setSendBufferSize(size); } @Override public void setReceiveBufferSize(int size) throws SocketException { delegate.setReceiveBufferSize(size); } @Override public void setSoLinger(boolean on, int timeout) throws SocketException { delegate.setSoLinger(on, timeout); } @Override public void setSoTimeout(int timeout) throws SocketException { delegate.setSoTimeout(timeout); } @Override public void setTcpNoDelay(boolean on) throws SocketException { delegate.setTcpNoDelay(on); } @Override public String toString() { return delegate.toString(); } @Override public SocketAddress getLocalSocketAddress() { return delegate.getLocalSocketAddress(); } @Override public SocketAddress getRemoteSocketAddress() { return delegate.getRemoteSocketAddress(); } @Override public boolean isBound() { return delegate.isBound(); } @Override public boolean isConnected() { return delegate.isConnected(); } @Override public boolean isClosed() { return delegate.isClosed(); } @Override public void bind(SocketAddress localAddr) throws IOException { delegate.bind(localAddr); } @Override public void connect(SocketAddress remoteAddr) throws IOException { delegate.connect(remoteAddr); } @Override public void connect(SocketAddress remoteAddr, int timeout) throws IOException { delegate.connect(remoteAddr, timeout); } @Override public boolean isInputShutdown() { return delegate.isInputShutdown(); } @Override public boolean isOutputShutdown() { return delegate.isOutputShutdown(); } @Override public void setReuseAddress(boolean reuse) throws SocketException { delegate.setReuseAddress(reuse); } @Override public boolean getReuseAddress() throws SocketException { return delegate.getReuseAddress(); } @Override public void setOOBInline(boolean oobinline) throws SocketException { delegate.setOOBInline(oobinline); } @Override public boolean getOOBInline() throws SocketException { return delegate.getOOBInline(); } @Override public void setTrafficClass(int value) throws SocketException { delegate.setTrafficClass(value); } @Override public int getTrafficClass() throws SocketException { return delegate.getTrafficClass(); } @Override public void sendUrgentData(int value) throws IOException { delegate.sendUrgentData(value); } @Override public SocketChannel getChannel() { return delegate.getChannel(); } @Override public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { delegate.setPerformancePreferences(connectionTime, latency, bandwidth); } } /** * An SSLSocketFactory that delegates calls. It keeps a record of any sockets created. * If {@link #disableTlsFallbackScsv} is set to {@code true} then sockets created by the * delegate are wrapped with ones that will not accept the {@link #TLS_FALLBACK_SCSV} cipher, * thus bypassing server-side fallback checks on platforms that support it. Unfortunately this * wrapping will disable any reflection-based calls to SSLSocket from Platform. */ private static class FallbackTestClientSocketFactory extends DelegatingSSLSocketFactory { /** * The cipher suite used during TLS connection fallback to indicate a fallback. * See https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00 */ public static final String TLS_FALLBACK_SCSV = "TLS_FALLBACK_SCSV"; private final boolean disableTlsFallbackScsv; private final List createdSockets = new ArrayList(); public FallbackTestClientSocketFactory(SSLSocketFactory delegate, boolean disableTlsFallbackScsv) { super(delegate); this.disableTlsFallbackScsv = disableTlsFallbackScsv; } @Override public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { SSLSocket socket = super.createSocket(s, host, port, autoClose); if (disableTlsFallbackScsv) { socket = new TlsFallbackDisabledScsvSSLSocket(socket); } createdSockets.add(socket); return socket; } @Override public SSLSocket createSocket() throws IOException { SSLSocket socket = super.createSocket(); if (disableTlsFallbackScsv) { socket = new TlsFallbackDisabledScsvSSLSocket(socket); } createdSockets.add(socket); return socket; } @Override public SSLSocket createSocket(String host,int port) throws IOException { SSLSocket socket = super.createSocket(host, port); if (disableTlsFallbackScsv) { socket = new TlsFallbackDisabledScsvSSLSocket(socket); } createdSockets.add(socket); return socket; } @Override public SSLSocket createSocket(String host,int port, InetAddress localHost, int localPort) throws IOException { SSLSocket socket = super.createSocket(host, port, localHost, localPort); if (disableTlsFallbackScsv) { socket = new TlsFallbackDisabledScsvSSLSocket(socket); } createdSockets.add(socket); return socket; } @Override public SSLSocket createSocket(InetAddress host,int port) throws IOException { SSLSocket socket = super.createSocket(host, port); if (disableTlsFallbackScsv) { socket = new TlsFallbackDisabledScsvSSLSocket(socket); } createdSockets.add(socket); return socket; } @Override public SSLSocket createSocket(InetAddress address,int port, InetAddress localAddress, int localPort) throws IOException { SSLSocket socket = super.createSocket(address, port, localAddress, localPort); if (disableTlsFallbackScsv) { socket = new TlsFallbackDisabledScsvSSLSocket(socket); } createdSockets.add(socket); return socket; } public List getCreatedSockets() { return createdSockets; } } private static class TlsFallbackDisabledScsvSSLSocket extends DelegatingSSLSocket { private boolean tlsFallbackScsvSet; public TlsFallbackDisabledScsvSSLSocket(SSLSocket socket) { super(socket); } @Override public void setEnabledCipherSuites(String[] suites) { List enabledCipherSuites = new ArrayList(suites.length); for (String suite : suites) { if (suite.equals(FallbackTestClientSocketFactory.TLS_FALLBACK_SCSV)) { // Record that an attempt was made to set TLS_FALLBACK_SCSV, but don't actually // set it. tlsFallbackScsvSet = true; } else { enabledCipherSuites.add(suite); } } delegate.setEnabledCipherSuites( enabledCipherSuites.toArray(new String[enabledCipherSuites.size()])); } public boolean wasTlsFallbackScsvSet() { return tlsFallbackScsvSet; } } }