Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow insecure HTTPS/TLS connection for HttpWaitStrategy #4951

Merged
merged 2 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,26 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.time.Duration;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess;

@Slf4j
Expand All @@ -47,6 +58,7 @@ public class HttpWaitStrategy extends AbstractWaitStrategy {
private Predicate<Integer> statusCodePredicate = null;
private Optional<Integer> livenessPort = Optional.empty();
private Duration readTimeout = Duration.ofSeconds(1);
private boolean allowInsecure;

/**
* Waits for the given status code.
Expand Down Expand Up @@ -112,6 +124,16 @@ public HttpWaitStrategy withMethod(String method) {
return this;
}

/**
* Indicates that HTTPS connection could use untrusted (self signed) certificate chains.
*
* @return this
*/
public HttpWaitStrategy allowInsecure() {
this.allowInsecure = true;
return this;
}

/**
* Authenticate with HTTP Basic Authorization credentials.
*
Expand Down Expand Up @@ -206,7 +228,7 @@ protected void waitUntilReady() {
retryUntilSuccess((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> {
getRateLimiter().doWhenReady(() -> {
try {
final HttpURLConnection connection = (HttpURLConnection) new URL(uri).openConnection();
final HttpURLConnection connection = openConnection(uri);
connection.setReadTimeout(Math.toIntExact(readTimeout.toMillis()));

// authenticate
Expand Down Expand Up @@ -267,6 +289,45 @@ protected void waitUntilReady() {
}
}

private HttpURLConnection openConnection(final String uri) throws IOException, MalformedURLException {
if (tlsEnabled) {
final HttpsURLConnection connection = (HttpsURLConnection)new URL(uri).openConnection();
if (allowInsecure) {
// Create a trust manager that does not validate certificate chains
// and trust all certificates
final TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}

@Override
public void checkClientTrusted(final X509Certificate[] certs, final String authType) {
}

@Override
public void checkServerTrusted(final X509Certificate[] certs, final String authType) {
}
}
};

try {
// Create custom SSL context and set the "trust all certificates" trust manager
final SSLContext sc = SSLContext.getInstance("SSL");
sc.init(new KeyManager[0], trustAllCerts, new SecureRandom());
connection.setSSLSocketFactory(sc.getSocketFactory());
} catch (final NoSuchAlgorithmException | KeyManagementException ex) {
throw new IOException("Unable to create custom SSL factory instance", ex);
}
}

return connection;
} else {
return (HttpURLConnection) new URL(uri).openConnection();
}
}

/**
* Build the URI on which to check if the container is ready.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ public void testWaitUntilReadyWithSuccessWithCustomHeaders() {
assertThat(logs, containsString("baz: boo"));
}

/**
* Ensures that HTTPS requests made with the HttpWaitStrategy can skip the
* certificate validation chains (to support self-signed certificates for example).
*/
@Test
public void testWaitUntilReadyWithTlsAndAllowUnsecure() {
waitUntilReadyAndSucceed(startContainerWithCommand(createHttpsShellCommand("200 OK", GOOD_RESPONSE_BODY, 8080),
createHttpWaitStrategy(ready).usingTls().allowInsecure()
));
}

/**
* Expects that the WaitStrategy returns successfully after receiving an HTTP 401 response from the container.
* This 401 response is checked with a lambda using {@link HttpWaitStrategy#forStatusCodeMatching(Predicate)}
Expand Down Expand Up @@ -206,4 +217,12 @@ private String createShellCommand(String header, String responseBody, int port)
"Content-Length: " + length + NEWLINE + "\";"
+ " echo \"" + responseBody + "\";} | nc -lp " + port + "; done";
}

private String createHttpsShellCommand(String header, String responseBody, int port) {
int length = responseBody.getBytes().length;
return "apk add nmap-ncat; while true; do { echo -e \"HTTP/1.1 " + header + NEWLINE +
"Content-Type: text/html" + NEWLINE +
"Content-Length: " + length + NEWLINE + "\";"
+ " echo \"" + responseBody + "\";} | ncat -lp " + port + " --ssl; done";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import org.junit.After;
import org.junit.Test;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.RemoteDockerImage;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.MountableFile;
Expand Down Expand Up @@ -282,6 +284,27 @@ public void testElasticsearch8SecureByDefaultCustomCaCertFails() throws Exceptio
}
}

@Test
public void testElasticsearch8SecureByDefaultHttpWaitStrategy() throws Exception {
final HttpWaitStrategy httpsWaitStrategy = Wait.forHttps("/")
.forPort(9200)
.forStatusCode(200)
.withBasicCredentials(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD)
// trusting self-signed certificate
.allowInsecure();

try (ElasticsearchContainer container = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:8.1.2")
.waitingFor(httpsWaitStrategy)) {

// Start the container. This step might take some time...
container.start();

Response response = getClusterHealth(container);
assertThat(response.getStatusLine().getStatusCode(), is(200));
assertThat(EntityUtils.toString(response.getEntity()), containsString("cluster_name"));
}
}

@Test
public void testElasticsearch8SecureByDefaultFailsSilentlyOnLatestImages() throws Exception {
// this test exists for custom images by users that use the `latest` tag
Expand Down