/*
* 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;
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.Socket;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ApnsServerStub {
/**
* Create an ApnsServerStub
*
* @param gatePort port for the gateway stub server
* @param feedPort port for the feedback stub server
* @return an ApnsServerStub
* @deprecated use prepareAndStartServer() without port numbers and query the port numbers from the server using
* ApnsServerStub.getEffectiveGatewayPort() and ApnsServerStub.getEffectiveFeedbackPort()
*/
@Deprecated
public static ApnsServerStub prepareAndStartServer(int gatePort, int feedPort) {
ApnsServerStub server = new ApnsServerStub(FixedCertificates.serverContext().getServerSocketFactory(), gatePort, feedPort);
server.start();
return server;
}
/**
* Create an ApnsServerStub that uses any free port for gateway and feedback.
*
* @return the server stub. Use getEffectiveGatewayPort() and getEffectiveFeedbackPort() to ask for ports.
*/
public static ApnsServerStub prepareAndStartServer() {
ApnsServerStub server = new ApnsServerStub(FixedCertificates.serverContext().getServerSocketFactory());
server.start();
return server;
}
private static final Logger LOGGER = LoggerFactory.getLogger(ApnsServerStub.class);
private final AtomicInteger toWaitBeforeSend = new AtomicInteger(0);
private final ByteArrayOutputStream received;
private final ByteArrayOutputStream toSend;
private final Semaphore messages = new Semaphore(0);
private final Semaphore startUp = new Semaphore(0);
private final Semaphore gatewayOutLock = new Semaphore(0);
private final Semaphore waitForError = new Semaphore(1);
private final ServerSocketFactory sslFactory;
private final int gatewayPort;
private final int feedbackPort;
private int effectiveGatewayPort;
private int effectiveFeedbackPort;
private OutputStream gatewayOutputStream;
public ApnsServerStub(ServerSocketFactory sslFactory) {
this(sslFactory, 0, 0);
}
public ApnsServerStub(ServerSocketFactory sslFactory, int gatewayPort, int feedbackPort) {
this.sslFactory = sslFactory;
this.gatewayPort = gatewayPort;
this.feedbackPort = feedbackPort;
this.received = new ByteArrayOutputStream();
this.toSend = new ByteArrayOutputStream();
}
Thread gatewayThread;
Thread feedbackThread;
SSLServerSocket gatewaySocket;
SSLServerSocket feedbackSocket;
public void start() {
gatewayThread = new GatewayRunner();
feedbackThread = new FeedbackRunner();
gatewayThread.start();
feedbackThread.start();
startUp.acquireUninterruptibly(2);
}
@SuppressWarnings("deprecation")
public void stop() {
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.stop();
}
if (feedbackThread != null) {
feedbackThread.stop();
}
}
public void sendError(int err, int id) {
ByteBuffer buf = ByteBuffer.allocate(6);
buf.put((byte) 8).put((byte) err).putInt(id);
try {
gatewayOutLock.acquire();
gatewayOutputStream.write(buf.array());
gatewayOutputStream.flush();
} catch (Exception ex) {
LOGGER.warn("An error occured with accessing gateway", ex);
}
}
public int getEffectiveGatewayPort() {
return effectiveGatewayPort;
}
public int getEffectiveFeedbackPort() {
return effectiveFeedbackPort;
}
public AtomicInteger getToWaitBeforeSend() {
return toWaitBeforeSend;
}
public ByteArrayOutputStream getReceived() {
return received;
}
public ByteArrayOutputStream getToSend() {
return toSend;
}
public Semaphore getMessages() {
return messages;
}
public Semaphore getWaitForError() {
return waitForError;
}
private class GatewayRunner extends Thread {
@SuppressWarnings("InfiniteLoopStatement")
public void run() {
Thread.currentThread().setName("GatewayThread");
try {
gatewaySocket = (SSLServerSocket)sslFactory.createServerSocket(gatewayPort);
gatewaySocket.setNeedClientAuth(true);
} catch (IOException e) {
messages.release();
throw new RuntimeException(e);
}
InputStream in = null;
effectiveGatewayPort = gatewaySocket.getLocalPort();
try {
// Listen for connections
startUp.release();
while (true) {
Socket socket = gatewaySocket.accept();
// Work around JVM deadlock ... https://community.oracle.com/message/10989561#10989561
socket.setSoLinger(true, 1);
// Create streams to securely send and receive data to the client
in = socket.getInputStream();
gatewayOutputStream = socket.getOutputStream();
gatewayOutLock.release();
// Read from in and write to out...
byte[] read = readFully(in);
waitBeforeSend();
received.write(read);
messages.release();
waitForError.acquire();
// Close the socket
in.close();
gatewayOutputStream.close();
}
} catch (Throwable e) {
try {
if (in != null) {
in.close();
}
} catch (IOException ioex) {
LOGGER.warn("Can not close socket properly", ioex);
}
try {
if (gatewayOutputStream != null) {
gatewayOutputStream.close();
}
} catch (IOException ioex) {
LOGGER.warn("Can not close gatewayOutputStream properly", ioex);
}
messages.release();
}
}
}
private class FeedbackRunner extends Thread {
public void run() {
Thread.currentThread().setName("FeedbackThread");
try {
feedbackSocket = (SSLServerSocket)sslFactory.createServerSocket(feedbackPort);
feedbackSocket.setNeedClientAuth(true);
} catch (IOException e) {
e.printStackTrace();
messages.release();
throw new RuntimeException(e);
}
effectiveFeedbackPort = feedbackSocket.getLocalPort();
try {
// Listen for connections
startUp.release();
Socket socket = feedbackSocket.accept();
// Work around JVM deadlock ... https://community.oracle.com/message/10989561#10989561
socket.setSoLinger(true, 1);
// Create streams to securely send and receive data to the client
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
waitBeforeSend();
// Read from in and write to out...
toSend.writeTo(out);
// Close the socket
in.close();
out.close();
} catch (SocketException se) {
// Ignore closed socket.
} catch (IOException ioex) {
ioex.printStackTrace();
}
messages.release();
}
}
AtomicInteger readLen = new AtomicInteger();
public void stopAt(int length) {
readLen.set(length);
}
public byte[] readFully(InputStream st) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
int read;
try {
while (readLen.getAndDecrement() > 0 && (read = st.read()) != -1) {
stream.write(read);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return stream.toByteArray();
}
/**
* Introduces a waiting time, used to trigger read timeouts.
*/
private void waitBeforeSend() {
int wait = toWaitBeforeSend.get();
if (wait != 0) {
try {
Thread.sleep(wait);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}