com.turo.pushy.apns.ApnsClientBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.turo.pushy.apns.ApnsClientBuilder.java

Source

/*
 * Copyright (c) 2013-2017 Turo
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.turo.pushy.apns;

import com.turo.pushy.apns.auth.ApnsSigningKey;
import com.turo.pushy.apns.proxy.ProxyHandlerFactory;
import io.netty.channel.EventLoopGroup;
import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.ssl.*;
import io.netty.util.ReferenceCounted;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;

/**
 * An {@code ApnsClientBuilder} constructs new {@link ApnsClient} instances. Callers must specify the APNs server to
 * which clients connect and must provide either TLS credentials or a signing key (but not both) before building a
 * client. Client builders may be reused to generate multiple clients, and their settings may be changed from one client
 * to the next. APNs client builders are <em>not</em> thread-safe, and should not be shared between threads.
 *
 * @author <a href="https://github.com/jchambers">Jon Chambers</a>
 */
public class ApnsClientBuilder {
    private InetSocketAddress apnsServerAddress;

    private X509Certificate clientCertificate;
    private PrivateKey privateKey;
    private String privateKeyPassword;

    private ApnsSigningKey signingKey;

    private File trustedServerCertificatePemFile;
    private InputStream trustedServerCertificateInputStream;
    private X509Certificate[] trustedServerCertificates;

    private EventLoopGroup eventLoopGroup;

    private int concurrentConnections = 1;

    private ApnsClientMetricsListener metricsListener;

    private ProxyHandlerFactory proxyHandlerFactory;

    private int connectionTimeoutMillis;
    private long idlePingIntervalMillis = DEFAULT_PING_IDLE_TIME_MILLIS;
    private long gracefulShutdownTimeoutMillis;

    private Http2FrameLogger frameLogger;

    /**
     * The default idle time in milliseconds after which the client will send a PING frame to the APNs server.
     *
     * @since 0.11
     */
    public static final int DEFAULT_PING_IDLE_TIME_MILLIS = 60_000;

    /**
     * The hostname for the production APNs gateway.
     *
     * @since 0.5
     */
    public static final String PRODUCTION_APNS_HOST = "api.push.apple.com";

    /**
     * The hostname for the development APNs gateway.
     *
     * @since 0.5
     */
    public static final String DEVELOPMENT_APNS_HOST = "api.development.push.apple.com";

    /**
     * The default (HTTPS) port for communication with the APNs gateway.
     *
     * @since 0.5
     */
    public static final int DEFAULT_APNS_PORT = 443;

    /**
     * <p>An alternative port for communication with the APNs gateway. According to Apple's documentation:</p>
     *
     * <blockquote>You can alternatively use port 2197 when communicating with APNs. You might do this, for example, to
     * allow APNs traffic through your firewall but to block other HTTPS traffic.</blockquote>
     *
     * @see <a href="https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns">Sending Notification Requests to APNs</a>
     *
     * @since 0.5
     */
    public static final int ALTERNATE_APNS_PORT = 2197;

    private static final Logger log = LoggerFactory.getLogger(ApnsClientBuilder.class);

    /**
     * Sets the hostname of the server to which the client under construction will connect. Apple provides a production
     * and development environment.
     *
     * @param hostname the hostname of the server to which the client under construction should connect
     *
     * @return a reference to this builder
     *
     * @see ApnsClientBuilder#DEVELOPMENT_APNS_HOST
     * @see ApnsClientBuilder#PRODUCTION_APNS_HOST
     *
     * @see <a href="https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns">Sending Notification Requests to APNs</a>
     *
     * @since 0.11
     */
    public ApnsClientBuilder setApnsServer(final String hostname) {
        return this.setApnsServer(hostname, DEFAULT_APNS_PORT);
    }

