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