EmissaryClient.java
package emissary.client;
import emissary.client.EmissaryResponse.EmissaryResponseHandler;
import emissary.config.ConfigUtil;
import emissary.config.Configurator;
import com.google.common.annotations.VisibleForTesting;
import jakarta.ws.rs.core.MediaType;
import org.apache.hc.client5.http.auth.AuthCache;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.Credentials;
import org.apache.hc.client5.http.auth.StandardAuthScheme;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.cookie.Cookie;
import org.apache.hc.client5.http.entity.EntityBuilder;
import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.util.Timeout;
import org.eclipse.jetty.util.security.Password;
import org.glassfish.jersey.server.filter.CsrfProtectionFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
/**
* Base class of all the actions that use HttpClient.
*/
@SuppressWarnings("NonFinalStaticField")
public class EmissaryClient {
public static final String DEFAULT_CONTEXT = "emissary";
public static final String JETTY_USER_FILE_PROPERTY_NAME = "emissary.jetty.users.file";
public static final int DEFAULT_CONNECTION_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(100L); // 2 X 50 min
public static final int DEFAULT_CONNECTION_MANAGER_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(2L);
public static final int DEFAULT_SOCKET_TIMEOUT = (int) TimeUnit.MINUTES.toMillis(1L);
public static final int DEFAULT_RETRIES = 3;
public static final String DEFAULT_USERNAME = "emissary";
public static final String DEFAULT_PASSWORD = "password";
public static final String CSRF_HEADER_PARAM = CsrfProtectionFilter.HEADER_NAME;
private static final Logger LOGGER = LoggerFactory.getLogger(EmissaryClient.class);
private static final PoolingHttpClientConnectionManager CONNECTION_MANAGER = HTTPConnectionFactory.getFactory().getDefaultConnectionManager();
// some default objects to use
private static final BasicCredentialsProvider CRED_PROV = new BasicCredentialsProvider();
protected static final int ANY_PORT = -1;
@Nullable
protected static final String ANY_HOST = null;
@Nullable
private static CloseableHttpClient staticClient = null;
@Nullable
private static RequestConfig staticRequestConfig = null;
@Nullable
private static ConnectionConfig staticConnectionConfig = null;
// static config variables
public static String context = DEFAULT_CONTEXT;
protected static int retries = DEFAULT_RETRIES;
protected static String username = DEFAULT_USERNAME;
// How long to wait while establishing a connection (ms)
protected static int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
// How long to wait for a connection from the pool (ms)
protected static int connectionManagerTimeout = DEFAULT_CONNECTION_MANAGER_TIMEOUT;
// How long to wait on a data read in a connection (ms)
protected static int socketTimeout = DEFAULT_SOCKET_TIMEOUT;
// class is thread-safe
protected static final AuthCache AUTH_CACHE = new BasicAuthCache();
private final CloseableHttpClient client;
private final RequestConfig requestConfig;
private ConnectionConfig connectionConfig;
static {
configure();
}
@VisibleForTesting
protected static void configure() {
LOGGER.debug("Configuring EmissaryClient");
// parse configs
try {
final Configurator c = ConfigUtil.getConfigInfo(EmissaryClient.class);
retries = c.findIntEntry("retries", DEFAULT_RETRIES);
username = c.findStringEntry("username", DEFAULT_USERNAME);
connectionTimeout = c.findIntEntry("connectionTimeout", DEFAULT_CONNECTION_TIMEOUT);
connectionManagerTimeout = c.findIntEntry("connectionManagerTimeout", DEFAULT_CONNECTION_MANAGER_TIMEOUT);
socketTimeout = c.findIntEntry("socketTimeout", DEFAULT_SOCKET_TIMEOUT);
context = c.findStringEntry("context", DEFAULT_CONTEXT);
} catch (IOException iox) {
LOGGER.warn("Cannot read EmissaryClient properties, configuring defaults: {}", iox.getMessage());
retries = DEFAULT_RETRIES;
username = DEFAULT_USERNAME;
connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
connectionManagerTimeout = DEFAULT_CONNECTION_MANAGER_TIMEOUT;
socketTimeout = DEFAULT_SOCKET_TIMEOUT;
context = DEFAULT_CONTEXT;
}
// Read the jetty user realm formatted property file for the password
// Value for password remains unchanged if there is a problem
try {
String userPropertiesFile = System.getProperty(JETTY_USER_FILE_PROPERTY_NAME);
if (null == userPropertiesFile) {
LOGGER.debug("System property '{}' not set, using default jetty-users.properties", JETTY_USER_FILE_PROPERTY_NAME);
userPropertiesFile = "jetty-users.properties";
}
LOGGER.debug("Reading password from {}", userPropertiesFile);
final Properties props = ConfigUtil.getPropertyInfo(userPropertiesFile);
String pass = DEFAULT_PASSWORD;
final String value = props.getProperty(username, pass);
if (value != null && value.indexOf(',') != -1) {
pass = value.substring(0, value.indexOf(',')).trim();
} else if (pass.equals(value)) {
LOGGER.error("Error reading password from {}", userPropertiesFile);
}
// Supply default credentials for anyone we want to connect to
final String decodedPassword = pass.startsWith("OBF:") ? Password.deobfuscate(pass) : pass;
final Credentials cred = new UsernamePasswordCredentials(username, decodedPassword.toCharArray());
CRED_PROV.setCredentials(new AuthScope(ANY_HOST, ANY_PORT), cred);
} catch (IOException iox) {
LOGGER.error("Cannot read {} in EmissaryClient, defaulting credentials", System.getProperty(JETTY_USER_FILE_PROPERTY_NAME));
final Credentials cred = new UsernamePasswordCredentials(username, DEFAULT_PASSWORD.toCharArray());
final AuthScope authScope = new AuthScope(ANY_HOST, ANY_PORT);
CRED_PROV.setCredentials(authScope, cred);
}
staticRequestConfig =
RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofMilliseconds(connectionManagerTimeout))
.setTargetPreferredAuthSchemes(Collections.singleton(StandardAuthScheme.DIGEST))
.setProxyPreferredAuthSchemes(Collections.singleton(StandardAuthScheme.DIGEST))
.build();
staticConnectionConfig = ConnectionConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout))
.setSocketTimeout(Timeout.ofMilliseconds(socketTimeout)).build();
CONNECTION_MANAGER.setDefaultConnectionConfig(staticConnectionConfig);
staticClient =
HttpClientBuilder.create().setConnectionManager(CONNECTION_MANAGER).setDefaultCredentialsProvider(CRED_PROV)
.setDefaultRequestConfig(staticRequestConfig).build();
}
public EmissaryClient() {
this(staticClient, staticRequestConfig, staticConnectionConfig);
}
public EmissaryClient(RequestConfig requestConfig, ConnectionConfig connectionConfig) {
this(staticClient, requestConfig, connectionConfig);
}
public EmissaryClient(CloseableHttpClient client) {
this(client, staticRequestConfig, staticConnectionConfig);
}
public EmissaryClient(CloseableHttpClient client, RequestConfig requestConfig, ConnectionConfig connectionConfig) {
this.client = client;
this.requestConfig = requestConfig;
this.connectionConfig = connectionConfig;
}
public EmissaryResponse send(final HttpUriRequestBase method) {
return send(method, null);
}
/**
* Sends a request to the web server. The request can be any HttpMethod. Adds the specified cookie to the Http State
*
* @param method the method to be sent
* @param cookie a cookie to set on the request
*/
public EmissaryResponse send(final HttpUriRequestBase method, @Nullable final Cookie cookie) {
try {
LOGGER.debug("Sending {} to {}", method.getMethod(), method.getUri());
} catch (URISyntaxException e) {
LOGGER.debug("Sending {} and failed to retrieve URI", method.getMethod());
}
HttpClientContext localContext = HttpClientContext.create();
localContext.setAttribute(HttpClientContext.AUTH_CACHE, EmissaryClient.AUTH_CACHE);
if (cookie != null) {
localContext.getCookieStore().addCookie(cookie);
}
try {
// This is thread safe. The client is instantiated in a static block above
// with a connection pool. Calling new EmissaryClient().send allows you
// to use a different context and request config per request
method.setConfig(requestConfig);
CloseableHttpClient thisClient = getHttpClient();
return thisClient.execute(method, localContext, new EmissaryResponseHandler());
} catch (IOException e) {
LOGGER.debug("Problem processing request:", e);
BasicClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage());
response.setEntity(EntityBuilder.create().setText(e.getClass() + ": " + e.getMessage()).setContentEncoding(MediaType.TEXT_PLAIN).build());
return new EmissaryResponse(response);
}
}
protected CloseableHttpClient getHttpClient() {
return client;
}
protected RequestConfig getRequestConfig() {
return requestConfig;
}
protected ConnectionConfig getConnectionConfig() {
return connectionConfig;
}
public void setConnectionTimeout(int timeout) {
if (timeout > 0) {
connectionConfig = ConnectionConfig.copy(connectionConfig).setConnectTimeout(Timeout.ofMilliseconds(timeout)).build();
} else {
LOGGER.warn("Tried to set timeout to {}", timeout);
}
}
protected String getCsrfToken() {
return DEFAULT_CONTEXT;
}
public HttpPost createHttpPost(String uri, String context, String endpoint) {
return createHttpPost(uri + context + endpoint, getCsrfToken());
}
public HttpPost createHttpPost(String uri) {
return createHttpPost(uri, getCsrfToken());
}
public HttpPost createHttpPost(String uri, String csrfToken) {
HttpPost method = new HttpPost(uri);
setCsrfHeader(method, csrfToken);
return method;
}
public HttpUriRequestBase setCsrfHeader(HttpUriRequestBase request, String csrfToken) {
return setCsrfHeader(request, CSRF_HEADER_PARAM, csrfToken);
}
public HttpUriRequestBase setCsrfHeader(HttpUriRequestBase request, String csrfParam, String csrfToken) {
request.addHeader(csrfParam, csrfToken);
return request;
}
}