    /**
     * Sets the hostname and port of the server to which the client under construction will connect. Apple provides a
     * production and development environment, both of which listen for traffic on the default HTTPS port
     * ({@value DEFAULT_APNS_PORT}) and an alternate port ({@value ALTERNATE_APNS_PORT}), which callers may use to work
     * around firewall or proxy restrictions.
     *
     * @param hostname the hostname of the server to which the client under construction should connect
     * @param port the port to which the client under contruction should connect
     *
     * @return a reference to this builder
     *
     * @see ApnsClientBuilder#DEVELOPMENT_APNS_HOST
     * @see ApnsClientBuilder#PRODUCTION_APNS_HOST
     * @see ApnsClientBuilder#DEFAULT_APNS_PORT
     * @see ApnsClientBuilder#ALTERNATE_APNS_PORT
     *
     * @see <a href="https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns">Sending Notification Requests to APNs</a>
     *
     * @since 0.11
     */
    public ApnsClientBuilder setApnsServer(final String hostname, final int port) {
        this.apnsServerAddress = InetSocketAddress.createUnresolved(hostname, port);
        return this;
    }

    /**
     * <p>Sets the TLS credentials for the client under construction using the contents of the given PKCS#12 file.
     * Clients constructed with TLS credentials will use TLS-based authentication when sending push notifications. The
     * PKCS#12 file <em>must</em> contain a certificate/private key pair.</p>
     *
     * <p>Clients may not have both TLS credentials and a signing key.</p>
     *
     * @param p12File a PKCS#12-formatted file containing the certificate and private key to be used to identify the
     * client to the APNs server
     * @param p12Password the password to be used to decrypt the contents of the given PKCS#12 file; passwords may be
     * blank (i.e. {@code ""}), but must not be {@code null}
     *
     * @throws SSLException if the given PKCS#12 file could not be loaded or if any other SSL-related problem arises
     * when constructing the context
     * @throws IOException if any IO problem occurred while attempting to read the given PKCS#12 file, or the PKCS#12
     * file could not be found
     *
     * @return a reference to this builder
     *
     * @since 0.8
     */
    public ApnsClientBuilder setClientCredentials(final File p12File, final String p12Password)
            throws SSLException, IOException {
        try (final InputStream p12InputStream = new FileInputStream(p12File)) {
            return this.setClientCredentials(p12InputStream, p12Password);
        }
    }

    /**
     * <p>Sets the TLS credentials for the client under construction using the data from the given PKCS#12 input stream.
     * Clients constructed with TLS credentials will use TLS-based authentication when sending push notifications. The
     * PKCS#12 data <em>must</em> contain a certificate/private key pair.</p>
     *
     * <p>Clients may not have both TLS credentials and a signing key.</p>
     *
     * @param p12InputStream an input stream to a PKCS#12-formatted file containing the certificate and private key to
     * be used to identify the client to the APNs server
     * @param p12Password the password to be used to decrypt the contents of the given PKCS#12 file; passwords may be
     * blank (i.e. {@code ""}), but must not be {@code null}
     *
     * @throws SSLException if the given PKCS#12 file could not be loaded or if any other SSL-related problem arises
     * when constructing the context
     * @throws IOException if any IO problem occurred while attempting to read the given PKCS#12 input stream
     *
     * @return a reference to this builder
     *
     * @since 0.8
     */
    public ApnsClientBuilder setClientCredentials(final InputStream p12InputStream, final String p12Password)
            throws SSLException, IOException {
        final X509Certificate x509Certificate;
        final PrivateKey privateKey;

        try {
            final KeyStore.PrivateKeyEntry privateKeyEntry = P12Util
                    .getFirstPrivateKeyEntryFromP12InputStream(p12InputStream, p12Password);

            final Certificate certificate = privateKeyEntry.getCertificate();

            if (!(certificate instanceof X509Certificate)) {
                throw new KeyStoreException(
                        "Found a certificate in the provided PKCS#12 file, but it was not an X.509 certificate.");
            }

            x509Certificate = (X509Certificate) certificate;
            privateKey = privateKeyEntry.getPrivateKey();
        } catch (final KeyStoreException e) {
            throw new SSLException(e);
        }

        return this.setClientCredentials(x509Certificate, privateKey, p12Password);
    }

