Skip to content

Commit

Permalink
Extend the HTTP request library to support NetworkEndpoints that have…
Browse files Browse the repository at this point in the history
… both hostnames and IP addresses.

PiperOrigin-RevId: 342623441
Change-Id: Ia4953a4bae34a8e32ef53db30b1ff43247e4998f
  • Loading branch information
Tsunami Team authored and copybara-github committed Nov 16, 2020
1 parent 3b6f801 commit 56e282c
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,21 @@
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
import com.google.tsunami.proto.NetworkService;
import java.io.IOException;
import javax.inject.Inject;
import javax.net.ssl.HttpsURLConnection;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Dns;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.checkerframework.checker.nullness.qual.Nullable;

/** A client library that communicates with remote servers via the HTTP protocol. */
public final class HttpClient {
Expand All @@ -50,21 +54,44 @@ public final class HttpClient {

/** Sends the given HTTP request using this client, blocking until full response is received. */
public HttpResponse send(HttpRequest httpRequest) throws IOException {
return send(httpRequest, null);
}

/**
* Sends the given HTTP request using this client blocking until full response is received. If
* {@code networkService} is not null, the host header is set according to the service's header
* field even if it resolves to a different ip.
*/
public HttpResponse send(HttpRequest httpRequest, @Nullable NetworkService networkService)
throws IOException {
logger.atInfo().log(
"Sending HTTP '%s' request to '%s'.", httpRequest.method(), httpRequest.url().toString());

OkHttpClient callHttpClient = clientWithHostnameAsProxy(networkService);
try (Response okHttpResponse =
okHttpClient.newCall(buildOkHttpRequest(httpRequest)).execute()) {
callHttpClient.newCall(buildOkHttpRequest(httpRequest)).execute()) {
return parseResponse(okHttpResponse);
}
}

/** Sends the given HTTP request using this client asynchronously. */
public ListenableFuture<HttpResponse> sendAsync(HttpRequest httpRequest) {
return sendAsync(httpRequest, null);
}

/**
* Sends the given HTTP request using this client asynchronously. If {@code networkService} is not
* null, the host header is set according to the service's header field even if it resolves to a
* different ip.
*/
public ListenableFuture<HttpResponse> sendAsync(
HttpRequest httpRequest, @Nullable NetworkService networkService) {
logger.atInfo().log(
"Sending async HTTP '%s' request to '%s'.",
httpRequest.method(), httpRequest.url().toString());
OkHttpClient callHttpClient = clientWithHostnameAsProxy(networkService);
SettableFuture<HttpResponse> responseFuture = SettableFuture.create();
Call requestCall = okHttpClient.newCall(buildOkHttpRequest(httpRequest));
Call requestCall = callHttpClient.newCall(buildOkHttpRequest(httpRequest));

try {
requestCall.enqueue(
Expand Down Expand Up @@ -98,6 +125,36 @@ public void onResponse(Call call, Response response) {
return responseFuture;
}

/**
* Returns a modified HTTP client that's configured to connect to the {@code networkService}'s IP
* and use its hostname in the host header, when both a hostname and an IP address is specified.
* Returns an unmodified HTTP client otherwise.
*/
private OkHttpClient clientWithHostnameAsProxy(NetworkService networkService) {
if (networkService == null) {
return this.okHttpClient;
}
String serviceIp = networkService.getNetworkEndpoint().getIpAddress().getAddress();
String serviceHostname = networkService.getNetworkEndpoint().getHostname().getName();
return this.okHttpClient
.newBuilder()
.dns(
hostname -> {
if (hostname.equals(serviceHostname)) {
hostname = serviceIp;
}
return Dns.SYSTEM.lookup(hostname);
})
.hostnameVerifier(
(hostname, session) -> {
if (hostname.equals(serviceHostname)) {
return true;
}
return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session);
})
.build();
}

private static Request buildOkHttpRequest(HttpRequest httpRequest) {
Request.Builder okRequestBuilder = new Request.Builder().url(httpRequest.url());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static com.google.common.net.HttpHeaders.ACCEPT;
import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.HttpHeaders.HOST;
import static com.google.common.net.HttpHeaders.LOCATION;
import static com.google.common.net.HttpHeaders.USER_AGENT;
import static com.google.common.truth.Truth.assertThat;
Expand All @@ -34,6 +35,8 @@
import com.google.common.util.concurrent.ListenableFuture;
import com.google.inject.Guice;
import com.google.protobuf.ByteString;
import com.google.tsunami.common.data.NetworkEndpointUtils;
import com.google.tsunami.proto.NetworkService;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
Expand Down Expand Up @@ -487,6 +490,29 @@ public void sendAsync_whenRequestFailed_returnsFutureWithException() {
assertThat(ex).hasCauseThat().isInstanceOf(IOException.class);
}

@Test
public void send_whenHostnameAndIpInRequest_useHostnameAsProxy() throws IOException {
// MockWebServer listens on the loopback ipv6 address by default.
String ip = "::1";
String host = "host.com";
mockWebServer.setDispatcher(new HostnameTestDispatcher(host));
mockWebServer.start();
int port = mockWebServer.url("/").port();

NetworkService networkService =
NetworkService.newBuilder()
.setNetworkEndpoint(NetworkEndpointUtils.forIpHostnameAndPort(ip, host, port))
.build();

// The request to host.com should be sent through mockWebServer's IP.
HttpResponse response =
httpClient.send(
get(String.format("http://host.com:%d/test/get", port)).withEmptyHeaders().build(),
networkService);

assertThat(response.status()).isEqualTo(HttpStatus.OK);
}

static final class RedirectDispatcher extends Dispatcher {
static final String REDIRECT_PATH = "/redirect";
static final String REDIRECT_DESTINATION_PATH = "/redirect-dest";
Expand Down Expand Up @@ -524,4 +550,20 @@ && nullToEmpty(recordedRequest.getHeader(USER_AGENT)).equals("TsunamiSecuritySca
return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code());
}
}

static final class HostnameTestDispatcher extends Dispatcher {
private final String expectedHost;

HostnameTestDispatcher(String expectedHost) {
this.expectedHost = checkNotNull(expectedHost);
}

@Override
public MockResponse dispatch(RecordedRequest recordedRequest) {
if (nullToEmpty(recordedRequest.getHeader(HOST)).startsWith(expectedHost)) {
return new MockResponse().setResponseCode(HttpStatus.OK.code());
}
return new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.code());
}
}
}

0 comments on commit 56e282c

Please sign in to comment.