package org.testcontainers.containers.wait;
import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import org.rnorth.ducttape.TimeoutException;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import static org.rnorth.ducttape.unreliables.Unreliables.retryUntilSuccess;
/**
* Waits until an HTTP(S) endpoint returns a given status code.
*
* @author Pete Cornish {@literal <outofcoffee@gmail.com>}
*/
public class HttpWaitStrategy extends GenericContainer.AbstractWaitStrategy {
/**
* Authorization HTTP header.
*/
private static final String HEADER_AUTHORIZATION = "Authorization";
/**
* Basic Authorization scheme prefix.
*/
private static final String AUTH_BASIC = "Basic ";
private String path = "/";
private int statusCode = HttpURLConnection.HTTP_OK;
private boolean tlsEnabled;
private String username;
private String password;
/**
* Waits for the given status code.
*
* @param statusCode the expected status code
* @return this
*/
public HttpWaitStrategy forStatusCode(int statusCode) {
this.statusCode = statusCode;
return this;
}
/**
* Waits for the given path.
*
* @param path the path to check
* @return this
*/
public HttpWaitStrategy forPath(String path) {
this.path = path;
return this;
}
/**
* Indicates that the status check should use HTTPS.
*
* @return this
*/
public HttpWaitStrategy usingTls() {
this.tlsEnabled = true;
return this;
}
/**
* Authenticate with HTTP Basic Authorization credentials.
*
* @param username the username
* @param password the password
* @return this
*/
public HttpWaitStrategy withBasicCredentials(String username, String password) {
this.username = username;
this.password = password;
return this;
}
@Override
protected void waitUntilReady() {
final Integer livenessCheckPort = getLivenessCheckPort();
if (null == livenessCheckPort) {
logger().warn("No exposed ports or mapped ports - cannot wait for status");
return;
}
final String uri = buildLivenessUri(livenessCheckPort).toString();
logger().info("Waiting for {} seconds for URL: {}", startupTimeout.getSeconds(), uri);
// try to connect to the URL
try {
retryUntilSuccess((int) startupTimeout.getSeconds(), TimeUnit.SECONDS, () -> {
getRateLimiter().doWhenReady(() -> {
try {
final HttpURLConnection connection = (HttpURLConnection) new URL(uri).openConnection();
// authenticate
if (!Strings.isNullOrEmpty(username)) {
connection.setRequestProperty(HEADER_AUTHORIZATION, buildAuthString(username, password));
connection.setUseCaches(false);
}
connection.setRequestMethod("GET");
connection.connect();
if (statusCode != connection.getResponseCode()) {
throw new RuntimeException(String.format("HTTP response code was: %s",
connection.getResponseCode()));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
return true;
});
} catch (TimeoutException e) {
throw new ContainerLaunchException(String.format(
"Timed out waiting for URL to be accessible (%s should return HTTP %s)", uri, statusCode));
}
}
/**
* Build the URI on which to check if the container is ready.
*
* @param livenessCheckPort the liveness port
* @return the liveness URI
*/
private URI buildLivenessUri(int livenessCheckPort) {
final String scheme = (tlsEnabled ? "https" : "http") + "://";
final String host = container.getContainerIpAddress();
final String portSuffix;
if ((tlsEnabled && 443 == livenessCheckPort) || (!tlsEnabled && 80 == livenessCheckPort)) {
portSuffix = "";
} else {
portSuffix = ":" + String.valueOf(livenessCheckPort);
}
return URI.create(scheme + host + portSuffix + path);
}
/**
* @param username the username
* @param password the password
* @return a basic authentication string for the given credentials
*/
private String buildAuthString(String username, String password) {
return AUTH_BASIC + BaseEncoding.base64().encode((username + ":" + password).getBytes());
}
}