allow the system truststore to be used if one is not specified

A truststore should not be required as the default system truststore can be used
to validate certificates that have been signed by most commercial CAs.

Additionally, the HttpClient is now a lifecycle component to prevent out of memory
exceptions when starting up with a bad configuration; when an exception is thrown
in the constructor, Guice will continue to try to create the object until the system runs
out of memory.

Closes elastic/elasticsearch#476

Original commit: elastic/x-pack-elasticsearch@2333e47ac1
This commit is contained in:
jaymode 2015-05-14 09:45:43 -04:00
parent 5796a771c0
commit 09621f1267
4 changed files with 86 additions and 29 deletions

View File

@ -15,6 +15,7 @@ import org.elasticsearch.plugins.AbstractPlugin;
import org.elasticsearch.watcher.actions.email.service.InternalEmailService;
import org.elasticsearch.watcher.history.HistoryModule;
import org.elasticsearch.watcher.license.LicenseService;
import org.elasticsearch.watcher.support.http.HttpClient;
import org.elasticsearch.watcher.support.init.InitializingService;
import java.util.Collection;
@ -65,7 +66,8 @@ public class WatcherPlugin extends AbstractPlugin {
// constructs
InitializingService.class,
LicenseService.class,
InternalEmailService.class);
InternalEmailService.class,
HttpClient.class);
}
@Override

View File