    /**
     * <p>Sets the TLS credentials for the client under construction. Clients constructed with TLS credentials will use
     * TLS-based authentication when sending push notifications.</p>
     *
     * <p>Clients may not have both TLS credentials and a signing key.</p>
     *
     * @param clientCertificate the certificate to be used to identify the client to the APNs server
     * @param privateKey the private key for the client certificate
     * @param privateKeyPassword the password to be used to decrypt the private key; may be {@code null} if the private
     * key does not require a password
     *
     * @return a reference to this builder
     *
     * @since 0.8
     */
    public ApnsClientBuilder setClientCredentials(final X509Certificate clientCertificate,
            final PrivateKey privateKey, final String privateKeyPassword) {
        this.clientCertificate = clientCertificate;
        this.privateKey = privateKey;
        this.privateKeyPassword = privateKeyPassword;

        return this;
    }

    /**
     * <p>Sets the signing key for the client under construction. Clients constructed with a signing key will use
     * token-based authentication when sending push notifications.</p>
     *
     * <p>Clients may not have both a signing key and TLS credentials.</p>
     *
     * @param signingKey the signing key to be used by the client under construction
     *
     * @return a reference to this builder
     *
     * @see ApnsSigningKey#loadFromPkcs8File(File, String, String)
     * @see ApnsSigningKey#loadFromInputStream(InputStream, String, String)
     *
     * @since 0.10
     */
    public ApnsClientBuilder setSigningKey(final ApnsSigningKey signingKey) {
        this.signingKey = signingKey;

        return this;
    }

    /**
     * <p>Sets the trusted certificate chain for the client under construction using the contents of the given PEM
     * file. If not set (or {@code null}), the client will use the JVM's default trust manager.</p>
     *
     * <p>Callers will generally not need to set a trusted server certificate chain in normal operation, but may wish
     * to do so for <a href="https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning">certificate pinning</a>
     * or connecting to a mock server for integration testing or benchmarking.</p>
     *
     * @param certificatePemFile a PEM file containing one or more trusted certificates
     *
     * @return a reference to this builder
     *
     * @since 0.8
     */
    public ApnsClientBuilder setTrustedServerCertificateChain(final File certificatePemFile) {
        this.trustedServerCertificatePemFile = certificatePemFile;
        this.trustedServerCertificateInputStream = null;
        this.trustedServerCertificates = null;

        return this;
    }

    /**
     * <p>Sets the trusted certificate chain for the client under construction using the contents of the given PEM
     * input stream. If not set (or {@code null}), the client will use the JVM's default trust manager.</p>
     *
     * <p>Callers will generally not need to set a trusted server certificate chain in normal operation, but may wish
     * to do so for <a href="https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning">certificate pinning</a>
     * or connecting to a mock server for integration testing or benchmarking.</p>
     *
     * @param certificateInputStream an input stream to PEM-formatted data containing one or more trusted certificates
     *
     * @return a reference to this builder
     *
     * @since 0.8
     */
    public ApnsClientBuilder setTrustedServerCertificateChain(final InputStream certificateInputStream) {
        this.trustedServerCertificatePemFile = null;
        this.trustedServerCertificateInputStream = certificateInputStream;
        this.trustedServerCertificates = null;

        return this;
    }

    /**
     * <p>Sets the trusted certificate chain for the client under construction. If not set (or {@code null}), the
     * client will use the JVM's default trust manager.</p>
     *
     * <p>Callers will generally not need to set a trusted server certificate chain in normal operation, but may wish
     * to do so for <a href="https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning">certificate pinning</a>
     * or connecting to a mock server for integration testing or benchmarking.</p>
     *
     * @param certificates one or more trusted certificates
     *
     * @return a reference to this builder
     *
     * @since 0.8
     */
    public ApnsClientBuilder setTrustedServerCertificateChain(final X509Certificate... certificates) {
        this.trustedServerCertificatePemFile = null;
        this.trustedServerCertificateInputStream = null;
        this.trustedServerCertificates = certificates;

        return this;
    }

