/* * Copyright 2009, Mahmood Ali. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Mahmood Ali. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.notnoop.apns.utils.Simulator; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.IOException; import java.net.ServerSocket; import java.net.SocketException; import java.nio.*; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; import javax.net.ServerSocketFactory; import com.notnoop.apns.internal.Utilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public abstract class ApnsServerSimulator { private static final Logger logger = LoggerFactory.getLogger(ApnsServerSimulator.class); private static AtomicInteger threadNameCount = new AtomicInteger(0); private final Semaphore startUp = new Semaphore(0); private final ServerSocketFactory sslFactory; private int effectiveGatewayPort; private int effectiveFeedbackPort; public ApnsServerSimulator(ServerSocketFactory sslFactory) { this.sslFactory = sslFactory; } Thread gatewayThread; Thread feedbackThread; ServerSocket gatewaySocket; ServerSocket feedbackSocket; public void start() { logger.debug("Starting APNSServerSimulator"); gatewayThread = new GatewayListener(); feedbackThread = new FeedbackRunner(); gatewayThread.start(); feedbackThread.start(); startUp.acquireUninterruptibly(2); } public void stop() { logger.debug("Stopping APNSServerSimulator"); try { if (gatewaySocket != null) { gatewaySocket.close(); } } catch (IOException e) { logger.warn("Can not close gatewaySocket properly", e); } try { if (feedbackSocket != null) { feedbackSocket.close(); } } catch (IOException e) { logger.warn("Can not close feedbackSocket properly", e); } if (gatewayThread != null) { gatewayThread.interrupt(); } if (feedbackThread != null) { feedbackThread.interrupt(); } logger.debug("Stopped - APNSServerSimulator"); } public int getEffectiveGatewayPort() { return effectiveGatewayPort; } public int getEffectiveFeedbackPort() { return effectiveFeedbackPort; } private class GatewayListener extends Thread { private GatewayListener() { super(new ThreadGroup("GatewayListener" + threadNameCount.incrementAndGet()), ""); setName(getThreadGroup().getName()); } public void run() { logger.debug("Launched " + Thread.currentThread().getName()); try { try { gatewaySocket = sslFactory.createServerSocket(0); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } effectiveGatewayPort = gatewaySocket.getLocalPort(); // Listen for connections startUp.release(); while (!isInterrupted()) { try { handleGatewayConnection(new InputOutputSocket(gatewaySocket.accept())); } catch (SocketException ex) { logger.warn("Interruption while handling gateway connection", ex); interrupt(); } catch (IOException ioe) { logger.warn("An error occured while handling gateway connection", ioe); } } } finally { logger.debug("Terminating " + Thread.currentThread().getName()); getThreadGroup().list(); getThreadGroup().interrupt(); } } private void handleGatewayConnection(final InputOutputSocket inputOutputSocket) throws IOException { Thread gatewayConnectionTread = new Thread() { @Override public void run() { try { parseNotifications(inputOutputSocket); } finally { inputOutputSocket.close(); } } }; gatewayConnectionTread.start(); } private void parseNotifications(final InputOutputSocket inputOutputSocket) { logger.debug("Running parseNotifications {}", inputOutputSocket.getSocket()); while (!Thread.interrupted()) { try { final ApnsInputStream inputStream = inputOutputSocket.getInputStream(); byte notificationType = inputStream.readByte(); logger.debug("Received Notification (type {})", notificationType); switch (notificationType) { case 0: readLegacyNotification(inputOutputSocket); break; case 1: readEnhancedNotification(inputOutputSocket); break; case 2: readFramedNotifications(inputOutputSocket); break; } } catch (IOException ioe) { logger.warn("An error occured while reading notification", ioe); Thread.currentThread().interrupt(); } } } private void readFramedNotifications(final InputOutputSocket inputOutputSocket) throws IOException { Map<Byte, ApnsInputStream.Item> map = new HashMap<Byte, ApnsInputStream.Item>(); ApnsInputStream frameStream = inputOutputSocket.getInputStream().readFrame(); try { while (!Thread.currentThread().isInterrupted()) { final ApnsInputStream.Item item = frameStream.readItem(); map.put(item.getItemId(), item); } } catch (EOFException eof) { // Done reading. } byte[] deviceToken = get(map, ApnsInputStream.Item.ID_DEVICE_TOKEN).getBlob(); byte[] payload = get(map, ApnsInputStream.Item.ID_PAYLOAD).getBlob(); int identifier = get(map, ApnsInputStream.Item.ID_NOTIFICATION_IDENTIFIER).getInt(); int expiry = get(map, ApnsInputStream.Item.ID_EXPIRATION_DATE).getInt(); byte priority = get(map, ApnsInputStream.Item.ID_PRIORITY).getByte(); final Notification notification = new Notification(2, identifier, expiry, deviceToken, payload, priority); logger.debug("Read framed notification {}", notification); onNotification(notification, inputOutputSocket); } private ApnsInputStream.Item get(final Map<Byte, ApnsInputStream.Item> map, final byte idDeviceToken) { ApnsInputStream.Item item = map.get(idDeviceToken); if (item == null) { item = ApnsInputStream.Item.DEFAULT; } return item; } private void readEnhancedNotification(final InputOutputSocket inputOutputSocket) throws IOException { ApnsInputStream inputStream = inputOutputSocket.getInputStream(); int identifier = inputStream.readInt(); int expiry = inputStream.readInt(); final byte[] deviceToken = inputStream.readBlob(); final byte[] payload = inputStream.readBlob(); final Notification notification = new Notification(1, identifier, expiry, deviceToken, payload); logger.debug("Read enhanced notification {}", notification); onNotification(notification, inputOutputSocket); } private void readLegacyNotification(final InputOutputSocket inputOutputSocket) throws IOException { ApnsInputStream inputStream = inputOutputSocket.getInputStream(); final byte[] deviceToken = inputStream.readBlob(); final byte[] payload = inputStream.readBlob(); final Notification notification = new Notification(0, deviceToken, payload); logger.debug("Read legacy notification {}", notification); onNotification(notification, inputOutputSocket); } @Override public void interrupt() { logger.debug("Interrupt, closing socket"); super.interrupt(); try { gatewaySocket.close(); } catch (IOException e) { logger.warn("Can not close gatewaySocket properly", e); } } } protected void fail(final byte status, final int identifier, final InputOutputSocket inputOutputSocket) throws IOException { logger.debug("FAIL {} {}", status, identifier); // Here comes the fun ... we need to write the feedback packet as one single packet // or the client will notice the connection to be closed before it read the complete packet. // But - only on linux, however. (I was not able to see that problem on Windows 7 or OS X) // What also helped was inserting a little sleep between the flush and closing the connection. // // I believe this is irregular (writing to a tcp socket then closing it should result in ALL data // being visible at the client) but interestingly in Netty there is (was) a similar problem: // https://github.com/netty/netty/issues/1952 // // Funnily that appeared as somebody ported this library to use netty. // // // ByteBuffer bb = ByteBuffer.allocate(6); bb.put((byte) 8); bb.put(status); bb.putInt(identifier); inputOutputSocket.syncWrite(bb.array()); inputOutputSocket.close(); logger.debug("FAIL - closed"); } private class FeedbackRunner extends Thread { private FeedbackRunner() { super(new ThreadGroup("FeedbackRunner" + threadNameCount.incrementAndGet()), ""); setName(getThreadGroup().getName()); } public void run() { try { logger.debug("Launched " + Thread.currentThread().getName()); try { feedbackSocket = sslFactory.createServerSocket(0); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } effectiveFeedbackPort = feedbackSocket.getLocalPort(); startUp.release(); while (!isInterrupted()) { try { handleFeedbackConnection(new InputOutputSocket(feedbackSocket.accept())); } catch (SocketException ex) { logger.warn("Interruption while handling feedback connection", ex); interrupt(); } catch (IOException ioe) { logger.warn("An error occured while handling feedback connection", ioe); } } } finally { logger.debug("Terminating " + Thread.currentThread().getName()); getThreadGroup().list(); getThreadGroup().interrupt(); } } private void handleFeedbackConnection(final InputOutputSocket inputOutputSocket) { Thread feedbackConnectionTread = new Thread() { @Override public void run() { try { logger.debug("Feedback connection sending feedback"); sendFeedback(inputOutputSocket); } catch (IOException ioe) { // An exception is unexpected here. Close the current connection and bail out. logger.warn("An error occured while sending feedback", ioe); } finally { inputOutputSocket.close(); } } }; feedbackConnectionTread.start(); } private void sendFeedback(final InputOutputSocket inputOutputSocket) throws IOException { List<byte[]> badTokens = getBadTokens(); for (byte[] token : badTokens) { writeFeedback(inputOutputSocket, token); } } private void writeFeedback(final InputOutputSocket inputOutputSocket, final byte[] token) throws IOException { ByteArrayOutputStream os = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(os); final int unixTime = (int) (new Date().getTime() / 1000); dos.writeInt(unixTime); dos.writeShort((short) token.length); dos.write(token); dos.close(); inputOutputSocket.syncWrite(os.toByteArray()); } } @SuppressWarnings("UnusedDeclaration") public class Notification { private final int type; private final int identifier; private final int expiry; private final byte[] deviceToken; private final byte[] payload; private final byte priority; public Notification(final int type, final byte[] deviceToken, final byte[] payload) { this(type, 0, 0, deviceToken, payload); } public Notification(final int type, final int identifier, final int expiry, final byte[] deviceToken, final byte[] payload) { this(type, identifier, expiry, deviceToken, payload, (byte) 10); } public Notification(final int type, final int identifier, final int expiry, final byte[] deviceToken, final byte[] payload, final byte priority) { this.priority = priority; this.type = type; this.identifier = identifier; this.expiry = expiry; this.deviceToken = deviceToken; this.payload = payload; } public byte[] getPayload() { return payload.clone(); } public byte[] getDeviceToken() { return deviceToken.clone(); } public int getType() { return type; } public int getExpiry() { return expiry; } public int getIdentifier() { return identifier; } public byte getPriority() { return priority; } @Override public String toString() { return "Notification{" + "type=" + type + ", identifier=" + identifier + ", expiry=" + expiry + ", deviceToken=" + Utilities.encodeHex(deviceToken) + //", payload=" + Utilities.encodeHex(payload) + ", priority=" + priority + '}'; } } protected void onNotification(final Notification notification, final InputOutputSocket inputOutputSocket) throws IOException { } protected List<byte[]> getBadTokens() { return new ArrayList<byte[]>(); } }