/* * Copyright 2009, Mahmood Ali. * 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 Mahmood Ali. 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 * OWNER 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.notnoop.apns; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; import java.util.concurrent.ExecutorService; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLContext; import com.notnoop.apns.internal.SSLContextBuilder; import com.notnoop.apns.internal.Utilities; import com.notnoop.exceptions.InvalidSSLConfig; import com.notnoop.exceptions.RuntimeIOException; import static com.notnoop.apns.internal.Utilities.*; /** * The class is used to create instances of {@link ApnsService}. * * Note that this class is not synchronized. If multiple threads access a * {@code ApnsServiceBuilder} instance concurrently, and at least on of the * threads modifies one of the attributes structurally, it must be synchronized * externally. * * Starting a new {@code ApnsService} is easy: * * <pre> * ApnsService = APNS.newService() * .withCert("/path/to/certificate.p12", "MyCertPassword") * .withSandboxDestination().build() * </pre> */ public class ApnsServerSocketBuilder { private static final String KEYSTORE_TYPE = "PKCS12"; private static final String KEY_ALGORITHM = "sunx509"; private SSLContext sslContext; private int gatewayPort = -1; private int feedbackPort = -1; private ExecutorService executor; private ApnsServerService serverService = ApnsServerService.EMPTY; private ApnsServerExceptionDelegate exceptionDelegate = ApnsServerExceptionDelegate.EMPTY; /** * Constructs a new instance of {@code ApnsServiceBuilder} */ public ApnsServerSocketBuilder() { sslContext = null; } /** * Specify the certificate used to connect to Apple APNS servers. This * relies on the path (absolute or relative to working path) to the keystore * (*.p12) containing the certificate, along with the given password. * * The keystore needs to be of PKCS12 and the keystore needs to be encrypted * using the SunX509 algorithm. Both of these settings are the default. * * This library does not support password-less p12 certificates, due to a * Oracle Java library <a * href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637"> Bug * 6415637</a>. There are three workarounds: use a password-protected * certificate, use a different boot Java SDK implementation, or construct * the `SSLContext` yourself! Needless to say, the password-protected * certificate is most recommended option. * * @param fileName * the path to the certificate * @param password * the password of the keystore * @return this * @throws RuntimeIOException * if it {@code fileName} cannot be found or read * @throws InvalidSSLConfig * if fileName is invalid Keystore or the password is invalid */ public ApnsServerSocketBuilder withCert(String fileName, String password) throws RuntimeIOException, InvalidSSLConfig { FileInputStream stream = null; try { stream = new FileInputStream(fileName); return withCert(stream, password); } catch (FileNotFoundException e) { throw new RuntimeIOException(e); } finally { Utilities.close(stream); } } /** * Specify the certificate used to connect to Apple APNS servers. This * relies on the stream of keystore (*.p12) containing the certificate, * along with the given password. * * The keystore needs to be of PKCS12 and the keystore needs to be encrypted * using the SunX509 algorithm. Both of these settings are the default. * * This library does not support password-less p12 certificates, due to a * Oracle Java library <a * href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637"> Bug * 6415637</a>. There are three workarounds: use a password-protected * certificate, use a different boot Java SDK implementation, or construct * the `SSLContext` yourself! Needless to say, the password-protected * certificate is most recommended option. * * @param stream * the keystore represented as input stream * @param password * the password of the keystore * @return this * @throws InvalidSSLConfig * if stream is invalid Keystore or the password is invalid */ public ApnsServerSocketBuilder withCert(InputStream stream, String password) throws InvalidSSLConfig { assertPasswordNotEmpty(password); return withSSLContext(new SSLContextBuilder() .withAlgorithm(KEY_ALGORITHM) .withCertificateKeyStore(stream, password, KEYSTORE_TYPE) .withDefaultTrustKeyStore() .build()); } /** * Specify the certificate used to connect to Apple APNS servers. This * relies on a keystore (*.p12) containing the certificate, along with the * given password. * * This library does not support password-less p12 certificates, due to a * Oracle Java library <a * href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637"> Bug * 6415637</a>. There are three workarounds: use a password-protected * certificate, use a different boot Java SDK implementation, or construct * the `SSLContext` yourself! Needless to say, the password-protected * certificate is most recommended option. * * @param keyStore the keystore * @param password the password of the keystore * @return this * @throws InvalidSSLConfig if stream is invalid Keystore or the password is invalid */ public ApnsServerSocketBuilder withCert(KeyStore keyStore, String password) throws InvalidSSLConfig { assertPasswordNotEmpty(password); return withSSLContext(new SSLContextBuilder() .withAlgorithm(KEY_ALGORITHM) .withCertificateKeyStore(keyStore, password) .withDefaultTrustKeyStore() .build()); } private void assertPasswordNotEmpty(String password) { if (password == null || password.length() == 0) { throw new IllegalArgumentException( "Passwords must be specified." + "Oracle Java SDK does not support passwordless p12 certificates"); } } /** * Specify the SSLContext that should be used to initiate the connection to * Apple Server. * * Most clients would use {@link #withCert(InputStream, String)} or * {@link #withCert(String, String)} instead. But some clients may need to * represent the Keystore in a different format than supported. * * @param sslContext * Context to be used to create secure connections * @return this */ public ApnsServerSocketBuilder withSSLContext(SSLContext sslContext) { this.sslContext = sslContext; return this; } /** * Specify the gateway server for sending Apple iPhone notifications. * * @param port * port of the notification gateway of Apple * @return this */ public ApnsServerSocketBuilder withGatewayDestination(int port) { this.gatewayPort = port; return this; } /** * Specify the Feedback for getting failed devices from Apple iPhone Push * servers. * * @param port * port of the feedback server of Apple * @return this */ public ApnsServerSocketBuilder withFeedbackDestination(int port) { this.feedbackPort = port; return this; } /** * Specify to use the Apple sandbox servers as iPhone gateway and feedback * servers. * * This is desired when in testing and pushing notifications with a * development provision. * * @return this */ public ApnsServerSocketBuilder withSandboxDestination() { return withGatewayDestination(SANDBOX_GATEWAY_PORT) .withFeedbackDestination(SANDBOX_FEEDBACK_PORT); } /** * Specify to use the Apple Production servers as iPhone gateway and * feedback servers. * * This is desired when sending notifications to devices with a production * provision (whether through App Store or Ad hoc distribution). * * @return this */ public ApnsServerSocketBuilder withProductionDestination() { return withGatewayDestination(PRODUCTION_GATEWAY_PORT) .withFeedbackDestination(PRODUCTION_FEEDBACK_PORT); } /** * Constructs a pool of connections to the notification servers. * * Apple servers recommend using a pooled connection up to 15 concurrent * persistent connections to the gateways. * * Note: This option has no effect when using non-blocking connections. */ public ApnsServerSocketBuilder asPool(int maxConnections) { return asPool(new ThreadPoolExecutor(maxConnections, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>())); } /** * Constructs a pool of connections to the notification servers. * * Apple servers recommend using a pooled connection up to 15 concurrent * persistent connections to the gateways. * * Note: This option has no effect when using non-blocking connections. */ public ApnsServerSocketBuilder asPool(ExecutorService executor) { this.executor = executor; return this; } /** * Sets the delegate of the service, that gets notified of the status of * message delivery. * * Note: This option has no effect when using non-blocking connections. */ public ApnsServerSocketBuilder withService(ApnsServerService serverService) { this.serverService = serverService == null ? ApnsServerService.EMPTY : serverService; return this; } /** * Sets the delegate of the service, that gets notified of the status of * message delivery. * * Note: This option has no effect when using non-blocking connections. */ public ApnsServerSocketBuilder withDelegate( ApnsServerExceptionDelegate exceptionDelegate) { this.exceptionDelegate = exceptionDelegate == null ? ApnsServerExceptionDelegate.EMPTY : exceptionDelegate; return this; } /** * Returns a fully initialized instance of {@link ApnsService}, according to * the requested settings. * * @return a new instance of ApnsService * @throws IOException */ public ApnsSocketService build() throws IOException { checkInitialization(); AbstractApnsServerSocket apnsPushServerSocket = new ApnsGatewayServerSocket( sslContext, gatewayPort, executor, serverService, exceptionDelegate); AbstractApnsServerSocket apnsFeedbackServerSocket = new ApnsFeedbackServerSocket( sslContext, feedbackPort, executor, serverService, exceptionDelegate); ApnsSocketService service = new ApnsSocketService(apnsPushServerSocket, apnsFeedbackServerSocket); service.start(); return service; } private void checkInitialization() { if (sslContext == null) { throw new IllegalStateException( "SSL Certificates and attribute are not initialized\n" + "Use .withCert() methods."); } if (executor == null) { throw new IllegalStateException( "SSL Certificates and attribute are not initialized\n" + "Use .withCert() methods."); } if (gatewayPort == -1 || feedbackPort == -1) { throw new IllegalStateException( "The Destination APNS server is not stated\n" + "Use .withDestination(), withSandboxDestination(), " + "or withProductionDestination()."); } } }