/* * 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.internal; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.Socket; import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import com.notnoop.apns.ApnsDelegate; import com.notnoop.apns.ApnsNotification; import com.notnoop.apns.DeliveryError; import com.notnoop.apns.EnhancedApnsNotification; import com.notnoop.apns.ReconnectPolicy; import com.notnoop.exceptions.ApnsDeliveryErrorException; import com.notnoop.exceptions.NetworkIOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ApnsConnectionImpl implements ApnsConnection { private static final Logger logger = LoggerFactory.getLogger(ApnsConnectionImpl.class); private final SocketFactory factory; private final String host; private final int port; private final int readTimeout; private final int connectTimeout; private final Proxy proxy; private final String proxyUsername; private final String proxyPassword; private final ReconnectPolicy reconnectPolicy; private final ApnsDelegate delegate; private int cacheLength; private final boolean errorDetection; private final ThreadFactory threadFactory; private final boolean autoAdjustCacheLength; private final ConcurrentLinkedQueue<ApnsNotification> cachedNotifications, notificationsBuffer; private Socket socket; private final AtomicInteger threadId = new AtomicInteger(0); public ApnsConnectionImpl(SocketFactory factory, String host, int port) { this(factory, host, port, new ReconnectPolicies.Never(), ApnsDelegate.EMPTY); } private ApnsConnectionImpl(SocketFactory factory, String host, int port, ReconnectPolicy reconnectPolicy, ApnsDelegate delegate) { this(factory, host, port, null, null, null, reconnectPolicy, delegate); } private ApnsConnectionImpl(SocketFactory factory, String host, int port, Proxy proxy, String proxyUsername, String proxyPassword, ReconnectPolicy reconnectPolicy, ApnsDelegate delegate) { this(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy, delegate, false, null, ApnsConnection.DEFAULT_CACHE_LENGTH, true, 0, 0); } public ApnsConnectionImpl(SocketFactory factory, String host, int port, Proxy proxy, String proxyUsername, String proxyPassword, ReconnectPolicy reconnectPolicy, ApnsDelegate delegate, boolean errorDetection, ThreadFactory tf, int cacheLength, boolean autoAdjustCacheLength, int readTimeout, int connectTimeout) { this.factory = factory; this.host = host; this.port = port; this.reconnectPolicy = reconnectPolicy; this.delegate = delegate == null ? ApnsDelegate.EMPTY : delegate; this.proxy = proxy; this.errorDetection = errorDetection; this.threadFactory = tf == null ? defaultThreadFactory() : tf; this.cacheLength = cacheLength; this.autoAdjustCacheLength = autoAdjustCacheLength; this.readTimeout = readTimeout; this.connectTimeout = connectTimeout; this.proxyUsername = proxyUsername; this.proxyPassword = proxyPassword; cachedNotifications = new ConcurrentLinkedQueue<ApnsNotification>(); notificationsBuffer = new ConcurrentLinkedQueue<ApnsNotification>(); } private ThreadFactory defaultThreadFactory() { return new ThreadFactory() { ThreadFactory wrapped = Executors.defaultThreadFactory(); @Override public Thread newThread( Runnable r ) { Thread result = wrapped.newThread(r); result.setName("MonitoringThread-"+threadId.incrementAndGet()); result.setDaemon(true); return result; } }; } public synchronized void close() { Utilities.close(socket); } private void monitorSocket(final Socket socket) { logger.debug("Launching Monitoring Thread for socket {}", socket); Thread t = threadFactory.newThread(new Runnable() { final static int EXPECTED_SIZE = 6; @SuppressWarnings("InfiniteLoopStatement") @Override public void run() { logger.debug("Started monitoring thread"); try { InputStream in; try { in = socket.getInputStream(); } catch (IOException ioe) { in = null; } byte[] bytes = new byte[EXPECTED_SIZE]; while (in != null && readPacket(in, bytes)) { logger.debug("Error-response packet {}", Utilities.encodeHex(bytes)); // Quickly close socket, so we won't ever try to send push notifications // using the defective socket. Utilities.close(socket); int command = bytes[0] & 0xFF; if (command != 8) { throw new IOException("Unexpected command byte " + command); } int statusCode = bytes[1] & 0xFF; DeliveryError e = DeliveryError.ofCode(statusCode); int id = Utilities.parseBytes(bytes[2], bytes[3], bytes[4], bytes[5]); logger.debug("Closed connection cause={}; id={}", e, id); delegate.connectionClosed(e, id); Queue<ApnsNotification> tempCache = new LinkedList<ApnsNotification>(); ApnsNotification notification = null; boolean foundNotification = false; while (!cachedNotifications.isEmpty()) { notification = cachedNotifications.poll(); logger.debug("Candidate for removal, message id {}", notification.getIdentifier()); if (notification.getIdentifier() == id) { logger.debug("Bad message found {}", notification.getIdentifier()); foundNotification = true; break; } tempCache.add(notification); } if (foundNotification) { logger.debug("delegate.messageSendFailed, message id {}", notification.getIdentifier()); delegate.messageSendFailed(notification, new ApnsDeliveryErrorException(e)); } else { cachedNotifications.addAll(tempCache); int resendSize = tempCache.size(); logger.warn("Received error for message that wasn't in the cache..."); if (autoAdjustCacheLength) { cacheLength = cacheLength + (resendSize / 2); delegate.cacheLengthExceeded(cacheLength); } logger.debug("delegate.messageSendFailed, unknown id"); delegate.messageSendFailed(null, new ApnsDeliveryErrorException(e)); } int resendSize = 0; while (!cachedNotifications.isEmpty()) { resendSize++; final ApnsNotification resendNotification = cachedNotifications.poll(); logger.debug("Queuing for resend {}", resendNotification.getIdentifier()); notificationsBuffer.add(resendNotification); } logger.debug("resending {} notifications", resendSize); delegate.notificationsResent(resendSize); drainBuffer(); } logger.debug("Monitoring input stream closed by EOF"); } catch (IOException e) { // An exception when reading the error code is non-critical, it will cause another retry // sending the message. Other than providing a more stable network connection to the APNS // server we can't do much about it - so let's not spam the application's error log. logger.info("Exception while waiting for error code", e); delegate.connectionClosed(DeliveryError.UNKNOWN, -1); } finally { close(); } } /** * Read a packet like in.readFully(bytes) does - but do not throw an exception and return false if nothing * could be read at all. * @param in the input stream * @param bytes the array to be filled with data * @return true if a packet as been read, false if the stream was at EOF right at the beginning. * @throws IOException When a problem occurs, especially EOFException when there's an EOF in the middle of the packet. */ private boolean readPacket(final InputStream in, final byte[] bytes) throws IOException { final int len = bytes.length; int n = 0; while (n < len) { try { int count = in.read(bytes, n, len - n); if (count < 0) { throw new EOFException("EOF after reading "+n+" bytes of new packet."); } n += count; } catch (IOException ioe) { if (n == 0) return false; throw new IOException("Error after reading "+n+" bytes of packet", ioe); } } return true; } }); t.start(); } private synchronized Socket getOrCreateSocket() throws NetworkIOException { if (reconnectPolicy.shouldReconnect()) { logger.debug("Reconnecting due to reconnectPolicy dictating it"); Utilities.close(socket); socket = null; } if (socket == null || socket.isClosed()) { try { if (proxy == null) { socket = factory.createSocket(host, port); logger.debug("Connected new socket {}", socket); } else if (proxy.type() == Proxy.Type.HTTP) { TlsTunnelBuilder tunnelBuilder = new TlsTunnelBuilder(); socket = tunnelBuilder.build((SSLSocketFactory) factory, proxy, proxyUsername, proxyPassword, host, port); logger.debug("Connected new socket through http tunnel {}", socket); } else { boolean success = false; Socket proxySocket = null; try { proxySocket = new Socket(proxy); proxySocket.connect(new InetSocketAddress(host, port), connectTimeout); socket = ((SSLSocketFactory) factory).createSocket(proxySocket, host, port, false); success = true; } finally { if (!success) { Utilities.close(proxySocket); } } logger.debug("Connected new socket through socks tunnel {}", socket); } socket.setSoTimeout(readTimeout); socket.setKeepAlive(true); if (errorDetection) { monitorSocket(socket); } reconnectPolicy.reconnected(); logger.debug("Made a new connection to APNS"); } catch (IOException e) { logger.error("Couldn't connect to APNS server", e); throw new NetworkIOException(e); } } return socket; } int DELAY_IN_MS = 1000; private static final int RETRIES = 3; public synchronized void sendMessage(ApnsNotification m) throws NetworkIOException { sendMessage(m, false); drainBuffer(); } private synchronized void sendMessage(ApnsNotification m, boolean fromBuffer) throws NetworkIOException { logger.debug("sendMessage {} fromBuffer: {}", m, fromBuffer); int attempts = 0; while (true) { try { attempts++; Socket socket = getOrCreateSocket(); socket.getOutputStream().write(m.marshall()); socket.getOutputStream().flush(); cacheNotification(m); delegate.messageSent(m, fromBuffer); //logger.debug("Message \"{}\" sent", m); attempts = 0; break; } catch (IOException e) { Utilities.close(socket); if (attempts >= RETRIES) { logger.error("Couldn't send message after " + RETRIES + " retries." + m, e); delegate.messageSendFailed(m, e); Utilities.wrapAndThrowAsRuntimeException(e); } // The first failure might be due to closed connection (which in turn might be caused by // a message containing a bad token), so don't delay for the first retry. // // Additionally we don't want to spam the log file in this case, only after the second retry // which uses the delay. if (attempts != 1) { logger.info("Failed to send message " + m + "... trying again after delay", e); Utilities.sleep(DELAY_IN_MS); } } } } private synchronized void drainBuffer() { logger.debug("draining buffer"); while (!notificationsBuffer.isEmpty()) { sendMessage(notificationsBuffer.poll(), true); } } private void cacheNotification(ApnsNotification notification) { cachedNotifications.add(notification); while (cachedNotifications.size() > cacheLength) { cachedNotifications.poll(); logger.debug("Removing notification from cache " + notification); } } public ApnsConnectionImpl copy() { return new ApnsConnectionImpl(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy.copy(), delegate, errorDetection, threadFactory, cacheLength, autoAdjustCacheLength, readTimeout, connectTimeout); } public void testConnection() throws NetworkIOException { ApnsConnectionImpl testConnection = null; try { testConnection = new ApnsConnectionImpl(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy.copy(), delegate); final ApnsNotification notification = new EnhancedApnsNotification(0, 0, new byte[]{0}, new byte[]{0}); testConnection.sendMessage(notification); } finally { if (testConnection != null) { testConnection.close(); } } } public void setCacheLength(int cacheLength) { this.cacheLength = cacheLength; } public int getCacheLength() { return cacheLength; } }