All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.clevertap.apns.clients.SyncOkHttpApnsClient Maven / Gradle / Ivy

There is a newer version: 2.1.0
Show newest version
/*
 * Copyright (c) 2016, CleverTap
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * - Neither the name of CleverTap nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

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.*;
import okio.BufferedSink;

import javax.net.ssl.*;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.UUID;

/**
 * A wrapper around OkHttp's http client to send out notifications using Apple's HTTP/2 API.
 */
public class SyncOkHttpApnsClient implements ApnsClient {

    private final String defaultTopic;
    private final String apnsAuthKey;
    private final String teamID;
    private final String keyID;
    protected final OkHttpClient client;
    private final String gateway;
    private static final MediaType mediaType = MediaType.parse("application/json");

    private long lastJWTTokenTS = 0;
    private String cachedJWTToken = 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
     * @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 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, 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 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
     * @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 gatewayUrl) {
        this.apnsAuthKey = apnsAuthKey;
        this.teamID = teamID;
        this.keyID = keyID;
        client = clientBuilder.build();

        this.defaultTopic = defaultTopic;

        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 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
     * @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
     */
    public SyncOkHttpApnsClient(String apnsAuthKey, String teamID, String keyID, boolean production,
            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.
     *
     * @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
     * @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 gatewayUrl)
            throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
            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.
     *
     * @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
     * @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 gatewayUrl)
            throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
            IOException, UnrecoverableKeyException, KeyManagementException, InvalidTrustManagerException {

        teamID = keyID = apnsAuthKey = null;

        password = password == null ? "" : password;
        KeyStore ks = KeyStore.getInstance("PKCS12");
        ks.load(certificate, password.toCharArray());

        final X509Certificate cert = (X509Certificate) ks.getCertificate(
                ks.aliases().nextElement());
        CertificateUtils.validateCertificate(production, cert);

        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());
        tmf.init((KeyStore) 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, (X509TrustManager) trustManagers[0]);

        client = builder.build();

        this.defaultTopic = defaultTopic;

        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.
     *
     * @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 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)
            throws CertificateException, NoSuchAlgorithmException, KeyStoreException,
            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.
     *
     * @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.
     */
    private static OkHttpClient.Builder getBuilder(ConnectionPool connectionPool) {
        OkHttpClient.Builder builder = ApnsClientBuilder.createDefaultOkHttpClientBuilder();
        if (connectionPool != null) {
            builder.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;
    }

    @Override
    public void push(Notification notification, NotificationResponseListener listener) {
        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 collapseId = notification.getCollapseId();
        final UUID uuid = notification.getUuid();
        final long expiration = notification.getExpiration();
        final Notification.Priority priority = notification.getPriority();
        final String pushType = notification.getPushType();
        Request.Builder rb = new Request.Builder()
                .url(gateway + "/3/device/" + notification.getToken())
                .post(new RequestBody() {
                    @Override
                    public MediaType contentType() {
                        return mediaType;
                    }

                    @Override
                    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 + "");

        if (topic != null) {
            rb.header("apns-topic", topic);
        }

        if (collapseId != null) {
            rb.header("apns-collapse-id", collapseId);
        }

        if (uuid != null) {
            rb.header("apns-id", uuid.toString());
        }

        if (expiration > -1) {
            rb.header("apns-expiration", String.valueOf(expiration));
        }

        if (priority != null) {
            rb.header("apns-priority", String.valueOf(priority.getCode()));
        }

        if (pushType != null) {
            rb.header("apns-push-type", pushType);
        }

        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) {
                try {
                    lastJWTTokenTS = System.currentTimeMillis();
                    cachedJWTToken = JWT.getToken(teamID, keyID, apnsAuthKey);
                } catch (InvalidKeySpecException | NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
                    return null;
                }
            }

            rb.header("authorization", "bearer " + cachedJWTToken);
        }

        return rb.build();
    }


    @Override
    public NotificationResponse push(Notification notification) {
        final Request request = buildRequest(notification);
        Response response = null;

        try {
            response = client.newCall(request).execute();
            return parseResponse(response);
        } catch (Throwable t) {
            return new NotificationResponse(null, -1, null, t);
        } finally {
            if (response != null) {
                response.body().close();
            }
        }
    }

    @Override
    public OkHttpClient getHttpClient() {
        return client;
    }

    protected NotificationResponse parseResponse(Response response) throws IOException {
        String contentBody = null;
        int statusCode = response.code();

        NotificationRequestError error = null;

        if (response.code() != 200) {
            error = NotificationRequestError.get(statusCode);
            contentBody = response.body() != null ? response.body().string() : null;
        }

        return new NotificationResponse(error, statusCode, contentBody, null);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy