1 /* 2 * Copyright (C) 2013 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.squareup.okhttp.internal.framed; 17 18 import com.squareup.okhttp.Cache; 19 import com.squareup.okhttp.ConnectionPool; 20 import com.squareup.okhttp.HttpUrl; 21 import com.squareup.okhttp.OkHttpClient; 22 import com.squareup.okhttp.OkUrlFactory; 23 import com.squareup.okhttp.Protocol; 24 import com.squareup.okhttp.internal.RecordingAuthenticator; 25 import com.squareup.okhttp.internal.SslContextBuilder; 26 import com.squareup.okhttp.internal.Util; 27 import com.squareup.okhttp.mockwebserver.MockResponse; 28 import com.squareup.okhttp.mockwebserver.MockWebServer; 29 import com.squareup.okhttp.mockwebserver.RecordedRequest; 30 import com.squareup.okhttp.mockwebserver.SocketPolicy; 31 import com.squareup.okhttp.testing.RecordingHostnameVerifier; 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.net.Authenticator; 35 import java.net.CookieManager; 36 import java.net.HttpURLConnection; 37 import java.net.SocketTimeoutException; 38 import java.util.Arrays; 39 import java.util.Collections; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.concurrent.CountDownLatch; 43 import java.util.concurrent.ExecutorService; 44 import java.util.concurrent.Executors; 45 import java.util.concurrent.TimeUnit; 46 import javax.net.ssl.HostnameVerifier; 47 import javax.net.ssl.SSLContext; 48 import okio.Buffer; 49 import okio.BufferedSink; 50 import okio.GzipSink; 51 import okio.Okio; 52 import org.junit.After; 53 import org.junit.Before; 54 import org.junit.Ignore; 55 import org.junit.Rule; 56 import org.junit.Test; 57 import org.junit.rules.TemporaryFolder; 58 59 import static java.util.concurrent.TimeUnit.SECONDS; 60 import static org.junit.Assert.assertArrayEquals; 61 import static org.junit.Assert.assertEquals; 62 import static org.junit.Assert.assertNull; 63 import static org.junit.Assert.fail; 64 65 /** Test how SPDY interacts with HTTP features. */ 66 public abstract class HttpOverSpdyTest { 67 @Rule public final TemporaryFolder tempDir = new TemporaryFolder(); 68 @Rule public final MockWebServer server = new MockWebServer(); 69 70 /** Protocol to test, for example {@link com.squareup.okhttp.Protocol#SPDY_3} */ 71 private final Protocol protocol; 72 protected String hostHeader = ":host"; 73 74 protected SSLContext sslContext = SslContextBuilder.localhost(); 75 protected HostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); 76 protected final OkUrlFactory client = new OkUrlFactory(new OkHttpClient()); 77 protected HttpURLConnection connection; 78 protected Cache cache; 79 80 protected HttpOverSpdyTest(Protocol protocol){ 81 this.protocol = protocol; 82 } 83 84 @Before public void setUp() throws Exception { 85 server.useHttps(sslContext.getSocketFactory(), false); 86 client.client().setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1)); 87 client.client().setSslSocketFactory(sslContext.getSocketFactory()); 88 client.client().setHostnameVerifier(hostnameVerifier); 89 cache = new Cache(tempDir.getRoot(), Integer.MAX_VALUE); 90 } 91 92 @After public void tearDown() throws Exception { 93 Authenticator.setDefault(null); 94 } 95 96 @Test public void get() throws Exception { 97 MockResponse response = new MockResponse().setBody("ABCDE").setStatus("HTTP/1.1 200 Sweet"); 98 server.enqueue(response); 99 100 connection = client.open(server.getUrl("/foo")); 101 assertContent("ABCDE", connection, Integer.MAX_VALUE); 102 assertEquals(200, connection.getResponseCode()); 103 assertEquals("Sweet", connection.getResponseMessage()); 104 105 RecordedRequest request = server.takeRequest(); 106 assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); 107 assertEquals("https", request.getHeader(":scheme")); 108 assertEquals(server.getHostName() + ":" + server.getPort(), request.getHeader(hostHeader)); 109 } 110 111 @Test public void emptyResponse() throws IOException { 112 server.enqueue(new MockResponse()); 113 114 connection = client.open(server.getUrl("/foo")); 115 assertEquals(-1, connection.getInputStream().read()); 116 } 117 118 byte[] postBytes = "FGHIJ".getBytes(Util.UTF_8); 119 120 @Test public void noDefaultContentLengthOnStreamingPost() throws Exception { 121 server.enqueue(new MockResponse().setBody("ABCDE")); 122 123 connection = client.open(server.getUrl("/foo")); 124 connection.setDoOutput(true); 125 connection.setChunkedStreamingMode(0); 126 connection.getOutputStream().write(postBytes); 127 assertContent("ABCDE", connection, Integer.MAX_VALUE); 128 129 RecordedRequest request = server.takeRequest(); 130 assertEquals("POST /foo HTTP/1.1", request.getRequestLine()); 131 assertArrayEquals(postBytes, request.getBody().readByteArray()); 132 assertNull(request.getHeader("Content-Length")); 133 } 134 135 @Test public void userSuppliedContentLengthHeader() throws Exception { 136 server.enqueue(new MockResponse().setBody("ABCDE")); 137 138 connection = client.open(server.getUrl("/foo")); 139 connection.setRequestProperty("Content-Length", String.valueOf(postBytes.length)); 140 connection.setDoOutput(true); 141 connection.getOutputStream().write(postBytes); 142 assertContent("ABCDE", connection, Integer.MAX_VALUE); 143 144 RecordedRequest request = server.takeRequest(); 145 assertEquals("POST /foo HTTP/1.1", request.getRequestLine()); 146 assertArrayEquals(postBytes, request.getBody().readByteArray()); 147 assertEquals(postBytes.length, Integer.parseInt(request.getHeader("Content-Length"))); 148 } 149 150 @Test public void closeAfterFlush() throws Exception { 151 server.enqueue(new MockResponse().setBody("ABCDE")); 152 153 connection = client.open(server.getUrl("/foo")); 154 connection.setRequestProperty("Content-Length", String.valueOf(postBytes.length)); 155 connection.setDoOutput(true); 156 connection.getOutputStream().write(postBytes); // push bytes into SpdyDataOutputStream.buffer 157 connection.getOutputStream().flush(); // FramedConnection.writeData subject to write window 158 connection.getOutputStream().close(); // FramedConnection.writeData empty frame 159 assertContent("ABCDE", connection, Integer.MAX_VALUE); 160 161 RecordedRequest request = server.takeRequest(); 162 assertEquals("POST /foo HTTP/1.1", request.getRequestLine()); 163 assertArrayEquals(postBytes, request.getBody().readByteArray()); 164 assertEquals(postBytes.length, Integer.parseInt(request.getHeader("Content-Length"))); 165 } 166 167 @Test public void setFixedLengthStreamingModeSetsContentLength() throws Exception { 168 server.enqueue(new MockResponse().setBody("ABCDE")); 169 170 connection = client.open(server.getUrl("/foo")); 171 connection.setFixedLengthStreamingMode(postBytes.length); 172 connection.setDoOutput(true); 173 connection.getOutputStream().write(postBytes); 174 assertContent("ABCDE", connection, Integer.MAX_VALUE); 175 176 RecordedRequest request = server.takeRequest(); 177 assertEquals("POST /foo HTTP/1.1", request.getRequestLine()); 178 assertArrayEquals(postBytes, request.getBody().readByteArray()); 179 assertEquals(postBytes.length, Integer.parseInt(request.getHeader("Content-Length"))); 180 } 181 182 @Test public void spdyConnectionReuse() throws Exception { 183 server.enqueue(new MockResponse().setBody("ABCDEF")); 184 server.enqueue(new MockResponse().setBody("GHIJKL")); 185 186 HttpURLConnection connection1 = client.open(server.getUrl("/r1")); 187 HttpURLConnection connection2 = client.open(server.getUrl("/r2")); 188 assertEquals("ABC", readAscii(connection1.getInputStream(), 3)); 189 assertEquals("GHI", readAscii(connection2.getInputStream(), 3)); 190 assertEquals("DEF", readAscii(connection1.getInputStream(), 3)); 191 assertEquals("JKL", readAscii(connection2.getInputStream(), 3)); 192 assertEquals(0, server.takeRequest().getSequenceNumber()); 193 assertEquals(1, server.takeRequest().getSequenceNumber()); 194 } 195 196 @Test @Ignore public void synchronousSpdyRequest() throws Exception { 197 server.enqueue(new MockResponse().setBody("A")); 198 server.enqueue(new MockResponse().setBody("A")); 199 200 ExecutorService executor = Executors.newCachedThreadPool(); 201 CountDownLatch countDownLatch = new CountDownLatch(2); 202 executor.execute(new SpdyRequest("/r1", countDownLatch)); 203 executor.execute(new SpdyRequest("/r2", countDownLatch)); 204 countDownLatch.await(); 205 assertEquals(0, server.takeRequest().getSequenceNumber()); 206 assertEquals(1, server.takeRequest().getSequenceNumber()); 207 } 208 209 @Test public void gzippedResponseBody() throws Exception { 210 server.enqueue( 211 new MockResponse().addHeader("Content-Encoding: gzip").setBody(gzip("ABCABCABC"))); 212 assertContent("ABCABCABC", client.open(server.getUrl("/r1")), Integer.MAX_VALUE); 213 } 214 215 @Test public void authenticate() throws Exception { 216 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_UNAUTHORIZED) 217 .addHeader("www-authenticate: Basic realm=\"protected area\"") 218 .setBody("Please authenticate.")); 219 server.enqueue(new MockResponse().setBody("Successful auth!")); 220 221 Authenticator.setDefault(new RecordingAuthenticator()); 222 connection = client.open(server.getUrl("/")); 223 assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); 224 225 RecordedRequest denied = server.takeRequest(); 226 assertNull(denied.getHeader("Authorization")); 227 RecordedRequest accepted = server.takeRequest(); 228 assertEquals("GET / HTTP/1.1", accepted.getRequestLine()); 229 assertEquals("Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS, 230 accepted.getHeader("Authorization")); 231 } 232 233 @Test public void redirect() throws Exception { 234 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) 235 .addHeader("Location: /foo") 236 .setBody("This page has moved!")); 237 server.enqueue(new MockResponse().setBody("This is the new location!")); 238 239 connection = client.open(server.getUrl("/")); 240 assertContent("This is the new location!", connection, Integer.MAX_VALUE); 241 242 RecordedRequest request1 = server.takeRequest(); 243 assertEquals("/", request1.getPath()); 244 RecordedRequest request2 = server.takeRequest(); 245 assertEquals("/foo", request2.getPath()); 246 } 247 248 @Test public void readAfterLastByte() throws Exception { 249 server.enqueue(new MockResponse().setBody("ABC")); 250 251 connection = client.open(server.getUrl("/")); 252 InputStream in = connection.getInputStream(); 253 assertEquals("ABC", readAscii(in, 3)); 254 assertEquals(-1, in.read()); 255 assertEquals(-1, in.read()); 256 } 257 258 @Ignore // See https://github.com/square/okhttp/issues/578 259 @Test(timeout = 3000) public void readResponseHeaderTimeout() throws Exception { 260 server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE)); 261 server.enqueue(new MockResponse().setBody("A")); 262 263 connection = client.open(server.getUrl("/")); 264 connection.setReadTimeout(1000); 265 assertContent("A", connection, Integer.MAX_VALUE); 266 } 267 268 /** 269 * Test to ensure we don't throw a read timeout on responses that are 270 * progressing. For this case, we take a 4KiB body and throttle it to 271 * 1KiB/second. We set the read timeout to two seconds. If our 272 * implementation is acting correctly, it will not throw, as it is 273 * progressing. 274 */ 275 @Test public void readTimeoutMoreGranularThanBodySize() throws Exception { 276 char[] body = new char[4096]; // 4KiB to read 277 Arrays.fill(body, 'y'); 278 server.enqueue(new MockResponse().setBody(new String(body)).throttleBody(1024, 1, SECONDS)); // slow connection 1KiB/second 279 280 connection = client.open(server.getUrl("/")); 281 connection.setReadTimeout(2000); // 2 seconds to read something. 282 assertContent(new String(body), connection, Integer.MAX_VALUE); 283 } 284 285 /** 286 * Test to ensure we throw a read timeout on responses that are progressing 287 * too slowly. For this case, we take a 2KiB body and throttle it to 288 * 1KiB/second. We set the read timeout to half a second. If our 289 * implementation is acting correctly, it will throw, as a byte doesn't 290 * arrive in time. 291 */ 292 @Test public void readTimeoutOnSlowConnection() throws Exception { 293 char[] body = new char[2048]; // 2KiB to read 294 Arrays.fill(body, 'y'); 295 server.enqueue(new MockResponse() 296 .setBody(new String(body)) 297 .throttleBody(1024, 1, SECONDS)); // slow connection 1KiB/second 298 299 connection = client.open(server.getUrl("/")); 300 connection.setReadTimeout(500); // half a second to read something 301 connection.connect(); 302 try { 303 readAscii(connection.getInputStream(), Integer.MAX_VALUE); 304 fail("Should have timed out!"); 305 } catch (SocketTimeoutException expected) { 306 assertEquals("timeout", expected.getMessage()); 307 } 308 } 309 310 @Test public void spdyConnectionTimeout() throws Exception { 311 MockResponse response = new MockResponse().setBody("A"); 312 response.setBodyDelay(1, TimeUnit.SECONDS); 313 server.enqueue(response); 314 315 HttpURLConnection connection1 = client.open(server.getUrl("/")); 316 connection1.setReadTimeout(2000); 317 HttpURLConnection connection2 = client.open(server.getUrl("/")); 318 connection2.setReadTimeout(200); 319 connection1.connect(); 320 connection2.connect(); 321 assertContent("A", connection1, Integer.MAX_VALUE); 322 } 323 324 @Test public void responsesAreCached() throws IOException { 325 client.client().setCache(cache); 326 327 server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("A")); 328 329 assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); 330 assertEquals(1, cache.getRequestCount()); 331 assertEquals(1, cache.getNetworkCount()); 332 assertEquals(0, cache.getHitCount()); 333 assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); 334 assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); 335 assertEquals(3, cache.getRequestCount()); 336 assertEquals(1, cache.getNetworkCount()); 337 assertEquals(2, cache.getHitCount()); 338 } 339 340 @Test public void conditionalCache() throws IOException { 341 client.client().setCache(cache); 342 343 server.enqueue(new MockResponse().addHeader("ETag: v1").setBody("A")); 344 server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); 345 346 assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); 347 assertEquals(1, cache.getRequestCount()); 348 assertEquals(1, cache.getNetworkCount()); 349 assertEquals(0, cache.getHitCount()); 350 assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); 351 assertEquals(2, cache.getRequestCount()); 352 assertEquals(2, cache.getNetworkCount()); 353 assertEquals(1, cache.getHitCount()); 354 } 355 356 @Test public void responseCachedWithoutConsumingFullBody() throws IOException { 357 client.client().setCache(cache); 358 359 server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("ABCD")); 360 server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("EFGH")); 361 362 HttpURLConnection connection1 = client.open(server.getUrl("/")); 363 InputStream in1 = connection1.getInputStream(); 364 assertEquals("AB", readAscii(in1, 2)); 365 in1.close(); 366 367 HttpURLConnection connection2 = client.open(server.getUrl("/")); 368 InputStream in2 = connection2.getInputStream(); 369 assertEquals("ABCD", readAscii(in2, Integer.MAX_VALUE)); 370 in2.close(); 371 } 372 373 @Test public void acceptAndTransmitCookies() throws Exception { 374 CookieManager cookieManager = new CookieManager(); 375 client.client().setCookieHandler(cookieManager); 376 377 server.enqueue(new MockResponse() 378 .addHeader("set-cookie: c=oreo; domain=" + server.getCookieDomain()) 379 .setBody("A")); 380 server.enqueue(new MockResponse() 381 .setBody("B")); 382 383 HttpUrl url = server.url("/"); 384 assertContent("A", client.open(url.url()), Integer.MAX_VALUE); 385 Map<String, List<String>> requestHeaders = Collections.emptyMap(); 386 assertEquals(Collections.singletonMap("Cookie", Arrays.asList("c=oreo")), 387 cookieManager.get(url.uri(), requestHeaders)); 388 389 assertContent("B", client.open(url.url()), Integer.MAX_VALUE); 390 RecordedRequest requestA = server.takeRequest(); 391 assertNull(requestA.getHeader("Cookie")); 392 RecordedRequest requestB = server.takeRequest(); 393 assertEquals("c=oreo", requestB.getHeader("Cookie")); 394 } 395 396 /** https://github.com/square/okhttp/issues/1191 */ 397 @Test public void disconnectWithStreamNotEstablished() throws Exception { 398 ConnectionPool connectionPool = new ConnectionPool(5, 5000); 399 client.client().setConnectionPool(connectionPool); 400 401 server.enqueue(new MockResponse().setBody("abc")); 402 403 // Disconnect before the stream is created. A connection is still established! 404 HttpURLConnection connection1 = client.open(server.getUrl("/")); 405 connection1.connect(); 406 connection1.disconnect(); 407 408 // That connection is pooled, and it works. 409 assertEquals(1, connectionPool.getSpdyConnectionCount()); 410 HttpURLConnection connection2 = client.open(server.getUrl("/")); 411 assertContent("abc", connection2, 3); 412 assertEquals(0, server.takeRequest().getSequenceNumber()); 413 } 414 415 void assertContent(String expected, HttpURLConnection connection, int limit) 416 throws IOException { 417 connection.connect(); 418 assertEquals(expected, readAscii(connection.getInputStream(), limit)); 419 } 420 421 private String readAscii(InputStream in, int count) throws IOException { 422 StringBuilder result = new StringBuilder(); 423 for (int i = 0; i < count; i++) { 424 int value = in.read(); 425 if (value == -1) { 426 in.close(); 427 break; 428 } 429 result.append((char) value); 430 } 431 return result.toString(); 432 } 433 434 public Buffer gzip(String bytes) throws IOException { 435 Buffer bytesOut = new Buffer(); 436 BufferedSink sink = Okio.buffer(new GzipSink(bytesOut)); 437 sink.writeUtf8(bytes); 438 sink.close(); 439 return bytesOut; 440 } 441 442 class SpdyRequest implements Runnable { 443 String path; 444 CountDownLatch countDownLatch; 445 public SpdyRequest(String path, CountDownLatch countDownLatch) { 446 this.path = path; 447 this.countDownLatch = countDownLatch; 448 } 449 450 @Override public void run() { 451 try { 452 HttpURLConnection conn = client.open(server.getUrl(path)); 453 assertEquals("A", readAscii(conn.getInputStream(), 1)); 454 countDownLatch.countDown(); 455 } catch (Exception e) { 456 throw new RuntimeException(e); 457 } 458 } 459 } 460 } 461