diff --git a/README.md b/README.md index 065a8ae..e594336 100644 --- a/README.md +++ b/README.md @@ -189,3 +189,25 @@ The following example shows how to obtain the output of a bulk job, as requested indixApiClient.close(); } ``` + +## Known issue(s) +If you're using the client on Android you might see the following error +``` +java.lang.NoSuchMethodError: No virtual method setSSLContext(Ljavax/net/ssl/SSLContext;)Lorg/apache/http/impl/client/HttpClientBuilder; +``` + +That's because the HttpClient that comes with this client is little newer than the one that's generally used in Android. The fix is to do the following + +``` +import com.indix.httpClient.HttpClient; +import com.indix.httpClient.impl.HttpClientFactory; +import com.indix.tools.SSLTrustCA; + +import org.apache.http.impl.client.HttpClients; + +HttpClient client = HttpClientFactory.newHttpClient(HttpClients.custom() + .setSslcontext(SSLTrustCA.trustLetsEncryptRootCA()) + .build()); +IndixApiClient indixApiClient = IndixApiClientFactory + .newIndixApiClient(appId, appKey, client); +``` diff --git a/src/main/java/com/indix/httpClient/impl/HttpClientFactory.java b/src/main/java/com/indix/httpClient/impl/HttpClientFactory.java index f00ac94..9b9d534 100644 --- a/src/main/java/com/indix/httpClient/impl/HttpClientFactory.java +++ b/src/main/java/com/indix/httpClient/impl/HttpClientFactory.java @@ -1,6 +1,7 @@ package com.indix.httpClient.impl; import com.indix.httpClient.HttpClient; +import org.apache.http.impl.client.CloseableHttpClient; /** * Instantiates http client instances @@ -10,4 +11,8 @@ public class HttpClientFactory { public static HttpClient newHttpClient() { return new HttpClientImpl(); } + + public static HttpClient newHttpClient(CloseableHttpClient httpClient) { + return new HttpClientImpl(httpClient); + } } diff --git a/src/main/java/com/indix/httpClient/impl/HttpClientImpl.java b/src/main/java/com/indix/httpClient/impl/HttpClientImpl.java index 13a6dce..8f69702 100644 --- a/src/main/java/com/indix/httpClient/impl/HttpClientImpl.java +++ b/src/main/java/com/indix/httpClient/impl/HttpClientImpl.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.indix.exception.*; import com.indix.httpClient.HttpClient; +import com.indix.tools.SSLTrustCA; import org.apache.http.*; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; @@ -35,7 +36,16 @@ class HttpClientImpl implements HttpClient { * configuration. */ public HttpClientImpl() { - closeableHttpClient = HttpClients.createDefault(); + this(HttpClients.custom().setSSLContext(SSLTrustCA.trustLetsEncryptRootCA()).build()); + } + + /** + * Creates with a custom {@link CloseableHttpClient} instance. + * + * @param closableHttpClient + */ + public HttpClientImpl(CloseableHttpClient closableHttpClient) { + closeableHttpClient = closableHttpClient; objectMapper = new ObjectMapper(); } diff --git a/src/main/java/com/indix/tools/SSLTrustCA.java b/src/main/java/com/indix/tools/SSLTrustCA.java new file mode 100644 index 0000000..180f3e0 --- /dev/null +++ b/src/main/java/com/indix/tools/SSLTrustCA.java @@ -0,0 +1,76 @@ +package com.indix.tools; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.*; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +/** + * Forked off from https://github.com/micw/ArduinoProjekte/blob/b7e308533d20c9d23fda5e08899c22afd1dc1303/java/ArduinoHomeServer/src/main/java/tools/SSLTrustCa.java + *

+ * Helps to add LetsEncrypt to current JVM instance's keystore so we can access api.indix.com. This change has no effect + * if the host JVM is >= JDK8u101, since this is already part of them. + *

+ * References + * - http://stackoverflow.com/questions/3508050/how-can-i-get-a-list-of-trusted-root-certificates-in-java/3508175#3508175 + * - http://stackoverflow.com/questions/34110426/does-java-support-lets-encrypt-certificates + * - https://community.letsencrypt.org/t/will-the-cross-root-cover-trust-by-the-default-list-in-the-jdk-jre/134/37 + */ +public final class SSLTrustCA { + + private static final char[] KEYSTORE_DEFAULT_PASSWORD = "changeit".toCharArray(); + + public static SSLContext trustLetsEncryptRootCA() { + return trustCa(SSLTrustCA.class.getResource("/ca/DSTRootCAX3.der")); + } + + private static KeyStore keyStore; + private final static Logger LOG = LoggerFactory.getLogger(SSLTrustCA.class); + + private synchronized static KeyStore initialize() + throws GeneralSecurityException, IOException { + + if (SSLTrustCA.keyStore == null) { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + + keyStore.load(null, KEYSTORE_DEFAULT_PASSWORD); + + SSLTrustCA.keyStore = keyStore; + } + + return SSLTrustCA.keyStore; + } + + private synchronized static SSLContext trustCa(URL caFile) { + try { + LOG.debug("Trusting CAFile: " + caFile.toExternalForm()); + Certificate crt; + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + try (InputStream caInput = new BufferedInputStream(caFile.openStream())) { + crt = cf.generateCertificate(caInput); + } + + String certName = ((X509Certificate) crt).getSubjectDN().getName(); + KeyStore keyStore = initialize(); + keyStore.setCertificateEntry(certName, crt); + + // Set this as the default keystore + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + SSLContext.setDefault(sslContext); + return sslContext; + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/src/main/resources/ca/DSTRootCAX3.der b/src/main/resources/ca/DSTRootCAX3.der new file mode 100644 index 0000000..95500f6 Binary files /dev/null and b/src/main/resources/ca/DSTRootCAX3.der differ