/* * Funambol is a mobile platform developed by Funambol, Inc. * Copyright (C) 2003 - 2007 Funambol, Inc. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by * the Free Software Foundation with the addition of the following permission * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED * WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with this program; if not, see http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA. * * You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite * 305, Redwood City, CA 94063, USA, or at email address info@funambol.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License * version 3, these Appropriate Legal Notices must retain the display of the * "Powered by Funambol" logo. If the display of the logo is not reasonably * feasible for technical reasons, the Appropriate Legal Notices must display * the words "Powered by Funambol". */ package com.funambol.push; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; import java.util.Vector; import java.util.Timer; import java.util.TimerTask; import com.funambol.platform.SocketAdapter; import com.funambol.util.Log; import com.funambol.util.MD5; import com.funambol.util.Base64; import com.funambol.util.ConnectionManager; /** * This class implements the CTPService. * A service which opens a CTP connection and communicates with the server * via the CTP protocol (see CTP design document). * The service is executed in a separate thread, and it can be stopped at * any time. The service uses socket connection and therefore requires * network access that may result in user questions. * The service is implemented by two threads plus a Timer, and all threads * can be created within a thread pool. * The service shall never throw any exception (even runtime ones) and on * errors it is implemented to retry connecting. If too many failures occurs * then the service is stopped (at the moment no notification is sent). * * The class performs connection and MD5 authentication on startup and if this * is succesfull then the listening/heartbeat phase starts. * * The main thread is controlled by the following FSM: * * open socket socket opened * DISCONNECTED ---------------> CONNECTING ----------------> CONNECTED * ^ | fail | * | V | * --------------------------------------------- | * | | | send MD5 * | ok | | auth * LISTENING <--- AUTHENTICATED <---------> AUTHENTICATING <-----| * fail */ public class CTPService implements Runnable { private static final String TAG_LOG = "CTPService"; /** CTP Protocol version = 1.0 */ private static final int PROTOCOL_VERSION = 0x10; /** * Commands */ protected static final int CM_AUTH = 0x01; protected static final int CM_READY = 0x02; protected static final int CM_BYE = 0x03; /** * Status */ protected static final int ST_OK = 0x20; protected static final int ST_JUMP = 0x37; protected static final int ST_ERROR = 0x50; protected static final int ST_NOT_AUTHENTICATED = 0x41; // Server auth failed, nonce param received protected static final int ST_UNAUTHORIZED = 0x42; // Server auth failed at 1st try, nonce param received protected static final int ST_FORBIDDEN = 0x43; // No Server auth, no nonce param protected static final int ST_SYNC = 0x29; protected static final int ST_RETRY = 0x53; /** * Parameters */ private static final int P_DEVID = 0x01; private static final int P_USERNAME = 0x02; private static final int P_CRED = 0x03; private static final int P_FROM = 0x04; private static final int P_TO = 0x05; private static final int P_NONCE = 0x06; private static final int P_SAN = 0x07; private static final int P_SLEEP = 0x09; private static final int MAX_MESSAGE_SIZE = 4096; /** * CTP server thread state */ protected static final int DISCONNECTED = 0; protected static final int CONNECTING = 1; protected static final int CONNECTED = 2; protected static final int AUTHENTICATING = 3; protected static final int AUTHENTICATED = 4; protected static final int LISTENING = 5; /** Singleton instance */ private static CTPService instance = null; /** This flag indicates if the service has been * started and not stopped */ private boolean instanceRunning = false; /** Specifies if the CTP service should terminate */ private boolean done = false; /** Thread pool to be used to create new threads. * Can be null if no pool is to be used. */ /** Socket connection, with input and output stream */ private SocketAdapter sc = null; private OutputStream os = null; private InputStream is = null; /** CTP push notification listener */ private CTPNotificationListener pushListener = null; /** Specifies if the server sent an OK status to our last command */ private boolean okReceived = false; /** Push configuration */ private PushConfig config = null; /** CTPService status */ protected int state = DISCONNECTED; /** Timer used to monitor connection timeouts */ private Timer timer = new Timer(); /** Instance of the heartbeat generator */ private HeartbeatGenerator heartbeatGenerator = null; private CTPListener ctpListener; //here's the lock! private Object lock = new Object(); private boolean offlineMode = false; private ConnectionManager connectionManager = ConnectionManager.getInstance(); public void setCTPListener(CTPListener ctpListener) { this.ctpListener = ctpListener; } public void startService() { if (this.config != null) { startService(config); } } /** * turn offline mode on/off. * in offline mode, ctp will close and not try to connect again. * if online mode is set from false to true, ctp service is <b>not</b> * restarted automatically. * @param mode the new offline mode */ public void setOfflineMode(boolean mode) { offlineMode = mode; if (mode && getInstance().isRunning()) { getInstance().stopService(); } } /** * * @return true if in offline mode */ public boolean isOfflineMode() { return offlineMode; } /** * send a ready message to the ctp server. * this need to be sent immediatly after the authentication has been * accepted, we cannot wait for the heartbeat timeout, * or the server will drop the connection. * * @throws java.io.IOException */ private void sendBlindReadyMessage() throws IOException { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Sending blind ready message to avoid server timeout"); } // sending ready message now to avoid server timeout CTPMessage readyMsg = new CTPMessage(); readyMsg.setCommand(CM_READY); // Send 'ready' message to Server sendMessage(readyMsg); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Blind ready message sent"); } } /** * This class is used (with a Timer) to monitor a connection and * interrupt it if it hangs for more than command timeout. * For each IO operation to be monitored, one such object must be created. * The client is responsible for notifying when the operation is terminated. * If by the time the alarm is triggered, the operation is not terminated, * then such an operation is considered timeout and closeConnection is * invoked. This will cause exceptions in any hanging read/write, allowing * each thread to resume execution. */ protected class ConnectionTimer extends TimerTask { /** IO timeout (timer delay) */ private int delay = -1; /** Specifies whether the IO operation has actually terminated */ private boolean terminated = false; /** Constructor. The delay is specified in the Configuration */ public ConnectionTimer() { delay = config.getCtpCmdTimeout() * 1000; } /** Notifies the ConnectionTimer that the IO operation has terminated. * Whenever the alarm will be triggered it won't cause a timeout because * the operation is finished. **/ public void endOperation() { terminated = true; } /** Returns the delay for this task */ public int getDelay() { return delay; } /** This method is invoked when the alarm expires. * If the operation this task is monitoring has not finished yet, then * we force the entire connection to shut down. This will cause * exceptions for all the pending read/write operations **/ public void run() { // We were monitoring an operation whose idx is nextTimed // check if it terminated if (terminated == false) { // The operation did not terminate // We force an exception to wake up the thread if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "An IO operation did not complete before" + " maximum allowed time. Restart the CTPService"); } disconnect(); } } } /** * This class implements an hearbeat generator. A thread which is in charge * of generating messages for the CTP server to signal that the client is * alive. The interval between successive messages is specified in the * configuration. * The thread sends a message and wait for an answer. The send operation is * monitored for timeout. The answer is caught by the main listener thread. * The heartbeat generator only checks whether an ok has been received. In * this case it consider everything is OK. This is a trick as we do not * really monitor the reading with a timeout. We rather expects that the OK * arrives before we have to generate a new READY message. This way we * simplify the implementation and the behavior seems still reasonable. */ protected class HeartbeatGenerator extends Thread { /** Indicates if the heart is beating (alive) */ private boolean isRunning = false; /** This is the heart beat main loop. It generates a beat every ready * period of time (as specified in the PushConfig. * The task runs until the termination flag (done) gets set. * If at the moment of generating a new beat, the previous has not * received an OK status, then a ctp restart is forced by closing the * connection. **/ public void run() { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Starting heartbeat generator"); } isRunning = true; // Load the sleep interval (ctpReady) int sleepTime = config.getCtpReady(); // Prepare the CTP message CTPMessage readyMsg = new CTPMessage(); readyMsg.setCommand(CM_READY); // Send 'ready' message to Server and sleep ctpReady seconds try { while (!done) { okReceived = false; if (state == LISTENING) { //(os != null && is != null) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Sending ready msg"); } sendMessage(readyMsg); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Next ready msg will be sent in " + sleepTime + " seconds..."); } sleepSecs(sleepTime); if (!okReceived) { // We consider this as a timeout throw new IOException("OK not received"); } } else { // This case may arise in this situation. The heartbeat // generator is sleeping and the main thread (which is // listening) gets an IOException. The main thread // restart the CTP connection sequence, but meanwhile we // may have the streams which are null. The heartbeat is // still running, so no new instance is created. // Therefore the heartbeat has to suspend waiting for // the main thread to re-connect. if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "HeartBeatGenerator sleeping 10 sec while CTP restarts"); } sleepSecs(10); } } } catch (IOException ioe) { Log.error(TAG_LOG, "HeartBeatGenerator error sending the heartbeat", ioe); disconnect(); } finally { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "HeartBeatGenerator exiting heartbeat generator"); } isRunning = false; } } /** Returns true if the heart is beating. */ public boolean isRunning() { return isRunning; } } /** * This class represents a CTP message. Messages format is described in the * CTP design document. * This class purpose is to allow users to easily create a CTP message, * setting all its properties and then to generate the byte sequence that * represent such a message. The second goal of the class is to allow the * opposite transformation. A byte stream received from the server can be * parsed and translated into a CTPMessage object. */ protected class CTPMessage { /** Parameters. Codes and values (must be kept in sync) */ private Vector paramsValue = new Vector(); private Vector paramsCode = new Vector(); /** protocol version */ private int protocolVersion = PROTOCOL_VERSION; /** Total message length */ private int length = -1; /** command or status code*/ private int commandCode = -1; /** Last received nonce. This is just a parameter but it is cached in * this field for performance reasons **/ private byte[] nonce = null; /** Build an empty message */ public CTPMessage() { } /** * Build a message decoding the given byte stream * @param rawMessage is the byte stream representing the CTP message */ public CTPMessage(byte[] rawMessage) { parsePacket(rawMessage); } /** * Sets the command or status code * @param the new code * */ public void setCommand(int commandCode) { this.commandCode = commandCode; } /** Returns the command or status code */ public int getCommand() { return commandCode; } /** * Add one parameter * @param code parameter code * @param value parameter value **/ public void addParameter(int code, byte[] value) { paramsCode.addElement(new Integer(code)); paramsValue.addElement(value); } /** * Returns the number of parameters. */ public int getParametersNumber() { return paramsCode.size(); } /** * Get the parameter code of a given parameter. * An exception maybe thrown if the index is out of bounds. * @param idx parameter index * @return the parameter code */ public int getParameterCode(int idx) { Integer code = (Integer) paramsCode.elementAt(idx); return code.intValue(); } /** * Get the parameter value of a given parameter. * An exception maybe thrown if the index is out of bounds. */ public byte[] getParameterValue(int idx) { byte[] value = (byte[]) paramsValue.elementAt(idx); return value; } /** * Get a byte representation of this message. This byte representation * is conformant to the CTP protocol specification (see the CTP design * document). * * @return a byte array representing this message in CTP protocol format */ public byte[] getBytes() { // Compute the total length (header + parameters size) int msgLength = 4; // header size (Protocol version, command) int numParams = paramsValue.size(); for (int i = 0; i < numParams; ++i) { byte[] param = (byte[]) paramsValue.elementAt(i); msgLength += (1 + 1 + param.length); // param-type + param-length + value } int idx = 0; byte[] bytes = new byte[msgLength]; // Msg length (does not include this field) bytes[idx++] = (byte) ((msgLength - 2) >> 8); bytes[idx++] = (byte) ((msgLength - 2) & 0xFF); // Protocol version bytes[idx++] = (byte) PROTOCOL_VERSION; // Command bytes[idx++] = (byte) commandCode; // Parameters for (int i = 0; i < numParams; ++i) { Integer codeInt = (Integer) paramsCode.elementAt(i); byte code = codeInt.byteValue(); byte[] param = (byte[]) paramsValue.elementAt(i); byte length = (byte) param.length; bytes[idx++] = code; bytes[idx++] = length; System.arraycopy(param, 0, bytes, idx, param.length); idx += param.length; } return bytes; } /** * Return the last nonce that has been parsed. * This method is not generic, it does not return the nonce for any CTP * message. But only for messages that have been created from a byte * stream and which contains a nonce parameter. * * @return the nonce value or null if not defined */ public byte[] getNonce() { return nonce; } // All the methods below are the implementation of a recursive parser in // charge of parsing an incoming message. The message (byte stream) is // translated into a CTPMessage object. // // CTP message grammar: // // PACKET -> MSGLEN MSG // MSG -> VERSION COMSTAT // MSGLEN -> short // VERSION -> byte // COMMAND -> CODE PARAM // CODE -> byte // PARAM -> CODE SLEN VALUE // PARAM -> eps // SLEN -> byte // VALUE -> byte VALUE // VALUE -> eps private void parsePacket(byte[] rawMessage) { // Length is big endian length = ((int) rawMessage[0]) * 10 + (int) rawMessage[1]; parseMessage(rawMessage, 2); } private int parseMessage(byte[] rawMessage, int offset) { protocolVersion = (int) rawMessage[offset]; offset = parseCommand(rawMessage, offset + 1); return offset; } private int parseCommand(byte[] rawMessage, int offset) { commandCode = (int) rawMessage[offset]; offset = parseParam(rawMessage, offset + 1); return offset; } private int parseParam(byte[] rawMessage, int offset) { int i; // Handle the stream end if (offset >= rawMessage.length) { return offset; } // Ok, we have another param to consume int code = (int) rawMessage[offset]; int len = (int) rawMessage[offset + 1]; paramsCode.addElement(new Integer(code)); byte[] value = new byte[len]; System.arraycopy(rawMessage, offset + 2, value, 0, len); paramsValue.addElement(value); // For convenience we save the nonce which needs to be retrieved // very often if (code == P_NONCE) { nonce = value; } offset = parseParam(rawMessage, offset + 2 + len); return offset; } } /** * Builds an empty service. This class is a singleton, therefore the user * is disallowed to create instances. @see getInstance. * */ protected CTPService() { } /** * Returns the instance of the CTPService. If the service has already been * created, then the existing instance is returned, otherwise a new one is * created. Before starting the service it is possible to set the * CTPNotificationListener. * * @return a valid CTPService instance */ public static CTPService getInstance() { if (instance == null) { instance = new CTPService(); } return instance; } /** * Restarts the service. This imply stopping and restarting it. * The new instance will work using the new configuration * * @param config the new push configuration */ public synchronized void restartService(PushConfig config) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "restarting service"); } try { if (getInstance().isRunning()) { stopService(); Thread.sleep(5000); } startService(config); } catch (InterruptedException ex) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Exception thrown while restarting CTP service: "); } } catch (Exception e) { Log.error(TAG_LOG, "Exception thrown while restarting CTP service: ", e); } } /** * Restarts the service. This imply stopping and restarting it. * if no config has been set, the method does nothing */ public void restartService() { if (this.config != null) { restartService(this.config); } else { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Cannot restart service: current config is null"); } } } /** * Start the complete CTP service. This includes the listener and the * heartbeat generator. Since this method fires a thread, the actual * startService implementation is in the run method. * * @param config the Push configuration for this CTPService (cannot be null) * */ public void startService(PushConfig config) { synchronized (lock) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "CTPstart service called"); } // Save the config this.config = config; if (!instanceRunning) { instanceRunning = true; timer = new Timer(); done = false; if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Starting CTPService WITHOUTH thread pool"); } Thread t = new Thread(this); t.start(); } else { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "CTPService already running"); } } } } /** * Returns true iff an instance of CTP is already running */ public boolean isRunning() { return instanceRunning; } /** * set the push config * @param config the new config */ public void setConfig(PushConfig config) { this.config = config; } /** * Returns true iff CTP is running and it is properly connected and * authenticated to the CTP service. In other words this method return true * iff push notifications can be properly received via CTP. * * @return true iff CTP push notifications are active */ public boolean isPushActive() { return state == LISTENING; } /** * Sets the push events listener. * * @param pushListener the listener or null to remove it. */ public void setPushNotificationListener(CTPNotificationListener pushListener) { this.pushListener = pushListener; } /** * Stops the service. * If a connection is active the server is notified by sending a bye * command. If IO operations are pending, they are terminated by closing the * socket connection. This will result in exceptions that will resume the * threads. They all will stop as the "done" flag is set to true. */ public void stopService() { synchronized (lock) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Asked to stop"); } // This will gently stop the threads of the CTP service done = true; try { if (state >= AUTHENTICATED) { // Send BYE command CTPMessage byeMessage = new CTPMessage(); byeMessage.setCommand(CM_BYE); sendMessage(byeMessage); } } catch (IOException e) { Log.error(TAG_LOG, "Send of BYE command failed", e); } finally { // CTPMessage response = receiveMessageWithTimeout(); // Cancel any pending timer (so the timer thread can be stopped) timer.cancel(); // If we have any IO operation pending we must awake the the // corresponding thread. We do this by closing the connection which will // cause some exceptions. disconnect(); instanceRunning = false; } } } /** * This is the thread entry point. This method implements the real service * startup sequence. Such a sequence is described in the CTP design * document. * Beside performing the service startup, this method activates the * heartbeat generator and if authentication is OK it invokes the listen * method that waits for server messages. */ public void run() { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Starting CTPService thread"); } state = DISCONNECTED; int ctpRetry = config.getCtpRetry(); int connectionTentatives = 0; done = false; // TODO FIXME: this is temporary. At the moment we do not store the // nonce across sessions. Therefore the first authentication attempt // always fails. We do not want to wait between the first and second // attempt, thus we need to track the fact this is the first attempt boolean first = true; while (!done) { try { if (state != CONNECTED) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Attempting Connection # " + connectionTentatives); } connect(connectionTentatives++); } if (state == CONNECTED) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Connection Successful. Authenticating..."); } // Authenticate int authStatus = authenticate(); if ((authStatus == ST_UNAUTHORIZED) || (authStatus == ST_FORBIDDEN)) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Authentication failed"); } // No point in trying again done = true; } else if (authStatus == ST_OK) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Authentication Successful"); } // Reset the ctpRetry time ctpRetry = config.getCtpRetry(); // Start the heartbeat generator if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Starting the heartbeat generator"); } if (heartbeatGenerator == null) { heartbeatGenerator = new HeartbeatGenerator(); } if (!heartbeatGenerator.isRunning()) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Running heartbeat generator in a thread"); } heartbeatGenerator.start(); } else { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Heartbeat generator is" + " altrady running, no need to start it again"); } } // Start the responses listener listenCTPMessages(); } } } catch (Throwable e) { // If we are done, then this is not a real error, just forced // the connection to go down if (!done) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Exception reading stream: " + e.toString()); } } else { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Forced Disconnection , disconnecting"); } } disconnect(); } try { if (!done && !first) { // we need to close the connection otherwise we'll keep it open // forever disconnect(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Suspending for " + ctpRetry + " seconds"); } sleepSecs(ctpRetry); // The retry time is doubled at each tentative to // minimize the impact of failures. We keep increasing // up to the configured limit. On success the ctpRetry // is reinitialized. if (ctpRetry * 2 < config.getCtpMaxRetry()) { ctpRetry *= 2; } } if (first) { // we need to close the connection otherwise we'll keep it open // forever first = false; disconnect(); } // this try / catch is to ensure that whatever happens we're // closing the connection (to avoid leaving opened connections // that can block some devices like the blackberry // TODO: fix this because it's Really Horrible Code } catch (Throwable e) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Throwable catched: disconnecting!"); } disconnect(); } if (isOfflineMode()) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Application is offline, stopping service"); } stopService(); } else if (!done) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Application is not offline, keep on ctp'ing!"); } } } if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Finishing CTPService thread"); } } /** * * @return current state */ public int getServiceState() { return state; } public String getCTPStringState() { switch (state) { case DISCONNECTED: return "Disconnected"; case CONNECTING: return "Connecting..."; case CONNECTED: return "Connected"; case AUTHENTICATING: return "Authenticating..."; case AUTHENTICATED: return "Authenticated"; case LISTENING: return "SAN Listening..."; } return ""; } //////////////////////////////////////////////// Private methods /** * Close the connection, forcing exceptions if there are pending network IO * operations. */ protected void closeConnection() { if (os != null) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Closing output stream"); } try { os.close(); } catch (IOException ioe) { // No problem here } finally { os = null; } } if (is != null) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Closing input stream"); } try { is.close(); } catch (IOException ioe) { // No problem here } finally { is = null; } } if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Checking if we need to close socket..."); } if (sc != null) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Closing socket connection"); } try { sc.close(); } catch (IOException e) { Log.error(TAG_LOG, "Cannot force socket closure"); } finally { sc = null; } } else { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "No need to close socket..."); } } if (ctpListener != null) { ctpListener.CTPDisconnected(); } } /** * Sleeps the given number of seconds. If the sleep gets interrupted, the * method simply returns and does not wait till the sleeping time is really * elapsed. * * @param secs number of seconds */ private void sleepSecs(int secs) { try { Thread.sleep(secs * 1000); } catch (InterruptedException e) { // Ignore it } if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Exiting from sleep"); } } /** * Perform the CTP connecting phase. The main purpose of the connecting * phase is to open the socket and the IO streams. The server addres is * retrived from the push configuration. * * @param retry the number of tentative */ protected void connect(int retry) throws IOException { String uri = "socket://" + config.getCtpServer() + ":" + config.getCtpPort(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Connecting to " + uri); } // Start connecting state = CONNECTING; if (ctpListener != null) { ctpListener.CTPConnecting(); } try { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Opening socket"); } sc = connectionManager.openSocketConnection(config.getCtpServer(), config.getCtpPort(), SocketAdapter.READ_WRITE, false); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Setting socket options"); } sc.setSocketOption(SocketAdapter.LINGER, 5); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Opening socket output stream"); } os = sc.openOutputStream(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Opening socket input stream"); } is = sc.openInputStream(); state = CONNECTED; if (ctpListener != null) { ctpListener.CTPConnected(); } if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Connection Successful"); } } catch (IOException ioe) { Log.error(TAG_LOG, "Cannot open CTP connection to: " + uri, ioe); state = DISCONNECTED; if (ctpListener != null) { ctpListener.CTPDisconnected(); } throw ioe; } } /** * Disconnect from server. This is not the CTP protocol disconnection, as we * do not send the BYE command. It is rather a socket disconnect where we * close the socket and its streams. */ protected void disconnect() { if (state != DISCONNECTED) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Putting CTP in in DISCONNECTED state..."); } state = DISCONNECTED; // set okreceived to true to avoid the heartbet to // kill the connection okReceived = true; closeConnection(); } else { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "CTP is already in DISCONNECTED state, I've nothing to do!"); } } } //TODO: remove once debug is done /** * force disconnection */ public void forceDisconnect() { disconnect(); } /** * make PushConfig available for CTPService clients */ public PushConfig getConfig() { return config; } /** * This method is for debugging purpose only. It prints a byte array into a * string. Each byte is dumped as hex. */ private String byteArrayToString(byte[] array) { StringBuffer res = new StringBuffer(); for (int i = 0; i < array.length; ++i) { String hexString = Integer.toHexString(array[i] & 0xFF); res.append(hexString + " "); } return res.toString(); } /** * This method creates the authentication message. The credentials are * grabbed from the push config and the message is encrypted using the MD5 * authentication algorithm. * The authentication information is packed into a valid CTP message which * is returned. * * @return the CTP message to be used for authentication. */ private CTPMessage createAuthMessage() { CTPMessage authMessage = new CTPMessage(); authMessage.setCommand(CM_AUTH); String username = config.getCtpUsername(); String password = config.getCtpPassword(); byte[] nonce = config.getCtpNonce(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Create credentials for " + username); Log.debug(TAG_LOG, "CreateAuthMessage nonce is " + new String(Base64.encode(nonce)) + " ---- " + byteArrayToString(nonce)); } authMessage.addParameter(P_DEVID, config.getDeviceId().getBytes()); authMessage.addParameter(P_USERNAME, username.getBytes()); MD5 md5 = new MD5(); byte[] credentials = md5.computeMD5Credentials(username, password, nonce); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Credentials " + new String(Base64.encode(credentials)) + " ---- " + byteArrayToString(credentials)); } authMessage.addParameter(P_CRED, credentials); // TODO handle the FROM here return authMessage; } /** * This method performs the CTP authentication. The authentication process * is described in the CTP design document. Basically we build an * authentication message using the last nonce saved in the configuration. * If the server authenticates us, then the method terminates, otherwise it * grabs the new nonce and retry authentication. The server may responds in * several different ways to the authentication request. Depending on such a * response we decide if re-authenticating or aborting the authentication * process. * * @return the last authentication status sent by the server. */ protected int authenticate() throws IOException { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Start CTP authentication"); } state = AUTHENTICATING; if (ctpListener != null) { ctpListener.CTPAuthenticating(); } CTPMessage authMessage = createAuthMessage(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Sending CTP authentication message"); } sendMessage(authMessage); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Waiting CTP response"); } CTPMessage response = receiveMessageWithTimeout(); int authStatus = response.getCommand(); switch (authStatus) { case ST_NOT_AUTHENTICATED: // // Retry with new nonce received // if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Client not authenticated: retry with new nonce"); } config.setCtpNonce(response.getNonce()); // Send 2nd auth msg if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Re-Sending CTP authentication message"); } authMessage = createAuthMessage(); sendMessage(authMessage); // Check 2nd status received, only OK allowed if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Waiting CTP response"); } response = receiveMessageWithTimeout(); authStatus = response.getCommand(); if (authStatus == ST_OK) { // *** Authentication OK! *** // Save nonce if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Client authenticated successfully!"); } config.setCtpNonce(response.getNonce()); state = AUTHENTICATED; if (ctpListener != null) { ctpListener.CTPAuthenticated(); } sendBlindReadyMessage(); } else { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "CTP error: Client not authenticated. Please check your credentials."); } } break; case ST_OK: // *** Authentication OK! *** if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "client authenticated successfully!"); } // Save nonce if any config.setCtpNonce(response.getNonce()); state = AUTHENTICATED; sendBlindReadyMessage(); if (ctpListener != null) { ctpListener.CTPAuthenticated(); } break; // --- note: JUMP not implemented Server side ---- case ST_JUMP: // // Jump to desired server 'to' and save the 'from' value // Log.error(TAG_LOG, "Server requested a JUMP. Not supported yet."); break; case ST_UNAUTHORIZED: // Not authorized -> save nonce if any, exit thread if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Unauthorized by the Server, please check your credentials."); } config.setCtpNonce(response.getNonce()); break; case ST_FORBIDDEN: // Authentication forbidden -> exit thread Log.error(TAG_LOG, "Authentication forbidden by the Server, please check your credentials."); break; case ST_ERROR: // Error -> restore connection if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Received ERROR status from Server: restore ctp connection"); } break; default: // Unexpected status -> restore connection Log.error(TAG_LOG, "Unexpected status received " + authStatus); } return authStatus; } /** * Sends a message to the server and monitor the operation via a timer. If * the operation does not terminate after the timeout, then the * ConnectionTimer will reset the connection, causing the method to trap an * exception that will be re-thrown. * * @param message the CTP message to be sent * @throws IOException if the socket writing fails */ protected void sendMessage(CTPMessage message) throws IOException { byte[] msgBytes = message.getBytes(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Sending message " + byteArrayToString(msgBytes)); } if (os == null) { throw new IOException("Output stream is null, cannot write"); } // Lock the output stream to avoid mixing messages synchronized (os) { // Set the timer ConnectionTimer connTimer = new ConnectionTimer(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "SendMessage: Programming alarm in " + connTimer.getDelay() + " msec"); } timer.schedule(connTimer, connTimer.getDelay()); // Perform the I/O operation try { os.write(msgBytes); os.flush(); connTimer.endOperation(); } catch (Exception e) { connTimer.endOperation(); throw new IOException("[CTPService.sendMessage]Cannot write output stream: " + e); } } } /** * Receives a message from the server. The read operation is guarded by a * timeout. If nothing is received within the timeout then ConnectionTimer * will reset the connection, causing the method to trap an * exception that will be re-thrown. * * @return the CTP message received * @throws IOException if the socket writing fails */ protected CTPMessage receiveMessageWithTimeout() throws IOException { ConnectionTimer connTimer = new ConnectionTimer(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "ReceivemessageWithTimeout: Programming alarm in " + connTimer.getDelay() + " msec"); } timer.schedule(connTimer, connTimer.getDelay()); try { CTPMessage res = receiveMessage(); connTimer.endOperation(); return res; } catch (IOException e) { connTimer.endOperation(); throw e; } } /** * Receives a message from the server (wait till a message is received or an * error is encountered). * * @return the CTP message received * @throws IOException if the socket writing fails */ protected CTPMessage receiveMessage() throws IOException { byte[] sizeBytes = new byte[2]; int bytesRead = is.read(sizeBytes, 0, 2); if (bytesRead != 2) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Read " + bytesRead + " bytes"); } throw new IOException("Cannot read message size"); } else { int size = (int) sizeBytes[0] * 10 + (int) sizeBytes[1]; if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "read size is " + size); } // The protocol specifies the max message length. If the received // message exceeds it, then we throw an exception if (size + 2 > MAX_MESSAGE_SIZE) { // This is not really an IOException, but we do not want to // introduce too many exceptions as each class takes some space // in the jar. And anyway the handling is the same. throw new IOException("Message length exceeds MAX_MESSAGE_SIZE"); } byte[] rawMessage = new byte[2 + size]; bytesRead = is.read(rawMessage, 2, size); if (bytesRead != size) { throw new IOException("Cannot read message content"); } else { rawMessage[0] = sizeBytes[0]; rawMessage[1] = sizeBytes[1]; CTPMessage message = new CTPMessage(rawMessage); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Received message " + byteArrayToString(rawMessage)); } return message; } } } /** * This method waits for messages from the server, till the service is * active. It aborts on error throwing an IOException, or if the server * communicates an error. * If the received message is a SYNC, then the listener (if any) is * notified with the proper SAN message. * * @throws IOException if there is an error during stream reading * */ private void listenCTPMessages() throws IOException { state = LISTENING; if (ctpListener != null) { ctpListener.CTPListening(); } if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "SAN Listening mode"); } SANMessageParser smp = new SANMessageParser(); while (!done) { CTPMessage message = null; try { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Reading message on input stream"); } message = receiveMessage(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Message received"); } } catch (IOException ioe) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Exception while reading server message:" + ioe); } throw ioe; } int status = message.getCommand(); switch (status) { case ST_OK: // 'OK' to our 'READY' command -> back to recv okReceived = true; if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "OK received"); } break; case ST_SYNC: // // Start the sync! // --------------- if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Notification received."); } // We could assert: number of params == 1, first param // code is SAN. If something is wrong we go to the // exception code, because the handling is the same if (pushListener != null) { byte[] sanBytes = message.getParameterValue(0); try { SANMessage sanMessage = smp.parseMessage(sanBytes, false); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Handle SAN message"); } pushListener.handleMessage(sanMessage); } catch (MessageParserException mpe) { Log.error(TAG_LOG, "Error parsing SAN message"); Log.error(TAG_LOG, "Sync cannot start"); } } // Back to recv if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Sync started"); } break; case ST_ERROR: if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "ERROR message received"); } disconnect(); return; default: // Error from server -> exit thread (will try restoring the socket from scratch) if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Bad status received " + status); } disconnect(); return; } } } }