@ -5,10 +5,11 @@
*/
package org.elasticsearch.watcher.support.http;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.ElasticsearchIllegalStateException;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.common.base.Charsets;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.settings.Settings;
@ -29,7 +30,7 @@ import java.util.Map;
/**
* Client class to wrap http connections
*/
public class HttpClient extends AbstractComponent {
public class HttpClient extends AbstractLifecycleComponent<HttpClient> {
private static final String SETTINGS_SSL_PREFIX = "watcher.http.ssl.";
private static final String SETTINGS_SSL_SHIELD_PREFIX = "shield.ssl.";
@ -43,13 +44,18 @@ public class HttpClient extends AbstractComponent {
public static final String SETTINGS_SSL_TRUSTSTORE_ALGORITHM = SETTINGS_SSL_PREFIX + "truststore.algorithm";
private static final String SETTINGS_SSL_SHIELD_TRUSTSTORE_ALGORITHM = SETTINGS_SSL_SHIELD_PREFIX + "truststore.algorithm";
private final SSLSocketFactory sslSocketFactory;
private final HttpAuthRegistry httpAuthRegistry;
private SSLSocketFactory sslSocketFactory;
@Inject
public HttpClient(Settings settings, HttpAuthRegistry httpAuthRegistry) {
super(settings);
this.httpAuthRegistry = httpAuthRegistry;
}
@Override
protected void doStart() throws ElasticsearchException {
if (!settings.getByPrefix(SETTINGS_SSL_PREFIX).getAsMap().isEmpty() ||
!settings.getByPrefix(SETTINGS_SSL_SHIELD_PREFIX).getAsMap().isEmpty()) {
sslSocketFactory = createSSLSocketFactory(settings);
@ -59,6 +65,14 @@ public class HttpClient extends AbstractComponent {
}
}
@Override
protected void doStop() throws ElasticsearchException {
}
@Override
protected void doClose() throws ElasticsearchException {
}
public HttpResponse execute(HttpRequest request) throws IOException {
String queryString = null;
if (request.params() != null && !request.params().isEmpty()) {
@ -135,7 +149,7 @@ public class HttpClient extends AbstractComponent {
String trustStoreAlgorithm = settings.get(SETTINGS_SSL_TRUSTSTORE_ALGORITHM, settings.get(SETTINGS_SSL_SHIELD_TRUSTSTORE_ALGORITHM, System.getProperty("ssl.TrustManagerFactory.algorithm")));
if (trustStore == null) {
throw new RuntimeException("truststore is not configured, use " + SETTINGS_SSL_TRUSTSTORE);
logger.debug("no truststore defined, using system default");
}
if (trustStoreAlgorithm == null) {
@ -148,27 +162,9 @@ public class HttpClient extends AbstractComponent {
throw new ElasticsearchIllegalStateException("could not find truststore [" + trustStore + "]");
}
KeyManager[] keyManagers;
TrustManager[] trustManagers;
try (InputStream trustStoreStream = Files.newInputStream(path)) {
// Load TrustStore
KeyStore ks = KeyStore.getInstance("jks");
ks.load(trustStoreStream, trustStorePassword == null ? null : trustStorePassword.toCharArray());
KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
factory.init(ks, trustStorePassword == null ? null : trustStorePassword.toCharArray());
keyManagers = factory.getKeyManagers();
// Initialize a trust manager factory with the trusted store
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(trustStoreAlgorithm);
trustFactory.init(ks);
// Retrieve the trust managers from the factory
trustManagers = trustFactory.getTrustManagers();
} catch (Exception e) {
throw new RuntimeException("http client failed to initialize a TrustManagerFactory", e);
}
// FIXME a keystore should be configurable and key_password also needs to be allowed. The fallback to Shield settings can be problematic without this
KeyManager[] keyManagers = keyManagers(trustStore, trustStorePassword, trustStoreAlgorithm, trustStorePassword);
TrustManager[] trustManagers = trustManagers(trustStore, trustStorePassword, trustStoreAlgorithm);
sslContext = SSLContext.getInstance(sslContextProtocol);
sslContext.init(keyManagers, trustManagers, new SecureRandom());
} catch (Exception e) {
@ -180,4 +176,49 @@ public class HttpClient extends AbstractComponent {
public SSLSocketFactory getSslSocketFactory() {
return sslSocketFactory;
}
private static KeyManager[] keyManagers(String keyStore, String keyStorePassword, String keyStoreAlgorithm, String keyPassword) {
if (keyStore == null) {
return null;
}
try {
// Load KeyStore
KeyStore ks = readKeystore(keyStore, keyStorePassword);
// Initialize KeyManagerFactory
KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyStoreAlgorithm);
kmf.init(ks, keyPassword.toCharArray());
return kmf.getKeyManagers();
} catch (Exception e) {
throw new RuntimeException("http client failed to initialize a KeyManagerFactory", e);
}
}
private static TrustManager[] trustManagers(String trustStorePath, String trustStorePassword, String trustStoreAlgorithm) {
try {
// Load TrustStore
KeyStore ks = null;
if (trustStorePath != null) {
ks = readKeystore(trustStorePath, trustStorePassword);
}
// Initialize a trust manager factory with the trusted store
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(trustStoreAlgorithm);
trustFactory.init(ks);
return trustFactory.getTrustManagers();
} catch (Exception e) {
throw new RuntimeException("http client failed to initialize a TrustManagerFactory", e);
}
}
private static KeyStore readKeystore(String path, String password) throws Exception {
try (InputStream in = Files.newInputStream(Paths.get(path))) {
// Load TrustStore
KeyStore ks = KeyStore.getInstance("jks");
assert password != null;
ks.load(in, password.toCharArray());
return ks;
}
}
}

View File

@ -59,7 +59,7 @@ public class IndexActionTests extends ElasticsearchIntegrationTest {
Watch watch = WatcherTestUtils.createTestWatch("test_watch",
ClientProxy.of(client()),
ScriptServiceProxy.of(internalCluster().getInstance(ScriptService.class)),
new HttpClient(ImmutableSettings.EMPTY, mock(HttpAuthRegistry.class)),
new HttpClient(ImmutableSettings.EMPTY, mock(HttpAuthRegistry.class)).start(),
new EmailService() {
@Override
public EmailService.EmailSent send(Email email, Authentication auth, Profile profile) {

View File

@ -14,6 +14,8 @@ import org.elasticsearch.common.base.Charsets;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.test.junit.annotations.Network;
import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
import org.elasticsearch.watcher.support.http.auth.HttpAuthFactory;
import org.elasticsearch.watcher.support.http.auth.HttpAuthRegistry;
import org.elasticsearch.watcher.support.http.auth.basic.BasicAuth;
@ -28,6 +30,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is;
@ -50,7 +53,7 @@ public class HttpClientTest extends ElasticsearchTestCase {
try {
webServer = new MockWebServer();
webServer.start(webPort);
httpClient = new HttpClient(ImmutableSettings.EMPTY, authRegistry);
httpClient = new HttpClient(ImmutableSettings.EMPTY, authRegistry).start();
return;
} catch (BindException be) {
logger.warn("port [{}] was already in use trying next port", webPort);
@ -139,7 +142,7 @@ public class HttpClientTest extends ElasticsearchTestCase {
ImmutableSettings.builder()
.put(HttpClient.SETTINGS_SSL_TRUSTSTORE, resource.toString())
.put(HttpClient.SETTINGS_SSL_TRUSTSTORE_PASSWORD, "testnode")
.build(), authRegistry);
.build(), authRegistry).start();
webServer.useHttps(httpClient.getSslSocketFactory(), false);
webServer.enqueue(new MockResponse().setResponseCode(200).setBody("body"));
@ -169,5 +172,16 @@ public class HttpClientTest extends ElasticsearchTestCase {
assertThat(response.body(), nullValue());
}
@Test
@Network
public void testHttpsWithoutTruststore() throws Exception {
HttpClient httpClient = new HttpClient(ImmutableSettings.EMPTY, authRegistry).start();
// Known server with a valid cert from a commercial CA
HttpRequest.Builder request = HttpRequest.builder("www.elastic.co", 443).scheme(Scheme.HTTPS);
HttpResponse response = httpClient.execute(request.build());
assertThat(response.status(), equalTo(200));
assertThat(response.hasContent(), is(true));
assertThat(response.body(), notNullValue());
}
}