    /**
     * <p>Sets the event loop group to be used by the client under construction. If not set (or if {@code null}), the
     * client will create and manage its own event loop group.</p>
     *
     * <p>Generally speaking, callers don't need to set event loop groups for clients, but it may be useful to specify
     * an event loop group under certain circumstances. In particular, specifying an event loop group that is shared
     * among multiple {@code ApnsClient} instances can keep thread counts manageable. Regardless of the number of
     * concurrent {@code ApnsClient} instances, callers may also wish to specify an event loop group to take advantage
     * of certain platform-specific optimizations (e.g. {@code epoll} or {@code KQueue} event loop groups).</p>
     *
     * @param eventLoopGroup the event loop group to use for this client, or {@code null} to let the client manage its
     * own event loop group
     *
     * @return a reference to this builder
     *
     * @since 0.8
     */
    public ApnsClientBuilder setEventLoopGroup(final EventLoopGroup eventLoopGroup) {
        this.eventLoopGroup = eventLoopGroup;
        return this;
    }

    /**
     * Sets the maximum number of concurrent connections the client under construction may attempt to maintain to the
     * APNs server. By default, clients will attempt to maintain a single connection to the APNs server.
     *
     * @param concurrentConnections the maximum number of concurrent connections the client under construction may
     * attempt to maintain
     *
     * @return a reference to this builder
     *
     * @since 0.11
     */
    public ApnsClientBuilder setConcurrentConnections(final int concurrentConnections) {
        this.concurrentConnections = concurrentConnections;
        return this;
    }

    /**
     * Sets the metrics listener for the client under construction. Metrics listeners gather information that describes
     * the performance and behavior of a client, and are completely optional.
     *
     * @param metricsListener the metrics listener for the client under construction, or {@code null} if this client
     * should not report metrics to a listener
     *
     * @return a reference to this builder
     *
     * @since 0.8
     */
    public ApnsClientBuilder setMetricsListener(final ApnsClientMetricsListener metricsListener) {
        this.metricsListener = metricsListener;
        return this;
    }

    /**
     * Sets the proxy handler factory to be used to construct proxy handlers when establishing a new connection to the
     * APNs gateway. A client's proxy handler factory may be {@code null}, in which case the client will connect to the
     * gateway directly and will not use a proxy. By default, clients will not use a proxy.
     *
     * @param proxyHandlerFactory the proxy handler factory to be used to construct proxy handlers, or {@code null} if
     * this client should not use a proxy
     *
     * @return a reference to this builder
     *
     * @since 0.8
     */
    public ApnsClientBuilder setProxyHandlerFactory(final ProxyHandlerFactory proxyHandlerFactory) {
        this.proxyHandlerFactory = proxyHandlerFactory;
        return this;
    }

    /**
     * Sets the maximum amount of time, in milliseconds, that the client under construction will wait to establish a
     * connection with the APNs server before the connection attempt is considered a failure.
     *
     * @param connectionTimeout the maximum amount of time to wait for a connection attempt to complete
     * @param timeoutUnit the time unit for the given timeout
     *
     * @return a reference to this builder
     *
     * @since 0.8
     */
    public ApnsClientBuilder setConnectionTimeout(final long connectionTimeout, final TimeUnit timeoutUnit) {
        this.connectionTimeoutMillis = (int) timeoutUnit.toMillis(connectionTimeout);
        return this;
    }

    /**
     * Sets the amount of idle time (in milliseconds) after which the client under construction will send a PING frame
     * to the APNs server. By default, clients will send a PING frame after
     * {@value com.turo.pushy.apns.ApnsClientBuilder#DEFAULT_PING_IDLE_TIME_MILLIS} milliseconds of inactivity.
     *
     * @param pingInterval the amount of idle time after which the client will send a PING frame
     * @param pingIntervalUnit the time unit for the given idle time
     *
     * @return a reference to this builder
     *
     * @since 0.10
     */
    public ApnsClientBuilder setIdlePingInterval(final long pingInterval, final TimeUnit pingIntervalUnit) {
        this.idlePingIntervalMillis = pingIntervalUnit.toMillis(pingInterval);
        return this;
    }

