From 577f8890eed88cdc186b0998bb3c3c523a78983f Mon Sep 17 00:00:00 2001 From: arashthearcher Date: Thu, 20 Nov 2014 09:40:23 -0800 Subject: [PATCH 01/30] Test Method Fixed. byte variable is a signed variable so if its value in memory is greater than 127 then it will be a minus number and the result of lines 53 and 72 will be negetive. --- .../com/notnoop/apns/internal/SimpleApnsNotificationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/notnoop/apns/internal/SimpleApnsNotificationTest.java b/src/test/java/com/notnoop/apns/internal/SimpleApnsNotificationTest.java index 0d839512..38e11f52 100644 --- a/src/test/java/com/notnoop/apns/internal/SimpleApnsNotificationTest.java +++ b/src/test/java/com/notnoop/apns/internal/SimpleApnsNotificationTest.java @@ -50,7 +50,7 @@ public void deviceTokenPart(String deviceToken, PayloadBuilder payload) { byte[] bytes = msg.marshall(); byte[] dt = decodeHex(deviceToken); - assertEquals(dt.length, /* found length */ (bytes[1] << 8) + bytes[2]); + assertEquals(dt.length, /* found length */ ((bytes[1] & 0xff) << 8) + (bytes[2]& 0xff)); // verify the device token part assertArrayEquals(dt, Utilities.copyOfRange(bytes, 3, 3 + dt.length)); @@ -69,7 +69,7 @@ public void payloadPart(String deviceToken, PayloadBuilder payload) { /// verify the payload part assertArrayEquals(pl, Utilities.copyOfRange(bytes, plBegin, bytes.length)); - assertEquals(pl.length, (bytes[plBegin - 2] << 8) + bytes[plBegin - 1]); + assertEquals(pl.length, ((bytes[plBegin - 2] & 0xff) << 8) + (bytes[plBegin - 1] & 0xff)); } @Theory From 5655f7cdf6cdfa68b7358817dd36cafb824d5c51 Mon Sep 17 00:00:00 2001 From: srinivasannanduri Date: Mon, 22 Dec 2014 16:10:54 +0530 Subject: [PATCH 02/30] Update ApnsConnectionImpl.java --- .../java/com/notnoop/apns/internal/ApnsConnectionImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java index 4f4f85ea..4062b3cb 100644 --- a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java +++ b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java @@ -209,8 +209,6 @@ public void run() { } logger.debug("resending {} notifications", resendSize); delegate.notificationsResent(resendSize); - - drainBuffer(); } logger.debug("Monitoring input stream closed by EOF"); @@ -222,6 +220,7 @@ public void run() { delegate.connectionClosed(DeliveryError.UNKNOWN, -1); } finally { close(); + drainBuffer(); } } From 53bcb414225423b8c00f0bd608edb459e6762698 Mon Sep 17 00:00:00 2001 From: "lasse.voss" Date: Wed, 7 Jan 2015 15:23:54 +0100 Subject: [PATCH 03/30] #212: added StartSendingApnsDelegate providing callback just before a notification is being sent --- .../apns/StartSendingApnsDelegate.java | 17 ++++++++++ .../apns/internal/ApnsConnectionImpl.java | 5 +++ .../integration/ApnsConnectionCacheTest.java | 34 +++++++++++++++++-- 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java diff --git a/src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java b/src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java new file mode 100644 index 00000000..a77f862b --- /dev/null +++ b/src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java @@ -0,0 +1,17 @@ +package com.notnoop.apns; + +/** + * A delegate that also gets notified just before a notification is being delivered to the + * Apple Server. + */ +public interface StartSendingApnsDelegate extends ApnsDelegate { + + /** + * Called when message is about to be sent to the Apple servers. + * + * @param message the notification that is about to be sent + * @param resent whether the notification is being resent after an error + */ + public void startSending(ApnsNotification message, boolean resent); + +} diff --git a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java index 4f4f85ea..b1c77557 100644 --- a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java +++ b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java @@ -46,6 +46,7 @@ import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import com.notnoop.apns.ApnsDelegate; +import com.notnoop.apns.StartSendingApnsDelegate; import com.notnoop.apns.ApnsNotification; import com.notnoop.apns.DeliveryError; import com.notnoop.apns.EnhancedApnsNotification; @@ -315,6 +316,10 @@ public synchronized void sendMessage(ApnsNotification m) throws NetworkIOExcepti private synchronized void sendMessage(ApnsNotification m, boolean fromBuffer) throws NetworkIOException { logger.debug("sendMessage {} fromBuffer: {}", m, fromBuffer); + if (delegate instanceof StartSendingApnsDelegate) { + ((StartSendingApnsDelegate) delegate).startSending(m, fromBuffer); + } + int attempts = 0; while (true) { try { diff --git a/src/test/java/com/notnoop/apns/integration/ApnsConnectionCacheTest.java b/src/test/java/com/notnoop/apns/integration/ApnsConnectionCacheTest.java index 27209a1b..b34e589d 100644 --- a/src/test/java/com/notnoop/apns/integration/ApnsConnectionCacheTest.java +++ b/src/test/java/com/notnoop/apns/integration/ApnsConnectionCacheTest.java @@ -4,6 +4,7 @@ import java.util.concurrent.atomic.AtomicInteger; import com.notnoop.apns.APNS; import com.notnoop.apns.ApnsDelegate; +import com.notnoop.apns.StartSendingApnsDelegate; import com.notnoop.apns.ApnsNotification; import com.notnoop.apns.ApnsService; import com.notnoop.apns.DeliveryError; @@ -53,6 +54,7 @@ public void handleReTransmissionError5Good1Bad7Good() throws InterruptedExceptio final CountDownLatch sync = new CountDownLatch(20); final AtomicInteger numResent = new AtomicInteger(); final AtomicInteger numSent = new AtomicInteger(); + final AtomicInteger numStartSend = new AtomicInteger(); int EXPECTED_RESEND_COUNT = 7; int EXPECTED_SEND_COUNT = 12; server.getWaitForError().acquire(); @@ -60,7 +62,14 @@ public void handleReTransmissionError5Good1Bad7Good() throws InterruptedExceptio ApnsService service = APNS.newService().withSSLContext(clientContext()) .withGatewayDestination(LOCALHOST, server.getEffectiveGatewayPort()) - .withDelegate(new ApnsDelegate() { + .withDelegate(new StartSendingApnsDelegate() { + + public void startSending(final ApnsNotification message, final boolean resent) { + if (!resent) { + numStartSend.incrementAndGet(); + } + } + public void messageSent(ApnsNotification message, boolean resent) { if (!resent) { numSent.incrementAndGet(); @@ -104,6 +113,7 @@ public void notificationsResent(int resendCount) { Assert.assertEquals(EXPECTED_RESEND_COUNT, numResent.get()); Assert.assertEquals(EXPECTED_SEND_COUNT, numSent.get()); + Assert.assertEquals(EXPECTED_SEND_COUNT + 1, numStartSend.get()); } @@ -119,6 +129,7 @@ public void handleReTransmissionError1Good1Bad2Good() throws InterruptedExceptio final CountDownLatch sync = new CountDownLatch(6); final AtomicInteger numResent = new AtomicInteger(); final AtomicInteger numSent = new AtomicInteger(); + final AtomicInteger numStartSend = new AtomicInteger(); int EXPECTED_RESEND_COUNT = 2; int EXPECTED_SEND_COUNT = 3; server.getWaitForError().acquire(); @@ -126,7 +137,14 @@ public void handleReTransmissionError1Good1Bad2Good() throws InterruptedExceptio ApnsService service = APNS.newService().withSSLContext(clientContext()) .withGatewayDestination(LOCALHOST, server.getEffectiveGatewayPort()) - .withDelegate(new ApnsDelegate() { + .withDelegate(new StartSendingApnsDelegate() { + + public void startSending(final ApnsNotification message, final boolean resent) { + if (!resent) { + numStartSend.incrementAndGet(); + } + } + public void messageSent(ApnsNotification message, boolean resent) { if (!resent) { numSent.incrementAndGet(); @@ -163,6 +181,7 @@ public void notificationsResent(int resendCount) { Assert.assertEquals(EXPECTED_RESEND_COUNT, numResent.get()); Assert.assertEquals(EXPECTED_SEND_COUNT, numSent.get()); + Assert.assertEquals(EXPECTED_SEND_COUNT + 1, numStartSend.get()); } @@ -177,13 +196,21 @@ public void handleReTransmissionError1Bad() throws InterruptedException { server = new ApnsServerStub(FixedCertificates.serverContext().getServerSocketFactory()); final CountDownLatch sync = new CountDownLatch(1); final AtomicInteger numError = new AtomicInteger(); + final AtomicInteger numStartSend = new AtomicInteger(); int EXPECTED_ERROR_COUNT = 1; server.getWaitForError().acquire(); server.start(); ApnsService service = APNS.newService().withSSLContext(clientContext()) .withGatewayDestination(LOCALHOST, server.getEffectiveGatewayPort()) - .withDelegate(new ApnsDelegate() { + .withDelegate(new StartSendingApnsDelegate() { + + public void startSending(final ApnsNotification message, final boolean resent) { + if (!resent) { + numStartSend.incrementAndGet(); + } + } + public void messageSent(ApnsNotification message, boolean resent) { } @@ -212,6 +239,7 @@ public void notificationsResent(int resendCount) { sync.await(); Assert.assertEquals(EXPECTED_ERROR_COUNT, numError.get()); + Assert.assertEquals(EXPECTED_ERROR_COUNT, numStartSend.get()); } /** From d58dab4d63bf14e1228801b4652f31594eff384e Mon Sep 17 00:00:00 2001 From: Matthias Wessendorf Date: Mon, 12 Jan 2015 10:38:29 +0100 Subject: [PATCH 04/30] [maven-release-plugin] prepare release apns-1.0.0.Beta5 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ded874b8..1011b9d8 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.notnoop.apns apns - 1.0.0.Beta5-SNAPSHOT + 1.0.0.Beta5 jar Java Apple Push Notification Service Library @@ -17,7 +17,7 @@ scm:git:git://github.com/notnoop/java-apns.git scm:git:git@github.com:notnoop/java-apns.git http://github.com/notnoop/java-apns - HEAD + apns-1.0.0.Beta5 From 7bb3f8691e76bd8876d8293bcb63b90e91f66b20 Mon Sep 17 00:00:00 2001 From: Matthias Wessendorf Date: Mon, 12 Jan 2015 10:38:32 +0100 Subject: [PATCH 05/30] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 1011b9d8..32153b1c 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.notnoop.apns apns - 1.0.0.Beta5 + 1.0.0.Beta6-SNAPSHOT jar Java Apple Push Notification Service Library @@ -17,7 +17,7 @@ scm:git:git://github.com/notnoop/java-apns.git scm:git:git@github.com:notnoop/java-apns.git http://github.com/notnoop/java-apns - apns-1.0.0.Beta5 + HEAD From 31c657cd6a76b713ee05bdc60b8fe4eca639caeb Mon Sep 17 00:00:00 2001 From: Per Holmberg Date: Thu, 22 Jan 2015 14:43:07 +0100 Subject: [PATCH 06/30] Added provided scope for findbugs --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 32153b1c..ed78653a 100644 --- a/pom.xml +++ b/pom.xml @@ -245,6 +245,7 @@ com.google.code.findbugs annotations 2.0.3 + provided From 370d4b8c6eb9b3b88171d90e4e10b6bac45e60b2 Mon Sep 17 00:00:00 2001 From: Justin McCartney Date: Mon, 26 Jan 2015 13:56:26 +0000 Subject: [PATCH 07/30] Ensure given a resend of a message a client of the APNS library is able to tell that this is a resend when its delegate has messageSendFailed called. --- .../apns/internal/ApnsConnectionImpl.java | 6 +++--- .../exceptions/NetworkIOException.java | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java index 4062b3cb..65dd672f 100644 --- a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java +++ b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java @@ -254,7 +254,7 @@ private boolean readPacket(final InputStream in, final byte[] bytes) throws IOEx t.start(); } - private synchronized Socket getOrCreateSocket() throws NetworkIOException { + private synchronized Socket getOrCreateSocket(boolean resend) throws NetworkIOException { if (reconnectPolicy.shouldReconnect()) { logger.debug("Reconnecting due to reconnectPolicy dictating it"); Utilities.close(socket); @@ -297,7 +297,7 @@ private synchronized Socket getOrCreateSocket() throws NetworkIOException { logger.debug("Made a new connection to APNS"); } catch (IOException e) { logger.error("Couldn't connect to APNS server", e); - throw new NetworkIOException(e); + throw new NetworkIOException(e, resend); } } return socket; @@ -318,7 +318,7 @@ private synchronized void sendMessage(ApnsNotification m, boolean fromBuffer) th while (true) { try { attempts++; - Socket socket = getOrCreateSocket(); + Socket socket = getOrCreateSocket(fromBuffer); socket.getOutputStream().write(m.marshall()); socket.getOutputStream().flush(); cacheNotification(m); diff --git a/src/main/java/com/notnoop/exceptions/NetworkIOException.java b/src/main/java/com/notnoop/exceptions/NetworkIOException.java index 73d47e87..03ef348b 100644 --- a/src/main/java/com/notnoop/exceptions/NetworkIOException.java +++ b/src/main/java/com/notnoop/exceptions/NetworkIOException.java @@ -41,9 +41,30 @@ public class NetworkIOException extends ApnsException { private static final long serialVersionUID = 3353516625486306533L; + private boolean resend; + public NetworkIOException() { super(); } public NetworkIOException(String message) { super(message); } public NetworkIOException(IOException cause) { super(cause); } public NetworkIOException(String m, IOException c) { super(m, c); } + public NetworkIOException(IOException cause, boolean resend) { + super(cause); + this.resend = resend; + } + + /** + * Identifies whether an exception was thrown during a resend of a + * message or not. In this case a resend refers to whether the + * message is being resent from the buffer of messages internal. + * This would occur if we sent 5 messages quickly to APNS: + * 1,2,3,4,5 and the 3 message was rejected. We would + * then need to resend 4 and 5. If a network exception was + * triggered when doing this, then the resend flag will be + * {@code true}. + * @return {@code true} for an exception trigger during a resend, otherwise {@code false}. + */ + public boolean isResend() { + return resend; + } } From 921139fb8d06e85e2a50157a3c108b28308874a5 Mon Sep 17 00:00:00 2001 From: Matthias Wessendorf Date: Tue, 3 Feb 2015 11:12:08 +0100 Subject: [PATCH 08/30] Adding support for localized title keys, new to iOS 8.2 --- .../java/com/notnoop/apns/PayloadBuilder.java | 36 ++++++++++++++++++- .../com/notnoop/apns/PayloadBuilderTest.java | 12 +++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/notnoop/apns/PayloadBuilder.java b/src/main/java/com/notnoop/apns/PayloadBuilder.java index 653d40fe..a5869c38 100644 --- a/src/main/java/com/notnoop/apns/PayloadBuilder.java +++ b/src/main/java/com/notnoop/apns/PayloadBuilder.java @@ -71,7 +71,9 @@ public PayloadBuilder alertBody(final String alert) { /** * Sets the alert title text, the text the appears to the user, - * to the passed value + * to the passed value. + * + * Used on iOS 8.2, iWatch and also Safari * * @param title the text to appear to the user * @return this @@ -81,6 +83,38 @@ public PayloadBuilder alertTitle(final String title) { return this; } + /** + * The key to a title string in the Localizable.strings file for the current localization. + * + * @param key the localizable message title key + * @return this + */ + public PayloadBuilder localizedTitleKey(final String key) { + customAlert.put("title-loc-key", key); + return this; + } + + /** + * Sets the arguments for the localizable title key. + * + * @param arguments the arguments to the localized alert message + * @return this + */ + public PayloadBuilder localizedTitleArguments(final Collection arguments) { + customAlert.put("title-loc-args", arguments); + return this; + } + + /** + * Sets the arguments for the localizable title key. + * + * @param arguments the arguments to the localized alert message + * @return this + */ + public PayloadBuilder localizedTitleArguments(final String... arguments) { + return localizedTitleArguments(Arrays.asList(arguments)); + } + /** * Sets the alert action text * diff --git a/src/test/java/com/notnoop/apns/PayloadBuilderTest.java b/src/test/java/com/notnoop/apns/PayloadBuilderTest.java index 4ea8190f..10f332ad 100644 --- a/src/test/java/com/notnoop/apns/PayloadBuilderTest.java +++ b/src/test/java/com/notnoop/apns/PayloadBuilderTest.java @@ -77,6 +77,18 @@ public void testIncludeBadge() { assertEqualsJson(expected, badgeNo); } + @Test + public void localizedTitleKeyAndArguments() { + final PayloadBuilder builder = new PayloadBuilder() + .localizedTitleKey("GAME_PLAY_REQUEST_FORMAT") + .localizedTitleArguments("Jenna", "Frank"); + builder.sound("chime"); + + final String expected = "{\"aps\":{\"sound\":\"chime\",\"alert\":{\"title-loc-key\":\"GAME_PLAY_REQUEST_FORMAT\",\"title-loc-args\":[\"Jenna\",\"Frank\"]}}}"; + final String actual = builder.toString(); + assertEqualsJson(expected, actual); + } + @Test public void localizedOneWithArray() { final PayloadBuilder builder = new PayloadBuilder() From af4f0b4aec44db04dfabd2d1bb35c427ae693cff Mon Sep 17 00:00:00 2001 From: Justin McCartney Date: Tue, 3 Feb 2015 15:12:29 +0000 Subject: [PATCH 09/30] Ensure that if a NetworkIO exception occurs when draining the buffer, we let the client know. Also provided a version of the ApnsSimulator that allows verification and setup as per a mocking framework. --- .../apns/internal/ApnsConnectionImpl.java | 11 +- .../integration/ApnsConnectionResendTest.java | 111 ++++++++++++ .../integration/ApnsDelegateRecorder.java | 93 ++++++++++ .../notnoop/apns/utils/Simulator/Action.java | 5 + .../Simulator/ApnsNotificationWithAction.java | 38 ++++ .../apns/utils/Simulator/ApnsResponse.java | 50 +++++ .../ApnsSimulatorWithVerification.java | 171 ++++++++++++++++++ 7 files changed, 478 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/notnoop/apns/integration/ApnsConnectionResendTest.java create mode 100644 src/test/java/com/notnoop/apns/integration/ApnsDelegateRecorder.java create mode 100644 src/test/java/com/notnoop/apns/utils/Simulator/Action.java create mode 100644 src/test/java/com/notnoop/apns/utils/Simulator/ApnsNotificationWithAction.java create mode 100644 src/test/java/com/notnoop/apns/utils/Simulator/ApnsResponse.java create mode 100644 src/test/java/com/notnoop/apns/utils/Simulator/ApnsSimulatorWithVerification.java diff --git a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java index 65dd672f..efbe25ac 100644 --- a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java +++ b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java @@ -297,6 +297,7 @@ private synchronized Socket getOrCreateSocket(boolean resend) throws NetworkIOEx logger.debug("Made a new connection to APNS"); } catch (IOException e) { logger.error("Couldn't connect to APNS server", e); + // indicate to clients whether this is a resend or initial send throw new NetworkIOException(e, resend); } } @@ -352,7 +353,15 @@ private synchronized void sendMessage(ApnsNotification m, boolean fromBuffer) th private synchronized void drainBuffer() { logger.debug("draining buffer"); while (!notificationsBuffer.isEmpty()) { - sendMessage(notificationsBuffer.poll(), true); + final ApnsNotification notification = notificationsBuffer.poll(); + try { + sendMessage(notification, true); + } + catch (NetworkIOException ex) { + // at this point we are retrying the submission of messages but failing to connect to APNS, therefore + // notify the client of this + delegate.messageSendFailed(notification, ex); + } } } diff --git a/src/test/java/com/notnoop/apns/integration/ApnsConnectionResendTest.java b/src/test/java/com/notnoop/apns/integration/ApnsConnectionResendTest.java new file mode 100644 index 00000000..8703146d --- /dev/null +++ b/src/test/java/com/notnoop/apns/integration/ApnsConnectionResendTest.java @@ -0,0 +1,111 @@ +package com.notnoop.apns.integration; + +import com.notnoop.apns.APNS; +import com.notnoop.apns.ApnsDelegate; +import com.notnoop.apns.ApnsNotification; +import com.notnoop.apns.ApnsService; +import com.notnoop.apns.DeliveryError; +import com.notnoop.apns.EnhancedApnsNotification; +import com.notnoop.apns.integration.ApnsDelegateRecorder.MessageSentFailedRecord; +import com.notnoop.apns.utils.FixedCertificates; +import com.notnoop.apns.utils.Simulator.ApnsResponse; +import com.notnoop.apns.utils.Simulator.ApnsSimulatorWithVerification; +import com.notnoop.exceptions.ApnsDeliveryErrorException; +import com.notnoop.exceptions.NetworkIOException; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +import static com.notnoop.apns.utils.FixedCertificates.LOCALHOST; +import static com.notnoop.apns.utils.FixedCertificates.clientContext; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class ApnsConnectionResendTest { + + private static EnhancedApnsNotification NOTIFICATION_0 = buildNotification(0); + private static EnhancedApnsNotification NOTIFICATION_1 = buildNotification(1); + private static EnhancedApnsNotification NOTIFICATION_2 = buildNotification(2); + private static ApnsSimulatorWithVerification apnsSim; + + private ApnsDelegateRecorder delegateRecorder; + private ApnsService testee; + + @Before + public void setUp() { + if (apnsSim == null) { + apnsSim = new ApnsSimulatorWithVerification(FixedCertificates.serverContext().getServerSocketFactory()); + apnsSim.start(); + } + apnsSim.reset(); + delegateRecorder = new ApnsDelegateRecorder(); + testee = build(delegateRecorder); + } + + @AfterClass + public static void tearDownClass() { + if (apnsSim != null) { + apnsSim.stop(); + apnsSim = null; + } + } + + /* + * Test when we submit 3 messages to APNS 0, 1, 2. 0 is an error but we don't see the error response back until + * 1,2 have already been submitted. Then at this point the network connection to APNS cannot be made, so that + * when retrying the submissions we have to notify the client that delivery failed for 1 and 2. + */ + @Test + public void testGivenFailedSubmissionDueToErrorThenApnsDownWithNotificationsInBufferEnsureClientNotified() + throws Exception { + + final DeliveryError deliveryError = DeliveryError.INVALID_PAYLOAD_SIZE; + + apnsSim.when(NOTIFICATION_0).thenDoNothing(); + apnsSim.when(NOTIFICATION_1).thenDoNothing(); + apnsSim.when(NOTIFICATION_2).thenRespond(ApnsResponse.returnErrorAndShutdown(deliveryError, NOTIFICATION_0)); + + testee.push(NOTIFICATION_0); + testee.push(NOTIFICATION_1); + testee.push(NOTIFICATION_2); + + // Give some time for connection failure to take place + Thread.sleep(5000); + // Verify received expected notifications + apnsSim.verify(); + + // verify delegate calls + assertEquals(3, delegateRecorder.getSent().size()); + final List failed = delegateRecorder.getFailed(); + assertEquals(3, failed.size()); + // first is failed delivery due to payload size + failed.get(0).assertRecord(NOTIFICATION_0, new ApnsDeliveryErrorException(deliveryError)); + // second and third are due to not being able to connect to APNS + assertNetworkIoExForRedelivery(NOTIFICATION_1, failed.get(1)); + assertNetworkIoExForRedelivery(NOTIFICATION_2, failed.get(2)); + } + + private void assertNetworkIoExForRedelivery(ApnsNotification notification, MessageSentFailedRecord failed) { + failed.assertRecord(notification, new NetworkIOException()); + final NetworkIOException found = failed.getException(); + assertTrue(found.isResend()); + } + + + private ApnsService build(ApnsDelegate delegate) { + return APNS.newService() + .withConnectTimeout(1000) + .withSSLContext(clientContext()) + .withGatewayDestination(LOCALHOST, apnsSim.getEffectiveGatewayPort()) + .withFeedbackDestination(LOCALHOST, apnsSim.getEffectiveFeedbackPort()) + .withDelegate(delegate).build(); + } + + private static EnhancedApnsNotification buildNotification(int id) { + final String deviceToken = ApnsSimulatorWithVerification.deviceTokenForId(id); + return new EnhancedApnsNotification(id, 1, deviceToken, "{\"aps\":{}}"); + } + +} diff --git a/src/test/java/com/notnoop/apns/integration/ApnsDelegateRecorder.java b/src/test/java/com/notnoop/apns/integration/ApnsDelegateRecorder.java new file mode 100644 index 00000000..9ba1ee24 --- /dev/null +++ b/src/test/java/com/notnoop/apns/integration/ApnsDelegateRecorder.java @@ -0,0 +1,93 @@ +package com.notnoop.apns.integration; + +import com.notnoop.apns.ApnsDelegate; +import com.notnoop.apns.ApnsNotification; +import com.notnoop.apns.DeliveryError; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class ApnsDelegateRecorder implements ApnsDelegate { + + private List sent = new ArrayList(); + private List failed = new ArrayList(); + + @Override + public void messageSent(ApnsNotification message, boolean resent) { + sent.add(new MessageSentRecord(message, resent)); + } + + @Override + public void messageSendFailed(ApnsNotification message, Throwable e) { + failed.add(new MessageSentFailedRecord(message, e)); + } + + @Override + public void connectionClosed(DeliveryError e, int messageIdentifier) { + // not stubbed + } + + @Override + public void cacheLengthExceeded(int newCacheLength) { + // not stubbed + } + + @Override + public void notificationsResent(int resendCount) { + // not stubbed + } + + public List getSent() { + return Collections.unmodifiableList(sent); + } + + public List getFailed() { + return Collections.unmodifiableList(failed); + } + + public static class MessageSentRecord { + private final ApnsNotification notification; + private final boolean resent; + + public MessageSentRecord(ApnsNotification notification, boolean resent) { + this.notification = notification; + this.resent = resent; + } + + public ApnsNotification getNotification() { + return notification; + } + + public boolean isResent() { + return resent; + } + } + + public static class MessageSentFailedRecord { + private final ApnsNotification notification; + private final Throwable ex; + + public MessageSentFailedRecord(ApnsNotification notification, Throwable ex) { + this.notification = notification; + this.ex = ex; + } + + public ApnsNotification getNotification() { + return notification; + } + + @SuppressWarnings("unchecked") + public T getException() { + return (T) ex; + } + + public void assertRecord(ApnsNotification notification, Throwable ex) { + assertEquals(notification, getNotification()); + assertEquals(ex.getClass(), this.ex.getClass()); + } + } + +} diff --git a/src/test/java/com/notnoop/apns/utils/Simulator/Action.java b/src/test/java/com/notnoop/apns/utils/Simulator/Action.java new file mode 100644 index 00000000..cc0f5588 --- /dev/null +++ b/src/test/java/com/notnoop/apns/utils/Simulator/Action.java @@ -0,0 +1,5 @@ +package com.notnoop.apns.utils.Simulator; + +public enum Action { + DO_NOTHING, RETURN_ERROR, RETURN_ERROR_AND_SHUTDOWN +} diff --git a/src/test/java/com/notnoop/apns/utils/Simulator/ApnsNotificationWithAction.java b/src/test/java/com/notnoop/apns/utils/Simulator/ApnsNotificationWithAction.java new file mode 100644 index 00000000..d0725b80 --- /dev/null +++ b/src/test/java/com/notnoop/apns/utils/Simulator/ApnsNotificationWithAction.java @@ -0,0 +1,38 @@ +package com.notnoop.apns.utils.Simulator; + +import com.notnoop.apns.utils.Simulator.ApnsServerSimulator.Notification; + +public class ApnsNotificationWithAction { + private final Notification notification; + private final ApnsResponse response; + + public ApnsNotificationWithAction(Notification notification) { + this(notification, ApnsResponse.doNothing()); + } + + public ApnsNotificationWithAction(Notification notification, ApnsResponse response) { + if (notification == null) + { + throw new NullPointerException("notification cannot be null"); + } + this.notification = notification; + if (response == null) + { + throw new NullPointerException("response cannot be null"); + } + this.response = response; + } + + public Notification getNotification() { + return notification; + } + + public int getId() { + return notification.getIdentifier(); + } + + public ApnsResponse getResponse() { + return response; + } + +} diff --git a/src/test/java/com/notnoop/apns/utils/Simulator/ApnsResponse.java b/src/test/java/com/notnoop/apns/utils/Simulator/ApnsResponse.java new file mode 100644 index 00000000..9d569c12 --- /dev/null +++ b/src/test/java/com/notnoop/apns/utils/Simulator/ApnsResponse.java @@ -0,0 +1,50 @@ +package com.notnoop.apns.utils.Simulator; + +import com.notnoop.apns.ApnsNotification; +import com.notnoop.apns.DeliveryError; + +public class ApnsResponse { + + private final Action action; + private final DeliveryError error; + private final int errorId; + + private ApnsResponse(Action action, DeliveryError error, int errorId) { + this.action = action; + this.error = error; + this.errorId = errorId; + } + + public boolean isDoNothing() { + return action == Action.DO_NOTHING; + } + + public Action getAction() { + return action; + } + + public DeliveryError getError() { + return error; + } + + public int getErrorId() { + return errorId; + } + + public static ApnsResponse doNothing() { + return new ApnsResponse(Action.DO_NOTHING, null, 0); + } + + public static ApnsResponse returnError(DeliveryError error, int errorId) { + return new ApnsResponse(Action.RETURN_ERROR, error, errorId); + } + + public static ApnsResponse returnErrorAndShutdown(DeliveryError error, int errorId) { + return new ApnsResponse(Action.RETURN_ERROR_AND_SHUTDOWN, error, errorId); + } + + public static ApnsResponse returnErrorAndShutdown(DeliveryError error, ApnsNotification notification) { + return new ApnsResponse(Action.RETURN_ERROR_AND_SHUTDOWN, error, notification.getIdentifier()); + } + +} diff --git a/src/test/java/com/notnoop/apns/utils/Simulator/ApnsSimulatorWithVerification.java b/src/test/java/com/notnoop/apns/utils/Simulator/ApnsSimulatorWithVerification.java new file mode 100644 index 00000000..bb5aa88f --- /dev/null +++ b/src/test/java/com/notnoop/apns/utils/Simulator/ApnsSimulatorWithVerification.java @@ -0,0 +1,171 @@ +package com.notnoop.apns.utils.Simulator; + +import com.google.common.base.Strings; +import com.notnoop.apns.EnhancedApnsNotification; +import com.notnoop.apns.internal.Utilities; + +import javax.net.ServerSocketFactory; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; + +/** + * Provides a simulator that receives connection over TCP as per a real APNS server. This class allows verification + * and prior configuration of responses in a manner similar to a mocking framework. + */ +public class ApnsSimulatorWithVerification extends ApnsServerSimulator { + + private List receivedNotifications; + private Queue expectedWithResponses; + private List unexpected; + + public ApnsSimulatorWithVerification(ServerSocketFactory sslFactory) { + super(sslFactory); + receivedNotifications = new ArrayList(); + expectedWithResponses = new ConcurrentLinkedQueue(); + unexpected = new ArrayList(); + } + + public void reset() { + receivedNotifications.clear(); + expectedWithResponses.clear(); + unexpected.clear(); + } + + public List getReceivedNotifications() { + return Collections.unmodifiableList(receivedNotifications); + } + + private void addExpected(ApnsNotificationWithAction notificationWithAction) { + expectedWithResponses.add(notificationWithAction); + } + + protected void onNotification(final Notification notification, final InputOutputSocket inOutSocket) + throws IOException { + receivedNotifications.add(notification); + pollExpectedResponses(notification, inOutSocket); + } + + protected void pollExpectedResponses(Notification notification, InputOutputSocket inOutSocket) throws IOException { + final ApnsNotificationWithAction withAction = expectedWithResponses.poll(); + if (withAction == null) { + unexpected.add(notification); + } else if (!matchNotificationWithExpected(withAction, notification)) { + unexpected.add(notification); + } else { + handleNotificationWithAction(withAction, inOutSocket); + } + } + + protected void handleNotificationWithAction(ApnsNotificationWithAction notificationWithAction, + InputOutputSocket inOutSocket) throws IOException { + final ApnsResponse response = notificationWithAction.getResponse(); + if (!response.isDoNothing()) { + if (response.getAction() == Action.RETURN_ERROR_AND_SHUTDOWN) { + // have to stop first before sending out the error + stop(); + sendError(response, inOutSocket); + } else { + sendError(response, inOutSocket); + } + } + } + + private boolean matchNotificationWithExpected(ApnsNotificationWithAction withAction, Notification found) { + if (withAction.getId() != found.getIdentifier()) { + return false; + } + return matchDeviceToken(withAction.getNotification().getDeviceToken(), found.getDeviceToken()); + } + + private boolean matchDeviceToken(byte[] expected, byte[] found) { + return Arrays.equals(expected, found); + } + + protected void sendError(ApnsResponse response, InputOutputSocket inOutSocket) throws IOException { + final byte status = (byte) response.getError().code(); + fail(status, response.getErrorId(), inOutSocket); + } + + public DoResponse when(Notification notification) { + return new DoResponse(notification); + } + + public DoResponse when(EnhancedApnsNotification notification) { + return new DoResponse(buildNotification(notification)); + } + + public void verify() { + final int size = expectedWithResponses.size(); + if (size > 0) { + final String error = String.format("[%d] Expected notification(s) were not received, first id was: [%d] ", + size, expectedWithResponses.poll().getId()); + throw new IllegalStateException(error); + } + verifyUnexpected(); + } + + public void verifyAndWait(int waitSecs) { + verifyUnexpected(); + + long timeRemaining = TimeUnit.SECONDS.toMillis(waitSecs); + final long sleepForMs = 250; + while (!expectedWithResponses.isEmpty() && timeRemaining > 0) { + sleep(sleepForMs < timeRemaining ? sleepForMs : timeRemaining); + timeRemaining -= sleepForMs; + } + verify(); + } + + private void verifyUnexpected() { + if (!unexpected.isEmpty()) { + final Notification firstUnexpected = this.unexpected.get(0); + throw new IllegalStateException(String.format("Unexpected notifications received, count: [%d]. First" + + " notification is for id: [%d], deviceToken: [%s]", unexpected.size(), + firstUnexpected.getIdentifier(), Utilities.encodeHex(firstUnexpected.getDeviceToken()))); + } + } + + private void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + public Notification buildNotification(EnhancedApnsNotification notification) { + return new Notification(1, notification.getIdentifier(), notification.getExpiry(), + notification.getDeviceToken(), notification.getPayload()); + } + + public class DoResponse { + private final Notification expected; + + public DoResponse(Notification notification) { + this.expected = notification; + } + + public ApnsSimulatorWithVerification thenRespond(ApnsResponse response) { + addExpected(new ApnsNotificationWithAction(expected, response)); + return ApnsSimulatorWithVerification.this; + } + + public ApnsSimulatorWithVerification thenDoNothing() { + addExpected(new ApnsNotificationWithAction(expected, ApnsResponse.doNothing())); + return ApnsSimulatorWithVerification.this; + } + } + + public static String deviceTokenForId(int id) + { + final String right = Integer.toHexString(id).toUpperCase(); + final int zeroedLength = 64 - right.length(); + return Strings.repeat("0", zeroedLength) + right; + } +} From 369cb506b02427bc5e635059b15a6afa69e1c712 Mon Sep 17 00:00:00 2001 From: Justin McCartney Date: Wed, 4 Feb 2015 12:07:37 +0000 Subject: [PATCH 10/30] Fixed a problem with the simulator not writing out the correct bytes for feedback connections. --- .../com/notnoop/apns/utils/Simulator/ApnsServerSimulator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/notnoop/apns/utils/Simulator/ApnsServerSimulator.java b/src/test/java/com/notnoop/apns/utils/Simulator/ApnsServerSimulator.java index e4776ea1..1fd1187f 100644 --- a/src/test/java/com/notnoop/apns/utils/Simulator/ApnsServerSimulator.java +++ b/src/test/java/com/notnoop/apns/utils/Simulator/ApnsServerSimulator.java @@ -323,8 +323,8 @@ private void writeFeedback(final InputOutputSocket inputOutputSocket, final byte ByteArrayOutputStream os = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(os); final int unixTime = (int) (new Date().getTime() / 1000); - dos.write(unixTime); - dos.write((short) token.length); + dos.writeInt(unixTime); + dos.writeShort((short) token.length); dos.write(token); dos.close(); inputOutputSocket.syncWrite(os.toByteArray()); From fd068b2e9def971001ea5902efd4b2596050bf3a Mon Sep 17 00:00:00 2001 From: Matthias Wessendorf Date: Fri, 6 Feb 2015 10:25:13 +0100 Subject: [PATCH 11/30] [maven-release-plugin] prepare release apns-1.0.0.Beta6 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ed78653a..eba75029 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.notnoop.apns apns - 1.0.0.Beta6-SNAPSHOT + 1.0.0.Beta6 jar Java Apple Push Notification Service Library @@ -17,7 +17,7 @@ scm:git:git://github.com/notnoop/java-apns.git scm:git:git@github.com:notnoop/java-apns.git http://github.com/notnoop/java-apns - HEAD + apns-1.0.0.Beta6 From f6f02ea2764e65747a4cd135ef87f0105d96ec0b Mon Sep 17 00:00:00 2001 From: Matthias Wessendorf Date: Fri, 6 Feb 2015 10:25:16 +0100 Subject: [PATCH 12/30] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index eba75029..123fbe9a 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.notnoop.apns apns - 1.0.0.Beta6 + 1.0.0.Beta7-SNAPSHOT jar Java Apple Push Notification Service Library @@ -17,7 +17,7 @@ scm:git:git://github.com/notnoop/java-apns.git scm:git:git@github.com:notnoop/java-apns.git http://github.com/notnoop/java-apns - apns-1.0.0.Beta6 + HEAD From 720e56852af4e33e591b1297bcdabf54a046179a Mon Sep 17 00:00:00 2001 From: Matthias Wessendorf Date: Fri, 6 Feb 2015 14:01:14 +0100 Subject: [PATCH 13/30] Update README.markdown --- README.markdown | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 80ad6f28..a4b7b87e 100644 --- a/README.markdown +++ b/README.markdown @@ -44,8 +44,20 @@ Features: * Supports connection pooling * Supports re-transmission of Notifications after error +Getting started +--------------- + +Add the following dependencies to your `pom.xml` file: + + + + com.notnoop.apns + apns + 1.0.0.Beta6 + + Sample Code ----------------- +----------- To send a notification, you can do it in two steps: From 6f92ea04b0aad2f54f99263f1d763d9ec5a54eb3 Mon Sep 17 00:00:00 2001 From: GabeK Date: Fri, 6 Feb 2015 15:33:58 +0100 Subject: [PATCH 14/30] use HTTP status instead of string comparison --- src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java b/src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java index 36da7fea..6c24da83 100644 --- a/src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java +++ b/src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java @@ -100,7 +100,7 @@ Socket makeTunnel(String host, int port, String proxyUsername, if (socket == null) { ConnectMethod method = response.getConnectMethod(); // Read the proxy's HTTP response. - if(method.getStatusLine().toString().matches("HTTP/1\\.\\d 407 Proxy Authentication Required")) { + if(method.getStatusLine().getStatusCode() == 407) { // Proxy server returned 407. We will now try to connect with auth Header if(proxyUsername != null && proxyPassword != null) { socket = AuthenticateProxy(method, client,proxyHost, proxyAddress.getPort(), From edb26e243b774adacef87c16e30f1b30c926b439 Mon Sep 17 00:00:00 2001 From: ozgunawesome Date: Tue, 31 Mar 2015 16:02:59 +0200 Subject: [PATCH 15/30] Update README.markdown It says 'only two steps' and then comes three steps, that's very autistic. --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index a4b7b87e..55b3519f 100644 --- a/README.markdown +++ b/README.markdown @@ -59,7 +59,7 @@ Add the following dependencies to your `pom.xml` file: Sample Code ----------- -To send a notification, you can do it in two steps: +To send a notification, you can do it in three steps: 1. Setup the connection From 6892254209fd5ceec47b51e2635becc94e801dfa Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Tue, 4 Aug 2015 18:14:18 -0700 Subject: [PATCH 16/30] Fixes #114 - BatchApnsService should allow ScheduledExecutorService to be passed into constructor --- .../com/notnoop/apns/ApnsServiceBuilder.java | 37 ++++++++++++++++--- .../apns/internal/BatchApnsService.java | 15 ++++++-- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java b/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java index f0188925..9280f6a2 100644 --- a/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java +++ b/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java @@ -30,6 +30,7 @@ */ package com.notnoop.apns; +import static java.util.concurrent.Executors.defaultThreadFactory; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; @@ -40,6 +41,8 @@ import java.security.KeyStore; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import javax.net.ssl.SSLContext; @@ -94,7 +97,7 @@ public class ApnsServiceBuilder { private boolean isBatched = false; private int batchWaitTimeInSec; private int batchMaxWaitTimeInSec; - private ThreadFactory batchThreadFactory = null; + private ScheduledExecutorService batchThreadPoolExecutor = null; private ApnsDelegate delegate = ApnsDelegate.EMPTY; private Proxy proxy = null; @@ -529,7 +532,7 @@ public ApnsServiceBuilder asBatched() { * maximum wait time for batch before executing */ public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec) { - return asBatched(waitTimeInSec, maxWaitTimeInSec, null); + return asBatched(waitTimeInSec, maxWaitTimeInSec, (ThreadFactory)null); } /** @@ -552,14 +555,36 @@ public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec) { * thread factory to use for batch processing */ public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec, ThreadFactory threadFactory) { + return asBatched(waitTimeInSec, maxWaitTimeInSec, new ScheduledThreadPoolExecutor(1, threadFactory != null ? threadFactory : defaultThreadFactory())); + } + + /** + * Construct service which will process notification requests in batch. + * After each request batch will wait waitTimeInSec for more request to come + * before executing but not more than maxWaitTimeInSec + * + * Each batch creates new connection and close it after finished. + * In case reconnect policy is specified it will be applied by batch processing. + * E.g.: {@link ReconnectPolicy.Provided#EVERY_HALF_HOUR} will reconnect the connection in case batch is running for more than half an hour + * + * Note: It is not recommended to use pooled connection + * + * @param waitTimeInSec + * time to wait for more notification request before executing + * batch + * @param maxWaitTimeInSec + * maximum wait time for batch before executing + * @param batchThreadPoolExecutor + * executor for batched processing (may be null) + */ + public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec, ScheduledExecutorService batchThreadPoolExecutor) { this.isBatched = true; this.batchWaitTimeInSec = waitTimeInSec; this.batchMaxWaitTimeInSec = maxWaitTimeInSec; - this.batchThreadFactory = threadFactory; + this.batchThreadPoolExecutor = batchThreadPoolExecutor; return this; } - - + /** * Sets the delegate of the service, that gets notified of the * status of message delivery. @@ -629,7 +654,7 @@ public ApnsService build() { } if (isBatched) { - service = new BatchApnsService(conn, feedback, batchWaitTimeInSec, batchMaxWaitTimeInSec, batchThreadFactory); + service = new BatchApnsService(conn, feedback, batchWaitTimeInSec, batchMaxWaitTimeInSec, batchThreadPoolExecutor); } service.start(); diff --git a/src/main/java/com/notnoop/apns/internal/BatchApnsService.java b/src/main/java/com/notnoop/apns/internal/BatchApnsService.java index cd38855f..e8c31c00 100644 --- a/src/main/java/com/notnoop/apns/internal/BatchApnsService.java +++ b/src/main/java/com/notnoop/apns/internal/BatchApnsService.java @@ -1,8 +1,9 @@ package com.notnoop.apns.internal; +import static java.util.concurrent.Executors.defaultThreadFactory; + import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -40,15 +41,21 @@ public class BatchApnsService extends AbstractApnsService { private ScheduledExecutorService scheduleService; private ScheduledFuture taskFuture; - + private Runnable batchRunner = new SendMessagesBatch(); - public BatchApnsService(ApnsConnection prototype, ApnsFeedbackConnection feedback, int batchWaitTimeInSec, int maxBachWaitTimeInSec, ThreadFactory tf) { + public BatchApnsService(ApnsConnection prototype, ApnsFeedbackConnection feedback, int batchWaitTimeInSec, int maxBachWaitTimeInSec, ThreadFactory tf) { + this(prototype, feedback, batchWaitTimeInSec, maxBachWaitTimeInSec, + new ScheduledThreadPoolExecutor(1, + tf != null ? tf : defaultThreadFactory())); + } + + public BatchApnsService(ApnsConnection prototype, ApnsFeedbackConnection feedback, int batchWaitTimeInSec, int maxBachWaitTimeInSec, ScheduledExecutorService executor) { super(feedback); this.prototype = prototype; this.batchWaitTimeInSec = batchWaitTimeInSec; this.maxBatchWaitTimeInSec = maxBachWaitTimeInSec; - this.scheduleService = new ScheduledThreadPoolExecutor(1, tf == null ? Executors.defaultThreadFactory() : tf); + this.scheduleService = executor != null ? executor : new ScheduledThreadPoolExecutor(1, defaultThreadFactory()); } public void start() { From fc66d1e37763fc666703bd0c9a8e94dadc574eaa Mon Sep 17 00:00:00 2001 From: Hugo Trippaers Date: Fri, 20 Nov 2015 16:28:30 +0100 Subject: [PATCH 17/30] Use a builder pattern for the SSLContext --- .../com/notnoop/apns/ApnsServiceBuilder.java | 15 ++-- .../apns/internal/SSLContextBuilder.java | 79 +++++++++++++++++++ .../com/notnoop/apns/internal/Utilities.java | 39 --------- .../notnoop/apns/ApnsServerSocketBuilder.java | 15 +++- .../notnoop/apns/utils/FixedCertificates.java | 22 +++--- 5 files changed, 113 insertions(+), 57 deletions(-) create mode 100644 src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java diff --git a/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java b/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java index 9280f6a2..00ba66c9 100644 --- a/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java +++ b/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java @@ -174,9 +174,11 @@ public ApnsServiceBuilder withCert(String fileName, String password) public ApnsServiceBuilder withCert(InputStream stream, String password) throws InvalidSSLConfig { assertPasswordNotEmpty(password); - return withSSLContext( - newSSLContext(stream, password, - KEYSTORE_TYPE, KEY_ALGORITHM)); + return withSSLContext(new SSLContextBuilder() + .withAlgorithm(KEY_ALGORITHM) + .withCertificateKeyStore(stream, password, KEYSTORE_TYPE) + .withDefaultTrustKeyStore() + .build()); } /** @@ -200,8 +202,11 @@ public ApnsServiceBuilder withCert(InputStream stream, String password) public ApnsServiceBuilder withCert(KeyStore keyStore, String password) throws InvalidSSLConfig { assertPasswordNotEmpty(password); - return withSSLContext( - newSSLContext(keyStore, password, KEY_ALGORITHM)); + return withSSLContext(new SSLContextBuilder() + .withAlgorithm(KEY_ALGORITHM) + .withCertificateKeyStore(keyStore, password) + .withDefaultTrustKeyStore() + .build()); } private void assertPasswordNotEmpty(String password) { diff --git a/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java b/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java new file mode 100644 index 00000000..2bc6d410 --- /dev/null +++ b/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java @@ -0,0 +1,79 @@ +package com.notnoop.apns.internal; + +import com.notnoop.exceptions.InvalidSSLConfig; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; + +public class SSLContextBuilder { + private String algorithm = "sunx509"; + private KeyManagerFactory keyManagerFactory; + private TrustManager[] trustManagers; + + public SSLContextBuilder withAlgorithm(String algorithm) { + this.algorithm = algorithm; + return this; + } + + public SSLContextBuilder withDefaultTrustKeyStore() throws InvalidSSLConfig { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm); + trustManagerFactory.init((KeyStore)null); + trustManagers = trustManagerFactory.getTrustManagers(); + return this; + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } + } + + public SSLContextBuilder withTrustManager(TrustManager trustManager) { + trustManagers = new TrustManager[] { trustManager }; + return this; + } + + public SSLContextBuilder withCertificateKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType) throws InvalidSSLConfig { + try { + final KeyStore ks = KeyStore.getInstance(keyStoreType); + ks.load(keyStoreStream, keyStorePassword.toCharArray()); + return withCertificateKeyStore(ks, keyStorePassword); + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } catch (IOException e) { + throw new InvalidSSLConfig(e); + } + } + + public SSLContextBuilder withCertificateKeyStore(KeyStore keyStore, String keyStorePassword) throws InvalidSSLConfig { + try { + keyManagerFactory = KeyManagerFactory.getInstance(algorithm); + keyManagerFactory.init(keyStore, keyStorePassword.toCharArray()); + return this; + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } + } + + public SSLContext build() throws InvalidSSLConfig { + if (keyManagerFactory == null) { + throw new InvalidSSLConfig("Missing KeyManagerFactory"); + } + + if (trustManagers == null) { + throw new InvalidSSLConfig("Missing TrustManagers"); + } + + try { + final SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagers, null); + return sslContext; + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } + } +} diff --git a/src/main/java/com/notnoop/apns/internal/Utilities.java b/src/main/java/com/notnoop/apns/internal/Utilities.java index 55afc4cd..7a62ab19 100644 --- a/src/main/java/com/notnoop/apns/internal/Utilities.java +++ b/src/main/java/com/notnoop/apns/internal/Utilities.java @@ -47,7 +47,6 @@ import java.util.regex.Pattern; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManagerFactory; import com.notnoop.exceptions.InvalidSSLConfig; import com.notnoop.exceptions.NetworkIOException; @@ -73,44 +72,6 @@ public final class Utilities { private Utilities() { throw new AssertionError("Uninstantiable class"); } - public static SSLSocketFactory newSSLSocketFactory(final InputStream cert, final String password, - final String ksType, final String ksAlgorithm) throws InvalidSSLConfig { - final SSLContext context = newSSLContext(cert, password, ksType, ksAlgorithm); - return context.getSocketFactory(); - } - - public static SSLContext newSSLContext(final InputStream cert, final String password, - final String ksType, final String ksAlgorithm) throws InvalidSSLConfig { - try { - final KeyStore ks = KeyStore.getInstance(ksType); - ks.load(cert, password.toCharArray()); - return newSSLContext(ks, password, ksAlgorithm); - } catch (final Exception e) { - throw new InvalidSSLConfig(e); - } - } - - public static SSLContext newSSLContext(final KeyStore ks, final String password, - final String ksAlgorithm) throws InvalidSSLConfig { - try { - // Get a KeyManager and initialize it - final KeyManagerFactory kmf = KeyManagerFactory.getInstance(ksAlgorithm); - kmf.init(ks, password.toCharArray()); - - // Get a TrustManagerFactory with the DEFAULT KEYSTORE, so we have all - // the certificates in cacerts trusted - final TrustManagerFactory tmf = TrustManagerFactory.getInstance(ksAlgorithm); - tmf.init((KeyStore)null); - - // Get the SSLContext to help create SSLSocketFactory - final SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - return sslContext; - } catch (final GeneralSecurityException e) { - throw new InvalidSSLConfig(e); - } - } - private static final Pattern pattern = Pattern.compile("[ -]"); public static byte[] decodeHex(final String deviceToken) { final String hex = pattern.matcher(deviceToken).replaceAll(""); diff --git a/src/test/java/com/notnoop/apns/ApnsServerSocketBuilder.java b/src/test/java/com/notnoop/apns/ApnsServerSocketBuilder.java index ced5cca1..66c88760 100644 --- a/src/test/java/com/notnoop/apns/ApnsServerSocketBuilder.java +++ b/src/test/java/com/notnoop/apns/ApnsServerSocketBuilder.java @@ -40,6 +40,8 @@ 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; @@ -145,8 +147,11 @@ public ApnsServerSocketBuilder withCert(String fileName, String password) public ApnsServerSocketBuilder withCert(InputStream stream, String password) throws InvalidSSLConfig { assertPasswordNotEmpty(password); - return withSSLContext(newSSLContext(stream, password, KEYSTORE_TYPE, - KEY_ALGORITHM)); + return withSSLContext(new SSLContextBuilder() + .withAlgorithm(KEY_ALGORITHM) + .withCertificateKeyStore(stream, password, KEYSTORE_TYPE) + .withDefaultTrustKeyStore() + .build()); } /** @@ -170,7 +175,11 @@ public ApnsServerSocketBuilder withCert(InputStream stream, String password) public ApnsServerSocketBuilder withCert(KeyStore keyStore, String password) throws InvalidSSLConfig { assertPasswordNotEmpty(password); - return withSSLContext(newSSLContext(keyStore, password, KEY_ALGORITHM)); + return withSSLContext(new SSLContextBuilder() + .withAlgorithm(KEY_ALGORITHM) + .withCertificateKeyStore(keyStore, password) + .withDefaultTrustKeyStore() + .build()); } private void assertPasswordNotEmpty(String password) { diff --git a/src/test/java/com/notnoop/apns/utils/FixedCertificates.java b/src/test/java/com/notnoop/apns/utils/FixedCertificates.java index 9a25bae6..8b215094 100644 --- a/src/test/java/com/notnoop/apns/utils/FixedCertificates.java +++ b/src/test/java/com/notnoop/apns/utils/FixedCertificates.java @@ -1,13 +1,10 @@ package com.notnoop.apns.utils; -import java.io.InputStream; -import java.security.SecureRandom; +import com.notnoop.apns.internal.SSLContextBuilder; import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; - -import com.notnoop.apns.internal.Utilities; +import java.io.InputStream; public class FixedCertificates { @@ -21,10 +18,13 @@ public class FixedCertificates { public static SSLContext serverContext() { try { - //System.setProperty("javax.net.ssl.trustStore", ClassLoader.getSystemResource(CLIENT_STORE).getPath()); InputStream stream = FixedCertificates.class.getResourceAsStream("/" + SERVER_STORE); assert stream != null; - return Utilities.newSSLContext(stream, SERVER_PASSWORD, "PKCS12", "sunx509"); + return new SSLContextBuilder() + .withAlgorithm("sunx509") + .withCertificateKeyStore(stream, SERVER_PASSWORD, "PKCS12") + .withTrustManager(new X509TrustManagerTrustAll()) + .build(); } catch (Exception e) { throw new RuntimeException(e); } @@ -34,9 +34,11 @@ public static SSLContext clientContext() { try { InputStream stream = FixedCertificates.class.getResourceAsStream("/" + CLIENT_STORE); assert stream != null; - SSLContext context = Utilities.newSSLContext(stream, CLIENT_PASSWORD, "PKCS12", "sunx509"); - context.init(null, new TrustManager[] { new X509TrustManagerTrustAll() }, new SecureRandom()); - return context; + return new SSLContextBuilder() + .withAlgorithm("sunx509") + .withCertificateKeyStore(stream, CLIENT_PASSWORD, "PKCS12") + .withTrustManager(new X509TrustManagerTrustAll()) + .build(); } catch (Exception e) { throw new RuntimeException(e); } From 9401bfa2dbefa4c98d37d6bf1afc41568a4bffe4 Mon Sep 17 00:00:00 2001 From: Hugo Trippaers Date: Fri, 20 Nov 2015 16:55:44 +0100 Subject: [PATCH 18/30] The test server should require and validate client certificates Add updated versions of the certifcates as the current ones are expired. --- .../apns/internal/SSLContextBuilder.java | 23 ++++++++++++++++++ .../notnoop/apns/utils/ApnsServerStub.java | 18 ++++++++------ .../notnoop/apns/utils/FixedCertificates.java | 6 ++++- src/test/resources/clientStore.p12 | Bin 2483 -> 2516 bytes src/test/resources/serverStore.p12 | Bin 2483 -> 1764 bytes src/test/resources/serverTrustStore.p12 | Bin 0 -> 1740 bytes 6 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 src/test/resources/serverTrustStore.p12 diff --git a/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java b/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java index 2bc6d410..3fbfa973 100644 --- a/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java +++ b/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java @@ -32,6 +32,29 @@ public SSLContextBuilder withDefaultTrustKeyStore() throws InvalidSSLConfig { } } + public SSLContextBuilder withTrustKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType) throws InvalidSSLConfig { + try { + final KeyStore ks = KeyStore.getInstance(keyStoreType); + ks.load(keyStoreStream, keyStorePassword.toCharArray()); + return withTrustKeyStore(ks, keyStorePassword); + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } catch (IOException e) { + throw new InvalidSSLConfig(e); + } + + } + public SSLContextBuilder withTrustKeyStore(KeyStore keyStore, String keyStorePassword) throws InvalidSSLConfig { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm); + trustManagerFactory.init(keyStore); + trustManagers = trustManagerFactory.getTrustManagers(); + return this; + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } + } + public SSLContextBuilder withTrustManager(TrustManager trustManager) { trustManagers = new TrustManager[] { trustManager }; return this; diff --git a/src/test/java/com/notnoop/apns/utils/ApnsServerStub.java b/src/test/java/com/notnoop/apns/utils/ApnsServerStub.java index bf1dff18..a608e691 100644 --- a/src/test/java/com/notnoop/apns/utils/ApnsServerStub.java +++ b/src/test/java/com/notnoop/apns/utils/ApnsServerStub.java @@ -1,16 +1,16 @@ package com.notnoop.apns.utils; +import javax.net.ServerSocketFactory; +import javax.net.ssl.SSLServerSocket; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.nio.ByteBuffer; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; -import javax.net.ServerSocketFactory; public class ApnsServerStub { @@ -70,8 +70,8 @@ public ApnsServerStub(ServerSocketFactory sslFactory, int gatewayPort, int feedb Thread gatewayThread; Thread feedbackThread; - ServerSocket gatewaySocket; - ServerSocket feedbackSocket; + SSLServerSocket gatewaySocket; + SSLServerSocket feedbackSocket; public void start() { gatewayThread = new GatewayRunner(); @@ -152,10 +152,10 @@ private class GatewayRunner extends Thread { @SuppressWarnings("InfiniteLoopStatement") public void run() { - - + Thread.currentThread().setName("GatewayThread"); try { - gatewaySocket = sslFactory.createServerSocket(gatewayPort); + gatewaySocket = (SSLServerSocket)sslFactory.createServerSocket(gatewayPort); + gatewaySocket.setNeedClientAuth(true); } catch (IOException e) { messages.release(); throw new RuntimeException(e); @@ -215,8 +215,10 @@ public void run() { private class FeedbackRunner extends Thread { public void run() { + Thread.currentThread().setName("FeedbackThread"); try { - feedbackSocket = sslFactory.createServerSocket(feedbackPort); + feedbackSocket = (SSLServerSocket)sslFactory.createServerSocket(feedbackPort); + feedbackSocket.setNeedClientAuth(true); } catch (IOException e) { e.printStackTrace(); messages.release(); diff --git a/src/test/java/com/notnoop/apns/utils/FixedCertificates.java b/src/test/java/com/notnoop/apns/utils/FixedCertificates.java index 8b215094..18b229a5 100644 --- a/src/test/java/com/notnoop/apns/utils/FixedCertificates.java +++ b/src/test/java/com/notnoop/apns/utils/FixedCertificates.java @@ -14,16 +14,20 @@ public class FixedCertificates { public static final String SERVER_STORE = "serverStore.p12"; public static final String SERVER_PASSWORD = "123456"; + public static final String SERVER_TRUST_STORE = "serverTrustStore.p12"; + public static final String SERVER_TRUST_PASSWORD = "123456"; + public static final String LOCALHOST = "localhost"; public static SSLContext serverContext() { try { InputStream stream = FixedCertificates.class.getResourceAsStream("/" + SERVER_STORE); + InputStream trustStream = FixedCertificates.class.getResourceAsStream("/" + SERVER_TRUST_STORE); assert stream != null; return new SSLContextBuilder() .withAlgorithm("sunx509") .withCertificateKeyStore(stream, SERVER_PASSWORD, "PKCS12") - .withTrustManager(new X509TrustManagerTrustAll()) + .withTrustKeyStore(trustStream, SERVER_TRUST_PASSWORD, "PKCS12") .build(); } catch (Exception e) { throw new RuntimeException(e); diff --git a/src/test/resources/clientStore.p12 b/src/test/resources/clientStore.p12 index 28ab70a12d6c8cbfe79a0765b5237f89d48db9d0..ef8243e15a1ccd24b12faaeddd1a030c7bdd0800 100644 GIT binary patch literal 2516 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 OVu40p>Yn-iD*gi}36Jss literal 2483 zcmY+EcRUmhAIERmn=>OL=_s68Mt*0Ubw;?%?0GmuWX4@&q|9IFY#CW+RI+83Ju`AL z4rN4SlgID%dY-4}`Q!WfzFwct>-GKX`+39hG(->(49C+1P|}D!(SCBk0Hgxu;%QtV zcpAqG+ZK)oAN+|ZbMfGB7Zw==1Y9iAp9Bc!rKJ7$1$rPQoDo7@Wi0FHBbV$B0#O4Z z@Zc)nam_3KgEk}Ngr=wjrq0zc-cTv& zD@&tS!~MaXV8R!0@8E%ynG4Hcu{^2lB%N@OazA`L=lN@}6Wy@6z9O+oER;oc_*3U=Ks zkq^asI`xSZ${*YlrFOXfW_ z?q+EhSsM+!5+9y-pAYKJVt0TUF(hQLZrm@~b0~kHYN-x0{t=#LXAC(DTl1L<$_Hl3 zx)9C>Yc|b~{Idh(77KkAiq#xqhJBq@PY;! z`T7iR-c`D|r-io>Cn6(Hbz0E(!_JA-`9_l&sF})fRl^o`P%mAg)O-WY2Wz`kp!%ajz0m`+( zdgxddKC^-_i*ZnL>fzOsN}I;lFe-2H!=Q7b8ggGnAh5&dxag8@eNL7kA-lv~7CP_NRNjPLXq@;N@G4Q65x_fHz$`o3{FyD#W6sr;xd?D&88pj;+^xl?dyQMVG}we)4-``Ptx@Kogyf zN0bGD;JL}i)(xmTmX8GDL^q&}*)lB!eG#hF1nkd8&_u~h(hWB(Vn#NHhc{{x-ck}K zhK~D2bm+~VOZ$690oH@F&FbU^ab}X4ngm0RrllCD3Nxq48Gj*@04q;VZ|Oa=9&n8@ zv$it*{+;P#SS;%@6wU>!4@Rh#armAl5N-)5W15Tw*{T`G)%Sx`>E75wD%{fL8cO5V zX3Ro3>BV-WnFP5)*xRa|N{4Dc_c{Db6`kGtm0l)uaxy@lr2(+v4c5=nIDU0I){^a4 zKfx}T(t5e$2^>#h_df;C#ZwqT@D%zNw$8;MDe3-GcUmAQ7Z3RX$3xcsj{@L-DDbJd z%$Z<=eE$yxAb7~fpLxsSZ+_d0glXSwJq1HsI83$Ly)CF=Jh6+qZB*MVCV^o?d0C@V zp*pMuRShkaSP4~Y51N4)uamBFf4(K`C=@qSWl?~tiC$<;#6D{-6aG*NW50sNT$|g^ zY^tO&zW74SGqY<43b1cS$`L*XKlw8o9x|N|aR&5PoVoe0Pd;7cDiQy55Zb+JWv|4# z01^;pG-#iFv0=Y|qno^1B<$JRNN`BYJAJ%Xb+!m!9u*^~iq;T?xFJRNt|Bt(lp9I( z^__olSAEH(4kq!?>Y6Cgn8$GU>aaG@yGO6tWlq+tNFeb5fvUP2U(Rp3c?kI z-lL}VJKWc^ZOh{`y{CA~_~X#cm_u6OV`v((P)7T4F=Hgv<-U}x=rwW9@_^mVMR3%? zhZ}A*?{@1BQI_)^yyfi2zJyKGvD?J(Y>z#nuUT=40I-Sp5d@}XyEiEENJ_~|*JVZI z^RH1BE_GTdmK|dIB(_G>axkS)uDF%$5~I&Yc6+OLm_70Svu?E|4sPc{N)x&20O?X< zN0+M!soM7ZxN#};jU;EF+&-rL&NVLj`ljgyZXXh2yH;1A7d ziKk>#jfWR&VyZmmB$z`{9ps^CR!FaM>)d^NXmt9NI=Y0Be_CK-i##@Xcwbuu{&10z z3=JlS4pe4x-8rh8a`^6S{!s_Y(CaQqJi&2gB0}vsUm&_$e*8K>5J*Ch6-lyb2sYVK z$IpugNOo4MtR1EI6T3s-Q$^!yC$(nQ&#=rlOlz1Xe#Y)JEguGh(CaCvlDF~h)L9&_FW+dig@kj>hQjm$W9U6Y)%+2r)S6s6t{a_*zN z>pihDy&4HFRh~7HUjVfK;Cyu#N_>pX+nC2ZSpPEOd{ve9*M)imeBf99#OzWW5GWhJm7gXbC~BkSD~{|=%nO{B?uWzSf^Zs2itCJE5Dx_a n0!8ONyee^^uk&($HawBPJI1G2jRQi>*4F!Z%h$gj1O)yIIS7g@ diff --git a/src/test/resources/serverStore.p12 b/src/test/resources/serverStore.p12 index b375de78b57744e2103ec1f822fcc1075f6f7645..808062693b31c8218b87a40c42f39a9131171ebd 100644 GIT binary patch literal 1764 zcmY+Dc{~&d8^`A|L*yLS5Gq*lz?MHKX{hl3wnPpAL8VksBa#ZLMQTA0{og(~ zj)1@e25W_D`z+kiI}!9aKYa623&{oqf&d^?5T(z%Ay;u=@yhnKGoN4f741EEB2R!( zyozuWJt`L~!j|xA1UZ*Cgwf?S0_B(T$zzeQ&p#%4>2%c0S1g=}8oA6(RtfG(R`5YuhtxQ1_X@!cs`yd! z1X!6;o#(Zjhd$TutR+=j89WIqZYaAY)}zU|u}Hl#22@~frVgN`v5agJB5$bTJ&!N# z>v7E?%Fp9O&>PNgjV-qlJm0WhYzzwe!!&p0Pmc?9RZ6C!7-KjtrYqj!iOD-MO54gR;d^U%%S>7`qK$i^L zZOI;zyb6?R844YE{R}!4yRE&>!1m7Lc~QI!-+9^0w##>XHk9g{TC>GRuSU}?1Jsju z!bP>o%vT`~&tqYzQ&C=xb?T|d)#kIF{b+`ks__Nxj{I>1@@;TSH|9)%g|p&MX~X`` zqNU0$v$`PJLOtG**YLQjWVQ41x1S}8RWHzrm%ID16I78K8+|fzW>n*kQ%iCNODxxC zq#!n)HapZ)m3r^5v$sRtr9P}sk zofrU(%@=)4C{pf%K)u}Rvg)ban+mZx<^^!p3>PzN1HPnzK>;mEm71L>7o_grA1(w( zDJdxc!U048`M*j8Tmk3sg3S)B6CfbqprQUrfd2vo`W=+(Vz>?mK~CK(#57!WTV0!cc_sQgC=@Da zGh^`d@%i@(l?z?)61{O>`AmS%6rXl7rlA-k%WkcG-bwHrxzW(!FsHO$n=?q$ruQSd z=j=Fw>%;ZtL}!VW;rrc%wN;Th1iC*ZKE>PRCCvHZX$fdwTKa{u)VlP7 zpKd`KVuxmeR!6il8M48X0*xA+O&#tX$d>xeRY%mvLmFJ#*jh)d(jd9ffO1yc0p=< za3hc|y#E1GL){g>?3K}%95VH(JwwomMc24Fh%`s4B88v=x}snZLI41PS2|WD?cL4q pdbJP$hlWr>=^2t`eH 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 diff --git a/src/test/resources/serverTrustStore.p12 b/src/test/resources/serverTrustStore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..458b8b6347976368995a3151541bca9b090ec363 GIT binary patch literal 1740 zcmY+DdpOhy7su!N!we(Su#xLhI~g<>mNcdr6=P(}dfPFhrCeIMjcX}|pIag1@>VXB zFp3?QOva_cj@+ZQ?opdXOu{o^hRx2tdY|{*efE!Y&hvcF_j~?2A0i0!z@Ts<2!Mc+ zaf(xlOa-a{EdhZD1PFxg$bm!N~R9PN@Ls|D}{sP*@2Ft0#i68X_J* z{a@ZWu7$t>=-S19#|F2!5PU1W?Y{f-!cRAQf?nM~B3#QRQc}P2DC5r6ZhWm*@+$Y;o%zQVqv0nry$y@K-+jL+ zZ?^Y+$!TK^4#%wh$$IEE-X;`haq5vl$2G&4iWx>c>tWYz*}?6!`YU4L+d=hsK^!}w zxiCNS0{K&5!#e-DHZyR5dif72S43Oh`t6icosDZ=Jm2<*_V7~a4Z*SPwQ4+p?|`)a z%@d}ab?9TM#GRY+;_Jm@{+n`qm&yXU_4#Gx8iNEluDEt zW!2cs@4GdS2XqUkeVfm#-iWXSN2POZILcw%4ngQ}Wn zvFbc5q~s{8E3IbeeQ^SFS%UU@uEQ>Ku3eCor+{(IzwXsht)1Qy!|%4)9P(Dt({Rq$ z$z^8x7f?+G0!ve(hP-x*Ejr&KGKP}KQY^4h1*v+n8={V1%W2WqWKEq0po*5sgw@?P%2*+ zhetQsznS@E-tBk{ch9X|)UY={v|-omw_m^0or{@PxG6y7j*1XNt z7pdCvyP=_rDl~(D2WK*FzSbL=a(B4wA6IU~$lFT%Mv}8Tn{cO&!Ea6MUpIFm8vga+ zN;q>3T^u9?5($XTMn-zIf)90Kbe5bURKP9oV5EiJ9BtSl@@J3!d| zWl{i2K)A_gd|*(>&iFg70R0Cbz-K@N-f#D|m`C5MgHoK_)t4-<9M;Et1|%MYEe;;E zPa>9#=H}5e9~${DrJV!PNAP(bmp_*O>`cku& zQy7alarlC)7!xwTE@%sxd z8U^h!WTCbOO8hiejE8AaERUDwbGr9VA}ODw($;`P(?`bhZwkc9Y0}%d7CeR3Cm-ap zSmM%(BSdo5OcFe#GxBT8Ol^~{vZljq_i3_r4n2crH9%jWCKmvyNwU;FZQv1;bcA5y z*qJc<_FQ{T|MTczR)g}Tg!yAg=D}Z>!O)el1Cf_EE?pSuL%hs4k+bD0geuHA(&_D9 z)uPVAo+z2ai^ChIG716c?-=|@uAR)QXU(U7({OXhwx!v&{g&yJ_thJ&2zKRUo8gqt zEl1LrbJMA0wQ??<%Q-54E8`11C$2? zy#fm!JI3pbklqwk(*kU=el)W7?bsVDA#v}<;#BzyP_ZDQsmAkRS4&0q_Y)f{0?sPI z$mQAiZsE|1V%17-`Llq@lVnEDiw8B)&UDqHRoU}StpwjHk&^_#lvZ<;9DMJDY}+yp zUqv=XaqENzb0IV14jC7eBFQHDb^yOW-}7{Yr*$`L>)HiF+QRh#oj*dDK2?|}y~Xbz zT}*|ko}iwI5la{@PpE2XDrWLzuQoETvQPzV*)>>q_UBYla%Xifb>A%(dy5Gly#@+z zHlbX86txs-bim*2R|qRlem{pvBdEmYO<)_zq%-fkV z6ZaKMMIKN4#%*6@3wGzK_@&#K?9p%h7<|?ddM;D{>p8>m;(WYUb<_vzhpjw6cA+u1 zFDzFfbP+vHBohsYN&u38fy1(tA0GoGZOnRdJ T6y?>@A{=`^tE@BSGb;Z9Vq6m} literal 0 HcmV?d00001 From d9f3b688b61166499cfbcdea08b6ecb1ec894ee5 Mon Sep 17 00:00:00 2001 From: Hugo Trippaers Date: Fri, 20 Nov 2015 17:19:40 +0100 Subject: [PATCH 19/30] Immediately fail if the SSL handshake fails --- .../java/com/notnoop/apns/internal/ApnsConnectionImpl.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java index b7a36397..8c819d2d 100644 --- a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java +++ b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java @@ -44,6 +44,7 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.net.SocketFactory; +import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSocketFactory; import com.notnoop.apns.ApnsDelegate; import com.notnoop.apns.StartSendingApnsDelegate; @@ -334,6 +335,9 @@ private synchronized void sendMessage(ApnsNotification m, boolean fromBuffer) th //logger.debug("Message \"{}\" sent", m); attempts = 0; break; + } catch (SSLHandshakeException e) { + // No use retrying this, it's dead Jim + throw new NetworkIOException(e); } catch (IOException e) { Utilities.close(socket); if (attempts >= RETRIES) { From 000b707794cb29db89bea97b26410c4cf3d3a2d3 Mon Sep 17 00:00:00 2001 From: Hugo Trippaers Date: Fri, 20 Nov 2015 17:35:40 +0100 Subject: [PATCH 20/30] Allow JKS files with multiple certificates and selection by alias --- .../com/notnoop/apns/ApnsServiceBuilder.java | 101 ++++++++++++++++-- .../apns/internal/SSLContextBuilder.java | 48 +++++++++ .../apns/integration/ApnsConnectionTest.java | 37 ++++++- .../notnoop/apns/utils/FixedCertificates.java | 17 +++ src/test/resources/clientStore.jks | Bin 0 -> 4209 bytes 5 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 src/test/resources/clientStore.jks diff --git a/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java b/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java index 00ba66c9..a1d39de5 100644 --- a/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java +++ b/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java @@ -30,14 +30,26 @@ */ package com.notnoop.apns; -import static java.util.concurrent.Executors.defaultThreadFactory; +import com.notnoop.apns.internal.ApnsConnection; +import com.notnoop.apns.internal.ApnsConnectionImpl; +import com.notnoop.apns.internal.ApnsFeedbackConnection; +import com.notnoop.apns.internal.ApnsPooledConnection; +import com.notnoop.apns.internal.ApnsServiceImpl; +import com.notnoop.apns.internal.BatchApnsService; +import com.notnoop.apns.internal.QueuedApnsService; +import com.notnoop.apns.internal.SSLContextBuilder; +import com.notnoop.apns.internal.Utilities; +import com.notnoop.exceptions.InvalidSSLConfig; +import com.notnoop.exceptions.RuntimeIOException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.Socket; - import java.security.KeyStore; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -45,14 +57,15 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; - -import com.notnoop.apns.internal.*; -import com.notnoop.exceptions.InvalidSSLConfig; -import com.notnoop.exceptions.RuntimeIOException; - -import static com.notnoop.apns.internal.Utilities.*; +import static com.notnoop.apns.internal.Utilities.PRODUCTION_FEEDBACK_HOST; +import static com.notnoop.apns.internal.Utilities.PRODUCTION_FEEDBACK_PORT; +import static com.notnoop.apns.internal.Utilities.PRODUCTION_GATEWAY_HOST; +import static com.notnoop.apns.internal.Utilities.PRODUCTION_GATEWAY_PORT; +import static com.notnoop.apns.internal.Utilities.SANDBOX_FEEDBACK_HOST; +import static com.notnoop.apns.internal.Utilities.SANDBOX_FEEDBACK_PORT; +import static com.notnoop.apns.internal.Utilities.SANDBOX_GATEWAY_HOST; +import static com.notnoop.apns.internal.Utilities.SANDBOX_GATEWAY_PORT; +import static java.util.concurrent.Executors.defaultThreadFactory; /** * The class is used to create instances of {@link ApnsService}. @@ -208,7 +221,73 @@ public ApnsServiceBuilder withCert(KeyStore keyStore, String password) .withDefaultTrustKeyStore() .build()); } - + + /** + * Specify the certificate store used to connect to Apple APNS + * servers. This relies on the stream of keystore (*.p12 | *.jks) + * containing the keys and certificates, along with the given + * password and alias. + * + * The keystore can be either PKCS12 or JKS and the keystore + * needs to be encrypted using the SunX509 algorithm. + * + * This library does not support password-less p12 certificates, due to a + * Oracle Java library + * Bug 6415637. There are three workarounds: use a password-protected + * certificate, use a different boot Java SDK implementation, or constract + * 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 + * @param alias the alias identifing the key to be used + * @return this + * @throws InvalidSSLConfig if stream is an invalid Keystore, + * the password is invalid or the alias is not found + */ + public ApnsServiceBuilder withCert(InputStream stream, String password, String alias) + throws InvalidSSLConfig { + assertPasswordNotEmpty(password); + return withSSLContext(new SSLContextBuilder() + .withAlgorithm(KEY_ALGORITHM) + .withCertificateKeyStore(stream, password, KEYSTORE_TYPE, alias) + .withDefaultTrustKeyStore() + .build()); + } + + /** + * Specify the certificate store used to connect to Apple APNS + * servers. This relies on the stream of keystore (*.p12 | *.jks) + * containing the keys and certificates, along with the given + * password and alias. + * + * The keystore can be either PKCS12 or JKS and the keystore + * needs to be encrypted using the SunX509 algorithm. + * + * This library does not support password-less p12 certificates, due to a + * Oracle Java library + * Bug 6415637. There are three workarounds: use a password-protected + * certificate, use a different boot Java SDK implementation, or constract + * 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 + * @param alias the alias identifing the key to be used + * @return this + * @throws InvalidSSLConfig if stream is an invalid Keystore, + * the password is invalid or the alias is not found + */ + public ApnsServiceBuilder withCert(KeyStore keyStore, String password, String alias) + throws InvalidSSLConfig { + assertPasswordNotEmpty(password); + return withSSLContext(new SSLContextBuilder() + .withAlgorithm(KEY_ALGORITHM) + .withCertificateKeyStore(keyStore, password, alias) + .withDefaultTrustKeyStore() + .build()); + } + private void assertPasswordNotEmpty(String password) { if (password == null || password.length() == 0) { throw new IllegalArgumentException("Passwords must be specified." + diff --git a/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java b/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java index 3fbfa973..88c9dd26 100644 --- a/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java +++ b/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java @@ -9,7 +9,13 @@ import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; +import java.security.Key; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; public class SSLContextBuilder { private String algorithm = "sunx509"; @@ -72,6 +78,18 @@ public SSLContextBuilder withCertificateKeyStore(InputStream keyStoreStream, Str } } + public SSLContextBuilder withCertificateKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType, String keyAlias) throws InvalidSSLConfig { + try { + final KeyStore ks = KeyStore.getInstance(keyStoreType); + ks.load(keyStoreStream, keyStorePassword.toCharArray()); + return withCertificateKeyStore(ks, keyStorePassword, keyAlias); + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } catch (IOException e) { + throw new InvalidSSLConfig(e); + } + } + public SSLContextBuilder withCertificateKeyStore(KeyStore keyStore, String keyStorePassword) throws InvalidSSLConfig { try { keyManagerFactory = KeyManagerFactory.getInstance(algorithm); @@ -82,6 +100,36 @@ public SSLContextBuilder withCertificateKeyStore(KeyStore keyStore, String keySt } } + public SSLContextBuilder withCertificateKeyStore(KeyStore keyStore, String keyStorePassword, String keyAlias) throws InvalidSSLConfig { + try { + if (!keyStore.containsAlias(keyAlias)) { + throw new InvalidSSLConfig("No key with alias " + keyAlias); + } + KeyStore singleKeyKeyStore = getKeyStoreWithSingleKey(keyStore, keyStorePassword, keyAlias); + return withCertificateKeyStore(singleKeyKeyStore, keyStorePassword); + } catch (GeneralSecurityException e) { + throw new InvalidSSLConfig(e); + } catch (IOException e) { + throw new InvalidSSLConfig(e); + } + } + + /* + * Workaround for keystores containing multiple keys. Java will take the first key that matches + * and this way we can still offer configuration for a keystore with multiple keys and a selection + * based on alias. Also much easier than making a subclass of a KeyManagerFactory + */ + private KeyStore getKeyStoreWithSingleKey(KeyStore keyStore, String keyStorePassword, String keyAlias) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException { + KeyStore singleKeyKeyStore = KeyStore.getInstance(keyStore.getType(), keyStore.getProvider()); + final char[] password = keyStorePassword.toCharArray(); + singleKeyKeyStore.load(null, password); + Key key = keyStore.getKey(keyAlias, password); + Certificate[] chain = keyStore.getCertificateChain(keyAlias); + singleKeyKeyStore.setKeyEntry(keyAlias, key, password, chain); + return singleKeyKeyStore; + } + public SSLContext build() throws InvalidSSLConfig { if (keyManagerFactory == null) { throw new InvalidSSLConfig("Missing KeyManagerFactory"); diff --git a/src/test/java/com/notnoop/apns/integration/ApnsConnectionTest.java b/src/test/java/com/notnoop/apns/integration/ApnsConnectionTest.java index 9ac4cd6e..c348b77a 100644 --- a/src/test/java/com/notnoop/apns/integration/ApnsConnectionTest.java +++ b/src/test/java/com/notnoop/apns/integration/ApnsConnectionTest.java @@ -8,14 +8,19 @@ import com.notnoop.apns.utils.junit.DumpThreadsOnErrorRule; import com.notnoop.apns.utils.junit.Repeat; import com.notnoop.apns.utils.junit.RepeatRule; +import com.notnoop.exceptions.NetworkIOException; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; -import static com.notnoop.apns.utils.FixedCertificates.*; -import static org.junit.Assert.*; +import static com.notnoop.apns.utils.FixedCertificates.LOCALHOST; +import static com.notnoop.apns.utils.FixedCertificates.clientContext; +import static com.notnoop.apns.utils.FixedCertificates.clientMultiKeyContext; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; @SuppressWarnings("ALL") @@ -122,4 +127,32 @@ public void sendOneSimpleWithTimeout() throws InterruptedException { assertArrayEquals(msg1.marshall(), server.getReceived().toByteArray()); } + @Test(timeout = 2000) + public void sendOneSimpleMultiKey() throws InterruptedException { + ApnsService service = + APNS.newService().withSSLContext(clientMultiKeyContext("notnoop-client")) + .withGatewayDestination(LOCALHOST, gatewayPort) + .build(); + server.stopAt(msg1.length()); + service.push(msg1); + server.getMessages().acquire(); + + assertArrayEquals(msg1.marshall(), server.getReceived().toByteArray()); + } + + @Test(timeout = 2000) + public void sendOneSimpleClientCertFail() throws InterruptedException { + ApnsService service = + APNS.newService().withSSLContext(clientMultiKeyContext("notused")) + .withGatewayDestination(LOCALHOST, gatewayPort) + .build(); + server.stopAt(msg1.length()); + try { + service.push(msg1); + fail(); + } catch (NetworkIOException e) { + assertTrue("Expected bad_certifcate exception", e.getMessage().contains("bad_certificate")); + } + } + } diff --git a/src/test/java/com/notnoop/apns/utils/FixedCertificates.java b/src/test/java/com/notnoop/apns/utils/FixedCertificates.java index 18b229a5..8af23561 100644 --- a/src/test/java/com/notnoop/apns/utils/FixedCertificates.java +++ b/src/test/java/com/notnoop/apns/utils/FixedCertificates.java @@ -11,6 +11,9 @@ public class FixedCertificates { public static final String CLIENT_STORE = "clientStore.p12"; public static final String CLIENT_PASSWORD = "123456"; + public static final String CLIENT_MULTI_KEY_STORE = "clientStore.jks"; + public static final String CLIENT_MULTI_KEY_PASSWORD = "123456"; + public static final String SERVER_STORE = "serverStore.p12"; public static final String SERVER_PASSWORD = "123456"; @@ -48,6 +51,20 @@ public static SSLContext clientContext() { } } + public static SSLContext clientMultiKeyContext(String keyAlias) { + try { + InputStream stream = FixedCertificates.class.getResourceAsStream("/" + CLIENT_MULTI_KEY_STORE); + assert stream != null; + return new SSLContextBuilder() + .withAlgorithm("sunx509") + .withCertificateKeyStore(stream, CLIENT_MULTI_KEY_PASSWORD, "JKS", keyAlias) + .withTrustManager(new X509TrustManagerTrustAll()) + .build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + public static String clientCertPath() { return ClassLoader.getSystemResource(CLIENT_STORE).getPath(); } diff --git a/src/test/resources/clientStore.jks b/src/test/resources/clientStore.jks new file mode 100644 index 0000000000000000000000000000000000000000..8564ff4a209a78b1d18f45f748d5e921493235f4 GIT binary patch literal 4209 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 literal 0 HcmV?d00001 From c179d5e1b4e0a9ffefc6dcb52d9f62c21af377cd Mon Sep 17 00:00:00 2001 From: Hugo Trippaers Date: Fri, 20 Nov 2015 17:48:47 +0100 Subject: [PATCH 21/30] Add a shell script to recreate the certificate stores --- tools/generate_test_stores.sh | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tools/generate_test_stores.sh diff --git a/tools/generate_test_stores.sh b/tools/generate_test_stores.sh new file mode 100644 index 00000000..2d3899b5 --- /dev/null +++ b/tools/generate_test_stores.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +KEYSIZE=1024 +VALIDITY=1460 +rm -f serverStore.p12 serverTrustStore.p12 caKey.pem caCert.pem request.crs clientStore.p12 clientCert.crt clientStore.jks + +echo ---=== Generating Certificates ===--- +echo This tools will request several parameters. +echo The names are free, but it is recommended +echo to enter a name that identifies the certificate +echo for example TestServer, TestCA, TestClient, TestInvalidClient +echo Enter 123456 when asked for a password +echo + +# Server Cert +echo --== Generating Server Certificate ==-- +keytool -genkey -keyalg RSA -alias notnoop-server \ + -keystore serverStore.p12 -storepass 123456 -storetype PKCS12 \ + -validity ${VALIDITY} -keysize ${KEYSIZE} +keytool -exportcert -alias notnoop-server \ + -keystore serverStore.p12 -storepass 123456 -storetype PKCS12 | keytool -printcert + +# Client Cert CA +echo --== Generating Client CA Certificate ==-- +keytool -genkey -keyalg RSA -alias notnoop-ca \ + -keystore serverTrustStore.p12 -storetype pkcs12 -storepass 123456 \ + -validity ${VALIDITY} -keysize ${KEYSIZE} +keytool -exportcert -alias notnoop-ca \ + -keystore serverTrustStore.p12 -storepass 123456 -storetype PKCS12 | keytool -printcert + +openssl pkcs12 -in serverTrustStore.p12 -nocerts -out caKey.pem +openssl pkcs12 -in serverTrustStore.p12 -clcerts -nokeys -out caCert.pem + +echo --== Generating Client Certificate ==-- +keytool -genkey -keyalg RSA -keystore clientStore.p12 -storepass 123456 -storetype PKCS12 -alias notnoop-client \ + -validity ${VALIDITY} -keysize ${KEYSIZE} +keytool -certreq -keystore clientStore.p12 -storepass 123456 -storetype PKCS12 -alias notnoop-client -file request.crs + +echo --== Signing Client Certificate with CA ==-- +openssl x509 -req -CA CACert.pem -CAkey CAKey.pem -in request.crs -out clientCert.crt \ + -days ${VALIDITY} -CAcreateserial -outform PEM -set_serial 1 +cat caCert.pem >> clientCert.crt +keytool -import -keystore clientStore.p12 -storepass 123456 -storetype PKCS12 -file clientCert.crt -alias notnoop-client +keytool -exportcert -alias notnoop-client \ + -keystore clientStore.p12 -storepass 123456 -storetype PKCS12 | keytool -printcert + +echo --== Generating JKS Client Keystore with Client Certificate ==-- +keytool -importkeystore \ + -srckeystore clientStore.p12 -srcstorepass 123456 -srcstoretype PKCS12 -srcalias notnoop-client \ + -destkeystore clientStore.jks -deststorepass 123456 -deststoretype JKS -destalias notnoop-client + +echo --== Generating Invalid Client Certificate ==-- +keytool -genkeypair -keyalg RSA -keystore clientStore.jks -storepass 123456 -storetype JKS -alias notused + +keytool -list -keystore clientStore.jks -storepass 123456 -storetype JKS + +rm caKey.pem caCert.pem request.crs clientCert.crt \ No newline at end of file From cfa26bc7953125507a1a4b082039b13c57fe0cde Mon Sep 17 00:00:00 2001 From: Hugo Trippaers Date: Fri, 20 Nov 2015 18:10:33 +0100 Subject: [PATCH 22/30] Adding a space to trigger a retest with travis --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 55b3519f..c78c07f4 100644 --- a/README.markdown +++ b/README.markdown @@ -160,4 +160,4 @@ Licensed under the [New 3-Clause BSD License](http://www.opensource.org/licenses Contact --------------- -Support mailing list: http://groups.google.com/group/java-apns-discuss +Support mailing list: http://groups.google.com/group/java-apns-discuss From 8342a68f772fc97e201e8927fde39c3babb878cd Mon Sep 17 00:00:00 2001 From: Hugo Trippaers Date: Mon, 23 Nov 2015 09:58:28 +0100 Subject: [PATCH 23/30] Add a check to the build to verify license details --- LICENSE.contributor | 27 +++++++++++++++++++++++++++ pom.xml | 31 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 LICENSE.contributor diff --git a/LICENSE.contributor b/LICENSE.contributor new file mode 100644 index 00000000..9713c165 --- /dev/null +++ b/LICENSE.contributor @@ -0,0 +1,27 @@ + 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. diff --git a/pom.xml b/pom.xml index 123fbe9a..84646588 100644 --- a/pom.xml +++ b/pom.xml @@ -138,6 +138,37 @@ + + com.mycila + license-maven-plugin + 2.11 + + + default-cli + process-classes + + check + + + + + true + true +
LICENSE
+ + LICENSE.contributor + + + XML_STYLE + SLASHSTAR_STYLE + SEMICOLON_STYLE + + true + + src/**/*.java + +
+