diff --git a/pom.xml b/pom.xml
index 9c50fd8..bc07077 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,6 +19,7 @@
UTF-8
1.8
${maven.compiler.source}
+ 4.8.1
@@ -55,8 +56,23 @@
com.squareup.okhttp3
okhttp
- 3.2.0
+ ${okhttp.version}
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ ${okhttp.version}
+ test
+
+
+
+ com.squareup.okhttp3
+ okhttp-tls
+ ${okhttp.version}
+ test
+
+
org.junit.jupiter
junit-jupiter
@@ -109,27 +125,27 @@
2.1.2
- attach-sources
-
- jar-no-fork
-
+ attach-sources
+
+ jar-no-fork
+
-
+
- org.apache.maven.plugins
- maven-javadoc-plugin
- 2.7
-
-
- attach-javadocs
-
- jar
-
-
-
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 2.7
+
+
+ attach-javadocs
+
+ jar
+
+
+
-
+
org.apache.maven.plugins
maven-assembly-plugin
@@ -181,4 +197,4 @@
https://opensource.org/licenses/BSD-3-Clause
-
+
\ No newline at end of file
diff --git a/src/main/java/com/clevertap/apns/clients/ApnsClientBuilder.java b/src/main/java/com/clevertap/apns/clients/ApnsClientBuilder.java
index ceaaf93..7b4c1a2 100644
--- a/src/main/java/com/clevertap/apns/clients/ApnsClientBuilder.java
+++ b/src/main/java/com/clevertap/apns/clients/ApnsClientBuilder.java
@@ -31,6 +31,7 @@
package com.clevertap.apns.clients;
import com.clevertap.apns.ApnsClient;
+import com.clevertap.apns.exceptions.InvalidTrustManagerException;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClient.Builder;
@@ -52,6 +53,7 @@ public class ApnsClientBuilder {
private boolean production;
private String password;
private int connectionPort = 443;
+ private String gatewayUrl;
private boolean asynchronous = false;
private String defaultTopic = null;
@@ -169,9 +171,14 @@ public ApnsClientBuilder withDefaultTopic(String defaultTopic) {
return this;
}
+ public ApnsClientBuilder withGatewayUrl(String url) {
+ this.gatewayUrl = url;
+ return this;
+ }
+
public ApnsClient build() throws CertificateException,
NoSuchAlgorithmException, KeyStoreException, IOException,
- UnrecoverableKeyException, KeyManagementException {
+ UnrecoverableKeyException, KeyManagementException, InvalidTrustManagerException {
if (builder == null) {
builder = createDefaultOkHttpClientBuilder();
@@ -183,15 +190,15 @@ public ApnsClient build() throws CertificateException,
if (certificate != null) {
if (asynchronous) {
- return new AsyncOkHttpApnsClient(certificate, password, production, defaultTopic, builder, connectionPort);
+ return new AsyncOkHttpApnsClient(certificate, password, production, defaultTopic, builder, connectionPort, gatewayUrl);
} else {
- return new SyncOkHttpApnsClient(certificate, password, production, defaultTopic, builder, connectionPort);
+ return new SyncOkHttpApnsClient(certificate, password, production, defaultTopic, builder, connectionPort, gatewayUrl);
}
} else if (keyID != null && teamID != null && apnsAuthKey != null) {
if (asynchronous) {
- return new AsyncOkHttpApnsClient(apnsAuthKey, teamID, keyID, production, defaultTopic, builder, connectionPort);
+ return new AsyncOkHttpApnsClient(apnsAuthKey, teamID, keyID, production, defaultTopic, builder, connectionPort, gatewayUrl);
} else {
- return new SyncOkHttpApnsClient(apnsAuthKey, teamID, keyID, production, defaultTopic, builder, connectionPort);
+ return new SyncOkHttpApnsClient(apnsAuthKey, teamID, keyID, production, defaultTopic, builder, connectionPort, gatewayUrl);
}
} else {
throw new IllegalArgumentException("Either the token credentials (team ID, key ID, and the private key) " +
diff --git a/src/main/java/com/clevertap/apns/clients/AsyncOkHttpApnsClient.java b/src/main/java/com/clevertap/apns/clients/AsyncOkHttpApnsClient.java
index 4e79ee1..ac3d558 100644
--- a/src/main/java/com/clevertap/apns/clients/AsyncOkHttpApnsClient.java
+++ b/src/main/java/com/clevertap/apns/clients/AsyncOkHttpApnsClient.java
@@ -33,6 +33,7 @@
import com.clevertap.apns.Notification;
import com.clevertap.apns.NotificationResponse;
import com.clevertap.apns.NotificationResponseListener;
+import com.clevertap.apns.exceptions.InvalidTrustManagerException;
import okhttp3.*;
import java.io.IOException;
@@ -56,7 +57,7 @@ public AsyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID,
public AsyncOkHttpApnsClient(InputStream certificate, String password, boolean production,
String defaultTopic, ConnectionPool connectionPool)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
- IOException, UnrecoverableKeyException, KeyManagementException {
+ IOException, UnrecoverableKeyException, KeyManagementException, InvalidTrustManagerException {
super(certificate, password, production, defaultTopic, connectionPool);
}
@@ -65,23 +66,36 @@ public AsyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID,
this(apnsAuthKey, teamID, keyID, production, defaultTopic, builder, 443);
}
+ public AsyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID,
+ boolean production, String defaultTopic, OkHttpClient.Builder builder, int connectionPort,
+ String gatewayUrl) {
+ super(apnsAuthKey, teamID, keyID, production, defaultTopic, builder, gatewayUrl);
+ }
+
public AsyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID,
boolean production, String defaultTopic, OkHttpClient.Builder builder, int connectionPort) {
- super(apnsAuthKey, teamID, keyID, production, defaultTopic, builder);
+ this(apnsAuthKey, teamID, keyID, production, defaultTopic, builder, 443, null);
}
public AsyncOkHttpApnsClient(InputStream certificate, String password, boolean production,
String defaultTopic, OkHttpClient.Builder builder)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
- IOException, UnrecoverableKeyException, KeyManagementException {
+ IOException, UnrecoverableKeyException, KeyManagementException, InvalidTrustManagerException {
this(certificate, password, production, defaultTopic, builder, 443);
}
+ public AsyncOkHttpApnsClient(InputStream certificate, String password, boolean production,
+ String defaultTopic, OkHttpClient.Builder builder, int connectionPort, String gatewayUrl)
+ throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
+ IOException, UnrecoverableKeyException, KeyManagementException, InvalidTrustManagerException {
+ super(certificate, password, production, defaultTopic, builder, gatewayUrl);
+ }
+
public AsyncOkHttpApnsClient(InputStream certificate, String password, boolean production,
String defaultTopic, OkHttpClient.Builder builder, int connectionPort)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
- IOException, UnrecoverableKeyException, KeyManagementException {
- super(certificate, password, production, defaultTopic, builder);
+ IOException, UnrecoverableKeyException, KeyManagementException, InvalidTrustManagerException {
+ this(certificate, password, production, defaultTopic, builder, 443, null);
}
@Override
diff --git a/src/main/java/com/clevertap/apns/clients/SyncOkHttpApnsClient.java b/src/main/java/com/clevertap/apns/clients/SyncOkHttpApnsClient.java
index 86e8c22..970b6a2 100644
--- a/src/main/java/com/clevertap/apns/clients/SyncOkHttpApnsClient.java
+++ b/src/main/java/com/clevertap/apns/clients/SyncOkHttpApnsClient.java
@@ -31,6 +31,7 @@
package com.clevertap.apns.clients;
import com.clevertap.apns.*;
+import com.clevertap.apns.exceptions.InvalidTrustManagerException;
import com.clevertap.apns.internal.Constants;
import com.clevertap.apns.internal.JWT;
import okhttp3.*;
@@ -43,6 +44,7 @@
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
import java.util.UUID;
/**
@@ -64,31 +66,43 @@ public class SyncOkHttpApnsClient implements ApnsClient {
/**
* Creates a new client which uses token authentication API.
*
- * @param apnsAuthKey The private key - exclude -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----
+ * @param apnsAuthKey The private key - exclude -----BEGIN PRIVATE KEY----- and -----END
+ * PRIVATE KEY-----
* @param teamID The team ID
* @param keyID The key ID (retrieved from the file name)
* @param production Whether to use the production endpoint or the sandbox endpoint
* @param defaultTopic A default topic (can be changed per message)
- * @param clientBuilder An OkHttp client builder, possibly pre-initialized, to build the actual client
+ * @param clientBuilder An OkHttp client builder, possibly pre-initialized, to build the actual
+ * client
+ * @param gatewayUrl The gateway url the APNS client should point to
*/
public SyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID, boolean production,
- String defaultTopic, OkHttpClient.Builder clientBuilder) {
- this(apnsAuthKey, teamID, keyID, production, defaultTopic, clientBuilder, 443);
+ String defaultTopic, OkHttpClient.Builder clientBuilder, String gatewayUrl) {
+ this(apnsAuthKey, teamID, keyID, production, defaultTopic, clientBuilder, 443, gatewayUrl);
+ }
+
+ public SyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID, boolean production,
+ String defaultTopic, OkHttpClient.Builder clientBuilder) {
+ this(apnsAuthKey, teamID, keyID, production, defaultTopic, clientBuilder, 443, null);
}
/**
* Creates a new client which uses token authentication API.
*
- * @param apnsAuthKey The private key - exclude -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----
+ * @param apnsAuthKey The private key - exclude -----BEGIN PRIVATE KEY----- and -----END
+ * PRIVATE KEY-----
* @param teamID The team ID
* @param keyID The key ID (retrieved from the file name)
* @param production Whether to use the production endpoint or the sandbox endpoint
* @param defaultTopic A default topic (can be changed per message)
- * @param clientBuilder An OkHttp client builder, possibly pre-initialized, to build the actual client
+ * @param clientBuilder An OkHttp client builder, possibly pre-initialized, to build the actual
+ * client
* @param connectionPort The port to establish a connection with APNs. Either 443 or 2197
+ * @param gatewayUrl The gateway url the APNS client should point to
*/
public SyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID, boolean production,
- String defaultTopic, OkHttpClient.Builder clientBuilder, int connectionPort) {
+ String defaultTopic, OkHttpClient.Builder clientBuilder, int connectionPort,
+ String gatewayUrl) {
this.apnsAuthKey = apnsAuthKey;
this.teamID = teamID;
this.keyID = keyID;
@@ -96,13 +110,39 @@ public SyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID, boo
this.defaultTopic = defaultTopic;
- gateway = (production ? Constants.ENDPOINT_PRODUCTION : Constants.ENDPOINT_SANDBOX) + ":" + connectionPort;
+ if (gatewayUrl == null) {
+ gateway =
+ (production ? Constants.ENDPOINT_PRODUCTION : Constants.ENDPOINT_SANDBOX) + ":"
+ + connectionPort;
+ } else {
+ gateway = gatewayUrl;
+ }
}
/**
* Creates a new client which uses token authentication API.
*
- * @param apnsAuthKey The private key - exclude -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----
+ * @param apnsAuthKey The private key - exclude -----BEGIN PRIVATE KEY----- and -----END
+ * PRIVATE KEY-----
+ * @param teamID The team ID
+ * @param keyID The key ID (retrieved from the file name)
+ * @param production Whether to use the production endpoint or the sandbox endpoint
+ * @param defaultTopic A default topic (can be changed per message)
+ * @param clientBuilder An OkHttp client builder, possibly pre-initialized, to build the actual
+ * client
+ * @param connectionPort The port to establish a connection with APNs. Either 443 or 2197
+ */
+ public SyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID, boolean production,
+ String defaultTopic, OkHttpClient.Builder clientBuilder, int connectionPort) {
+ this(apnsAuthKey, teamID, keyID, production, defaultTopic, clientBuilder, connectionPort,
+ null);
+ }
+
+ /**
+ * Creates a new client which uses token authentication API.
+ *
+ * @param apnsAuthKey The private key - exclude -----BEGIN PRIVATE KEY----- and -----END
+ * PRIVATE KEY-----
* @param teamID The team ID
* @param keyID The key ID (retrieved from the file name)
* @param production Whether to use the production endpoint or the sandbox endpoint
@@ -110,57 +150,105 @@ public SyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID, boo
* @param connectionPool A connection pool to use. If null, a new one will be generated
*/
public SyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID, boolean production,
- String defaultTopic, ConnectionPool connectionPool) {
+ String defaultTopic, ConnectionPool connectionPool) {
this(apnsAuthKey, teamID, keyID, production, defaultTopic, getBuilder(connectionPool));
}
/**
- * Creates a new client and automatically loads the key store
- * with the push certificate read from the input stream.
+ * Creates a new client and automatically loads the key store with the push certificate read
+ * from the input stream.
+ *
+ * @param certificate The client certificate to be used
+ * @param password The password (if required, else null)
+ * @param production Whether to use the production endpoint or the sandbox endpoint
+ * @param defaultTopic A default topic (can be changed per message)
+ * @param builder An OkHttp client builder, possibly pre-initialized, to build the actual
+ * client
+ * @throws UnrecoverableKeyException If the key cannot be recovered
+ * @throws KeyManagementException if the key failed to be loaded
+ * @throws CertificateException if any of the certificates in the keystore could not be
+ * loaded
+ * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the
+ * keystore cannot be found
+ * @throws IOException if there is an I/O or format problem with the keystore
+ * data, if a password is required but not given, or if the
+ * given password was incorrect
+ * @throws KeyStoreException if no Provider supports a KeyStoreSpi implementation for
+ * the specified type
+ * @throws InvalidTrustManagerException if two or more TrustManagers were found (unsupoprted by
+ * the underlying OkHttp library)
+ */
+ public SyncOkHttpApnsClient(InputStream certificate, String password, boolean production,
+ String defaultTopic, OkHttpClient.Builder builder)
+ throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
+ IOException, UnrecoverableKeyException, KeyManagementException, InvalidTrustManagerException {
+ this(certificate, password, production, defaultTopic, builder, 443, null);
+ }
+
+ /**
+ * Creates a new client and automatically loads the key store with the push certificate read
+ * from the input stream.
*
* @param certificate The client certificate to be used
* @param password The password (if required, else null)
* @param production Whether to use the production endpoint or the sandbox endpoint
* @param defaultTopic A default topic (can be changed per message)
- * @param builder An OkHttp client builder, possibly pre-initialized, to build the actual client
- * @throws UnrecoverableKeyException If the key cannot be recovered
- * @throws KeyManagementException if the key failed to be loaded
- * @throws CertificateException if any of the certificates in the keystore could not be loaded
- * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found
- * @throws IOException if there is an I/O or format problem with the keystore data,
- * if a password is required but not given, or if the given password was incorrect
- * @throws KeyStoreException if no Provider supports a KeyStoreSpi implementation for the specified type
+ * @param builder An OkHttp client builder, possibly pre-initialized, to build the actual
+ * client
+ * @param gatewayUrl The gateway url the APNS client should point to
+ * @throws UnrecoverableKeyException If the key cannot be recovered
+ * @throws KeyManagementException if the key failed to be loaded
+ * @throws CertificateException if any of the certificates in the keystore could not be
+ * loaded
+ * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the
+ * keystore cannot be found
+ * @throws IOException if there is an I/O or format problem with the keystore
+ * data, if a password is required but not given, or if the
+ * given password was incorrect
+ * @throws KeyStoreException if no Provider supports a KeyStoreSpi implementation for
+ * the specified type
+ * @throws InvalidTrustManagerException if two or more TrustManagers were found (unsupoprted by
+ * the underlying OkHttp library)
*/
public SyncOkHttpApnsClient(InputStream certificate, String password, boolean production,
- String defaultTopic, OkHttpClient.Builder builder)
+ String defaultTopic, OkHttpClient.Builder builder, String gatewayUrl)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
- IOException, UnrecoverableKeyException, KeyManagementException {
- this(certificate, password, production, defaultTopic, builder, 443);
+ IOException, UnrecoverableKeyException, KeyManagementException, InvalidTrustManagerException {
+ this(certificate, password, production, defaultTopic, builder, 443, gatewayUrl);
}
/**
- * Creates a new client and automatically loads the key store
- * with the push certificate read from the input stream.
+ * Creates a new client and automatically loads the key store with the push certificate read
+ * from the input stream.
*
* @param certificate The client certificate to be used
* @param password The password (if required, else null)
* @param production Whether to use the production endpoint or the sandbox endpoint
* @param defaultTopic A default topic (can be changed per message)
- * @param builder An OkHttp client builder, possibly pre-initialized, to build the actual client
+ * @param builder An OkHttp client builder, possibly pre-initialized, to build the actual
+ * client
* @param connectionPort The port to establish a connection with APNs. Either 443 or 2197
- * @throws UnrecoverableKeyException If the key cannot be recovered
- * @throws KeyManagementException if the key failed to be loaded
- * @throws CertificateException if any of the certificates in the keystore could not be loaded
- * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found
- * @throws IOException if there is an I/O or format problem with the keystore data,
- * if a password is required but not given, or if the given password was incorrect
- * @throws KeyStoreException if no Provider supports a KeyStoreSpi implementation for the specified type
+ * @param gatewayUrl The gateway url the APNS client should point to
+ * @throws UnrecoverableKeyException If the key cannot be recovered
+ * @throws KeyManagementException if the key failed to be loaded
+ * @throws CertificateException if any of the certificates in the keystore could not be
+ * loaded
+ * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the
+ * keystore cannot be found
+ * @throws IOException if there is an I/O or format problem with the keystore
+ * data, if a password is required but not given, or if the
+ * given password was incorrect
+ * @throws KeyStoreException if no Provider supports a KeyStoreSpi implementation for
+ * the specified type
+ * @throws InvalidTrustManagerException if two or more TrustManagers were found (unsupoprted by
+ * the underlying OkHttp library)
*/
public SyncOkHttpApnsClient(InputStream certificate, String password, boolean production,
- String defaultTopic, OkHttpClient.Builder builder, int connectionPort)
+ String defaultTopic, OkHttpClient.Builder builder, int connectionPort,
+ String gatewayUrl)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
- IOException, UnrecoverableKeyException, KeyManagementException {
+ IOException, UnrecoverableKeyException, KeyManagementException, InvalidTrustManagerException {
teamID = keyID = apnsAuthKey = null;
@@ -168,57 +256,115 @@ public SyncOkHttpApnsClient(InputStream certificate, String password, boolean pr
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(certificate, password.toCharArray());
- final X509Certificate cert = (X509Certificate) ks.getCertificate(ks.aliases().nextElement());
+ final X509Certificate cert = (X509Certificate) ks.getCertificate(
+ ks.aliases().nextElement());
CertificateUtils.validateCertificate(production, cert);
- KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(
+ KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password.toCharArray());
KeyManager[] keyManagers = kmf.getKeyManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
- final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ final TrustManagerFactory tmf = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
- sslContext.init(keyManagers, tmf.getTrustManagers(), null);
+
+ // check if there is an existing TrustManager configured in the builder
+ TrustManager[] trustManagers = (builder.getX509TrustManagerOrNull$okhttp() != null) ?
+ new TrustManager[]{builder.getX509TrustManagerOrNull$okhttp()}
+ : tmf.getTrustManagers();
+ sslContext.init(keyManagers, trustManagers, null);
+
+ if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
+ throw new InvalidTrustManagerException(
+ "Unexpected default trust managers:" + Arrays.toString(trustManagers));
+ }
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
- builder.sslSocketFactory(sslSocketFactory);
+ builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustManagers[0]);
client = builder.build();
this.defaultTopic = defaultTopic;
- gateway = (production ? Constants.ENDPOINT_PRODUCTION : Constants.ENDPOINT_SANDBOX) + ":" + connectionPort;
+
+ if (gatewayUrl == null) {
+ gateway =
+ (production ? Constants.ENDPOINT_PRODUCTION : Constants.ENDPOINT_SANDBOX) + ":"
+ + connectionPort;
+ } else {
+ gateway = gatewayUrl;
+ }
+ }
+
+ /**
+ * Creates a new client and automatically loads the key store with the push certificate read
+ * from the input stream.
+ *
+ * @param certificate The client certificate to be used
+ * @param password The password (if required, else null)
+ * @param production Whether to use the production endpoint or the sandbox endpoint
+ * @param defaultTopic A default topic (can be changed per message)
+ * @param builder An OkHttp client builder, possibly pre-initialized, to build the actual
+ * client
+ * @param connectionPort The port to establish a connection with APNs. Either 443 or 2197
+ * @throws UnrecoverableKeyException If the key cannot be recovered
+ * @throws KeyManagementException if the key failed to be loaded
+ * @throws CertificateException if any of the certificates in the keystore could not be
+ * loaded
+ * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the
+ * keystore cannot be found
+ * @throws IOException if there is an I/O or format problem with the keystore
+ * data, if a password is required but not given, or if the
+ * given password was incorrect
+ * @throws KeyStoreException if no Provider supports a KeyStoreSpi implementation for
+ * the specified type
+ * @throws InvalidTrustManagerException if two or more TrustManagers were found (unsupoprted by
+ * the underlying OkHttp library)
+ */
+ public SyncOkHttpApnsClient(InputStream certificate, String password, boolean production,
+ String defaultTopic, OkHttpClient.Builder builder, int connectionPort)
+ throws CertificateException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException,
+ KeyManagementException, IOException, InvalidTrustManagerException {
+ this(certificate, password, production, defaultTopic, builder, connectionPort, null);
}
/**
- * Creates a new client and automatically loads the key store
- * with the push certificate read from the input stream.
+ * Creates a new client and automatically loads the key store with the push certificate read
+ * from the input stream.
*
* @param certificate The client certificate to be used
* @param password The password (if required, else null)
* @param production Whether to use the production endpoint or the sandbox endpoint
* @param defaultTopic A default topic (can be changed per message)
* @param connectionPool A connection pool to use. If null, a new one will be generated
- * @throws UnrecoverableKeyException If the key cannot be recovered
- * @throws KeyManagementException if the key failed to be loaded
- * @throws CertificateException if any of the certificates in the keystore could not be loaded
- * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found
- * @throws IOException if there is an I/O or format problem with the keystore data,
- * if a password is required but not given, or if the given password was incorrect
- * @throws KeyStoreException if no Provider supports a KeyStoreSpi implementation for the specified type
+ * @throws UnrecoverableKeyException If the key cannot be recovered
+ * @throws KeyManagementException if the key failed to be loaded
+ * @throws CertificateException if any of the certificates in the keystore could not be
+ * loaded
+ * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the
+ * keystore cannot be found
+ * @throws IOException if there is an I/O or format problem with the keystore
+ * data, if a password is required but not given, or if the
+ * given password was incorrect
+ * @throws KeyStoreException if no Provider supports a KeyStoreSpi implementation for
+ * the specified type
+ * @throws InvalidTrustManagerException if two or more TrustManagers were found (unsupoprted by
+ * the underlying OkHttp library)
*/
public SyncOkHttpApnsClient(InputStream certificate, String password, boolean production,
- String defaultTopic, ConnectionPool connectionPool)
+ String defaultTopic, ConnectionPool connectionPool)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
- IOException, UnrecoverableKeyException, KeyManagementException {
+ IOException, UnrecoverableKeyException, KeyManagementException, InvalidTrustManagerException {
this(certificate, password, production, defaultTopic, getBuilder(connectionPool));
}
/**
- * Creates a default builder that can be customized later and then passed to one of
- * the constructors taking a builder instance. The constructors that don't take
- * builders themselves use this method internally to create their client builders.
+ * Creates a default builder that can be customized later and then passed to one of the
+ * constructors taking a builder instance. The constructors that don't take builders themselves
+ * use this method internally to create their client builders.
*
* @param connectionPool A connection pool to use. If null, a new one will be generated
* @return a new OkHttp client builder, intialized with default settings.
@@ -232,6 +378,26 @@ private static OkHttpClient.Builder getBuilder(ConnectionPool connectionPool) {
return builder;
}
+ public String getDefaultTopic() {
+ return defaultTopic;
+ }
+
+ public String getApnsAuthKey() {
+ return apnsAuthKey;
+ }
+
+ public String getTeamID() {
+ return teamID;
+ }
+
+ public String getKeyID() {
+ return keyID;
+ }
+
+ public String getGateway() {
+ return gateway;
+ }
+
@Override
public boolean isSynchronous() {
return true;
@@ -239,11 +405,13 @@ public boolean isSynchronous() {
@Override
public void push(Notification notification, NotificationResponseListener listener) {
- throw new UnsupportedOperationException("Asynchronous requests are not supported by this client");
+ throw new UnsupportedOperationException(
+ "Asynchronous requests are not supported by this client");
}
protected final Request buildRequest(Notification notification) {
- final String topic = notification.getTopic() != null ? notification.getTopic() : defaultTopic;
+ final String topic =
+ notification.getTopic() != null ? notification.getTopic() : defaultTopic;
final String collapseId = notification.getCollapseId();
final UUID uuid = notification.getUuid();
final long expiration = notification.getExpiration();
@@ -262,7 +430,8 @@ public void writeTo(BufferedSink sink) throws IOException {
sink.write(notification.getPayload().getBytes(Constants.UTF_8));
}
})
- .header("content-length", notification.getPayload().getBytes(Constants.UTF_8).length + "");
+ .header("content-length",
+ notification.getPayload().getBytes(Constants.UTF_8).length + "");
if (topic != null) {
rb.header("apns-topic", topic);
@@ -291,7 +460,8 @@ public void writeTo(BufferedSink sink) throws IOException {
if (keyID != null && teamID != null && apnsAuthKey != null) {
// Generate a new JWT token if it's null, or older than 55 minutes
- if (cachedJWTToken == null || System.currentTimeMillis() - lastJWTTokenTS > 55 * 60 * 1000) {
+ if (cachedJWTToken == null
+ || System.currentTimeMillis() - lastJWTTokenTS > 55 * 60 * 1000) {
try {
lastJWTTokenTS = System.currentTimeMillis();
cachedJWTToken = JWT.getToken(teamID, keyID, apnsAuthKey);
@@ -342,4 +512,4 @@ protected NotificationResponse parseResponse(Response response) throws IOExcepti
return new NotificationResponse(error, statusCode, contentBody, null);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/clevertap/apns/exceptions/InvalidTrustManagerException.java b/src/main/java/com/clevertap/apns/exceptions/InvalidTrustManagerException.java
new file mode 100644
index 0000000..97333c5
--- /dev/null
+++ b/src/main/java/com/clevertap/apns/exceptions/InvalidTrustManagerException.java
@@ -0,0 +1,7 @@
+package com.clevertap.apns.exceptions;
+
+public class InvalidTrustManagerException extends Exception {
+ public InvalidTrustManagerException(String s) {
+ super(s);
+ }
+}
diff --git a/src/test/java/com/clevertap/apns/LocalHttpServer.java b/src/test/java/com/clevertap/apns/LocalHttpServer.java
new file mode 100644
index 0000000..faf010b
--- /dev/null
+++ b/src/test/java/com/clevertap/apns/LocalHttpServer.java
@@ -0,0 +1,66 @@
+package com.clevertap.apns;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class LocalHttpServer {
+
+ private final ExecutorService executorService = Executors.newFixedThreadPool(10);
+ HttpServer httpServer;
+ int port;
+ String LOCAL_HOST = "http://127.0.0.1";
+ String CONTEXT = "/serveRequest";
+
+ public static int nextFreePort() {
+ try {
+ try (ServerSocket tempSocket = new ServerSocket(0)) {
+ return tempSocket.getLocalPort();
+ }
+ } catch (IOException e) {
+ return -1;
+ }
+ }
+
+ public int init() throws Exception {
+ port = nextFreePort();
+ baseConfig(port);
+ return port;
+ }
+
+ public String getUrl() {
+ return LOCAL_HOST + ":" + port + CONTEXT;
+ }
+
+ public void baseConfig(int port) throws Exception {
+ httpServer = HttpServer.create(new InetSocketAddress(port), 0);
+ httpServer.createContext(CONTEXT, new RequestHandler());
+ httpServer.setExecutor(executorService); // creates a default executor
+ httpServer.start();
+ System.out.println("Local Http server created on port " + port);
+ }
+
+ public void shutDownServer() {
+ httpServer.stop(1);
+ executorService.shutdown();
+ }
+
+ static class RequestHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange t) throws IOException {
+ String response = "Request Executed";
+ t.getResponseHeaders().set("Content-Type", "text/plain");
+ t.sendResponseHeaders(200, response.length());
+ OutputStream os = t.getResponseBody();
+ os.write(response.getBytes());
+ os.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/clevertap/apns/clients/SyncOkHttpApnsClientTest.java b/src/test/java/com/clevertap/apns/clients/SyncOkHttpApnsClientTest.java
new file mode 100644
index 0000000..164a3b0
--- /dev/null
+++ b/src/test/java/com/clevertap/apns/clients/SyncOkHttpApnsClientTest.java
@@ -0,0 +1,209 @@
+package com.clevertap.apns.clients;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.clevertap.apns.ApnsClient;
+import com.clevertap.apns.LocalHttpServer;
+import com.clevertap.apns.Notification;
+import com.clevertap.apns.NotificationResponse;
+import com.clevertap.apns.exceptions.InvalidTrustManagerException;
+import com.clevertap.apns.internal.Constants;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import okhttp3.OkHttpClient;
+import okhttp3.OkHttpClient.Builder;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import okhttp3.tls.HandshakeCertificates;
+import okhttp3.tls.HeldCertificate;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+
+public class SyncOkHttpApnsClientTest {
+
+ protected static final String DEFAULT_TOPIC = "com.clevertap.testTopic";
+ protected static final String CERT_PASSWD = "cert-password";
+ protected static final String DEVICE_TOKEN = "vaild-device-token";
+ protected static final String INVALID_DEVICE_TOKEN = "invaild-device-token";
+
+ protected static HeldCertificate rootCertificate;
+ protected static HeldCertificate serverCertificate;
+ protected static HeldCertificate clientCertificate;
+ protected static HandshakeCertificates serverCertificateChain;
+ protected static HandshakeCertificates clientCertificateChain;
+
+ @BeforeAll
+ public static void initCertificates() {
+ rootCertificate = new HeldCertificate.Builder()
+ .certificateAuthority(0)
+ .build();
+
+ serverCertificate = new HeldCertificate.Builder()
+ .addSubjectAlternativeName("localhost")
+ .commonName("localhost")
+ .signedBy(rootCertificate)
+ .build();
+
+ clientCertificate = new HeldCertificate.Builder()
+ .commonName("push")
+ .signedBy(rootCertificate)
+ .build();
+
+ serverCertificateChain = new HandshakeCertificates.Builder()
+ .heldCertificate(serverCertificate)
+ .addTrustedCertificate(rootCertificate.certificate())
+ .build();
+
+ // Don't add client cert to client cert chain b/c it will be added via the apns api
+ clientCertificateChain = new HandshakeCertificates.Builder()
+ .addTrustedCertificate(rootCertificate.certificate())
+ .build();
+ }
+
+ /**
+ * Convert client cert to PKCS12 Format and return as InputStream.
+ */
+ protected InputStream getClientCertPKCS12()
+ throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
+ KeyStore pkcs12 = KeyStore.getInstance("PKCS12");
+ pkcs12.load(null, null);
+ Certificate[] chain = {clientCertificate.certificate()};
+ pkcs12.setKeyEntry("privatekeyalias", clientCertificate.keyPair().getPrivate(),
+ CERT_PASSWD.toCharArray(), chain);
+
+ ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+ pkcs12.store(outStream, CERT_PASSWD.toCharArray());
+
+ return new ByteArrayInputStream(outStream.toByteArray());
+ }
+
+
+ /**
+ * Build ApnsClient with valid client cert in synchronous mode.
+ *
+ * @return apnsClient
+ */
+ private ApnsClient buildClientWithCert(boolean withOkHttpClientBuilder, String gatewayUrl)
+ throws CertificateException,
+ UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, IOException, InvalidTrustManagerException {
+ ApnsClientBuilder builder = new ApnsClientBuilder()
+ .withDefaultTopic(DEFAULT_TOPIC)
+ .withCertificate(getClientCertPKCS12())
+ .withPassword(CERT_PASSWD)
+ .inSynchronousMode()
+ .withProductionGateway();
+
+ if (withOkHttpClientBuilder) {
+ builder.withOkHttpClientBuilder(new OkHttpClient.Builder().sslSocketFactory(
+ clientCertificateChain.sslSocketFactory(),
+ clientCertificateChain.trustManager()));
+ }
+
+ if (gatewayUrl != null) {
+ builder.withGatewayUrl(gatewayUrl);
+ }
+
+ return builder.build();
+ }
+
+ @Test
+ void pushTestWithCert()
+ throws IOException, CertificateException, InterruptedException, UnrecoverableKeyException,
+ NoSuchAlgorithmException, KeyStoreException, KeyManagementException, InvalidTrustManagerException {
+ try (MockWebServer server = new MockWebServer()) {
+ server.useHttps(serverCertificateChain.sslSocketFactory(), false);
+ server.requestClientAuth();
+ server.enqueue(new MockResponse().setResponseCode(200).setBody("Hello world!"));
+
+ String url = server.url("").toString();
+ url = url.substring(0,
+ url.length() - 1); // Above method gives a trailing "/" which we want to remove
+
+ ApnsClient client = buildClientWithCert(true, url);
+
+ NotificationResponse response = client.push(
+ new Notification.Builder(DEVICE_TOKEN)
+ .alertBody("Notification Body")
+ .alertTitle("Alert Title")
+ .badge(10)
+ .sound("sound")
+ .build()
+ );
+ assertEquals(200, response.getHttpStatusCode());
+
+ RecordedRequest request = server.takeRequest();
+ assertEquals("/3/device/" + DEVICE_TOKEN, request.getPath());
+ assertEquals(DEFAULT_TOPIC, request.getHeader("apns-topic"));
+
+ assert request.getHandshake() != null;
+ X509Certificate clientCert = (X509Certificate) request.getHandshake().peerCertificates()
+ .get(0);
+ X509Certificate[] clientChain = {clientCert};
+ serverCertificateChain.trustManager().checkClientTrusted(clientChain, "RSA");
+ }
+ }
+
+ @Test
+ void pushTestWithCertificateWithLocalHttpServer() throws Exception {
+ LocalHttpServer localHttpServer = new LocalHttpServer();
+ localHttpServer.init();
+ ApnsClient client = buildClientWithCert(true, localHttpServer.getUrl());
+
+ NotificationResponse response = client.push(
+ new Notification.Builder(DEVICE_TOKEN)
+ .alertBody("Notification Body")
+ .alertTitle("Alert Title")
+ .badge(10)
+ .sound("sound")
+ .build()
+ );
+ assertEquals(200, response.getHttpStatusCode(),
+ "Server should be hit and should return 200");
+
+ // Should have the same result as above if the trust manager isn't passed as well
+ client = buildClientWithCert(false, localHttpServer.getUrl());
+ response = client.push(
+ new Notification.Builder(DEVICE_TOKEN)
+ .alertBody("Notification Body")
+ .alertTitle("Alert Title")
+ .badge(10)
+ .sound("sound")
+ .build()
+ );
+ assertEquals(200, response.getHttpStatusCode(),
+ "Server should be hit and should return 200 without trust manager set");
+
+ localHttpServer.shutDownServer();
+ }
+
+ @Test
+ void constructor() {
+ final SyncOkHttpApnsClient client = new SyncOkHttpApnsClient("authKey",
+ "teamID", "keyID", true, "defaultTopic", new Builder(), 443, "myGateway");
+
+ assertEquals("authKey", client.getApnsAuthKey());
+ assertEquals("teamID", client.getTeamID());
+ assertEquals("keyID", client.getKeyID());
+ assertEquals("defaultTopic", client.getDefaultTopic());
+ assertEquals("myGateway", client.getGateway());
+ }
+
+ @Test
+ void defaultGateway() {
+ final SyncOkHttpApnsClient client = new SyncOkHttpApnsClient("authKey",
+ "teamID", "keyID", true, "defaultTopic", new Builder(), 443, null);
+ assertEquals(Constants.ENDPOINT_PRODUCTION + ":443", client.getGateway());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/clevertap/apns/exceptions/InvalidTrustManagerExceptionTest.java b/src/test/java/com/clevertap/apns/exceptions/InvalidTrustManagerExceptionTest.java
new file mode 100644
index 0000000..aafd699
--- /dev/null
+++ b/src/test/java/com/clevertap/apns/exceptions/InvalidTrustManagerExceptionTest.java
@@ -0,0 +1,17 @@
+package com.clevertap.apns.exceptions;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Created by Jude Pereira, at 12:21 on 25/10/2021.
+ */
+class InvalidTrustManagerExceptionTest {
+
+ @Test
+ void constructor() {
+ final InvalidTrustManagerException ex = new InvalidTrustManagerException("foo");
+ assertEquals("foo", ex.getMessage());
+ }
+}
\ No newline at end of file