    /**
     * Sets the amount of time clients should wait for in-progress requests to complete before closing a connection
     * during a graceful shutdown.
     *
     * @param gracefulShutdownTimeout the amount of time to wait for in-progress requests to complete before closing a
     * connection
     * @param timeoutUnit the time unit for the given timeout
     *
     * @return a reference to this builder
     *
     * @see ApnsClient#close()
     *
     * @since 0.8
     */
    public ApnsClientBuilder setGracefulShutdownTimeout(final long gracefulShutdownTimeout,
            final TimeUnit timeoutUnit) {
        this.gracefulShutdownTimeoutMillis = timeoutUnit.toMillis(gracefulShutdownTimeout);
        return this;
    }

    /**
     * Sets the HTTP/2 frame logger for the client under construction. HTTP/2 frame loggers log all HTTP/2 frames sent
     * to or from the client to the logging system of your choice via SLF4J. Frame logging is extremely verbose and is
     * recommended only for debugging purposes.
     *
     * @param frameLogger the frame logger to be used by the client under construction or {@code null} if the client
     * should not log individual HTTP/2 frames
     *
     * @return a reference to this builder
     *
     * @see <a href="https://www.slf4j.org/">SLF4J</a>
     *
     * @since 0.12
     */
    public ApnsClientBuilder setFrameLogger(final Http2FrameLogger frameLogger) {
        this.frameLogger = frameLogger;
        return this;
    }

    /**
     * Constructs a new {@link ApnsClient} with the previously-set configuration.
     *
     * @return a new ApnsClient instance with the previously-set configuration
     *
     * @throws SSLException if an SSL context could not be created for the new client for any reason
     * @throws IllegalStateException if this method is called without specifying an APNs server address, if this method
     * is called without providing TLS credentials or a signing key, or if this method is called with both TLS
     * credentials and a signing key
     *
     * @since 0.8
     */
    public ApnsClient build() throws SSLException {
        if (this.apnsServerAddress == null) {
            throw new IllegalStateException("No APNs server address specified.");
        }

        if (this.clientCertificate == null && this.privateKey == null && this.signingKey == null) {
            throw new IllegalStateException("No client credentials specified; either TLS credentials (a "
                    + "certificate/private key) or an APNs signing key must be provided before building a client.");
        } else if ((this.clientCertificate != null || this.privateKey != null) && this.signingKey != null) {
            throw new IllegalStateException("Clients may not have both a signing key and TLS credentials.");
        }

        final SslContext sslContext;
        {
            final SslProvider sslProvider;

            if (OpenSsl.isAvailable()) {
                log.info("Native SSL provider is available; will use native provider.");
                sslProvider = SslProvider.OPENSSL_REFCNT;
            } else {
                log.info("Native SSL provider not available; will use JDK SSL provider.");
                sslProvider = SslProvider.JDK;
            }

            final SslContextBuilder sslContextBuilder = SslContextBuilder.forClient().sslProvider(sslProvider)
                    .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE);

            if (this.clientCertificate != null && this.privateKey != null) {
                sslContextBuilder.keyManager(this.privateKey, this.privateKeyPassword, this.clientCertificate);
            }

            if (this.trustedServerCertificatePemFile != null) {
                sslContextBuilder.trustManager(this.trustedServerCertificatePemFile);
            } else if (this.trustedServerCertificateInputStream != null) {
                sslContextBuilder.trustManager(this.trustedServerCertificateInputStream);
            } else if (this.trustedServerCertificates != null) {
                sslContextBuilder.trustManager(this.trustedServerCertificates);
            }

            sslContext = sslContextBuilder.build();
        }

        final ApnsClient client = new ApnsClient(this.apnsServerAddress, sslContext, this.signingKey,
                this.proxyHandlerFactory, this.connectionTimeoutMillis, this.idlePingIntervalMillis,
                this.gracefulShutdownTimeoutMillis, this.concurrentConnections, this.metricsListener,
                this.frameLogger, this.eventLoopGroup);

        if (sslContext instanceof ReferenceCounted) {
            ((ReferenceCounted) sslContext).release();
        }

        return client;
    }
}