diff --git a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliConfiguration.java b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliConfiguration.java index 874f816dd6f..91dccc94c91 100644 --- a/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliConfiguration.java +++ b/sql/cli/src/main/java/org/elasticsearch/xpack/sql/cli/CliConfiguration.java @@ -5,12 +5,12 @@ */ package org.elasticsearch.xpack.sql.cli; +import org.elasticsearch.xpack.sql.net.client.ConnectionConfiguration; + import java.net.MalformedURLException; import java.net.URL; import java.util.Properties; -import org.elasticsearch.xpack.sql.net.client.ConnectionConfiguration; - // // Supports the following syntax // @@ -74,7 +74,7 @@ public class CliConfiguration extends ConnectionConfiguration { public URL asUrl() { // TODO: need to assemble all the various params here try { - return new URL(isSSL() ? "https" : "http", hostAndPort.ip, port(), urlFile); + return new URL(isSSLEnabled() ? "https" : "http", hostAndPort.ip, port(), urlFile); } catch (MalformedURLException ex) { throw new IllegalArgumentException("Cannot connect to server " + originalUrl, ex); } diff --git a/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcConfiguration.java b/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcConfiguration.java index 92fa71b42cb..b6cc11ebf07 100644 --- a/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcConfiguration.java +++ b/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcConfiguration.java @@ -176,7 +176,7 @@ public class JdbcConfiguration extends ConnectionConfiguration { public URL asUrl() { // TODO: need to assemble all the various params here try { - return new URL(isSSL() ? "https" : "http", hostAndPort.ip, port(), urlFile); + return new URL(isSSLEnabled() ? "https" : "http", hostAndPort.ip, port(), urlFile); } catch (MalformedURLException ex) { throw new JdbcException(ex, "Cannot connect to server %s", originalUrl); } diff --git a/sql/net-client/build.gradle b/sql/net-client/build.gradle index d206c959ef3..1556c553b01 100644 --- a/sql/net-client/build.gradle +++ b/sql/net-client/build.gradle @@ -4,3 +4,21 @@ forbiddenApisMain { // does not depend on core, so only jdk and http signatures should be checked signaturesURLs = [this.class.getResource('/forbidden/jdk-signatures.txt')] } + +forbiddenApisTest { + bundledSignatures -= 'jdk-non-portable' + bundledSignatures += 'jdk-internal' +} + +// Allow for com.sun.net.httpserver.* usage for testing +eclipse { + classpath.file { + whenMerged { cp -> + def con = entries.find { e -> + e.kind == "con" && e.toString().contains("org.eclipse.jdt.launching.JRE_CONTAINER") + } + con.accessRules.add(new org.gradle.plugins.ide.eclipse.model.AccessRule( + "accessible", "com/sun/net/httpserver/*")) + } + } +} diff --git a/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/ConnectionConfiguration.java b/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/ConnectionConfiguration.java index 578aaa7562a..116cff59c04 100644 --- a/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/ConnectionConfiguration.java +++ b/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/ConnectionConfiguration.java @@ -5,8 +5,6 @@ */ package org.elasticsearch.xpack.sql.net.client; -import org.elasticsearch.xpack.sql.net.client.util.StringUtils; - import java.util.Properties; import java.util.concurrent.TimeUnit; @@ -35,48 +33,30 @@ public class ConnectionConfiguration { // Timeouts // 30s - static final String CONNECT_TIMEOUT = "connect.timeout"; - static final String CONNECT_TIMEOUT_DEFAULT = String.valueOf(TimeUnit.SECONDS.toMillis(30)); + private static final String CONNECT_TIMEOUT = "connect.timeout"; + private static final String CONNECT_TIMEOUT_DEFAULT = String.valueOf(TimeUnit.SECONDS.toMillis(30)); // 1m - static final String NETWORK_TIMEOUT = "network.timeout"; - static final String NETWORK_TIMEOUT_DEFAULT = String.valueOf(TimeUnit.MINUTES.toMillis(1)); + private static final String NETWORK_TIMEOUT = "network.timeout"; + private static final String NETWORK_TIMEOUT_DEFAULT = String.valueOf(TimeUnit.MINUTES.toMillis(1)); // 1m - static final String QUERY_TIMEOUT = "query.timeout"; - static final String QUERY_TIMEOUT_DEFAULT = String.valueOf(TimeUnit.MINUTES.toMillis(1)); + private static final String QUERY_TIMEOUT = "query.timeout"; + private static final String QUERY_TIMEOUT_DEFAULT = String.valueOf(TimeUnit.MINUTES.toMillis(1)); // 5m - static final String PAGE_TIMEOUT = "page.timeout"; - static final String PAGE_TIMEOUT_DEFAULT = String.valueOf(TimeUnit.MINUTES.toMillis(5)); + private static final String PAGE_TIMEOUT = "page.timeout"; + private static final String PAGE_TIMEOUT_DEFAULT = String.valueOf(TimeUnit.MINUTES.toMillis(5)); - static final String PAGE_SIZE = "page.size"; - static final String PAGE_SIZE_DEFAULT = "1000"; + private static final String PAGE_SIZE = "page.size"; + private static final String PAGE_SIZE_DEFAULT = "1000"; - static final String SSL = "ssl"; - static final String SSL_DEFAULT = "false"; + // Auth - static final String SSL_PROTOCOL = "ssl.protocol"; - static final String SSL_PROTOCOL_DEFAULT = "TLS"; // SSL alternative + private static final String AUTH_USER = "user"; + private static final String AUTH_PASS = "pass"; - static final String SSL_KEYSTORE_LOCATION = "ssl.keystore.location"; - static final String SSL_KEYSTORE_LOCATION_DEFAULT = ""; - - static final String SSL_KEYSTORE_PASS = "ssl.keystore.location"; - static final String SSL_KEYSTORE_PASS_DEFAULT = ""; - - static final String SSL_KEYSTORE_TYPE = "ssl.keystore.type"; - static final String SSL_KEYSTORE_TYPE_DEFAULT = "JKS"; // PCKS12 - - static final String SSL_TRUSTSTORE_LOCATION = "ssl.keystore.location"; - static final String SSL_TRUSTSTORE_LOCATION_DEFAULT = ""; - - static final String SSL_TRUSTSTORE_PASS = "ssl.keystore.location"; - static final String SSL_TRUSTSTORE_PASS_DEFAULT = ""; - - static final String SSL_TRUSTSTORE_TYPE = "ssl.keystore.location"; - static final String SSL_TRUSTSTORE_TYPE_DEFAULT = "ssl.keystore.location"; - + // Proxy private final Properties settings; @@ -86,7 +66,12 @@ public class ConnectionConfiguration { private long pageTimeout; private int pageSize; - private final boolean ssl; + + private final String user, pass; + + + private final SslConfig sslConfig; + private final ProxyConfig proxyConfig; public ConnectionConfiguration(Properties props) { settings = props != null ? new Properties(props) : new Properties(); @@ -97,17 +82,31 @@ public class ConnectionConfiguration { // page pageTimeout = Long.parseLong(settings.getProperty(PAGE_TIMEOUT, PAGE_TIMEOUT_DEFAULT)); pageSize = Integer.parseInt(settings.getProperty(PAGE_SIZE, PAGE_SIZE_DEFAULT)); - ssl = StringUtils.parseBoolean(settings.getProperty(SSL, SSL_DEFAULT)); + + // auth + user = settings.getProperty(AUTH_USER); + pass = settings.getProperty(AUTH_PASS); + + sslConfig = new SslConfig(props); + proxyConfig = new ProxyConfig(props); + } + + protected boolean isSSLEnabled() { + return sslConfig.isEnabled(); + } + + SslConfig sslConfig() { + return sslConfig; + } + + ProxyConfig proxyConfig() { + return proxyConfig; } protected Properties settings() { return settings; } - protected boolean isSSL() { - return ssl; - } - public void connectTimeout(long millis) { connectTimeout = millis; } @@ -139,4 +138,14 @@ public class ConnectionConfiguration { public int pageSize() { return pageSize; } + + // auth + + public String authUser() { + return user; + } + + public String authPass() { + return pass; + } } \ No newline at end of file diff --git a/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/JreHttpUrlConnection.java b/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/JreHttpUrlConnection.java index 801d4611f51..d13d7fcfd6d 100644 --- a/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/JreHttpUrlConnection.java +++ b/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/JreHttpUrlConnection.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.sql.net.client; import org.elasticsearch.xpack.sql.net.client.util.Bytes; import org.elasticsearch.xpack.sql.net.client.util.CheckedConsumer; import org.elasticsearch.xpack.sql.net.client.util.IOUtils; +import org.elasticsearch.xpack.sql.net.client.util.StringUtils; import java.io.Closeable; import java.io.DataOutput; @@ -16,10 +17,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; +import java.net.Proxy; import java.net.URL; +import java.util.Base64; import java.util.function.Function; import java.util.zip.GZIPInputStream; +import javax.net.ssl.HttpsURLConnection; + public class JreHttpUrlConnection implements Closeable { public static R http(URL url, ConnectionConfiguration cfg, Function handler) { try (JreHttpUrlConnection con = new JreHttpUrlConnection(url, cfg)) { @@ -35,20 +40,51 @@ public class JreHttpUrlConnection implements Closeable { public JreHttpUrlConnection(URL url, ConnectionConfiguration cfg) throws ClientException { this.url = url; try { - con = (HttpURLConnection) url.openConnection(); + // due to the way the URL API is designed, the proxy needs to be passed in first + Proxy p = cfg.proxyConfig().proxy(); + con = (HttpURLConnection) (p != null ? url.openConnection(p) : url.openConnection()); } catch (IOException ex) { throw new ClientException(ex, "Cannot setup connection to %s (%s)", url, ex.getMessage()); } + // the rest of the connection setup + setupConnection(cfg); + } + + private void setupConnection(ConnectionConfiguration cfg) { + // setup basic stuff first + + // timeouts con.setConnectTimeout((int) cfg.connectTimeout()); con.setReadTimeout((int) cfg.networkTimeout()); + // disable content caching con.setAllowUserInteraction(false); con.setUseCaches(false); + + // HTTP params // HttpURL adds this header by default, HttpS does not // adding it here to be consistent con.setRequestProperty("Accept-Charset", "UTF-8"); //con.setRequestProperty("Accept-Encoding", GZIP); + + setupSSL(cfg); + setupBasicAuth(cfg); + } + + private void setupSSL(ConnectionConfiguration cfg) { + if (cfg.sslConfig().isEnabled()) { + HttpsURLConnection https = (HttpsURLConnection) con; + https.setSSLSocketFactory(cfg.sslConfig().sslSocketFactory()); + } + } + + private void setupBasicAuth(ConnectionConfiguration cfg) { + if (StringUtils.hasText(cfg.authUser())) { + String basicValue = cfg.authUser() + ":" + cfg.authPass(); + String encoded = StringUtils.asUTFString(Base64.getEncoder().encode(StringUtils.toUTF(basicValue))); + con.setRequestProperty("Authorization", "Basic " + encoded); + } } public boolean head() throws ClientException { diff --git a/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/ProxyConfig.java b/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/ProxyConfig.java new file mode 100644 index 00000000000..762520013df --- /dev/null +++ b/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/ProxyConfig.java @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.net.client; + +import org.elasticsearch.xpack.sql.net.client.util.StringUtils; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Properties; + +class ProxyConfig { + + private static final String HTTP_PROXY = "proxy.http"; + private static final String HTTP_PROXY_DEFAULT = StringUtils.EMPTY; + private static final String SOCKS_PROXY = "proxy.socks"; + private static final String SOCKS_PROXY_DEFAULT = StringUtils.EMPTY; + + private final Proxy proxy; + + ProxyConfig(Properties settings) { + Proxy.Type type = null; + // try http first + Object[] address = host(settings.getProperty(HTTP_PROXY, HTTP_PROXY_DEFAULT), 80); + type = Proxy.Type.HTTP; + // nope, check socks + if (address == null) { + address = host(settings.getProperty(SOCKS_PROXY, SOCKS_PROXY_DEFAULT), 1080); + type = Proxy.Type.SOCKS; + } + if (address != null) { + proxy = createProxy(type, address); + } + else { + proxy = null; + } + } + + @SuppressForbidden(reason = "create the actual proxy") + private Proxy createProxy(Proxy.Type type, Object[] address) { + return new Proxy(type, new InetSocketAddress((String) address[0], (int) address[1])); + } + + boolean enabled() { + return proxy != null; + } + + Proxy proxy() { + return proxy; + } + + // returns hostname (string), port (int) + private static Object[] host(String address, int defaultPort) { + if (!StringUtils.hasText(address)) { + return null; + } + try { + URI uri = new URI(address); + Object[] results = { uri.getHost(), uri.getPort() > 0 ? uri.getPort() : defaultPort }; + return results; + } catch (URISyntaxException ex) { + throw new ClientException("Unrecognized address format %s", address); + } + } +} diff --git a/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/SslConfig.java b/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/SslConfig.java new file mode 100644 index 00000000000..a135734f9e8 --- /dev/null +++ b/sql/net-client/src/main/java/org/elasticsearch/xpack/sql/net/client/SslConfig.java @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.net.client; + +import org.elasticsearch.xpack.sql.net.client.util.StringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.Objects; +import java.util.Properties; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +class SslConfig { + + private static final String SSL = "ssl"; + private static final String SSL_DEFAULT = "false"; + + private static final String SSL_PROTOCOL = "ssl.protocol"; + private static final String SSL_PROTOCOL_DEFAULT = "TLS"; // SSL alternative + + private static final String SSL_KEYSTORE_LOCATION = "ssl.keystore.location"; + private static final String SSL_KEYSTORE_LOCATION_DEFAULT = ""; + + private static final String SSL_KEYSTORE_PASS = "ssl.keystore.pass"; + private static final String SSL_KEYSTORE_PASS_DEFAULT = ""; + + private static final String SSL_KEYSTORE_TYPE = "ssl.keystore.type"; + private static final String SSL_KEYSTORE_TYPE_DEFAULT = "JKS"; // PCKS12 + + private static final String SSL_TRUSTSTORE_LOCATION = "ssl.truststore.location"; + private static final String SSL_TRUSTSTORE_LOCATION_DEFAULT = ""; + + private static final String SSL_TRUSTSTORE_PASS = "ssl.truststore.pass"; + private static final String SSL_TRUSTSTORE_PASS_DEFAULT = ""; + + private static final String SSL_TRUSTSTORE_TYPE = "ssl.truststore.type"; + private static final String SSL_TRUSTSTORE_TYPE_DEFAULT = "JKS"; + + private final boolean enabled; + private final String protocol, keystoreLocation, keystorePass, keystoreType; + private final String truststoreLocation, truststorePass, truststoreType; + + private final SSLContext sslContext; + + SslConfig(Properties settings) { + // ssl + enabled = StringUtils.parseBoolean(settings.getProperty(SSL, SSL_DEFAULT)); + protocol = settings.getProperty(SSL_PROTOCOL, SSL_PROTOCOL_DEFAULT); + keystoreLocation = settings.getProperty(SSL_KEYSTORE_LOCATION, SSL_KEYSTORE_LOCATION_DEFAULT); + keystorePass = settings.getProperty(SSL_KEYSTORE_PASS, SSL_KEYSTORE_PASS_DEFAULT); + keystoreType = settings.getProperty(SSL_KEYSTORE_TYPE, SSL_KEYSTORE_TYPE_DEFAULT); + truststoreLocation = settings.getProperty(SSL_TRUSTSTORE_LOCATION, SSL_TRUSTSTORE_LOCATION_DEFAULT); + truststorePass = settings.getProperty(SSL_TRUSTSTORE_PASS, SSL_TRUSTSTORE_PASS_DEFAULT); + truststoreType = settings.getProperty(SSL_TRUSTSTORE_TYPE, SSL_TRUSTSTORE_TYPE_DEFAULT); + + sslContext = enabled ? createSSLContext() : null; + } + + // ssl + boolean isEnabled() { + return enabled; + } + + SSLSocketFactory sslSocketFactory() { + return sslContext.getSocketFactory(); + } + + private SSLContext createSSLContext() { + SSLContext ctx; + try { + ctx = SSLContext.getInstance(protocol); + ctx.init(loadKeyManagers(), loadTrustManagers(), null); + } catch (Exception ex) { + throw new ClientException(ex, "Failed to initialize SSL - %s", ex.getMessage()); + } + + return ctx; + } + + private KeyManager[] loadKeyManagers() throws GeneralSecurityException, IOException { + if (!StringUtils.hasText(keystoreLocation)) { + return null; + } + + char[] pass = (StringUtils.hasText(keystorePass) ? keystorePass.trim().toCharArray() : null); + KeyStore keyStore = loadKeyStore(keystoreLocation, pass, keystoreType); + KeyManagerFactory kmFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmFactory.init(keyStore, pass); + return kmFactory.getKeyManagers(); + } + + + private KeyStore loadKeyStore(String location, char[] pass, String keyStoreType) throws GeneralSecurityException, IOException { + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + Path path = Paths.get(location); + + if (!Files.exists(path)) { + throw new ClientException( + "Expected to find keystore file at [%s] but was unable to. Make sure you have specified a valid URI.", location); + } + + try (InputStream in = Files.newInputStream(Paths.get(location), StandardOpenOption.READ)) { + keyStore.load(in, pass); + } catch (Exception ex) { + throw new ClientException(ex, "Cannot open keystore [%s] - %s", location, ex.getMessage()); + } finally { + + } + return keyStore; + } + + private TrustManager[] loadTrustManagers() throws GeneralSecurityException, IOException { + KeyStore keyStore = null; + + if (StringUtils.hasText(truststoreLocation)) { + char[] pass = (StringUtils.hasText(truststorePass) ? truststorePass.trim().toCharArray() : null); + keyStore = loadKeyStore(truststoreLocation, pass, truststoreType); + } + + TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmFactory.init(keyStore); + return tmFactory.getTrustManagers(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + SslConfig other = (SslConfig) obj; + return Objects.equals(enabled, other.enabled) + && Objects.equals(protocol, other.protocol) + && Objects.equals(keystoreLocation, other.keystoreLocation) + && Objects.equals(keystorePass, other.keystorePass) + && Objects.equals(keystoreType, other.keystoreType) + && Objects.equals(truststoreLocation, other.truststoreLocation) + && Objects.equals(truststorePass, other.truststorePass) + && Objects.equals(truststoreType, other.truststoreType); + } + + public int hashCode() { + return getClass().hashCode(); + } +} \ No newline at end of file diff --git a/sql/net-client/src/test/java/org/elasticsearch/xpack/sql/net/client/BasicSSLServer.java b/sql/net-client/src/test/java/org/elasticsearch/xpack/sql/net/client/BasicSSLServer.java new file mode 100644 index 00000000000..d15c72cbe76 --- /dev/null +++ b/sql/net-client/src/test/java/org/elasticsearch/xpack/sql/net/client/BasicSSLServer.java @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.net.client; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsParameters; +import com.sun.net.httpserver.HttpsServer; + +import org.elasticsearch.common.io.Streams; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +@SuppressWarnings("restriction") +public class BasicSSLServer { + + @SuppressForbidden(reason = "it's a test, not production code") + private static class EchoHandler implements HttpHandler { + public void handle(HttpExchange e) throws IOException { + e.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0); + Streams.copy(e.getRequestBody(), e.getResponseBody()); + } + } + + private HttpsServer server; + private ExecutorService executor; + + public void start(int port) throws Exception { + + // similar to Executors.newCached but with a smaller bound and much smaller keep-alive + executor = Executors.newCachedThreadPool(); + + server = HttpsServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), port), port); + + server.setHttpsConfigurator(httpConfigurator()); + server.createContext("/ssl", new EchoHandler()); + server.setExecutor(executor); + server.start(); + } + + private static HttpsConfigurator httpConfigurator() throws Exception { + char[] pass = "password".toCharArray(); + // so this works on JDK 7 as well + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + KeyStore ks = KeyStore.getInstance("JKS"); + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + + ks.load(BasicSSLServer.class.getResourceAsStream("/ssl/server.keystore"), pass); + kmf.init(ks, pass); + + TrustManager[] trustAll = new TrustManager[] { + new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {} + } + }; + + // chain + sslContext.init(kmf.getKeyManagers(), trustAll, null); + + HttpsConfigurator configurator = new HttpsConfigurator(sslContext) { + public void configure(HttpsParameters params) { + try { + SSLContext c = getSSLContext(); + SSLParameters defaults = c.getDefaultSSLParameters(); + params.setSSLParameters(defaults); + // client can send a cert if they want to (use Need to force the client to present one) + //params.setWantClientAuth(true); + + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }; + + return configurator; + } + + public void stop() { + server.stop(1); + server = null; + executor.shutdownNow(); + executor = null; + } + + public InetSocketAddress address() { + return server != null ? server.getAddress() : null; + } + + public String url() { + return server != null ? "https://localhost:" + address().getPort() + "/ssl" : null; + } +} \ No newline at end of file diff --git a/sql/net-client/src/test/java/org/elasticsearch/xpack/sql/net/client/SSLTests.java b/sql/net-client/src/test/java/org/elasticsearch/xpack/sql/net/client/SSLTests.java new file mode 100644 index 00000000000..1809dcfe2db --- /dev/null +++ b/sql/net-client/src/test/java/org/elasticsearch/xpack/sql/net/client/SSLTests.java @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.sql.net.client; + + +import org.elasticsearch.common.io.PathUtils; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.net.client.util.Bytes; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.rules.ExternalResource; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.net.URL; +import java.util.Arrays; +import java.util.Properties; +import java.util.UUID; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +public class SSLTests extends ESTestCase { + + private static URL sslServer; + + @ClassRule + public static ExternalResource SSL_SERVER = new ExternalResource() { + private BasicSSLServer server; + + @Override + protected void before() throws Throwable { + server = new BasicSSLServer(); + server.start(0); + + sslServer = new URL(server.url()); + } + + @Override + protected void after() { + sslServer = null; + try { + server.stop(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }; + + private ConnectionConfiguration cfg; + + @Before + public void setup() throws Exception { + Properties prop = new Properties(); + // ssl config + prop.setProperty("ssl", "true"); + // specify the TLS just in case (who knows what else will be deprecated across JDKs) + prop.setProperty("ssl.protocol", "TLSv1.2"); + prop.setProperty("ssl.keystore.location", + PathUtils.get(getClass().getResource("/ssl/client.keystore").toURI()).toRealPath().toString()); + prop.setProperty("ssl.keystore.pass", "password"); + // set the truststore as well since otherwise there will be cert errors ... + prop.setProperty("ssl.truststore.location", + PathUtils.get(getClass().getResource("/ssl/client.keystore").toURI()).toRealPath().toString()); + prop.setProperty("ssl.truststore.pass", "password"); + //prop.setProperty("ssl.accept.self.signed.certs", "true"); + + cfg = new ConnectionConfiguration(prop); + } + + @After + public void destroy() { + cfg = null; + } + + public void testSslSetup() throws Exception { + SSLContext context = SSLContext.getDefault(); + SSLSocketFactory factory = context.getSocketFactory(); + SSLSocket socket = (SSLSocket) factory.createSocket(); + + String[] protocols = socket.getSupportedProtocols(); + + logger.info("Supported Protocols: {}", protocols.length); + logger.info("{}", Arrays.toString(protocols)); + + protocols = socket.getEnabledProtocols(); + + logger.info("Enabled Protocols: {}", protocols.length); + logger.info("{}", Arrays.toString(protocols)); + + String[] ciphers = socket.getSupportedCipherSuites(); + logger.info("{}", Arrays.toString(ciphers)); + } + + public void testSslHead() throws Exception { + assertTrue(JreHttpUrlConnection.http(sslServer, cfg, JreHttpUrlConnection::head)); + } + + public void testSslPost() throws Exception { + String message = UUID.randomUUID().toString(); + Bytes b = JreHttpUrlConnection.http(sslServer, cfg, c -> { + return c.post(o -> { + o.writeUTF(message); + }); + }); + + String received = new DataInputStream(new ByteArrayInputStream(b.bytes())).readUTF(); + assertEquals(message, received); + } +} \ No newline at end of file diff --git a/sql/net-client/src/test/resources/ssl/client.keystore b/sql/net-client/src/test/resources/ssl/client.keystore new file mode 100644 index 00000000000..07d44636234 Binary files /dev/null and b/sql/net-client/src/test/resources/ssl/client.keystore differ diff --git a/sql/net-client/src/test/resources/ssl/readme.txt b/sql/net-client/src/test/resources/ssl/readme.txt new file mode 100644 index 00000000000..769aa43abf0 --- /dev/null +++ b/sql/net-client/src/test/resources/ssl/readme.txt @@ -0,0 +1,13 @@ +# setup of the SSL files + +# generate keys for server and client +$ keytool -v -genkey -keyalg rsa -alias server -keypass password -keystore server.keystore -storepass password -validity 99999 -ext SAN=dns:localhost,ip:127.0.0.1 +$ keytool -v -genkey -keyalg rsa -alias client -keypass password -keystore client.keystore -storepass password -validity 99999 -ext SAN=dns:localhost,ip:127.0.0.1 + +# generate certificates +$ keytool -v -export -alias server -file server.crt -keystore server.keystore -storepass password +$ keytool -v -export -alias client -file client.crt -keystore client.keystore -storepass password + +# import the client cert into the server keystore and vice-versa +$ keytool -v -importcert -alias client -file client.crt -keystore server.keystore -storepass password +$ keytool -v -importcert -alias server -file server.crt -keystore client.keystore -storepass password \ No newline at end of file diff --git a/sql/net-client/src/test/resources/ssl/server.keystore b/sql/net-client/src/test/resources/ssl/server.keystore new file mode 100644 index 00000000000..3a2e80d77bb Binary files /dev/null and b/sql/net-client/src/test/resources/ssl/server.keystore differ