use Nb classes for auth and proxy selection

This commit is contained in:
Greg DiCristofaro 2023-08-04 20:54:24 -04:00
parent 8ed27b935d
commit 5f35af2247

View File

@ -22,12 +22,11 @@ import com.basistech.df.cybertriage.autopsy.ctapi.util.ObjectMapperUtil;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.Authenticator; import java.net.Proxy;
import java.net.InetAddress; import java.net.ProxySelector;
import java.net.PasswordAuthentication; import java.net.SocketAddress;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
@ -35,14 +34,13 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.UnrecoverableKeyException; import java.security.UnrecoverableKeyException;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.Collection;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Objects;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Stream;
import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
@ -50,14 +48,10 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager; import javax.net.ssl.X509TrustManager;
import org.apache.commons.collections4.MapUtils; import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.NTCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.config.RequestConfig; import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
@ -71,29 +65,27 @@ import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.SystemDefaultCredentialsProvider;
import org.apache.http.impl.client.WinHttpClients; import org.apache.http.impl.client.WinHttpClients;
import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
import org.apache.http.ssl.SSLInitializationException; import org.apache.http.ssl.SSLInitializationException;
import org.netbeans.core.ProxySettings; import org.netbeans.core.ProxySettings;
import org.openide.util.NetworkSettings; import org.openide.util.Lookup;
import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.autopsy.coreutils.Version;
/** /**
* Makes the http requests to CT cloud. * Makes the http requests to CT cloud.
*
* NOTE: regarding proxy settings, the host and port are handled by the
* NbProxySelector. Any proxy authentication is handled by NbAuthenticator which
* is installed at startup (i.e. NbAuthenticator.install). See
* GeneralOptionsModel.testHttpConnection to see how the general options panel
* tests the connection.
*/ */
class CTCloudHttpClient { class CTCloudHttpClient {
private static final Logger LOGGER = Logger.getLogger(CTCloudHttpClient.class.getName()); private static final Logger LOGGER = Logger.getLogger(CTCloudHttpClient.class.getName());
private static final String HOST_URL = Version.getBuildType() == Version.Type.RELEASE ? Constants.CT_CLOUD_SERVER : Constants.CT_CLOUD_DEV_SERVER; private static final String HOST_URL = Version.getBuildType() == Version.Type.RELEASE ? Constants.CT_CLOUD_SERVER : Constants.CT_CLOUD_DEV_SERVER;
private static final String NB_PROXY_SELECTOR_NAME = "org.netbeans.core.NbProxySelector";
private static final List<String> DEFAULT_SCHEME_PRIORITY
= new ArrayList<>(Arrays.asList(
AuthSchemes.SPNEGO,
AuthSchemes.KERBEROS,
AuthSchemes.NTLM,
AuthSchemes.CREDSSP,
AuthSchemes.DIGEST,
AuthSchemes.BASIC));
private static final int CONNECTION_TIMEOUT_MS = 58 * 1000; // milli sec private static final int CONNECTION_TIMEOUT_MS = 58 * 1000; // milli sec
@ -105,48 +97,12 @@ class CTCloudHttpClient {
private final ObjectMapper mapper = ObjectMapperUtil.getInstance().getDefaultObjectMapper(); private final ObjectMapper mapper = ObjectMapperUtil.getInstance().getDefaultObjectMapper();
private final SSLContext sslContext; private final SSLContext sslContext;
private String hostName = null; private final ProxySelector proxySelector;
private CTCloudHttpClient() { private CTCloudHttpClient() {
// leave as null for now unless we want to customize this at a later date // leave as null for now unless we want to customize this at a later date
this.sslContext = createSSLContext(); this.sslContext = createSSLContext();
} this.proxySelector = getProxySelector();
private ProxySettingArgs getProxySettings(URI uri) {
if (StringUtils.isBlank(hostName)) {
try {
hostName = InetAddress.getLocalHost().getCanonicalHostName();
} catch (UnknownHostException ex) {
LOGGER.log(Level.WARNING, "An error occurred while fetching the hostname", ex);
}
}
String proxyPortStr = uri != null ? NetworkSettings.getProxyPort(uri) : ProxySettings.getHttpPort();
int proxyPort = 0;
if (StringUtils.isNotBlank(proxyPortStr)) {
try {
proxyPort = Integer.parseInt(proxyPortStr);
} catch (NumberFormatException ex) {
LOGGER.log(Level.WARNING, "Unable to convert port to integer");
}
}
String proxyHost = uri != null ? NetworkSettings.getProxyHost(uri) : ProxySettings.getHttpHost();
ProxySettingArgs proxySettings = new ProxySettingArgs(
ProxySettings.getProxyType() != ProxySettings.DIRECT_CONNECTION,
hostName,
proxyHost,
proxyPort,
ProxySettings.getAuthenticationUsername(),
ProxySettings.getAuthenticationPassword(),
null
);
// TODO comment out later
LOGGER.log(Level.INFO, MessageFormat.format("Proxy settings to be used with {0} are {1}.", uri, proxySettings));
return proxySettings;
} }
private static URI getUri(String host, String path, Map<String, String> urlReqParams) throws URISyntaxException { private static URI getUri(String host, String path, Map<String, String> urlReqParams) throws URISyntaxException {
@ -171,12 +127,12 @@ class CTCloudHttpClient {
} }
public <O> O doPost(String urlPath, Map<String, String> urlReqParams, Object jsonBody, Class<O> classType) throws CTCloudException { public <O> O doPost(String urlPath, Map<String, String> urlReqParams, Object jsonBody, Class<O> classType) throws CTCloudException {
URI postURI = null; URI postURI = null;
try { try {
postURI = getUri(HOST_URL, urlPath, urlReqParams); postURI = getUri(HOST_URL, urlPath, urlReqParams);
LOGGER.log(Level.INFO, "initiating http connection to ctcloud server"); LOGGER.log(Level.INFO, "initiating http connection to ctcloud server");
try (CloseableHttpClient httpclient = createConnection(getProxySettings(postURI), sslContext)) { try (CloseableHttpClient httpclient = createConnection(proxySelector, sslContext)) {
HttpPost postRequest = new HttpPost(postURI); HttpPost postRequest = new HttpPost(postURI);
@ -237,7 +193,7 @@ class CTCloudHttpClient {
throw new CTCloudException(CTCloudException.ErrorCode.UNKNOWN, ex); throw new CTCloudException(CTCloudException.ErrorCode.UNKNOWN, ex);
} }
try (CloseableHttpClient httpclient = createConnection(getProxySettings(postUri), sslContext)) { try (CloseableHttpClient httpclient = createConnection(proxySelector, sslContext)) {
LOGGER.log(Level.INFO, "initiating http post request to ctcloud server " + fullUrlPath); LOGGER.log(Level.INFO, "initiating http post request to ctcloud server " + fullUrlPath);
HttpPost post = new HttpPost(postUri); HttpPost post = new HttpPost(postUri);
configureRequestTimeout(post); configureRequestTimeout(post);
@ -335,19 +291,30 @@ class CTCloudHttpClient {
} }
/** /**
* Creates a connection to CT Cloud with the given arguments. * Get ProxySelector present (favoring NbProxySelector if present).
* *
* @param proxySettings The network proxy settings. * @return The found ProxySelector or null.
* @param sslContext The ssl context or null.
* @return The connection to CT Cloud.
*/ */
private static CloseableHttpClient createConnection(ProxySettingArgs proxySettings, SSLContext sslContext) throws SSLInitializationException { private static ProxySelector getProxySelector() {
HttpClientBuilder builder = getHttpClientBuilder(proxySettings); Collection<? extends ProxySelector> selectors = Lookup.getDefault().lookupAll(ProxySelector.class);
if (sslContext != null) { return (selectors != null ? selectors.stream() : Stream.empty())
builder.setSSLContext(sslContext); .filter(s -> s != null)
} .map(s -> (ProxySelector) s)
.sorted((a, b) -> {
return builder.build(); String aName = a.getClass().getCanonicalName();
String bName = b.getClass().getCanonicalName();
boolean aIsNb = aName.equalsIgnoreCase(NB_PROXY_SELECTOR_NAME);
boolean bIsNb = bName.equalsIgnoreCase(NB_PROXY_SELECTOR_NAME);
if (aIsNb == bIsNb) {
return StringUtils.compareIgnoreCase(aName, bName);
} else {
return aIsNb ? -1 : 1;
}
})
.findFirst()
// TODO take this out to remove proxy selector logging
.map(s -> new LoggingProxySelector(s))
.orElse(null);
} }
/** /**
@ -371,6 +338,7 @@ class CTCloudHttpClient {
} }
} }
// jvm default key manager
// based in part on this: https://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm/16229909 // based in part on this: https://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm/16229909
private static KeyManager[] getKeyManagers() { private static KeyManager[] getKeyManagers() {
LOGGER.log(Level.INFO, "Using default algorithm to create trust store: " + KeyManagerFactory.getDefaultAlgorithm()); LOGGER.log(Level.INFO, "Using default algorithm to create trust store: " + KeyManagerFactory.getDefaultAlgorithm());
@ -385,6 +353,7 @@ class CTCloudHttpClient {
} }
// jvm default trust store
// based in part on this: https://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm/16229909 // based in part on this: https://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm/16229909
private static TrustManager[] getTrustManagers() { private static TrustManager[] getTrustManagers() {
try { try {
@ -401,138 +370,60 @@ class CTCloudHttpClient {
} }
} }
private static HttpClientBuilder getHttpClientBuilder(ProxySettingArgs proxySettings) {
if (proxySettings.isSystemOrManualProxy()) {
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
LOGGER.info("Requesting Password Authentication...");
return super.getPasswordAuthentication();
}
});
HttpClientBuilder builder = null;
HttpHost proxyHost = null;
CredentialsProvider proxyCredsProvider = null;
RequestConfig config = null;
if (Objects.nonNull(proxySettings.getProxyHostname()) && proxySettings.getProxyPort() > 0) {
proxyHost = new HttpHost(proxySettings.getProxyHostname(), proxySettings.getProxyPort());
proxyCredsProvider = getProxyCredentialsProvider(proxySettings);
if (StringUtils.isNotBlank(proxySettings.getAuthScheme())) {
if (!DEFAULT_SCHEME_PRIORITY.get(0).equalsIgnoreCase(proxySettings.getAuthScheme())) {
DEFAULT_SCHEME_PRIORITY.removeIf(s -> s.equalsIgnoreCase(proxySettings.getAuthScheme()));
DEFAULT_SCHEME_PRIORITY.add(0, proxySettings.getAuthScheme());
}
}
config = RequestConfig.custom().setProxyPreferredAuthSchemes(DEFAULT_SCHEME_PRIORITY).build();
}
if (Objects.isNull(proxyCredsProvider) && WinHttpClients.isWinAuthAvailable()) {
builder = WinHttpClients.custom();
builder.useSystemProperties();
LOGGER.log(Level.WARNING, "Using Win HTTP Client");
} else {
builder = HttpClients.custom();
builder.setDefaultRequestConfig(config);
if (Objects.nonNull(proxyCredsProvider)) { // make sure non null proxycreds before setting it
builder.setDefaultCredentialsProvider(proxyCredsProvider);
}
LOGGER.log(Level.WARNING, "Using default http client");
}
if (Objects.nonNull(proxyHost)) {
builder.setProxy(proxyHost);
LOGGER.log(Level.WARNING, MessageFormat.format("Using proxy {0}", proxyHost));
}
return builder;
} else {
return HttpClients.custom();
}
}
/** /**
* Returns a CredentialsProvider for proxy, if one is configured. * Creates a connection to CT Cloud with the given arguments.
* *
* @return CredentialsProvider, if a proxy is configured with credentials, * @param proxySelector The proxy selector.
* null otherwise * @param sslContext The ssl context or null.
* @return The connection to CT Cloud.
*/ */
private static CredentialsProvider getProxyCredentialsProvider(ProxySettingArgs proxySettings) { private static CloseableHttpClient createConnection(ProxySelector proxySelector, SSLContext sslContext) throws SSLInitializationException {
CredentialsProvider proxyCredsProvider = null; HttpClientBuilder builder;
if (proxySettings.isSystemOrManualProxy()) {
if (StringUtils.isNotBlank(proxySettings.getProxyUserId())) { if (ProxySettings.getProxyType() != ProxySettings.DIRECT_CONNECTION
if (null != proxySettings.getProxyPassword() && proxySettings.getProxyPassword().length > 0) { // Password will be blank for KERBEROS / NEGOTIATE schemes. && StringUtils.isBlank(ProxySettings.getAuthenticationUsername())
proxyCredsProvider = new SystemDefaultCredentialsProvider(); && ArrayUtils.isEmpty(ProxySettings.getAuthenticationPassword())
String userId = proxySettings.getProxyUserId(); && WinHttpClients.isWinAuthAvailable()) {
String domain = null;
if (userId.contains("\\")) { builder = WinHttpClients.custom();
domain = userId.split("\\\\")[0]; builder.useSystemProperties();
userId = userId.split("\\\\")[1]; LOGGER.log(Level.WARNING, "Using Win HTTP Client");
} } else {
String workStation = proxySettings.getHostName(); builder = HttpClients.custom();
proxyCredsProvider.setCredentials(new AuthScope(proxySettings.getProxyHostname(), proxySettings.getProxyPort()), // builder.setDefaultRequestConfig(config);
new NTCredentials(userId, new String(proxySettings.getProxyPassword()), workStation, domain)); LOGGER.log(Level.WARNING, "Using default http client");
}
}
} }
return proxyCredsProvider; if (sslContext != null) {
builder.setSSLContext(sslContext);
}
if (proxySelector != null) {
builder.setRoutePlanner(new SystemDefaultRoutePlanner(proxySelector));
}
return builder.build();
} }
private static class ProxySettingArgs { private static class LoggingProxySelector extends ProxySelector {
private final boolean systemOrManualProxy; private final ProxySelector delegate;
private final String hostName;
private final String proxyHostname;
private final int proxyPort;
private final String proxyUserId;
private final char[] proxyPassword;
private final String authScheme;
ProxySettingArgs(boolean systemOrManualProxy, String hostName, String proxyHostname, int proxyPort, String proxyUserId, char[] proxyPassword, String authScheme) { public LoggingProxySelector(ProxySelector delegate) {
this.systemOrManualProxy = systemOrManualProxy; this.delegate = delegate;
this.hostName = hostName;
this.proxyHostname = proxyHostname;
this.proxyPort = proxyPort;
this.proxyUserId = proxyUserId;
this.proxyPassword = proxyPassword;
this.authScheme = authScheme;
}
boolean isSystemOrManualProxy() {
return systemOrManualProxy;
}
String getHostName() {
return hostName;
}
String getProxyHostname() {
return proxyHostname;
}
int getProxyPort() {
return proxyPort;
}
String getProxyUserId() {
return proxyUserId;
}
char[] getProxyPassword() {
return proxyPassword;
}
public String getAuthScheme() {
return authScheme;
} }
@Override @Override
public String toString() { public List<Proxy> select(URI uri) {
return "ProxySettingArgs{" + "systemOrManualProxy=" + systemOrManualProxy + ", hostName=" + hostName + ", proxyHostname=" + proxyHostname + ", proxyPort=" + proxyPort + ", proxyUserId=" + proxyUserId + ", proxyPassword set=" + (proxyPassword != null && proxyPassword.length > 0) + ", authScheme=" + authScheme + '}'; List<Proxy> selectedProxies = delegate.select(uri);
LOGGER.log(Level.FINE, MessageFormat.format("Proxy selected for {0} are {1}", uri, selectedProxies));
return selectedProxies;
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
LOGGER.log(Level.WARNING, MessageFormat.format("Connection failed connecting to {0} socket address {1}", uri, sa), ioe);
delegate.connectFailed(uri, sa, ioe);
} }
} }