/* * Copyright (C) 2013 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.okhttp.internal.spdy; import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.RecordedRequest; import com.squareup.okhttp.HttpResponseCache; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.internal.RecordingAuthenticator; import com.squareup.okhttp.internal.SslContextBuilder; import com.squareup.okhttp.internal.Util; import com.squareup.okhttp.internal.mockspdyserver.MockSpdyServer; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Authenticator; import java.net.CookieManager; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; import java.security.GeneralSecurityException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.zip.GZIPOutputStream; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** Test how SPDY interacts with HTTP features. */ public final class HttpOverSpdyTest { private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() { public boolean verify(String hostname, SSLSession session) { return true; } }; private static final SSLContext sslContext; static { try { sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build(); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } catch (UnknownHostException e) { throw new RuntimeException(e); } } private final MockSpdyServer server = new MockSpdyServer(sslContext.getSocketFactory()); private final String hostName = server.getHostName(); private final OkHttpClient client = new OkHttpClient(); private HttpResponseCache cache; @Before public void setUp() throws Exception { client.setSslSocketFactory(sslContext.getSocketFactory()); client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); String systemTmpDir = System.getProperty("java.io.tmpdir"); File cacheDir = new File(systemTmpDir, "HttpCache-" + UUID.randomUUID()); cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); } @After public void tearDown() throws Exception { server.shutdown(); } @Test public void get() throws Exception { MockResponse response = new MockResponse().setBody("ABCDE").setStatus("HTTP/1.1 200 Sweet"); server.enqueue(response); server.play(); HttpURLConnection connection = client.open(server.getUrl("/foo")); assertContent("ABCDE", connection, Integer.MAX_VALUE); assertEquals(200, connection.getResponseCode()); assertEquals("Sweet", connection.getResponseMessage()); RecordedRequest request = server.takeRequest(); assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); assertContains(request.getHeaders(), ":scheme: https"); assertContains(request.getHeaders(), ":host: " + hostName + ":" + server.getPort()); } @Test public void emptyResponse() throws IOException { server.enqueue(new MockResponse()); server.play(); HttpURLConnection connection = client.open(server.getUrl("/foo")); assertEquals(-1, connection.getInputStream().read()); } @Test public void post() throws Exception { MockResponse response = new MockResponse().setBody("ABCDE"); server.enqueue(response); server.play(); HttpURLConnection connection = client.open(server.getUrl("/foo")); connection.setDoOutput(true); connection.getOutputStream().write("FGHIJ".getBytes(Util.UTF_8)); assertContent("ABCDE", connection, Integer.MAX_VALUE); RecordedRequest request = server.takeRequest(); assertEquals("POST /foo HTTP/1.1", request.getRequestLine()); assertEquals("FGHIJ", request.getUtf8Body()); } @Test public void spdyConnectionReuse() throws Exception { server.enqueue(new MockResponse().setBody("ABCDEF")); server.enqueue(new MockResponse().setBody("GHIJKL")); server.play(); HttpURLConnection connection1 = client.open(server.getUrl("/r1")); HttpURLConnection connection2 = client.open(server.getUrl("/r2")); assertEquals("ABC", readAscii(connection1.getInputStream(), 3)); assertEquals("GHI", readAscii(connection2.getInputStream(), 3)); assertEquals("DEF", readAscii(connection1.getInputStream(), 3)); assertEquals("JKL", readAscii(connection2.getInputStream(), 3)); assertEquals(0, server.takeRequest().getSequenceNumber()); assertEquals(0, server.takeRequest().getSequenceNumber()); } @Test public void gzippedResponseBody() throws Exception { server.enqueue(new MockResponse().addHeader("Content-Encoding: gzip") .setBody(gzip("ABCABCABC".getBytes(Util.UTF_8)))); server.play(); assertContent("ABCABCABC", client.open(server.getUrl("/r1")), Integer.MAX_VALUE); } @Test public void authenticate() throws Exception { server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_UNAUTHORIZED) .addHeader("www-authenticate: Basic realm=\"protected area\"") .setBody("Please authenticate.")); server.enqueue(new MockResponse().setBody("Successful auth!")); server.play(); Authenticator.setDefault(new RecordingAuthenticator()); HttpURLConnection connection = client.open(server.getUrl("/")); assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); RecordedRequest denied = server.takeRequest(); assertContainsNoneMatching(denied.getHeaders(), "authorization: Basic .*"); RecordedRequest accepted = server.takeRequest(); assertEquals("GET / HTTP/1.1", accepted.getRequestLine()); assertContains(accepted.getHeaders(), "authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS); } @Test public void redirect() throws Exception { 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(); HttpURLConnection connection = client.open(server.getUrl("/")); assertContent("This is the new location!", connection, Integer.MAX_VALUE); RecordedRequest request1 = server.takeRequest(); assertEquals("/", request1.getPath()); RecordedRequest request2 = server.takeRequest(); assertEquals("/foo", request2.getPath()); } @Test public void readAfterLastByte() throws Exception { server.enqueue(new MockResponse().setBody("ABC")); server.play(); HttpURLConnection connection = client.open(server.getUrl("/")); InputStream in = connection.getInputStream(); assertEquals("ABC", readAscii(in, 3)); assertEquals(-1, in.read()); assertEquals(-1, in.read()); } @Test public void responsesAreCached() throws IOException { client.setResponseCache(cache); server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("A")); server.play(); assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); assertEquals(1, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); assertEquals(3, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(2, cache.getHitCount()); } @Test public void conditionalCache() throws IOException { client.setResponseCache(cache); server.enqueue(new MockResponse().addHeader("ETag: v1").setBody("A")); server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); server.play(); assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); assertEquals(1, cache.getRequestCount()); assertEquals(1, cache.getNetworkCount()); assertEquals(0, cache.getHitCount()); assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); assertEquals(2, cache.getRequestCount()); assertEquals(2, cache.getNetworkCount()); assertEquals(1, cache.getHitCount()); } @Test public void acceptAndTransmitCookies() throws Exception { CookieManager cookieManager = new CookieManager(); client.setCookieHandler(cookieManager); server.enqueue( new MockResponse().addHeader("set-cookie: c=oreo; domain=" + server.getCookieDomain()) .setBody("A")); server.enqueue(new MockResponse().setBody("B")); server.play(); URL url = server.getUrl("/"); assertContent("A", client.open(url), Integer.MAX_VALUE); Map> requestHeaders = Collections.emptyMap(); assertEquals(Collections.singletonMap("Cookie", Arrays.asList("c=oreo")), cookieManager.get(url.toURI(), requestHeaders)); assertContent("B", client.open(url), Integer.MAX_VALUE); RecordedRequest requestA = server.takeRequest(); assertContainsNoneMatching(requestA.getHeaders(), "Cookie.*"); RecordedRequest requestB = server.takeRequest(); assertContains(requestB.getHeaders(), "cookie: c=oreo"); } private void assertContains(Collection collection, T value) { assertTrue(collection.toString(), collection.contains(value)); } private void assertContent(String expected, URLConnection connection, int limit) throws IOException { connection.connect(); assertEquals(expected, readAscii(connection.getInputStream(), limit)); ((HttpURLConnection) connection).disconnect(); } private void assertContainsNoneMatching(List headers, String pattern) { for (String header : headers) { if (header.matches(pattern)) { fail("Header " + header + " matches " + pattern); } } } 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 byte[] gzip(byte[] bytes) throws IOException { ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); OutputStream gzippedOut = new GZIPOutputStream(bytesOut); gzippedOut.write(bytes); gzippedOut.close(); return bytesOut.toByteArray(); } }