From 30625e373549b909e589e74aa3cfe637d70b973f Mon Sep 17 00:00:00 2001 From: Alexandre Garnier Date: Mon, 1 Aug 2022 16:26:54 +0200 Subject: [PATCH] Handle other type of truststore Only the discouraged JKS was available --- src/main/java/com/splunk/hecclient/Hec.java | 7 ++++--- .../java/com/splunk/hecclient/HecConfig.java | 8 ++++++++ .../connect/SplunkSinkConnectorConfig.java | 9 ++++++++- .../com/splunk/hecclient/HecConfigTest.java | 2 ++ .../splunk/hecclient/HttpClientBuilderTest.java | 16 ++++++++++++++-- .../com/splunk/kafka/connect/ConfigProfile.java | 15 +++++++++++++-- .../connect/SplunkSinkConnectorConfigTest.java | 5 ++++- .../java/com/splunk/kafka/connect/UnitUtil.java | 1 + src/test/resources/keystoretest.p12 | Bin 0 -> 2724 bytes 9 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 src/test/resources/keystoretest.p12 diff --git a/src/main/java/com/splunk/hecclient/Hec.java b/src/main/java/com/splunk/hecclient/Hec.java index 802804ba..1a965bd3 100644 --- a/src/main/java/com/splunk/hecclient/Hec.java +++ b/src/main/java/com/splunk/hecclient/Hec.java @@ -287,7 +287,7 @@ public static CloseableHttpClient createHttpClient(final HecConfig config) { } // Code block for custom keystore client construction - SSLContext context = loadCustomSSLContext(config.getTrustStorePath(), config.getTrustStorePassword()); + SSLContext context = loadCustomSSLContext(config.getTrustStorePath(), config.getTrustStoreType(), config.getTrustStorePassword()); if (context != null) { return new HttpClientBuilder() @@ -309,6 +309,7 @@ public static CloseableHttpClient createHttpClient(final HecConfig config) { * a Hec Client with custom key store functionality. * * @param path A file path to the custom key store to be used. + * @param type The type of the key store file. * @param pass The password for the key store file. * @since 1.1.0 * @throws HecException @@ -316,9 +317,9 @@ public static CloseableHttpClient createHttpClient(final HecConfig config) { * @see KeyStore * @see SSLContext */ - public static SSLContext loadCustomSSLContext(String path, String pass) { + public static SSLContext loadCustomSSLContext(String path, String type, String pass) { try { - KeyStore ks = KeyStore.getInstance("JKS"); + KeyStore ks = KeyStore.getInstance(type); FileInputStream fileInputStream = new FileInputStream(path); ks.load(fileInputStream, pass.toCharArray()); diff --git a/src/main/java/com/splunk/hecclient/HecConfig.java b/src/main/java/com/splunk/hecclient/HecConfig.java index d6421603..d29eccb5 100644 --- a/src/main/java/com/splunk/hecclient/HecConfig.java +++ b/src/main/java/com/splunk/hecclient/HecConfig.java @@ -34,6 +34,7 @@ public final class HecConfig { private boolean enableChannelTracking = false; private boolean hasCustomTrustStore = false; private String trustStorePath; + private String trustStoreType = "JKS"; private String trustStorePassword; private int lbPollInterval = 120; // in seconds private String kerberosPrincipal; @@ -104,6 +105,8 @@ public int getBackoffThresholdSeconds() { public String getTrustStorePath() { return trustStorePath; } + public String getTrustStoreType() { return trustStoreType; } + public String getTrustStorePassword() { return trustStorePassword; } public HecConfig setDisableSSLCertVerification(boolean disableVerfication) { @@ -161,6 +164,11 @@ public HecConfig setTrustStorePath(String path) { return this; } + public HecConfig setTrustStoreType(String type) { + trustStoreType = type; + return this; + } + public HecConfig setTrustStorePassword(String pass) { trustStorePassword = pass; return this; diff --git a/src/main/java/com/splunk/kafka/connect/SplunkSinkConnectorConfig.java b/src/main/java/com/splunk/kafka/connect/SplunkSinkConnectorConfig.java index 937c2bea..dddd3291 100644 --- a/src/main/java/com/splunk/kafka/connect/SplunkSinkConnectorConfig.java +++ b/src/main/java/com/splunk/kafka/connect/SplunkSinkConnectorConfig.java @@ -74,6 +74,7 @@ public final class SplunkSinkConnectorConfig extends AbstractConfig { static final String HEC_EVENT_FORMATTED_CONF = "splunk.hec.json.event.formatted"; // Trust store static final String SSL_TRUSTSTORE_PATH_CONF = "splunk.hec.ssl.trust.store.path"; + static final String SSL_TRUSTSTORE_TYPE_CONF = "splunk.hec.ssl.trust.store.type"; static final String SSL_TRUSTSTORE_PASSWORD_CONF = "splunk.hec.ssl.trust.store.password"; //Headers static final String HEADER_SUPPORT_CONF = "splunk.header.support"; @@ -178,6 +179,7 @@ public final class SplunkSinkConnectorConfig extends AbstractConfig { + "correctly by Splunk."; // TBD static final String SSL_TRUSTSTORE_PATH_DOC = "Path on the local disk to the certificate trust store."; + static final String SSL_TRUSTSTORE_TYPE_DOC = "Type of the trust store (JKS, PKCS12, ...)."; static final String SSL_TRUSTSTORE_PASSWORD_DOC = "Password for the trust store."; static final String HEADER_SUPPORT_DOC = "Setting will enable Kafka Record headers to be used for meta data override"; @@ -236,6 +238,7 @@ public final class SplunkSinkConnectorConfig extends AbstractConfig { final boolean hasTrustStorePath; final String trustStorePath; + final String trustStoreType; final String trustStorePassword; final boolean headerSupport; @@ -265,6 +268,7 @@ public final class SplunkSinkConnectorConfig extends AbstractConfig { validateCertificates = getBoolean(SSL_VALIDATE_CERTIFICATES_CONF); trustStorePath = getString(SSL_TRUSTSTORE_PATH_CONF); hasTrustStorePath = StringUtils.isNotBlank(trustStorePath); + trustStoreType = getString(SSL_TRUSTSTORE_TYPE_CONF); trustStorePassword = getPassword(SSL_TRUSTSTORE_PASSWORD_CONF).value(); validateHttpsConfig(splunkURI); eventBatchTimeout = getInt(EVENT_TIMEOUT_CONF); @@ -318,6 +322,7 @@ public static ConfigDef conf() { .define(HTTP_KEEPALIVE_CONF, ConfigDef.Type.BOOLEAN, true, ConfigDef.Importance.MEDIUM, HTTP_KEEPALIVE_DOC) .define(SSL_VALIDATE_CERTIFICATES_CONF, ConfigDef.Type.BOOLEAN, true, ConfigDef.Importance.MEDIUM, SSL_VALIDATE_CERTIFICATES_DOC) .define(SSL_TRUSTSTORE_PATH_CONF, ConfigDef.Type.STRING, "", ConfigDef.Importance.HIGH, SSL_TRUSTSTORE_PATH_DOC) + .define(SSL_TRUSTSTORE_TYPE_CONF, ConfigDef.Type.STRING, "JKS", ConfigDef.Importance.LOW, SSL_TRUSTSTORE_TYPE_DOC) .define(SSL_TRUSTSTORE_PASSWORD_CONF, ConfigDef.Type.PASSWORD, "", ConfigDef.Importance.HIGH, SSL_TRUSTSTORE_PASSWORD_DOC) .define(EVENT_TIMEOUT_CONF, ConfigDef.Type.INT, 300, ConfigDef.Importance.MEDIUM, EVENT_TIMEOUT_DOC) .define(ACK_POLL_INTERVAL_CONF, ConfigDef.Type.INT, 10, ConfigDef.Importance.MEDIUM, ACK_POLL_INTERVAL_DOC) @@ -368,6 +373,7 @@ public HecConfig getHecConfig() { .setEnableChannelTracking(trackData) .setBackoffThresholdSeconds(backoffThresholdSeconds) .setTrustStorePath(trustStorePath) + .setTrustStoreType(trustStoreType) .setTrustStorePassword(trustStorePassword) .setHasCustomTrustStore(hasTrustStorePath) .setKerberosPrincipal(kerberosUserPrincipal) @@ -393,6 +399,7 @@ public String toString() { + "httpKeepAlive:" + httpKeepAlive + ", " + "validateCertificates:" + validateCertificates + ", " + "trustStorePath:" + trustStorePath + ", " + + "trustStoreType:" + trustStoreType + ", " + "socketTimeout:" + socketTimeout + ", " + "eventBatchTimeout:" + eventBatchTimeout + ", " + "ackPollInterval:" + ackPollInterval + ", " @@ -544,4 +551,4 @@ private static boolean getNamedGroupCandidates(String regex) { } return false; } -} \ No newline at end of file +} diff --git a/src/test/java/com/splunk/hecclient/HecConfigTest.java b/src/test/java/com/splunk/hecclient/HecConfigTest.java index a43d9054..3826bff4 100644 --- a/src/test/java/com/splunk/hecclient/HecConfigTest.java +++ b/src/test/java/com/splunk/hecclient/HecConfigTest.java @@ -44,6 +44,7 @@ public void getterSetter() { .setEnableChannelTracking(true) .setEventBatchTimeout(7) .setTrustStorePath("test") + .setTrustStoreType("PKCS12") .setTrustStorePassword("pass") .setHasCustomTrustStore(true) .setBackoffThresholdSeconds(10) @@ -60,6 +61,7 @@ public void getterSetter() { Assert.assertEquals(6, config.getAckPollThreads()); Assert.assertEquals(7, config.getEventBatchTimeout()); Assert.assertEquals("test", config.getTrustStorePath()); + Assert.assertEquals("PKCS12", config.getTrustStoreType()); Assert.assertEquals("pass", config.getTrustStorePassword()); Assert.assertEquals(10000, config.getBackoffThresholdSeconds()); Assert.assertEquals(120000, config.getlbPollInterval()); diff --git a/src/test/java/com/splunk/hecclient/HttpClientBuilderTest.java b/src/test/java/com/splunk/hecclient/HttpClientBuilderTest.java index 3d5fbfff..80490f4f 100644 --- a/src/test/java/com/splunk/hecclient/HttpClientBuilderTest.java +++ b/src/test/java/com/splunk/hecclient/HttpClientBuilderTest.java @@ -52,7 +52,19 @@ public void buildSecureCustomKeystore() { .setSocketSendBufferSize(1024) .setSocketTimeout(120) .setDisableSSLCertVerification(false) - .setSslContext(Hec.loadCustomSSLContext("./src/test/resources/keystoretest.jks","Notchangeme")) + .setSslContext(Hec.loadCustomSSLContext("./src/test/resources/keystoretest.jks", "JKS", "Notchangeme")) + .build(); + Assert.assertNotNull(client); + } + @Test + public void buildSecureCustomKeystorePkcs12() { + HttpClientBuilder builder = new HttpClientBuilder(); + CloseableHttpClient client = builder.setMaxConnectionPoolSizePerDestination(1) + .setMaxConnectionPoolSize(2) + .setSocketSendBufferSize(1024) + .setSocketTimeout(120) + .setDisableSSLCertVerification(false) + .setSslContext(Hec.loadCustomSSLContext("./src/test/resources/keystoretest.p12", "PKCS12", "Notchangeme")) .build(); Assert.assertNotNull(client); } @@ -63,4 +75,4 @@ public void buildDefault() { CloseableHttpClient client = builder.build(); Assert.assertNotNull(client); } -} \ No newline at end of file +} diff --git a/src/test/java/com/splunk/kafka/connect/ConfigProfile.java b/src/test/java/com/splunk/kafka/connect/ConfigProfile.java index b8085d4e..f6c75b9f 100644 --- a/src/test/java/com/splunk/kafka/connect/ConfigProfile.java +++ b/src/test/java/com/splunk/kafka/connect/ConfigProfile.java @@ -17,6 +17,7 @@ public class ConfigProfile { private boolean validateCertificates; private boolean hasTrustStorePath; private String trustStorePath; + private String trustStoreType; private String trustStorePassword; private int eventBatchTimeout; private int ackPollInterval; @@ -77,6 +78,7 @@ public ConfigProfile buildProfileDefault() { this.validateCertificates = true; this.hasTrustStorePath = true; this.trustStorePath = "./src/test/resources/keystoretest.jks"; + this.trustStoreType = "JKS"; this.trustStorePassword = "Notchangeme"; this.eventBatchTimeout = 1; this.ackPollInterval = 1; @@ -110,7 +112,8 @@ public ConfigProfile buildProfileOne() { this.httpKeepAlive = true; this.validateCertificates = true; this.hasTrustStorePath = true; - this.trustStorePath = "./src/test/resources/keystoretest.jks"; + this.trustStorePath = "./src/test/resources/keystoretest.p12"; + this.trustStoreType = "PKCS12"; this.trustStorePassword = "Notchangeme"; this.eventBatchTimeout = 1; this.ackPollInterval = 1; @@ -332,6 +335,14 @@ public void setTrustStorePath(String trustStorePath) { this.trustStorePath = trustStorePath; } + public String getTrustStoreType() { + return trustStoreType; + } + + public void setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + } + public String getTrustStorePassword() { return trustStorePassword; } @@ -461,6 +472,6 @@ public void setHeaderHost(String headerHost) { } @Override public String toString() { - return "ConfigProfile{" + "topics='" + topics + '\'' + ", topics.regex='" + topicsRegex + '\'' + ", token='" + token + '\'' + ", uri='" + uri + '\'' + ", raw=" + raw + ", ack=" + ack + ", indexes='" + indexes + '\'' + ", sourcetypes='" + sourcetypes + '\'' + ", sources='" + sources + '\'' + ", httpKeepAlive=" + httpKeepAlive + ", validateCertificates=" + validateCertificates + ", hasTrustStorePath=" + hasTrustStorePath + ", trustStorePath='" + trustStorePath + '\'' + ", trustStorePassword='" + trustStorePassword + '\'' + ", eventBatchTimeout=" + eventBatchTimeout + ", ackPollInterval=" + ackPollInterval + ", ackPollThreads=" + ackPollThreads + ", maxHttpConnPerChannel=" + maxHttpConnPerChannel + ", totalHecChannels=" + totalHecChannels + ", socketTimeout=" + socketTimeout + ", enrichements='" + enrichements + '\'' + ", enrichementMap=" + enrichementMap + ", trackData=" + trackData + ", maxBatchSize=" + maxBatchSize + ", numOfThreads=" + numOfThreads + '}'; + return "ConfigProfile{" + "topics='" + topics + '\'' + ", topics.regex='" + topicsRegex + '\'' + ", token='" + token + '\'' + ", uri='" + uri + '\'' + ", raw=" + raw + ", ack=" + ack + ", indexes='" + indexes + '\'' + ", sourcetypes='" + sourcetypes + '\'' + ", sources='" + sources + '\'' + ", httpKeepAlive=" + httpKeepAlive + ", validateCertificates=" + validateCertificates + ", hasTrustStorePath=" + hasTrustStorePath + ", trustStorePath='" + trustStorePath + '\'' + ", trustStoreType='" + trustStoreType + '\'' + ", trustStorePassword='" + trustStorePassword + '\'' + ", eventBatchTimeout=" + eventBatchTimeout + ", ackPollInterval=" + ackPollInterval + ", ackPollThreads=" + ackPollThreads + ", maxHttpConnPerChannel=" + maxHttpConnPerChannel + ", totalHecChannels=" + totalHecChannels + ", socketTimeout=" + socketTimeout + ", enrichements='" + enrichements + '\'' + ", enrichementMap=" + enrichementMap + ", trackData=" + trackData + ", maxBatchSize=" + maxBatchSize + ", numOfThreads=" + numOfThreads + '}'; } } diff --git a/src/test/java/com/splunk/kafka/connect/SplunkSinkConnectorConfigTest.java b/src/test/java/com/splunk/kafka/connect/SplunkSinkConnectorConfigTest.java index 82f1c997..2113fade 100644 --- a/src/test/java/com/splunk/kafka/connect/SplunkSinkConnectorConfigTest.java +++ b/src/test/java/com/splunk/kafka/connect/SplunkSinkConnectorConfigTest.java @@ -83,6 +83,7 @@ public void getHecConfigCustomKeystore() { HecConfig config = connectorConfig.getHecConfig(); Assert.assertEquals(true, config.getHasCustomTrustStore()); Assert.assertEquals(uu.configProfile.getTrustStorePath(), config.getTrustStorePath()); + Assert.assertEquals(uu.configProfile.getTrustStoreType(), config.getTrustStoreType()); Assert.assertEquals(uu.configProfile.getTrustStorePassword(), config.getTrustStorePassword()); } @@ -95,9 +96,10 @@ public void testCustomKeystore() throws KeyStoreException { HecConfig config = connectorConfig.getHecConfig(); Assert.assertEquals(true, config.getHasCustomTrustStore()); Assert.assertEquals(uu.configProfile.getTrustStorePath(), config.getTrustStorePath()); + Assert.assertEquals(uu.configProfile.getTrustStoreType(), config.getTrustStoreType()); Assert.assertEquals(uu.configProfile.getTrustStorePassword(), config.getTrustStorePassword()); - SSLContext context = Hec.loadCustomSSLContext(config.getTrustStorePath(),config.getTrustStorePassword()); + SSLContext context = Hec.loadCustomSSLContext(config.getTrustStorePath(), config.getTrustStoreType(), config.getTrustStorePassword()); Assert.assertNotNull(context); } @@ -315,6 +317,7 @@ private void commonAssert(final SplunkSinkConnectorConfig connectorConfig) { Assert.assertEquals(uu.configProfile.isHttpKeepAlive(), connectorConfig.httpKeepAlive); Assert.assertEquals(uu.configProfile.isValidateCertificates(), connectorConfig.validateCertificates); Assert.assertEquals(uu.configProfile.getTrustStorePath(), connectorConfig.trustStorePath); + Assert.assertEquals(uu.configProfile.getTrustStoreType(), connectorConfig.trustStoreType); Assert.assertEquals(uu.configProfile.getTrustStorePassword(), connectorConfig.trustStorePassword); Assert.assertEquals(uu.configProfile.getEventBatchTimeout(), connectorConfig.eventBatchTimeout); Assert.assertEquals(uu.configProfile.getAckPollInterval(), connectorConfig.ackPollInterval); diff --git a/src/test/java/com/splunk/kafka/connect/UnitUtil.java b/src/test/java/com/splunk/kafka/connect/UnitUtil.java index 1c2b3296..85ae6e04 100644 --- a/src/test/java/com/splunk/kafka/connect/UnitUtil.java +++ b/src/test/java/com/splunk/kafka/connect/UnitUtil.java @@ -45,6 +45,7 @@ public Map createTaskConfig() { if(configProfile.getTrustStorePath() != null ) { config.put(SplunkSinkConnectorConfig.SSL_TRUSTSTORE_PATH_CONF, configProfile.getTrustStorePath()); + config.put(SplunkSinkConnectorConfig.SSL_TRUSTSTORE_TYPE_CONF, configProfile.getTrustStoreType()); config.put(SplunkSinkConnectorConfig.SSL_TRUSTSTORE_PASSWORD_CONF, configProfile.getTrustStorePassword()); } diff --git a/src/test/resources/keystoretest.p12 b/src/test/resources/keystoretest.p12 new file mode 100644 index 0000000000000000000000000000000000000000..5aa24124725ef40fe93e796818e01996318c1d3e GIT binary patch literal 2724 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 literal 0 HcmV?d00001