├── .codeclimate.yml ├── .gitignore ├── .editorconfig ├── src ├── main │ └── java │ │ └── javapns │ │ ├── devices │ │ ├── package.html │ │ ├── exceptions │ │ │ ├── package.html │ │ │ ├── InvalidDeviceTokenFormatException.java │ │ │ ├── NullIdException.java │ │ │ ├── NullDeviceTokenException.java │ │ │ ├── UnknownDeviceException.java │ │ │ └── DuplicateDeviceException.java │ │ ├── implementations │ │ │ ├── basic │ │ │ │ ├── package.html │ │ │ │ ├── BasicDeviceFactory.java │ │ │ │ └── BasicDevice.java │ │ │ ├── package.html │ │ │ └── jpa │ │ │ │ ├── package.html │ │ │ │ ├── README.md │ │ │ │ └── injection.xml │ │ ├── Device.java │ │ ├── DeviceFactory.java │ │ └── Devices.java │ │ ├── communication │ │ ├── package.html │ │ ├── exceptions │ │ │ ├── package.html │ │ │ ├── CommunicationException.java │ │ │ ├── InvalidKeystoreFormatException.java │ │ │ ├── KeystoreException.java │ │ │ ├── InvalidKeystorePasswordException.java │ │ │ ├── InvalidCertificateChainException.java │ │ │ └── InvalidKeystoreReferenceException.java │ │ ├── WrappedKeystore.java │ │ ├── ServerTrustingTrustManager.java │ │ ├── AppleServer.java │ │ ├── AppleServerBasicImpl.java │ │ ├── ProxyManager.java │ │ ├── ConnectionToAppleServer.java │ │ └── KeystoreManager.java │ │ ├── feedback │ │ ├── package.html │ │ ├── AppleFeedbackServer.java │ │ ├── ConnectionToFeedbackServer.java │ │ ├── AppleFeedbackServerBasicImpl.java │ │ └── FeedbackServiceManager.java │ │ ├── notification │ │ ├── package.html │ │ ├── exceptions │ │ │ ├── package.html │ │ │ ├── PayloadIsEmptyException.java │ │ │ ├── PayloadAlertAlreadyExistsException.java │ │ │ ├── ErrorResponsePacketReceivedException.java │ │ │ ├── PayloadMaxSizeExceededException.java │ │ │ └── PayloadMaxSizeProbablyExceededException.java │ │ ├── transmission │ │ │ ├── package.html │ │ │ ├── NotificationProgressListener.java │ │ │ └── PushQueue.java │ │ ├── management │ │ │ ├── package.html │ │ │ ├── RemovalPasswordPayload.java │ │ │ ├── WebClipPayload.java │ │ │ ├── WiFiPayload.java │ │ │ ├── VPNPayload.java │ │ │ ├── CalDAVPayload.java │ │ │ ├── APNPayload.java │ │ │ ├── CalendarSubscriptionPayload.java │ │ │ ├── RestrictionsPayload.java │ │ │ ├── MobileConfigPayload.java │ │ │ ├── PasswordPolicyPayload.java │ │ │ ├── SCEPPayload.java │ │ │ ├── LDAPPayload.java │ │ │ └── EmailPayload.java │ │ ├── AppleNotificationServer.java │ │ ├── ConnectionToNotificationServer.java │ │ ├── PayloadPerDevice.java │ │ ├── PushNotificationBigPayload.java │ │ ├── NewsstandNotificationPayload.java │ │ ├── AppleNotificationServerBasicImpl.java │ │ ├── ResponsePacketReader.java │ │ ├── ResponsePacket.java │ │ ├── PushedNotifications.java │ │ ├── PushedNotification.java │ │ ├── PushNotificationPayload.java │ │ └── Payload.java │ │ ├── package.html │ │ └── overview.html └── test │ └── java │ └── javapns │ └── test │ ├── package.html │ ├── FeedbackTest.java │ ├── TestFoundation.java │ ├── NotificationTest.java │ └── SpecificNotificationTests.java ├── .travis.yml ├── README.md ├── LICENSE └── pom.xml /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | fixme: 3 | enabled: true 4 | ratings: 5 | paths: [] 6 | exclude_paths: [] 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | .project 4 | .classpath 5 | .settings 6 | target 7 | 8 | # Package Files # 9 | *.jar 10 | *.war 11 | *.ear 12 | 13 | .idea 14 | *.iml 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | 6 | indent_style = space 7 | indent_size = 2 8 | 9 | end_of_line = lf 10 | 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true -------------------------------------------------------------------------------- /src/main/java/javapns/devices/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 6 | 7 | 8 | 9 | Classes representing mobile devices. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/java/javapns/test/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Testing tools for the javapns library. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Classes for communicating with Apple servers. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/feedback/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Classes for interacting with the Apple Feedback Service. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Classes for pushing notifications through Apple servers. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/exceptions/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Device-related exceptions thrown by the javapns library. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/implementations/basic/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | A basic non-persistent implementation for devices. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/implementations/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Concrete implementations of devices and device factories. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: java 4 | 5 | jdk: 6 | - oraclejdk8 7 | 8 | cache: 9 | apt: true 10 | directories: 11 | - $HOME/.m2 12 | 13 | script: 14 | - mvn clean compile -Dmaven.test.skip=true 15 | 16 | after_success: 17 | - mvn clean test jacoco:report coveralls:report 18 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/exceptions/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Communication-related exceptions thrown by the javapns library. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/exceptions/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Notification-related exceptions thrown by the javapns library. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/implementations/jpa/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Details on implementing devices and device factories using JPA. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/transmission/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Specialized classes for transmitting notifications to large number of devices. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 |See the javapns.Push class for easy push notifications.
MDM is not involved in Apple Push Notification, but uses the 12 | same communication technologies as APN.
13 |Nevertheless, this package is provided in an effort to help 14 | javapns become more widely used.
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/javapns/feedback/AppleFeedbackServer.java: -------------------------------------------------------------------------------- 1 | package javapns.feedback; 2 | 3 | import javapns.communication.AppleServer; 4 | 5 | /** 6 | * Interface representing a connection to an Apple Feedback Server 7 | * 8 | * @author Sylvain Pedneault 9 | */ 10 | public interface AppleFeedbackServer extends AppleServer { 11 | String PRODUCTION_HOST = "feedback.push.apple.com"; 12 | int PRODUCTION_PORT = 2196; 13 | 14 | String DEVELOPMENT_HOST = "feedback.sandbox.push.apple.com"; 15 | int DEVELOPMENT_PORT = 2196; 16 | 17 | String getFeedbackServerHost(); 18 | 19 | int getFeedbackServerPort(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/implementations/jpa/README.md: -------------------------------------------------------------------------------- 1 | ### To support JPA 2 | 3 | 1. Implement javapns.devices.Device as a POJO 4 | 1. Implement javapns.devices.DeviceFactory to hook up to your own JPA-backed DAO 5 | 1. See "injection.xml" file for wiring JavaPNS using Spring 6 | 1. Define a getter/setter pair for pushNotificationManager and feedbackServiceManager on any Spring-managed bean to get access to fully-functional PushNotificationManager and FeedbackServiceManager 7 | 8 | Notes: 9 | 10 | - AppleNotificationServer and AppleFeedbackServer objects can also be injected instead of using *BasicImpl implementations 11 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/AppleNotificationServer.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import javapns.communication.AppleServer; 4 | 5 | /** 6 | * Interface representing a connection to an Apple Notification Server 7 | * 8 | * @author Sylvain Pedneault 9 | */ 10 | public interface AppleNotificationServer extends AppleServer { 11 | String PRODUCTION_HOST = "gateway.push.apple.com"; 12 | int PRODUCTION_PORT = 2195; 13 | 14 | String DEVELOPMENT_HOST = "gateway.sandbox.push.apple.com"; 15 | int DEVELOPMENT_PORT = 2195; 16 | 17 | String getNotificationServerHost(); 18 | 19 | int getNotificationServerPort(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/WrappedKeystore.java: -------------------------------------------------------------------------------- 1 | package javapns.communication; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.security.KeyStore; 6 | 7 | /** 8 | * Special wrapper for a KeyStore. 9 | * 10 | * @author Sylvain Pedneault 11 | */ 12 | class WrappedKeystore extends InputStream { 13 | private final KeyStore keystore; 14 | 15 | WrappedKeystore(final KeyStore keystore) { 16 | this.keystore = keystore; 17 | } 18 | 19 | public KeyStore getKeystore() { 20 | return keystore; 21 | } 22 | 23 | @Override 24 | public int read() throws IOException { 25 | return 0; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/exceptions/PayloadIsEmptyException.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.exceptions; 2 | 3 | /** 4 | * Thrown when a payload is empty. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | 9 | public class PayloadIsEmptyException extends Exception { 10 | 11 | private static final long serialVersionUID = 8142083854784121700L; 12 | 13 | public PayloadIsEmptyException() { 14 | super("Payload is empty"); 15 | } 16 | 17 | /** 18 | * Constructor with custom message 19 | * 20 | * @param message 21 | */ 22 | public PayloadIsEmptyException(final String message) { 23 | super(message); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/exceptions/InvalidDeviceTokenFormatException.java: -------------------------------------------------------------------------------- 1 | package javapns.devices.exceptions; 2 | 3 | /** 4 | * Thrown when a device token cannot be parsed (invalid format). 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | 9 | public class InvalidDeviceTokenFormatException extends Exception { 10 | 11 | private static final long serialVersionUID = -8571997399252125457L; 12 | 13 | public InvalidDeviceTokenFormatException(final String message) { 14 | super(message); 15 | } 16 | 17 | public InvalidDeviceTokenFormatException(final String token, final String problem) { 18 | super(String.format("Device token cannot be parsed, most likely because it contains invalid hexadecimal characters: %s in %s", problem, token)); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/RemovalPasswordPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | 5 | /** 6 | * An MDM payload for RemovalPassword. 7 | * 8 | * @author Sylvain Pedneault 9 | */ 10 | class RemovalPasswordPayload extends MobileConfigPayload { 11 | public RemovalPasswordPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName) throws JSONException { 12 | super(payloadVersion, "com.apple.profileRemovalPassword", payloadOrganization, payloadIdentifier, payloadDisplayName); 13 | } 14 | 15 | public void setRemovalPasword(final String value) throws JSONException { 16 | getPayload().put("RemovalPassword", value); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/exceptions/InvalidKeystoreFormatException.java: -------------------------------------------------------------------------------- 1 | package javapns.communication.exceptions; 2 | 3 | /** 4 | * Thrown when we try to contact Apple with an invalid keystore format. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | public class InvalidKeystoreFormatException extends KeystoreException { 9 | 10 | private static final long serialVersionUID = 8822634206752412121L; 11 | 12 | /** 13 | * Constructor 14 | */ 15 | public InvalidKeystoreFormatException() { 16 | super("Invalid keystore format! Make sure it is PKCS12..."); 17 | } 18 | 19 | /** 20 | * Constructor with custom message 21 | * 22 | * @param message 23 | */ 24 | public InvalidKeystoreFormatException(final String message) { 25 | super(message); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/exceptions/KeystoreException.java: -------------------------------------------------------------------------------- 1 | package javapns.communication.exceptions; 2 | 3 | /** 4 | * Thrown when we try to contact Apple with an invalid keystore format. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | 9 | public class KeystoreException extends Exception { 10 | 11 | private static final long serialVersionUID = 2549063865160633139L; 12 | 13 | /** 14 | * Constructor with custom message 15 | * 16 | * @param message 17 | */ 18 | public KeystoreException(final String message) { 19 | super(message); 20 | } 21 | 22 | /** 23 | * Constructor with custom message 24 | * 25 | * @param message 26 | */ 27 | public KeystoreException(final String message, final Exception cause) { 28 | super(message, cause); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/transmission/NotificationProgressListener.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.transmission; 2 | 3 | /** 4 | *A Java library for interacting with Apple's Push Notification Service.
11 | 12 |The simplest way of pushing notifications with JavaPNS is to use the javapns.Push class:
15 | import javapns.Push;
16 |
17 | public class PushTest {
18 |
19 | public static void main(String[] args) {
20 |
21 | Push.alert("Hello World!", "keystore.p12", "keystore_password", false, "Your token");
22 | }
23 | }
24 |
25 |
26 | For more details about using the library, see the on-line wiki at:
27 |
28 | http://code.google.com/p/javapns/w/list
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/main/java/javapns/devices/exceptions/UnknownDeviceException.java:
--------------------------------------------------------------------------------
1 | package javapns.devices.exceptions;
2 |
3 | /**
4 | * Thrown when we try to retrieve a device that doesn't exist
5 | *
6 | * @author Maxime Peron
7 | */
8 |
9 | public class UnknownDeviceException extends Exception {
10 |
11 | private static final long serialVersionUID = -322193098126184434L;
12 | /* Custom message for this exception */
13 | private final String message;
14 |
15 | /**
16 | * Constructor
17 | */
18 | public UnknownDeviceException() {
19 | this.message = "Unknown client";
20 | }
21 |
22 | /**
23 | * Constructor with custom message
24 | *
25 | * @param message
26 | */
27 | public UnknownDeviceException(final String message) {
28 | this.message = message;
29 | }
30 |
31 | /**
32 | * String representation
33 | */
34 | public String toString() {
35 | return this.message;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/javapns/communication/ServerTrustingTrustManager.java:
--------------------------------------------------------------------------------
1 | package javapns.communication;
2 |
3 | import javax.net.ssl.X509TrustManager;
4 | import java.security.cert.CertificateException;
5 | import java.security.cert.X509Certificate;
6 |
7 | /**
8 | * A trust manager that automatically trusts all servers.
9 | * Used to avoid having handshake errors with Apple's sandbox servers.
10 | *
11 | * @author Sylvain Pedneault
12 | */
13 | class ServerTrustingTrustManager implements X509TrustManager {
14 | public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {
15 | throw new CertificateException("Client is not trusted.");
16 | }
17 |
18 | public void checkServerTrusted(final X509Certificate[] chain, final String authType) {
19 | // trust all servers
20 | }
21 |
22 | public X509Certificate[] getAcceptedIssuers() {
23 | return null;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/javapns/devices/exceptions/DuplicateDeviceException.java:
--------------------------------------------------------------------------------
1 | package javapns.devices.exceptions;
2 |
3 | /**
4 | * Thrown when a Device already exist and we try to add it a second time
5 | *
6 | * @author Maxime Peron
7 | */
8 |
9 | public class DuplicateDeviceException extends Exception {
10 |
11 | private static final long serialVersionUID = -7116507420722667479L;
12 | /* Custom message for this exception */
13 | private final String message;
14 |
15 | /**
16 | * Constructor
17 | */
18 | public DuplicateDeviceException() {
19 | this.message = "Client already exists";
20 | }
21 |
22 | /**
23 | * Constructor with custom message
24 | *
25 | * @param message
26 | */
27 | public DuplicateDeviceException(final String message) {
28 | this.message = message;
29 | }
30 |
31 | /**
32 | * String representation
33 | */
34 | public String toString() {
35 | return this.message;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/javapns/communication/exceptions/InvalidCertificateChainException.java:
--------------------------------------------------------------------------------
1 | package javapns.communication.exceptions;
2 |
3 | /**
4 | * Thrown when we try to contact Apple with an invalid keystore or certificate chain.
5 | *
6 | * @author Sylvain Pedneault
7 | */
8 | public class InvalidCertificateChainException extends KeystoreException {
9 |
10 | private static final long serialVersionUID = -1978821654637371922L;
11 |
12 | /**
13 | * Constructor
14 | */
15 | public InvalidCertificateChainException() {
16 | super("Invalid certificate chain! Verify that the keystore you provided was produced according to specs...");
17 | }
18 |
19 | /**
20 | * Constructor with custom message
21 | *
22 | * @param message
23 | */
24 | public InvalidCertificateChainException(final String message) {
25 | super("Invalid certificate chain (" + message + ")! Verify that the keystore you provided was produced according to specs...");
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/javapns/notification/management/WebClipPayload.java:
--------------------------------------------------------------------------------
1 | package javapns.notification.management;
2 |
3 | import org.json.JSONException;
4 | import org.json.JSONObject;
5 |
6 | /**
7 | * An MDM payload for WebClip.
8 | *
9 | * @author Sylvain Pedneault
10 | */
11 | class WebClipPayload extends MobileConfigPayload {
12 | public WebClipPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final String url, final String label) throws JSONException {
13 | super(payloadVersion, "com.apple.webClip.managed", payloadOrganization, payloadIdentifier, payloadDisplayName);
14 | final JSONObject payload = getPayload();
15 | payload.put("URL", url);
16 | payload.put("Label", label);
17 | }
18 |
19 | public void setIcon(final Object data) throws JSONException {
20 | getPayload().put("Icon", data);
21 | }
22 |
23 | public void setIsRemovable(final boolean value) throws JSONException {
24 | getPayload().put("IsRemovable", value);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/javapns/notification/ConnectionToNotificationServer.java:
--------------------------------------------------------------------------------
1 | package javapns.notification;
2 |
3 | import javapns.communication.ConnectionToAppleServer;
4 | import javapns.communication.exceptions.KeystoreException;
5 |
6 | import java.security.KeyStore;
7 |
8 | /**
9 | * Connection details specific to the Notification Service.
10 | *
11 | * @author Sylvain Pedneault
12 | */
13 | public class ConnectionToNotificationServer extends ConnectionToAppleServer {
14 | public ConnectionToNotificationServer(final AppleNotificationServer server) throws KeystoreException {
15 | super(server);
16 | }
17 |
18 | public ConnectionToNotificationServer(final AppleNotificationServer server, final KeyStore keystore) throws KeystoreException {
19 | super(server, keystore);
20 | }
21 |
22 | @Override
23 | public String getServerHost() {
24 | return ((AppleNotificationServer) getServer()).getNotificationServerHost();
25 | }
26 |
27 | @Override
28 | public int getServerPort() {
29 | return ((AppleNotificationServer) getServer()).getNotificationServerPort();
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/javapns/notification/PayloadPerDevice.java:
--------------------------------------------------------------------------------
1 | package javapns.notification;
2 |
3 | import javapns.devices.Device;
4 | import javapns.devices.exceptions.InvalidDeviceTokenFormatException;
5 | import javapns.devices.implementations.basic.BasicDevice;
6 |
7 | /**
8 | * A one-to-one link between a payload and device.
9 | * Provides support for a typical payload-per-device scenario.
10 | *
11 | * @author Sylvain Pedneault
12 | */
13 | public class PayloadPerDevice {
14 | private final Payload payload;
15 | private final Device device;
16 |
17 | public PayloadPerDevice(final Payload payload, final String token) throws InvalidDeviceTokenFormatException {
18 | super();
19 | this.payload = payload;
20 | this.device = new BasicDevice(token);
21 | }
22 |
23 | public PayloadPerDevice(final Payload payload, final Device device) {
24 | super();
25 | this.payload = payload;
26 | this.device = device;
27 | }
28 |
29 | public Payload getPayload() {
30 | return payload;
31 | }
32 |
33 | public Device getDevice() {
34 | return device;
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/javapns/feedback/ConnectionToFeedbackServer.java:
--------------------------------------------------------------------------------
1 | package javapns.feedback;
2 |
3 | import javapns.communication.ConnectionToAppleServer;
4 | import javapns.communication.exceptions.KeystoreException;
5 | import javapns.notification.AppleNotificationServer;
6 |
7 | import java.security.KeyStore;
8 |
9 | /**
10 | * Class representing a connection to a specific Feedback Server.
11 | *
12 | * @author Sylvain Pedneault
13 | */
14 | public class ConnectionToFeedbackServer extends ConnectionToAppleServer {
15 | public ConnectionToFeedbackServer(final AppleFeedbackServer feedbackServer) throws KeystoreException {
16 | super(feedbackServer);
17 | }
18 |
19 | public ConnectionToFeedbackServer(final AppleNotificationServer server, final KeyStore keystore) throws KeystoreException {
20 | super(server, keystore);
21 | }
22 |
23 | @Override
24 | public String getServerHost() {
25 | return ((AppleFeedbackServer) getServer()).getFeedbackServerHost();
26 | }
27 |
28 | @Override
29 | public int getServerPort() {
30 | return ((AppleFeedbackServer) getServer()).getFeedbackServerPort();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/javapns/notification/PushNotificationBigPayload.java:
--------------------------------------------------------------------------------
1 | package javapns.notification;
2 |
3 | import org.json.JSONException;
4 |
5 | public class PushNotificationBigPayload extends PushNotificationPayload {
6 | /* Maximum total length (serialized) of a payload */
7 | private static final int MAXIMUM_PAYLOAD_LENGTH = 2048;
8 |
9 | private PushNotificationBigPayload() {
10 | super();
11 | }
12 |
13 | private PushNotificationBigPayload(final String rawJSON) throws JSONException {
14 | super(rawJSON);
15 | }
16 |
17 | public static PushNotificationBigPayload complex() {
18 | return new PushNotificationBigPayload();
19 | }
20 |
21 | public static PushNotificationBigPayload fromJSON(final String rawJSON) throws JSONException {
22 | return new PushNotificationBigPayload(rawJSON);
23 | }
24 |
25 | /**
26 | * Return the maximum payload size in bytes.
27 | * For APNS payloads, since iOS8, this method returns 2048.
28 | *
29 | * @return the maximum payload size in bytes (2048)
30 | */
31 | @Override
32 | public int getMaximumPayloadSize() {
33 | return MAXIMUM_PAYLOAD_LENGTH;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/javapns/communication/exceptions/InvalidKeystoreReferenceException.java:
--------------------------------------------------------------------------------
1 | package javapns.communication.exceptions;
2 |
3 | /**
4 | * Thrown when we try to contact Apple with an invalid keystore format.
5 | *
6 | * @author Sylvain Pedneault
7 | */
8 |
9 | public class InvalidKeystoreReferenceException extends KeystoreException {
10 |
11 | private static final long serialVersionUID = 3144387163593035745L;
12 |
13 | /**
14 | * Constructor
15 | */
16 | public InvalidKeystoreReferenceException() {
17 | super("Invalid keystore parameter. Must be InputStream, File, String (as a file path), or byte[].");
18 | }
19 |
20 | /**
21 | * Constructor with custom message
22 | *
23 | * @param keystore
24 | */
25 | public InvalidKeystoreReferenceException(final Object keystore) {
26 | super("Invalid keystore parameter (" + keystore + "). Must be InputStream, File, String (as a file path), or byte[].");
27 | }
28 |
29 | /**
30 | * Constructor with custom message
31 | *
32 | * @param message
33 | */
34 | public InvalidKeystoreReferenceException(final String message) {
35 | super(message);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/javapns/notification/exceptions/PayloadMaxSizeExceededException.java:
--------------------------------------------------------------------------------
1 | package javapns.notification.exceptions;
2 |
3 | /**
4 | * Thrown when a payload exceeds the maximum size allowed.
5 | *
6 | * @author Sylvain Pedneault
7 | */
8 |
9 | public class PayloadMaxSizeExceededException extends Exception {
10 |
11 | private static final long serialVersionUID = 2896151447959250527L;
12 |
13 | /**
14 | * Default constructor
15 | */
16 | public PayloadMaxSizeExceededException() {
17 | super("Total payload size exceeds allowed limit");
18 | }
19 |
20 | public PayloadMaxSizeExceededException(final int maxSize) {
21 | super(String.format("Total payload size exceeds allowed limit (%s bytes max)", maxSize));
22 | }
23 |
24 | public PayloadMaxSizeExceededException(final int maxSize, final int currentSize) {
25 | super(String.format("Total payload size exceeds allowed limit (payload is %s bytes, limit is %s)", currentSize, maxSize));
26 | }
27 |
28 | /**
29 | * Constructor with custom message
30 | *
31 | * @param message
32 | */
33 | public PayloadMaxSizeExceededException(final String message) {
34 | super(message);
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/javapns/notification/management/WiFiPayload.java:
--------------------------------------------------------------------------------
1 | package javapns.notification.management;
2 |
3 | import org.json.JSONException;
4 | import org.json.JSONObject;
5 |
6 | /**
7 | * An MDM payload for Wi-Fi.
8 | *
9 | * @author Sylvain Pedneault
10 | */
11 | class WiFiPayload extends MobileConfigPayload {
12 | public WiFiPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final String ssidStr, final boolean hiddenNetwork, final String encryptionType) throws JSONException {
13 | super(payloadVersion, "com.apple.wifi.managed", payloadOrganization, payloadIdentifier, payloadDisplayName);
14 | final JSONObject payload = getPayload();
15 | payload.put("SSID_STR", ssidStr);
16 | payload.put("HIDDEN_NETWORK", hiddenNetwork);
17 | payload.put("EncryptionType", encryptionType);
18 | }
19 |
20 | public void setPassword(final String value) throws JSONException {
21 | getPayload().put("Password", value);
22 | }
23 |
24 | public JSONObject addEAPClientConfiguration() throws JSONException {
25 | final JSONObject object = new JSONObject();
26 | getPayload().put("EAPClientConfiguration", object);
27 | return object;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/javapns/notification/management/VPNPayload.java:
--------------------------------------------------------------------------------
1 | package javapns.notification.management;
2 |
3 | import org.json.JSONException;
4 | import org.json.JSONObject;
5 |
6 | /**
7 | * An MDM payload for VPN.
8 | *
9 | * @author Sylvain Pedneault
10 | */
11 | class VPNPayload extends MobileConfigPayload {
12 | public static final String VPNTYPE_L2TP = "L2TP";
13 | public static final String VPNTYPE_PPTP = "PPTP";
14 | public static final String VPNTYPE_IP_SEC = "IPSec";
15 |
16 | public VPNPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final String userDefinedName, final boolean overridePrimary, final String vpnType) throws JSONException {
17 | super(payloadVersion, "com.apple.vpn.managed", payloadOrganization, payloadIdentifier, payloadDisplayName);
18 | final JSONObject payload = getPayload();
19 | payload.put("UserDefinedName", userDefinedName);
20 | payload.put("OverridePrimary", overridePrimary);
21 | payload.put("VPNType", vpnType);
22 | }
23 |
24 | public JSONObject addPPP() throws JSONException {
25 | final JSONObject object = new JSONObject();
26 | getPayload().put("PPP", object);
27 | return object;
28 | }
29 |
30 | public JSONObject addIPSec() throws JSONException {
31 | final JSONObject object = new JSONObject();
32 | getPayload().put("IPSec", object);
33 | return object;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/javapns/notification/exceptions/PayloadMaxSizeProbablyExceededException.java:
--------------------------------------------------------------------------------
1 | package javapns.notification.exceptions;
2 |
3 | import org.json.JSONException;
4 |
5 | /**
6 | * Thrown when a payload is expected to exceed the maximum size allowed after adding a given property.
7 | * Invoke payload.setPayloadSizeEstimatedWhenAdding(false) to disable this automatic checking.
8 | *
9 | * @author Sylvain Pedneault
10 | */
11 |
12 | public class PayloadMaxSizeProbablyExceededException extends JSONException {
13 |
14 | private static final long serialVersionUID = 580227446786047134L;
15 |
16 | /**
17 | * Default constructor
18 | */
19 | public PayloadMaxSizeProbablyExceededException() {
20 | super("Total payload size will most likely exceed allowed limit");
21 | }
22 |
23 | public PayloadMaxSizeProbablyExceededException(final int maxSize) {
24 | super(String.format("Total payload size will most likely exceed allowed limit (%s bytes max)", maxSize));
25 | }
26 |
27 | public PayloadMaxSizeProbablyExceededException(final int maxSize, final int estimatedSize) {
28 | super(String.format("Total payload size will most likely exceed allowed limit (estimated to become %s bytes, limit is %s)", estimatedSize, maxSize));
29 | }
30 |
31 | /**
32 | * Constructor with custom message
33 | *
34 | * @param message
35 | */
36 | public PayloadMaxSizeProbablyExceededException(final String message) {
37 | super(message);
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## JavaPNS
2 |
3 | [](https://travis-ci.org/mlaccetti/JavaPNS)
4 | [](https://coveralls.io/github/mlaccetti/JavaPNS?branch=develop)
5 | [](https://codeclimate.com/github/mlaccetti/JavaPNS)
6 |
7 | Apple Push Notification Service Provider for Java
8 |
9 | Fork of JavaPNS to include Maven support - http://code.google.com/p/javapns/
10 |
11 | Java 1.8+ compatible
12 |
13 | ### Updates
14 |
15 | Version 2.3.2 released!
16 |
17 | #### 2.3.2 Changes
18 | * 1.8 tweaks
19 | * General cleanup and overhaul
20 |
21 | #### 2.3.1 Changes
22 | * PushNotificationBigPayload ```complex``` and ```fromJson``` methods fixed
23 | * Fix to make trust store work on IBM JVM
24 |
25 | #### 2.3 Changes
26 | * iOS>=8 bigger notification payload support (2KB)
27 | * iOS>=7 Silent push notifications support ("content-available":1)
28 |
29 | ### Installation through Central Maven Repository
30 | javapns is available on the Central Maven Repository.
31 | To use javapns-jdk16 in your project, please add the following dependency to your pom.xml file:
32 | ```
33 | 16 | * Note that this is a local reference to the device, 17 | * which is not related to the actual device UUID or 18 | * other device-specific identification. Most of the 19 | * time, this deviceId should be the same as the token. 20 | * 21 | * @return the device id 22 | */ 23 | String getDeviceId(); 24 | 25 | /** 26 | * An id representing a particular device. 27 | *
28 | * Note that this is a local reference to the device, 29 | * which is not related to the actual device UUID or 30 | * other device-specific identification. Most of the 31 | * time, this deviceId should be the same as the token. 32 | * 33 | * @param id the device id 34 | */ 35 | void setDeviceId(String id); 36 | 37 | /** 38 | * A device token. 39 | * 40 | * @return the device token 41 | */ 42 | String getToken(); 43 | 44 | /** 45 | * Set the device token 46 | * 47 | * @param token 48 | */ 49 | void setToken(String token); 50 | 51 | /** 52 | * @return the last register 53 | */ 54 | Timestamp getLastRegister(); 55 | 56 | /** 57 | * @param lastRegister the last register 58 | */ 59 | void setLastRegister(Timestamp lastRegister); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/RestrictionsPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | 5 | /** 6 | * An MDM payload for Restrictions. 7 | * 8 | * @author Sylvain Pedneault 9 | */ 10 | class RestrictionsPayload extends MobileConfigPayload { 11 | public RestrictionsPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName) throws JSONException { 12 | super(payloadVersion, "com.apple.applicationaccess", payloadOrganization, payloadIdentifier, payloadDisplayName); 13 | } 14 | 15 | public void setAllowAppInstallation(final boolean value) throws JSONException { 16 | getPayload().put("allowAppInstallation", value); 17 | } 18 | 19 | public void setAllowCamera(final boolean value) throws JSONException { 20 | getPayload().put("allowCamera", value); 21 | } 22 | 23 | public void setAllowExplicitContent(final boolean value) throws JSONException { 24 | getPayload().put("allowExplicitContent", value); 25 | } 26 | 27 | public void setAllowScreenShot(final boolean value) throws JSONException { 28 | getPayload().put("allowScreenShot", value); 29 | } 30 | 31 | public void setAllowYouTube(final boolean value) throws JSONException { 32 | getPayload().put("allowYouTube", value); 33 | } 34 | 35 | public void setAllowiTunes(final boolean value) throws JSONException { 36 | getPayload().put("allowAppInstallation", value); 37 | } 38 | 39 | public void setAllowSafari(final boolean value) throws JSONException { 40 | getPayload().put("allowSafari", value); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/DeviceFactory.java: -------------------------------------------------------------------------------- 1 | package javapns.devices; 2 | 3 | import javapns.devices.exceptions.DuplicateDeviceException; 4 | import javapns.devices.exceptions.NullDeviceTokenException; 5 | import javapns.devices.exceptions.NullIdException; 6 | import javapns.devices.exceptions.UnknownDeviceException; 7 | 8 | /** 9 | * This is the common interface for all DeviceFactories. 10 | * It allows the PushNotificationManager to support multiple 11 | * implementations of DeviceFactory (in-memory, JPA-backed, etc.) 12 | * 13 | * @author Sylvain Pedneault 14 | * @deprecated Phasing out DeviceFactory because it has become irrelevant in the new library architecture 15 | */ 16 | @Deprecated 17 | public interface DeviceFactory { 18 | /** 19 | * Add a device to the map 20 | * 21 | * @param id The local device id 22 | * @param token The device token 23 | * @return The device created 24 | * @throws DuplicateDeviceException 25 | * @throws NullIdException 26 | * @throws NullDeviceTokenException 27 | */ 28 | Device addDevice(String id, String token) throws Exception; 29 | 30 | /** 31 | * Get a device according to his id 32 | * 33 | * @param id The local device id 34 | * @return The device 35 | * @throws UnknownDeviceException 36 | * @throws NullIdException 37 | */ 38 | Device getDevice(String id) throws UnknownDeviceException, NullIdException; 39 | 40 | /** 41 | * Remove a device 42 | * 43 | * @param id The local device id 44 | * @throws UnknownDeviceException 45 | * @throws NullIdException 46 | */ 47 | void removeDevice(String id) throws UnknownDeviceException, NullIdException; 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/MobileConfigPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import javapns.notification.Payload; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | 7 | /** 8 | * A payload template compatible with Apple Mobile Device Management's Config Payload specification (beta version). 9 | * 10 | * @author Sylvain Pedneault 11 | */ 12 | abstract class MobileConfigPayload extends Payload { 13 | private static long serialuuid = 10000000; 14 | 15 | MobileConfigPayload(final int payloadVersion, final String payloadType, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName) throws JSONException { 16 | this(payloadVersion, generateUUID(), payloadType, payloadOrganization, payloadIdentifier, payloadDisplayName); 17 | } 18 | 19 | private MobileConfigPayload(final int payloadVersion, final String payloadUUID, final String payloadType, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName) throws JSONException { 20 | super(); 21 | final JSONObject payload = getPayload(); 22 | payload.put("PayloadVersion", payloadVersion); 23 | payload.put("PayloadUUID", payloadUUID); 24 | payload.put("PayloadType", payloadType); 25 | payload.put("PayloadOrganization", payloadOrganization); 26 | payload.put("PayloadIdentifier", payloadIdentifier); 27 | payload.put("PayloadDisplayName", payloadDisplayName); 28 | } 29 | 30 | private static String generateUUID() { 31 | return System.nanoTime() + "." + (++serialuuid); 32 | } 33 | 34 | public void setPayloadDescription(final String description) throws JSONException { 35 | getPayload().put("PayloadDescription", description); 36 | } 37 | 38 | public void setPayloadRemovalDisallowed(final boolean disallowed) throws JSONException { 39 | getPayload().put("PayloadRemovalDisallowed", disallowed); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/javapns/test/FeedbackTest.java: -------------------------------------------------------------------------------- 1 | package javapns.test; 2 | 3 | import javapns.Push; 4 | import javapns.communication.exceptions.CommunicationException; 5 | import javapns.communication.exceptions.KeystoreException; 6 | import javapns.devices.Device; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * A command-line test facility for the Feedback Service. 12 | *
Example: java -cp "[required libraries]" FeedbackTest keystore.p12 mypass
14 | *
By default, this test uses the sandbox service. To switch, add "production" as a third parameter:
15 | *Example: java -cp "[required libraries]" FeedbackTest keystore.p12 mypass production
19 | * NB : Future Improvement :
20 | * - Add a method to find a device knowing his token
21 | * - Add a method to update a device (timestamp or token)
22 | * - method to compare two devices, and replace when the device token has changed
23 | *
24 | * @author Maxime Peron
25 | */
26 | @Deprecated
27 | public class BasicDeviceFactory implements DeviceFactory {
28 | /* synclock */
29 | private static final Object synclock = new Object();
30 | /* A map containing all the devices, identified with their id */
31 | private final Map
38 | * Note: this method automatically sets the socket's timeout
39 | * to TIMEOUT, so not to block the socket's input stream.
40 | *
41 | * @param socket
42 | * @return
43 | */
44 | private static List A list of PushedNotification objects.
11 | * This list can be configured to retain a maximum number of objects. When that maximum is reached, older objects are removed from the list before new ones are added.
13 | * Internally, this list extends Vector.
19 | * Communication protocol differences between Notification and Feedback servers are
20 | * implemented in {@link javapns.notification.ConnectionToNotificationServer} and {@link javapns.feedback.ConnectionToFeedbackServer}.
21 | *
22 | * @author Sylvain Pedneault
23 | */
24 | public abstract class ConnectionToAppleServer {
25 | /* PKCS12 */
26 | public static final String KEYSTORE_TYPE_PKCS12 = "PKCS12";
27 | /* JKS */
28 | public static final String KEYSTORE_TYPE_JKS = "JKS";
29 | private static final Logger logger = LoggerFactory.getLogger(ConnectionToAppleServer.class);
30 | /* The algorithm used by KeyManagerFactory */
31 | private static final String ALGORITHM = KeyManagerFactory.getDefaultAlgorithm();
32 |
33 | /* The protocol used to create the SSLSocket */
34 | private static final String PROTOCOL = "TLS";
35 |
36 | private final AppleServer server;
37 |
38 | private KeyStore keyStore;
39 | private SSLSocketFactory socketFactory;
40 |
41 | /**
42 | * Builds a connection to an Apple server.
43 | *
44 | * @param server connection details
45 | * @throws KeystoreException thrown if an error occurs when loading the keystore
46 | */
47 | protected ConnectionToAppleServer(final AppleServer server) throws KeystoreException {
48 | this.server = server;
49 | this.keyStore = KeystoreManager.loadKeystore(server);
50 | }
51 |
52 | /**
53 | * Builds a connection to an Apple server.
54 | *
55 | * @param server connection details
56 | * @param keystore
57 | */
58 | protected ConnectionToAppleServer(final AppleServer server, final KeyStore keystore) {
59 | this.server = server;
60 | this.keyStore = keystore;
61 | }
62 |
63 | public AppleServer getServer() {
64 | return server;
65 | }
66 |
67 | private KeyStore getKeystore() {
68 | return keyStore;
69 | }
70 |
71 | public void setKeystore(final KeyStore ks) {
72 | this.keyStore = ks;
73 | }
74 |
75 | /**
76 | * Generic SSLSocketFactory builder
77 | *
78 | * @param trustManagers
79 | * @return SSLSocketFactory
80 | * @throws KeystoreException
81 | */
82 | private SSLSocketFactory createSSLSocketFactoryWithTrustManagers(final TrustManager[] trustManagers) throws KeystoreException {
83 | logger.debug("Creating SSLSocketFactory");
84 | // Get a KeyManager and initialize it
85 | try {
86 | final KeyStore keystore = getKeystore();
87 | final KeyManagerFactory kmf = KeyManagerFactory.getInstance(ALGORITHM);
88 | try {
89 | final char[] password = KeystoreManager.getKeystorePasswordForSSL(server);
90 | kmf.init(keystore, password);
91 | } catch (Exception e) {
92 | final KeystoreException wrappedKeystoreException = KeystoreManager.wrapKeystoreException(e);
93 | throw wrappedKeystoreException;
94 | }
95 |
96 | // Get the SSLContext to help create SSLSocketFactory
97 | final SSLContext sslc = SSLContext.getInstance(PROTOCOL);
98 | sslc.init(kmf.getKeyManagers(), trustManagers, null);
99 |
100 | return sslc.getSocketFactory();
101 | } catch (final Exception e) {
102 | throw new KeystoreException("Keystore exception: " + e.getMessage(), e);
103 | }
104 | }
105 |
106 | public abstract String getServerHost();
107 |
108 | protected abstract int getServerPort();
109 |
110 | /**
111 | * Return a SSLSocketFactory for creating sockets to communicate with Apple.
112 | *
113 | * @return SSLSocketFactory
114 | * @throws KeystoreException
115 | */
116 | private SSLSocketFactory createSSLSocketFactory() throws KeystoreException {
117 | return createSSLSocketFactoryWithTrustManagers(new TrustManager[]{new ServerTrustingTrustManager()});
118 | }
119 |
120 | private SSLSocketFactory getSSLSocketFactory() throws KeystoreException {
121 | if (socketFactory == null) {
122 | socketFactory = createSSLSocketFactory();
123 | }
124 | return socketFactory;
125 | }
126 |
127 | /**
128 | * Create a SSLSocket which will be used to send data to Apple
129 | *
130 | * @return the SSLSocket
131 | * @throws KeystoreException
132 | * @throws CommunicationException
133 | */
134 | public SSLSocket getSSLSocket() throws KeystoreException, CommunicationException {
135 | final SSLSocketFactory sslSocketFactory = getSSLSocketFactory();
136 | logger.debug("Creating SSLSocket to " + getServerHost() + ":" + getServerPort());
137 |
138 | try {
139 | if (ProxyManager.isUsingProxy(server)) {
140 | return tunnelThroughProxy(sslSocketFactory);
141 | } else {
142 | return (SSLSocket) sslSocketFactory.createSocket(getServerHost(), getServerPort());
143 | }
144 | } catch (final Exception e) {
145 | throw new CommunicationException("Communication exception: " + e, e);
146 | }
147 | }
148 |
149 | private SSLSocket tunnelThroughProxy(final SSLSocketFactory socketFactory) throws IOException {
150 | final SSLSocket socket;
151 |
152 | // If a proxy was set, tunnel through the proxy to create the connection
153 | final String tunnelHost = ProxyManager.getProxyHost(server);
154 | final Integer tunnelPort = ProxyManager.getProxyPort(server);
155 |
156 | final Socket tunnel = new Socket(tunnelHost, tunnelPort);
157 | doTunnelHandshake(tunnel, getServerHost(), getServerPort());
158 |
159 | /* overlay the tunnel socket with SSL */
160 | socket = (SSLSocket) socketFactory.createSocket(tunnel, getServerHost(), getServerPort(), true);
161 |
162 | /* register a callback for handshaking completion event */
163 | socket.addHandshakeCompletedListener(event -> {
164 | logger.debug("Handshake finished!");
165 | logger.debug("\t CipherSuite:" + event.getCipherSuite());
166 | logger.debug("\t SessionId " + event.getSession());
167 | logger.debug("\t PeerHost " + event.getSession().getPeerHost());
168 | });
169 |
170 | return socket;
171 | }
172 |
173 | private void doTunnelHandshake(final Socket tunnel, final String host, final int port) throws IOException {
174 |
175 | final OutputStream out = tunnel.getOutputStream();
176 |
177 | final String msg = "CONNECT " + host + ":" + port + " HTTP/1.0\n" + "User-Agent: BoardPad Server" + "\r\n\r\n";
178 | byte[] b;
179 | try { //We really do want ASCII7 -- the http protocol doesn't change with locale.
180 | b = msg.getBytes("ASCII7");
181 | } catch (final UnsupportedEncodingException ignored) { //If ASCII7 isn't there, something serious is wrong, but Paranoia Is Good (tm)
182 | b = msg.getBytes();
183 | }
184 | out.write(b);
185 | out.flush();
186 |
187 | // We need to store the reply so we can create a detailed error message to the user.
188 | final byte[] reply = new byte[200];
189 | int replyLen = 0;
190 | int newlinesSeen = 0;
191 | boolean headerDone = false; //Done on first newline
192 |
193 | final InputStream in = tunnel.getInputStream();
194 |
195 | while (newlinesSeen < 2) {
196 | final int i = in.read();
197 | if (i < 0) {
198 | throw new IOException("Unexpected EOF from proxy");
199 | }
200 | if (i == '\n') {
201 | headerDone = true;
202 | ++newlinesSeen;
203 | } else if (i != '\r') {
204 | newlinesSeen = 0;
205 | if (!headerDone && replyLen < reply.length) {
206 | reply[replyLen++] = (byte) i;
207 | }
208 | }
209 | }
210 |
211 | /*
212 | * Converting the byte array to a string is slightly wasteful
213 | * in the case where the connection was successful, but it's
214 | * insignificant compared to the network overhead.
215 | */
216 | String replyStr;
217 | try {
218 | replyStr = new String(reply, 0, replyLen, "ASCII7");
219 | } catch (final UnsupportedEncodingException ignored) {
220 | replyStr = new String(reply, 0, replyLen);
221 | }
222 |
223 | /* We check for Connection Established because our proxy returns HTTP/1.1 instead of 1.0 */
224 | if (!replyStr.toLowerCase().contains("200 connection established")) {
225 | throw new IOException("Unable to tunnel through. Proxy returns \"" + replyStr + "\"");
226 | }
227 |
228 | /* tunneling Handshake was successful! */
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/src/main/java/javapns/notification/PushedNotification.java:
--------------------------------------------------------------------------------
1 | package javapns.notification;
2 |
3 | import javapns.devices.Device;
4 | import javapns.notification.exceptions.ErrorResponsePacketReceivedException;
5 |
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | /**
10 | * An object representing the result of a push notification to a specific payload to a single device.
12 | * If any error occurred while trying to push the notification, an exception is attached.
14 | * If Apple's Push Notification Service returned an error-response packet, it is linked to the related PushedNotification
15 | * so you can find out what the actual error was. Returns true if no response packet was received for this notification,
210 | * or if one was received but is not an error-response (ie command 8),
211 | * or if one was received but its status is 0 (no error occurred).
213 | * Returns false if an error-response packet is attached and has
214 | * a non-zero status code.
216 | * Returns false if an exception is attached.
218 | * Make sure you use the Feedback Service to cleanup your list of
219 | * invalid device tokens, as Apple's documentation says. Example:
21 | * By default, this test uses the sandbox service. To switch, add "production" as a fourth parameter: Example:
24 | * Also by default, this test pushes a simple alert. To send a complex payload, add "complex" as a fifth parameter: Example:
27 | * To send a simple payload to a large number of fake devices, add "threads" as a fifth parameter, the number of fake devices to construct, and the number of threads to use: Example:
258 | * This method simply invokes the other getCompatibleProperty method with the root aps dictionary.
259 | *
260 | * @param
276 | * This method is useful for properly supporting properties that can have a simple
277 | * or complex value (such as "alert")
278 | *
279 | * @param
15 | * By default, this class has no payload content at all. Subclasses are
16 | * responsible for imposing specific content based on the specifications
17 | * they are intended to implement (such as the 'aps' dictionnary for APS
18 | * payloads).
19 | *
20 | * @author Sylvain Pedneault
21 | */
22 | public abstract class Payload {
23 | static final Logger logger = LoggerFactory.getLogger(Payload.class);
24 |
25 | /* Character encoding specified by Apple documentation */
26 | private static final String DEFAULT_CHARACTER_ENCODING = "UTF-8";
27 | private static final String ADDING_CUSTOM_DICTIONARY = "Adding custom Dictionary [";
28 | private static final String DELIMITER_START = "] = [";
29 | private static final String DELIMITED_END = "]";
30 | /* The root Payload */
31 | private final JSONObject payload;
32 |
33 | /* Character encoding to use for streaming the payload (should be UTF-8) */
34 | private String characterEncoding = DEFAULT_CHARACTER_ENCODING;
35 |
36 | /* Number of seconds after which this payload should expire */
37 | @SuppressWarnings("PointlessArithmeticExpression")
38 | private int expiry = 1 * 24 * 60 * 60;
39 |
40 | private boolean payloadSizeEstimatedWhenAdding = false;
41 |
42 | private int preSendConfiguration = 0;
43 |
44 | /**
45 | * Construct a Payload object with a blank root JSONObject
46 | */
47 | protected Payload() {
48 | super();
49 | this.payload = new JSONObject();
50 | }
51 |
52 | /**
53 | * Construct a Payload object from a JSON-formatted string
54 | *
55 | * @param rawJSON a JSON-formatted string (ex: {"aps":{"alert":"Hello World!"}} )
56 | * @throws JSONException thrown if a exception occurs while parsing the JSON string
57 | */
58 | Payload(final String rawJSON) throws JSONException {
59 | super();
60 | this.payload = new JSONObject(rawJSON);
61 | }
62 |
63 | /**
64 | * Get the actual JSON object backing this payload.
65 | *
66 | * @return a JSONObject
67 | */
68 | public JSONObject getPayload() {
69 | return this.payload;
70 | }
71 |
72 | /**
73 | * Add a custom dictionnary with a string value
74 | *
75 | * @param name
76 | * @param value
77 | * @throws JSONException
78 | */
79 | public void addCustomDictionary(final String name, final String value) throws JSONException {
80 | logger.debug(ADDING_CUSTOM_DICTIONARY + name + DELIMITER_START + value + DELIMITED_END);
81 | put(name, value, payload, false);
82 | }
83 |
84 | /**
85 | * Add a custom dictionnary with a int value
86 | *
87 | * @param name
88 | * @param value
89 | * @throws JSONException
90 | */
91 | public void addCustomDictionary(final String name, final int value) throws JSONException {
92 | logger.debug(ADDING_CUSTOM_DICTIONARY + name + DELIMITER_START + value + DELIMITED_END);
93 | put(name, value, payload, false);
94 | }
95 |
96 | /**
97 | * Add a custom dictionnary with multiple values
98 | *
99 | * @param name
100 | * @param values
101 | * @throws JSONException
102 | */
103 | public void addCustomDictionary(final String name, final List values) throws JSONException {
104 | logger.debug(ADDING_CUSTOM_DICTIONARY + name + "] = (list)");
105 | put(name, values, payload, false);
106 | }
107 |
108 | /**
109 | * Add a custom dictionnary with object
110 | *
111 | * @param name
112 | * @param value
113 | * @throws JSONException
114 | */
115 | public void addCustomDictionary(final String name, final Object value) throws JSONException {
116 | logger.debug(ADDING_CUSTOM_DICTIONARY + name + DELIMITER_START + value + DELIMITED_END);
117 | put(name, value, payload, false);
118 | }
119 |
120 | /**
121 | * Get the string representation
122 | */
123 | public String toString() {
124 | return this.payload.toString();
125 | }
126 |
127 | void verifyPayloadIsNotEmpty() {
128 | if (getPreSendConfiguration() != 0) {
129 | return;
130 | }
131 | if (toString().equals("{}")) {
132 | throw new IllegalArgumentException("Payload cannot be empty");
133 | }
134 | }
135 |
136 | /**
137 | * Get this payload as a byte array using the preconfigured character encoding.
138 | *
139 | * @return byte[] bytes ready to be streamed directly to Apple servers
140 | */
141 | public byte[] getPayloadAsBytes() throws Exception {
142 | final byte[] payloadBytesUnchecked = getPayloadAsBytesUnchecked();
143 | validateMaximumPayloadSize(payloadBytesUnchecked.length);
144 | return payloadBytesUnchecked;
145 | }
146 |
147 | /**
148 | * Get this payload as a byte array using the preconfigured character encoding.
149 | * This method does NOT check if the payload exceeds the maximum payload length.
150 | *
151 | * @return byte[] bytes ready to be streamed directly to Apple servers (but that might exceed the maximum size limit)
152 | */
153 | private byte[] getPayloadAsBytesUnchecked() throws Exception {
154 | byte[] bytes;
155 | try {
156 | bytes = toString().getBytes(characterEncoding);
157 | } catch (final Exception ex) {
158 | bytes = toString().getBytes();
159 | }
160 | return bytes;
161 | }
162 |
163 | /**
164 | * Get the number of bytes that the payload will occupy when streamed.
165 | *
166 | * @return a number of bytes
167 | * @throws Exception
168 | */
169 | public int getPayloadSize() throws Exception {
170 | return getPayloadAsBytesUnchecked().length;
171 | }
172 |
173 | /**
174 | * Check if the payload exceeds the maximum size allowed.
175 | * The maximum size allowed is returned by the getMaximumPayloadSize() method.
176 | *
177 | * @return true if the payload exceeds the maximum size allowed, false otherwise
178 | */
179 | private boolean isPayloadTooLong() {
180 | try {
181 | final byte[] bytes = getPayloadAsBytesUnchecked();
182 | if (bytes.length > getMaximumPayloadSize()) {
183 | return true;
184 | }
185 | } catch (final Exception e) {
186 | // empty
187 | }
188 | return false;
189 | }
190 |
191 | /**
192 | * Estimate the size that this payload will take after adding a given property.
193 | * For performance reasons, this estimate is not as reliable as actually adding
194 | * the property and checking the payload size afterwards.
195 | *
196 | * Currently works well with strings and numbers.
197 | *
198 | * @param propertyName the name of the property to use for calculating the estimation
199 | * @param propertyValue the value of the property to use for calculating the estimation
200 | * @return an estimated payload size if the property were to be added to the payload
201 | */
202 | private int estimatePayloadSizeAfterAdding(final String propertyName, final Object propertyValue) {
203 | try {
204 | int estimatedSize = getPayloadAsBytesUnchecked().length;
205 | if (propertyName != null && propertyValue != null) {
206 | estimatedSize += 6; // ,"":""
207 | estimatedSize += propertyName.getBytes(getCharacterEncoding()).length;
208 | int estimatedValueSize = 0;
209 |
210 | if (propertyValue instanceof String || propertyValue instanceof Number) {
211 | estimatedValueSize = propertyValue.toString().getBytes(getCharacterEncoding()).length;
212 | }
213 |
214 | estimatedSize += estimatedValueSize;
215 | }
216 | return estimatedSize;
217 | } catch (final Exception e) {
218 | try {
219 | return getPayloadSize();
220 | } catch (final Exception e1) {
221 | return 0;
222 | }
223 | }
224 | }
225 |
226 | /**
227 | * Validate if the estimated payload size after adding a given property will be allowed.
228 | * For performance reasons, this estimate is not as reliable as actually adding
229 | * the property and checking the payload size afterwards.
230 | *
231 | * @param propertyName the name of the property to use for calculating the estimation
232 | * @param propertyValue the value of the property to use for calculating the estimation
233 | * @return true if the payload size is not expected to exceed the maximum allowed, false if it might be too big
234 | */
235 | public boolean isEstimatedPayloadSizeAllowedAfterAdding(final String propertyName, final Object propertyValue) {
236 | final int maximumPayloadSize = getMaximumPayloadSize();
237 | final int estimatedPayloadSize = estimatePayloadSizeAfterAdding(propertyName, propertyValue);
238 | return estimatedPayloadSize <= maximumPayloadSize;
239 | }
240 |
241 | /**
242 | * Validate that the payload does not exceed the maximum size allowed.
243 | * If the limit is exceeded, a PayloadMaxSizeExceededException is thrown.
244 | *
245 | * @param currentPayloadSize the total size of the payload in bytes
246 | * @throws PayloadMaxSizeExceededException if the payload exceeds the maximum size allowed
247 | */
248 | private void validateMaximumPayloadSize(final int currentPayloadSize) throws PayloadMaxSizeExceededException {
249 | final int maximumPayloadSize = getMaximumPayloadSize();
250 | if (currentPayloadSize > maximumPayloadSize) {
251 | throw new PayloadMaxSizeExceededException(maximumPayloadSize, currentPayloadSize);
252 | }
253 | }
254 |
255 | /**
256 | * Puts a property in a JSONObject, while possibly checking for estimated payload size violation.
257 | *
258 | * @param propertyName the name of the property to use for calculating the estimation
259 | * @param propertyValue the value of the property to use for calculating the estimation
260 | * @param object the JSONObject to put the property in
261 | * @param opt true to use putOpt, false to use put
262 | * @throws JSONException
263 | */
264 | void put(final String propertyName, final Object propertyValue, final JSONObject object, final boolean opt) throws JSONException {
265 | try {
266 | if (isPayloadSizeEstimatedWhenAdding()) {
267 | final int maximumPayloadSize = getMaximumPayloadSize();
268 | final int estimatedPayloadSize = estimatePayloadSizeAfterAdding(propertyName, propertyValue);
269 | final boolean estimatedToExceed = estimatedPayloadSize > maximumPayloadSize;
270 | if (estimatedToExceed) {
271 | throw new PayloadMaxSizeProbablyExceededException(maximumPayloadSize, estimatedPayloadSize);
272 | }
273 | }
274 | } catch (final PayloadMaxSizeProbablyExceededException e) {
275 | throw e;
276 | } catch (final Exception e) {
277 | // empty
278 | }
279 | if (opt) {
280 | object.putOpt(propertyName, propertyValue);
281 | } else {
282 | object.put(propertyName, propertyValue);
283 | }
284 | }
285 |
286 | Object remove(final String propertyName, final JSONObject object) {
287 | return object.remove(propertyName);
288 | }
289 |
290 | /**
291 | * Indicates if payload size is estimated and controlled when adding properties (default is false).
292 | *
293 | * @return true to throw an exception if the estimated size is too big when adding a property, false otherwise
294 | */
295 | public boolean isPayloadSizeEstimatedWhenAdding() {
296 | return payloadSizeEstimatedWhenAdding;
297 | }
298 |
299 | /**
300 | * Indicate if payload size should be estimated and controlled when adding properties (default is false).
301 | *
302 | * @param checked true to throw an exception if the estimated size is too big when adding a property, false otherwise
303 | */
304 | public void setPayloadSizeEstimatedWhenAdding(final boolean checked) {
305 | this.payloadSizeEstimatedWhenAdding = checked;
306 | }
307 |
308 | /**
309 | * Return the maximum payload size in bytes.
310 | * By default, this method returns Integer.MAX_VALUE.
311 | * Subclasses should override this method to provide their own limit.
312 | *
313 | * @return the maximum payload size in bytes
314 | */
315 | int getMaximumPayloadSize() {
316 | return Integer.MAX_VALUE;
317 | }
318 |
319 | /**
320 | * Returns the character encoding that will be used by getPayloadAsBytes().
321 | * Default is UTF-8, as per Apple documentation.
322 | *
323 | * @return a character encoding
324 | */
325 | public String getCharacterEncoding() {
326 | return characterEncoding;
327 | }
328 |
329 | /**
330 | * Changes the character encoding for streaming the payload.
331 | * Character encoding is preset to UTF-8, as Apple documentation specifies.
332 | * Therefore, unless you are working on a special project, you should leave it as is.
333 | *
334 | * @param characterEncoding a valid character encoding that String.getBytes(encoding) will accept
335 | */
336 | public void setCharacterEncoding(final String characterEncoding) {
337 | this.characterEncoding = characterEncoding;
338 | }
339 |
340 | /**
341 | * Return the number of seconds after which this payload should expire.
342 | *
343 | * @return a number of seconds
344 | */
345 | public int getExpiry() {
346 | return expiry;
347 | }
348 |
349 | /**
350 | * Set the number of seconds after which this payload should expire.
351 | * Default is one (1) day.
352 | *
353 | * @param seconds
354 | */
355 | public void setExpiry(final int seconds) {
356 | this.expiry = seconds;
357 | }
358 |
359 | /**
360 | * Enables a special simulation mode which causes the library to behave
361 | * as usual *except* that at the precise point where the payload would
362 | * actually be streamed out to Apple, it is not.
363 | *
364 | * @return the same payload
365 | */
366 | public Payload asSimulationOnly() {
367 | setExpiry(919191);
368 | return this;
369 | }
370 |
371 | int getPreSendConfiguration() {
372 | return preSendConfiguration;
373 | }
374 |
375 | void setPreSendConfiguration(final int preSendConfiguration) {
376 | this.preSendConfiguration = preSendConfiguration;
377 | }
378 |
379 | }
380 |
--------------------------------------------------------------------------------
/src/test/java/javapns/test/SpecificNotificationTests.java:
--------------------------------------------------------------------------------
1 | package javapns.test;
2 |
3 | import javapns.Push;
4 | import javapns.communication.exceptions.CommunicationException;
5 | import javapns.communication.exceptions.KeystoreException;
6 | import javapns.devices.Device;
7 | import javapns.devices.implementations.basic.BasicDevice;
8 | import javapns.notification.*;
9 | import javapns.notification.transmission.NotificationThread;
10 | import javapns.notification.transmission.NotificationThreads;
11 | import javapns.notification.transmission.PushQueue;
12 | import org.json.JSONException;
13 |
14 | import java.io.BufferedInputStream;
15 | import java.io.FileInputStream;
16 | import java.io.InputStream;
17 | import java.util.ArrayList;
18 | import java.util.List;
19 |
20 | /**
21 | * Specific test cases intended for the project's developers.
22 | *
23 | * @author Sylvain Pedneault
24 | */
25 | class SpecificNotificationTests extends TestFoundation {
26 | private SpecificNotificationTests() {
27 | // empty
28 | }
29 |
30 | /**
31 | * Execute this class from the command line to run tests.
32 | *
33 | * @param args
34 | */
35 | public static void main(final String[] args) {
36 | /* Verify that the test is being invoked */
37 | if (!verifyCorrectUsage(NotificationTest.class, args, "keystore-path", "keystore-password", "device-token", "[production|sandbox]", "[test-name]")) {
38 | return;
39 | }
40 |
41 | /* Push an alert */
42 | runTest(args);
43 | }
44 |
45 | /**
46 | * Push a test notification to a device, given command-line parameters.
47 | *
48 | * @param args
49 | */
50 | private static void runTest(final String[] args) {
51 | final String keystore = args[0];
52 | final String password = args[1];
53 | final String token = args[2];
54 | final boolean production = args.length >= 4 && args[3].equalsIgnoreCase("production");
55 |
56 | String testName = args.length >= 5 ? args[4] : null;
57 | if (testName == null || testName.length() == 0) {
58 | testName = "default";
59 | }
60 |
61 | try {
62 | SpecificNotificationTests.class.getDeclaredMethod("test_" + testName, String.class, String.class, String.class, boolean.class).invoke(null, keystore, password, token, production);
63 | } catch (final NoSuchMethodException e) {
64 | System.out.println(String.format("Error: test '%s' not found. Test names are case-sensitive", testName));
65 | } catch (final Exception e) {
66 | (e.getCause() != null ? e.getCause() : e).printStackTrace();
67 | }
68 | }
69 |
70 | private static void test_PushHelloWorld(final String keystore, final String password, final String token, final boolean production) throws CommunicationException, KeystoreException {
71 | final ListClass representing an abstract connection to an Apple server
18 | * java -cp "[required libraries]" NotificationTest keystore.p12 mypass 2ed202ac08ea9033665e853a3dc8bc4c5e78f7a6cf8d55910df230567037dcc4java -cp "[required libraries]" NotificationTest keystore.p12 mypass 2ed202ac08ea9033665e853a3dc8bc4c5e78f7a6cf8d55910df230567037dcc4 productionjava -cp "[required libraries]" NotificationTest keystore.p12 mypass 2ed202ac08ea9033665e853a3dc8bc4c5e78f7a6cf8d55910df230567037dcc4 production complexjava -cp "[required libraries]" NotificationTest keystore.p12 mypass 2ed202ac08ea9033665e853a3dc8bc4c5e78f7a6cf8d55910df230567037dcc4 sandbox threads 1000 5