All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jvmTest.okhttp3.ConnectionCoalescingTest Maven / Gradle / Ivy

There is a newer version: 5.0.0-alpha.14
Show newest version
/*
 * Copyright (C) 2017 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 okhttp3;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.X509TrustManager;
import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import okhttp3.testing.PlatformRule;
import okhttp3.tls.HandshakeCertificates;
import okhttp3.tls.HeldCertificate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;

@Tag("Slowish")
public final class ConnectionCoalescingTest {
  @RegisterExtension public final PlatformRule platform = new PlatformRule();
  @RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();

  private MockWebServer server;
  private OkHttpClient client;
  private HeldCertificate rootCa;
  private HeldCertificate certificate;
  private final FakeDns dns = new FakeDns();
  private HttpUrl url;
  private List serverIps;

  @BeforeEach public void setUp(MockWebServer server) throws Exception {
    this.server = server;

    platform.assumeHttp2Support();
    platform.assumeNotBouncyCastle();

    rootCa = new HeldCertificate.Builder()
        .serialNumber(1L)
        .certificateAuthority(0)
        .commonName("root")
        .build();
    certificate = new HeldCertificate.Builder()
        .signedBy(rootCa)
        .serialNumber(2L)
        .commonName(server.getHostName())
        .addSubjectAlternativeName(server.getHostName())
        .addSubjectAlternativeName("san.com")
        .addSubjectAlternativeName("*.wildcard.com")
        .addSubjectAlternativeName("differentdns.com")
        .build();

    serverIps = Dns.SYSTEM.lookup(server.getHostName());

    dns.set(server.getHostName(), serverIps);
    dns.set("san.com", serverIps);
    dns.set("nonsan.com", serverIps);
    dns.set("www.wildcard.com", serverIps);
    dns.set("differentdns.com", Collections.emptyList());

    HandshakeCertificates handshakeCertificates = new HandshakeCertificates.Builder()
        .addTrustedCertificate(rootCa.certificate())
        .build();

    client = clientTestRule.newClientBuilder()
        .fastFallback(false) // Avoid data races.
        .dns(dns)
        .sslSocketFactory(
            handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager())
        .build();

    HandshakeCertificates serverHandshakeCertificates = new HandshakeCertificates.Builder()
        .heldCertificate(certificate)
        .build();
    server.useHttps(serverHandshakeCertificates.sslSocketFactory());

    url = server.url("/robots.txt");
  }

  /**
   * Test connecting to the main host then an alternative, although only subject alternative names
   * are used if present no special consideration of common name.
   */
  @Test public void commonThenAlternative() throws Exception {
    server.enqueue(new MockResponse());
    server.enqueue(new MockResponse());

    assert200Http2Response(execute(url), server.getHostName());

    HttpUrl sanUrl = url.newBuilder().host("san.com").build();
    assert200Http2Response(execute(sanUrl), "san.com");

    assertThat(client.connectionPool().connectionCount()).isEqualTo(1);
  }

  /**
   * Test connecting to an alternative host then common name, although only subject alternative
   * names are used if present no special consideration of common name.
   */
  @Test public void alternativeThenCommon() throws Exception {
    server.enqueue(new MockResponse());
    server.enqueue(new MockResponse());

    HttpUrl sanUrl = url.newBuilder().host("san.com").build();
    assert200Http2Response(execute(sanUrl), "san.com");

    assert200Http2Response(execute(url), server.getHostName());

    assertThat(client.connectionPool().connectionCount()).isEqualTo(1);
  }

  /** Test a previously coalesced connection that's no longer healthy. */
  @Test public void staleCoalescedConnection() throws Exception {
    server.enqueue(new MockResponse());
    server.enqueue(new MockResponse());

    AtomicReference connection = new AtomicReference<>();
    client = client.newBuilder()
        .addNetworkInterceptor(chain -> {
          connection.set(chain.connection());
          return chain.proceed(chain.request());
        })
        .build();
    dns.set("san.com", Dns.SYSTEM.lookup(server.getHostName()).subList(0, 1));

    assert200Http2Response(execute(url), server.getHostName());

    // Simulate a stale connection in the pool.
    connection.get().socket().close();

    HttpUrl sanUrl = url.newBuilder().host("san.com").build();
    assert200Http2Response(execute(sanUrl), "san.com");

    assertThat(client.connectionPool().connectionCount()).isEqualTo(1);
  }

  /**
   * This is an extraordinary test case. Here's what it's trying to simulate.
   * - 2 requests happen concurrently to a host that can be coalesced onto a single connection.
   * - Both request discover no existing connection. They both make a connection.
   * - The first request "wins the race".
   * - The second request discovers it "lost the race" and closes the connection it just opened.
   * - The second request uses the coalesced connection from request1.
   * - The coalesced connection is violently closed after servicing the first request.
   * - The second request discovers the coalesced connection is unhealthy just after acquiring it.
   */
  @Test public void coalescedConnectionDestroyedAfterAcquire() throws Exception {
    server.enqueue(new MockResponse());
    server.enqueue(new MockResponse());

    dns.set("san.com", Dns.SYSTEM.lookup(server.getHostName()).subList(0, 1));
    HttpUrl sanUrl = url.newBuilder().host("san.com").build();

    CountDownLatch latch1 = new CountDownLatch(1);
    CountDownLatch latch2 = new CountDownLatch(1);
    CountDownLatch latch3 = new CountDownLatch(1);
    CountDownLatch latch4 = new CountDownLatch(1);
    EventListener listener1 = new EventListener() {
      @Override public void connectStart(Call call, InetSocketAddress inetSocketAddress,
          Proxy proxy) {
        try {
          // Wait for request2 to guarantee we make 2 separate connections to the server.
          latch1.await();
        } catch (InterruptedException e) {
          throw new AssertionError(e);
        }
      }

      @Override public void connectionAcquired(Call call, Connection connection) {
        // We have the connection and it's in the pool. Let request2 proceed to make a connection.
        latch2.countDown();
      }
    };

    EventListener request2Listener = new EventListener() {
      @Override public void connectStart(Call call, InetSocketAddress inetSocketAddress,
          Proxy proxy) {
        // Let request1 proceed to make a connection.
        latch1.countDown();
        try {
          // Wait until request1 makes the connection and puts it in the connection pool.
          latch2.await();
        } catch (InterruptedException e) {
          throw new AssertionError(e);
        }
      }

      @Override public void connectionAcquired(Call call, Connection connection) {
        // We obtained the coalesced connection. Let request1 violently destroy it.
        latch3.countDown();
        try {
          latch4.await();
        } catch (InterruptedException e) {
          throw new AssertionError(e);
        }
      }
    };

    // Get a reference to the connection so we can violently destroy it.
    AtomicReference connection = new AtomicReference<>();
    OkHttpClient client1 = client.newBuilder()
        .addNetworkInterceptor(chain -> {
          connection.set(chain.connection());
          return chain.proceed(chain.request());
        })
        .eventListenerFactory(clientTestRule.wrap(listener1))
        .build();

    Request request = new Request.Builder().url(sanUrl).build();
    Call call1 = client1.newCall(request);
    call1.enqueue(new Callback() {
      @Override public void onResponse(Call call, Response response) throws IOException {
        try {
          // Wait until request2 acquires the connection before we destroy it violently.
          latch3.await();
        } catch (InterruptedException e) {
          throw new AssertionError(e);
        }
        assert200Http2Response(response, "san.com");
        connection.get().socket().close();
        latch4.countDown();
      }

      @Override public void onFailure(Call call, IOException e) {
        fail();
      }
    });

    OkHttpClient client2 = client.newBuilder()
        .eventListenerFactory(clientTestRule.wrap(request2Listener))
        .build();
    Call call2 = client2.newCall(request);
    Response response = call2.execute();

    assert200Http2Response(response, "san.com");
  }

  /** If the existing connection matches a SAN but not a match for DNS then skip. */
  @Test public void skipsWhenDnsDontMatch() throws Exception {
    server.enqueue(new MockResponse());

    assert200Http2Response(execute(url), server.getHostName());

    HttpUrl differentDnsUrl = url.newBuilder().host("differentdns.com").build();
    try {
      execute(differentDnsUrl);
      fail("expected a failed attempt to connect");
    } catch (IOException expected) {
    }
  }

  @Test public void skipsOnRedirectWhenDnsDontMatch() throws Exception {
    server.enqueue(new MockResponse()
        .setResponseCode(301)
        .addHeader("Location", url.newBuilder().host("differentdns.com").build()));
    server.enqueue(new MockResponse()
        .setBody("unexpected call"));

    try {
      Response response = execute(url);
      response.close();
      fail("expected a failed attempt to connect");
    } catch (IOException expected) {
    }
  }

  /** Not in the certificate SAN. */
  @Test public void skipsWhenNotSubjectAltName() throws Exception {
    server.enqueue(new MockResponse());
    server.enqueue(new MockResponse());

    assert200Http2Response(execute(url), server.getHostName());

    HttpUrl nonsanUrl = url.newBuilder().host("nonsan.com").build();

    try {
      execute(nonsanUrl);
      fail("expected a failed attempt to connect");
    } catch (SSLPeerUnverifiedException expected) {
    }
  }

  @Test public void skipsOnRedirectWhenNotSubjectAltName() throws Exception {
    server.enqueue(new MockResponse()
        .setResponseCode(301)
        .addHeader("Location", url.newBuilder().host("nonsan.com").build()));
    server.enqueue(new MockResponse());

    try {
      Response response = execute(url);
      response.close();
      fail("expected a failed attempt to connect");
    } catch (SSLPeerUnverifiedException expected) {
    }
  }

  /** Can still coalesce when pinning is used if pins match. */
  @Test public void coalescesWhenCertificatePinsMatch() throws Exception {
    CertificatePinner pinner = new CertificatePinner.Builder()
        .add("san.com", CertificatePinner.pin(certificate.certificate()))
        .build();
    client = client.newBuilder().certificatePinner(pinner).build();

    server.enqueue(new MockResponse());
    server.enqueue(new MockResponse());

    assert200Http2Response(execute(url), server.getHostName());

    HttpUrl sanUrl = url.newBuilder().host("san.com").build();

    assert200Http2Response(execute(sanUrl), "san.com");

    assertThat(client.connectionPool().connectionCount()).isEqualTo(1);
  }

  /** Certificate pinning used and not a match will avoid coalescing and try to connect. */
  @Test public void skipsWhenCertificatePinningFails() throws Exception {
    CertificatePinner pinner = new CertificatePinner.Builder()
        .add("san.com", "sha1/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
        .build();
    client = client.newBuilder().certificatePinner(pinner).build();

    server.enqueue(new MockResponse());

    assert200Http2Response(execute(url), server.getHostName());

    HttpUrl sanUrl = url.newBuilder().host("san.com").build();

    try {
      execute(sanUrl);
      fail("expected a failed attempt to connect");
    } catch (IOException expected) {
    }
  }

  @Test public void skipsOnRedirectWhenCertificatePinningFails() throws Exception {
    CertificatePinner pinner = new CertificatePinner.Builder()
        .add("san.com", "sha1/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
        .build();
    client = client.newBuilder().certificatePinner(pinner).build();

    server.enqueue(new MockResponse()
        .setResponseCode(301)
        .addHeader("Location", url.newBuilder().host("san.com").build()));
    server.enqueue(new MockResponse());

    try {
      execute(url);
      fail("expected a failed attempt to connect");
    } catch (SSLPeerUnverifiedException expected) {
    }
  }

  /**
   * Skips coalescing when hostname verifier is overridden since the intention of the hostname
   * verification is a black box.
   */
  @Test public void skipsWhenHostnameVerifierUsed() throws Exception {
    HostnameVerifier verifier = (name, session) -> true;
    client = client.newBuilder().hostnameVerifier(verifier).build();

    server.enqueue(new MockResponse());
    server.enqueue(new MockResponse());

    assert200Http2Response(execute(url), server.getHostName());

    HttpUrl sanUrl = url.newBuilder().host("san.com").build();

    assert200Http2Response(execute(sanUrl), "san.com");

    assertThat(client.connectionPool().connectionCount()).isEqualTo(2);
  }

  @Test public void skipsOnRedirectWhenHostnameVerifierUsed() throws Exception {
    HostnameVerifier verifier = (name, session) -> true;
    client = client.newBuilder().hostnameVerifier(verifier).build();

    server.enqueue(new MockResponse()
        .setResponseCode(301)
        .addHeader("Location", url.newBuilder().host("san.com").build()));
    server.enqueue(new MockResponse());

    assert200Http2Response(execute(url), "san.com");

    assertThat(client.connectionPool().connectionCount()).isEqualTo(2);
    assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(0); // Fresh connection.
    assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(0); // Fresh connection.
  }

  /**
   * Check we would use an existing connection to a later DNS result instead of connecting to the
   * first DNS result for the first time.
   */
  @Test public void prefersExistingCompatible() throws Exception {
    server.enqueue(new MockResponse());
    server.enqueue(new MockResponse());

    AtomicInteger connectCount = new AtomicInteger();
    EventListener listener = new EventListener() {
      @Override public void connectStart(
          Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {
        connectCount.getAndIncrement();
      }
    };
    client = client.newBuilder()
        .eventListenerFactory(clientTestRule.wrap(listener))
        .build();

    assert200Http2Response(execute(url), server.getHostName());

    HttpUrl sanUrl = url.newBuilder().host("san.com").build();
    dns.set("san.com",
        asList(InetAddress.getByAddress("san.com", new byte[] {0, 0, 0, 0}),
            serverIps.get(0)));
    assert200Http2Response(execute(sanUrl), "san.com");

    assertThat(client.connectionPool().connectionCount()).isEqualTo(1);
    assertThat(connectCount.get()).isEqualTo(1);
  }

  /** Check that wildcard SANs are supported. */
  @Test public void commonThenWildcard() throws Exception {
    server.enqueue(new MockResponse());
    server.enqueue(new MockResponse());

    assert200Http2Response(execute(url), server.getHostName());

    HttpUrl sanUrl = url.newBuilder().host("www.wildcard.com").build();
    assert200Http2Response(execute(sanUrl), "www.wildcard.com");

    assertThat(client.connectionPool().connectionCount()).isEqualTo(1);
  }

  /** Network interceptors check for changes to target. */
  @Test public void worksWithNetworkInterceptors() throws Exception {
    client = client.newBuilder()
        .addNetworkInterceptor(chain -> chain.proceed(chain.request()))
        .build();

    server.enqueue(new MockResponse());
    server.enqueue(new MockResponse());

    assert200Http2Response(execute(url), server.getHostName());

    HttpUrl sanUrl = url.newBuilder().host("san.com").build();
    assert200Http2Response(execute(sanUrl), "san.com");

    assertThat(client.connectionPool().connectionCount()).isEqualTo(1);
  }

  @Test public void misdirectedRequestResponseCode() throws Exception {
    server.enqueue(new MockResponse()
        .setBody("seed connection"));
    server.enqueue(new MockResponse()
        .setResponseCode(421)
        .setBody("misdirected!"));
    server.enqueue(new MockResponse()
        .setBody("after misdirect"));

    // Seed the connection pool.
    assert200Http2Response(execute(url), server.getHostName());

    // Use the coalesced connection which should retry on a fresh connection.
    HttpUrl sanUrl = url.newBuilder()
        .host("san.com")
        .build();
    try (Response response = execute(sanUrl)) {
      assertThat(response.code()).isEqualTo(200);
      assertThat(response.priorResponse().code()).isEqualTo(421);
      assertThat(response.body().string()).isEqualTo("after misdirect");
    }

    assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(0);
    assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(1);
    assertThat(server.takeRequest().getSequenceNumber()).isEqualTo(0); // Fresh connection.

    assertThat(client.connectionPool().connectionCount()).isEqualTo(2);
  }

  /**
   * Won't coalesce if we can't clean certs e.g. a dev setup.
   */
  @Test public void redirectWithDevSetup() throws Exception {
    X509TrustManager TRUST_MANAGER = new X509TrustManager() {
      @Override
      public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
      }

      @Override
      public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
      }

      @Override
      public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
      }
    };

    client = client.newBuilder().sslSocketFactory(client.sslSocketFactory(), TRUST_MANAGER).build();

    server.enqueue(new MockResponse());
    server.enqueue(new MockResponse());

    assert200Http2Response(execute(url), server.getHostName());

    HttpUrl sanUrl = url.newBuilder().host("san.com").build();
    assert200Http2Response(execute(sanUrl), "san.com");

    assertThat(client.connectionPool().connectionCount()).isEqualTo(2);
  }

  private Response execute(HttpUrl url) throws IOException {
    return client.newCall(new Request.Builder().url(url).build()).execute();
  }

  private void assert200Http2Response(Response response, String expectedHost) {
    assertThat(response.code()).isEqualTo(200);
    assertThat(response.request().url().host()).isEqualTo(expectedHost);
    assertThat(response.protocol()).isEqualTo(Protocol.HTTP_2);
    response.body().close();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy