diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java index 003a9e4347a65c2c33ac212affa349a006d2f07b..039d1d9e83788795e23973b07321d14ec6e34bf6 100644 --- a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java +++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudHttpClient.java @@ -22,12 +22,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; -import java.net.Authenticator; -import java.net.InetAddress; -import java.net.PasswordAuthentication; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; -import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; @@ -35,14 +34,13 @@ import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; import java.util.logging.Level; +import java.util.stream.Stream; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -50,14 +48,10 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; -import org.apache.http.HttpHost; 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.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; @@ -71,29 +65,27 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.apache.http.impl.client.HttpClientBuilder; 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.conn.SystemDefaultRoutePlanner; import org.apache.http.ssl.SSLInitializationException; import org.netbeans.core.ProxySettings; -import org.openide.util.NetworkSettings; +import org.openide.util.Lookup; import org.sleuthkit.autopsy.coreutils.Version; /** * 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 { 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 List<String> DEFAULT_SCHEME_PRIORITY - = new ArrayList<>(Arrays.asList( - AuthSchemes.SPNEGO, - AuthSchemes.KERBEROS, - AuthSchemes.NTLM, - AuthSchemes.CREDSSP, - AuthSchemes.DIGEST, - AuthSchemes.BASIC)); + private static final String NB_PROXY_SELECTOR_NAME = "org.netbeans.core.NbProxySelector"; private static final int CONNECTION_TIMEOUT_MS = 58 * 1000; // milli sec @@ -105,48 +97,12 @@ public static CTCloudHttpClient getInstance() { private final ObjectMapper mapper = ObjectMapperUtil.getInstance().getDefaultObjectMapper(); private final SSLContext sslContext; - private String hostName = null; + private final ProxySelector proxySelector; private CTCloudHttpClient() { // leave as null for now unless we want to customize this at a later date this.sslContext = createSSLContext(); - } - - 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; + this.proxySelector = getProxySelector(); } private static URI getUri(String host, String path, Map<String, String> urlReqParams) throws URISyntaxException { @@ -171,12 +127,12 @@ public <O> O doPost(String urlPath, Object jsonBody, Class<O> classType) throws } public <O> O doPost(String urlPath, Map<String, String> urlReqParams, Object jsonBody, Class<O> classType) throws CTCloudException { - + URI postURI = null; try { postURI = getUri(HOST_URL, urlPath, urlReqParams); 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); @@ -237,7 +193,7 @@ public void doFileUploadPost(String fullUrlPath, String fileName, InputStream fi 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); HttpPost post = new HttpPost(postUri); configureRequestTimeout(post); @@ -335,19 +291,30 @@ private void configureRequestTimeout(HttpRequestBase request) { } /** - * Creates a connection to CT Cloud with the given arguments. + * Get ProxySelector present (favoring NbProxySelector if present). * - * @param proxySettings The network proxy settings. - * @param sslContext The ssl context or null. - * @return The connection to CT Cloud. + * @return The found ProxySelector or null. */ - private static CloseableHttpClient createConnection(ProxySettingArgs proxySettings, SSLContext sslContext) throws SSLInitializationException { - HttpClientBuilder builder = getHttpClientBuilder(proxySettings); - if (sslContext != null) { - builder.setSSLContext(sslContext); - } - - return builder.build(); + private static ProxySelector getProxySelector() { + Collection<? extends ProxySelector> selectors = Lookup.getDefault().lookupAll(ProxySelector.class); + return (selectors != null ? selectors.stream() : Stream.empty()) + .filter(s -> s != null) + .map(s -> (ProxySelector) s) + .sorted((a, b) -> { + 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 @@ private static SSLContext createSSLContext() { } } + // jvm default key manager // based in part on this: https://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm/16229909 private static KeyManager[] getKeyManagers() { LOGGER.log(Level.INFO, "Using default algorithm to create trust store: " + KeyManagerFactory.getDefaultAlgorithm()); @@ -385,6 +353,7 @@ private static KeyManager[] getKeyManagers() { } + // jvm default trust store // based in part on this: https://stackoverflow.com/questions/1793979/registering-multiple-keystores-in-jvm/16229909 private static TrustManager[] getTrustManagers() { try { @@ -401,138 +370,60 @@ private static TrustManager[] getTrustManagers() { } } - 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, - * null otherwise + * @param proxySelector The proxy selector. + * @param sslContext The ssl context or null. + * @return The connection to CT Cloud. */ - private static CredentialsProvider getProxyCredentialsProvider(ProxySettingArgs proxySettings) { - CredentialsProvider proxyCredsProvider = null; - if (proxySettings.isSystemOrManualProxy()) { - if (StringUtils.isNotBlank(proxySettings.getProxyUserId())) { - if (null != proxySettings.getProxyPassword() && proxySettings.getProxyPassword().length > 0) { // Password will be blank for KERBEROS / NEGOTIATE schemes. - proxyCredsProvider = new SystemDefaultCredentialsProvider(); - String userId = proxySettings.getProxyUserId(); - String domain = null; - if (userId.contains("\\")) { - domain = userId.split("\\\\")[0]; - userId = userId.split("\\\\")[1]; - } - String workStation = proxySettings.getHostName(); - proxyCredsProvider.setCredentials(new AuthScope(proxySettings.getProxyHostname(), proxySettings.getProxyPort()), - new NTCredentials(userId, new String(proxySettings.getProxyPassword()), workStation, domain)); - } - } - } + private static CloseableHttpClient createConnection(ProxySelector proxySelector, SSLContext sslContext) throws SSLInitializationException { + HttpClientBuilder builder; - return proxyCredsProvider; - } + if (ProxySettings.getProxyType() != ProxySettings.DIRECT_CONNECTION + && StringUtils.isBlank(ProxySettings.getAuthenticationUsername()) + && ArrayUtils.isEmpty(ProxySettings.getAuthenticationPassword()) + && WinHttpClients.isWinAuthAvailable()) { - private static class ProxySettingArgs { - - private final boolean systemOrManualProxy; - 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) { - this.systemOrManualProxy = systemOrManualProxy; - this.hostName = hostName; - this.proxyHostname = proxyHostname; - this.proxyPort = proxyPort; - this.proxyUserId = proxyUserId; - this.proxyPassword = proxyPassword; - this.authScheme = authScheme; + builder = WinHttpClients.custom(); + builder.useSystemProperties(); + LOGGER.log(Level.WARNING, "Using Win HTTP Client"); + } else { + builder = HttpClients.custom(); + // builder.setDefaultRequestConfig(config); + LOGGER.log(Level.WARNING, "Using default http client"); } - boolean isSystemOrManualProxy() { - return systemOrManualProxy; + if (sslContext != null) { + builder.setSSLContext(sslContext); } - String getHostName() { - return hostName; + if (proxySelector != null) { + builder.setRoutePlanner(new SystemDefaultRoutePlanner(proxySelector)); } - String getProxyHostname() { - return proxyHostname; - } + return builder.build(); + } - int getProxyPort() { - return proxyPort; - } + private static class LoggingProxySelector extends ProxySelector { - String getProxyUserId() { - return proxyUserId; - } + private final ProxySelector delegate; - char[] getProxyPassword() { - return proxyPassword; + public LoggingProxySelector(ProxySelector delegate) { + this.delegate = delegate; } - public String getAuthScheme() { - return authScheme; + @Override + public List<Proxy> select(URI uri) { + List<Proxy> selectedProxies = delegate.select(uri); + LOGGER.log(Level.FINE, MessageFormat.format("Proxy selected for {0} are {1}", uri, selectedProxies)); + return selectedProxies; } @Override - public String toString() { - return "ProxySettingArgs{" + "systemOrManualProxy=" + systemOrManualProxy + ", hostName=" + hostName + ", proxyHostname=" + proxyHostname + ", proxyPort=" + proxyPort + ", proxyUserId=" + proxyUserId + ", proxyPassword set=" + (proxyPassword != null && proxyPassword.length > 0) + ", authScheme=" + authScheme + '}'; + 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); } }