package org.ripple.power.server.socket; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Logger; class IOConnection implements IOCallback { static final Logger logger = Logger.getLogger("io.socket"); public static final String FRAME_DELIMITER = "\ufffd"; private static final int STATE_INIT = 0; private static final int STATE_HANDSHAKE = 1; private static final int STATE_CONNECTING = 2; private static final int STATE_READY = 3; private static final int STATE_INTERRUPTED = 4; private static final int STATE_INVALID = 6; private int state = STATE_INIT; public static final String SOCKET_IO_1 = "/socket.io/1/"; private static SSLContext sslContext = null; private static HashMap<String, List<IOConnection>> connections = new HashMap<String, List<IOConnection>>(); private URL url; private IOTransport transport; private int connectTimeout = 10000; private String sessionId; private long heartbeatTimeout; private long closingTimeout; private List<String> protocols; private ConcurrentLinkedQueue<String> outputBuffer = new ConcurrentLinkedQueue<String>(); private HashMap<String, SocketIO> sockets = new HashMap<String, SocketIO>(); private Properties headers; private SocketIO firstSocket = null; final private Timer backgroundTimer = new Timer("backgroundTimer"); private String urlStr; private Exception lastException; private int nextId = 1; HashMap<Integer, IOAcknowledge> acknowledge = new HashMap<Integer, IOAcknowledge>(); private boolean keepAliveInQueue; private HearbeatTimeoutTask heartbeatTimeoutTask; private class HearbeatTimeoutTask extends TimerTask { @Override public void run() { error(new SocketIOException( "Timeout Error. No heartbeat from server within life time of the socket. closing.", lastException)); } } private ReconnectTask reconnectTask = null; private class ReconnectTask extends TimerTask { @Override public void run() { connectTransport(); if (!keepAliveInQueue) { sendPlain("2::"); keepAliveInQueue = true; } } } private class ConnectThread extends Thread { public ConnectThread() { super("ConnectThread"); } @Override public void run() { if (IOConnection.this.getState() == STATE_INIT) { handshake(); } connectTransport(); } }; public static void setSslContext(SSLContext sslContext) { IOConnection.sslContext = sslContext; } public static SSLContext getSslContext() { return sslContext; } static public IOConnection register(String origin, SocketIO socket) { List<IOConnection> list = connections.get(origin); if (list == null) { list = new LinkedList<IOConnection>(); connections.put(origin, list); } else { synchronized (list) { for (IOConnection connection : list) { if (connection.register(socket)) return connection; } } } IOConnection connection = new IOConnection(origin, socket); list.add(connection); return connection; } public synchronized boolean register(SocketIO socket) { String namespace = socket.getNamespace(); if (sockets.containsKey(namespace)) return false; sockets.put(namespace, socket); socket.setHeaders(headers); IOMessage connect = new IOMessage(IOMessage.TYPE_CONNECT, socket.getNamespace(), ""); sendPlain(connect.toString()); return true; } public synchronized void unregister(SocketIO socket) { sendPlain("0::" + socket.getNamespace()); sockets.remove(socket.getNamespace()); socket.getCallback().onDisconnect(); if (sockets.size() == 0) { cleanup(); } } private void handshake() { URL url; String response; URLConnection connection; try { setState(STATE_HANDSHAKE); url = new URL(IOConnection.this.url.toString() + SOCKET_IO_1); connection = url.openConnection(); if (connection instanceof HttpsURLConnection) { ((HttpsURLConnection) connection) .setSSLSocketFactory(sslContext.getSocketFactory()); } connection.setConnectTimeout(connectTimeout); connection.setReadTimeout(connectTimeout); for (Entry<Object, Object> entry : headers.entrySet()) { connection.setRequestProperty((String) entry.getKey(), (String) entry.getValue()); } InputStream stream = connection.getInputStream(); Scanner in = new Scanner(stream); response = in.nextLine(); String[] data = response.split(":"); sessionId = data[0]; heartbeatTimeout = Long.parseLong(data[1]) * 1000; closingTimeout = Long.parseLong(data[2]) * 1000; protocols = Arrays.asList(data[3].split(",")); in.close(); } catch (Exception e) { error(new SocketIOException("Error while handshaking", e)); } } private synchronized void connectTransport() { if (getState() == STATE_INVALID) return; setState(STATE_CONNECTING); if (protocols.contains(WebsocketTransport.TRANSPORT_NAME)) transport = WebsocketTransport.create(url, this); else if (protocols.contains(XHRTransport.TRANSPORT_NAME)) transport = XHRTransport.create(url, this); else { error(new SocketIOException( "Server supports no available transports. You should reconfigure the server to support a available transport")); return; } transport.connect(); } private IOAcknowledge remoteAcknowledge(IOMessage message) { String _id = message.getId(); if (_id.equals("")) return null; else if (_id.endsWith("+") == false) _id = _id + "+"; final String id = _id; final String endPoint = message.getEndpoint(); return new IOAcknowledge() { @Override public void ack(Object... args) { JSONArray array = new JSONArray(); for (Object o : args) { try { array.put(o == null ? JSONObject.NULL : o); } catch (Exception e) { error(new SocketIOException( "You can only put values in IOAcknowledge.ack() which can be handled by JSONArray.put()", e)); } } IOMessage ackMsg = new IOMessage(IOMessage.TYPE_ACK, endPoint, id + array.toString()); sendPlain(ackMsg.toString()); } }; } private void synthesizeAck(IOMessage message, IOAcknowledge ack) { if (ack != null) { int id = nextId++; acknowledge.put(id, ack); message.setId(id + "+"); } } private IOConnection(String url, SocketIO socket) { try { this.url = new URL(url); this.urlStr = url; } catch (MalformedURLException e) { throw new RuntimeException(e); } firstSocket = socket; headers = socket.getHeaders(); sockets.put(socket.getNamespace(), socket); new ConnectThread().start(); } private synchronized void cleanup() { setState(STATE_INVALID); if (transport != null) transport.disconnect(); sockets.clear(); synchronized (connections) { List<IOConnection> con = connections.get(urlStr); if (con != null && con.size() > 1) con.remove(this); else connections.remove(urlStr); } logger.info("Cleanup"); backgroundTimer.cancel(); } private void error(SocketIOException e) { for (SocketIO socket : sockets.values()) { socket.getCallback().onError(e); } cleanup(); } private synchronized void sendPlain(String text) { if (getState() == STATE_READY) try { logger.info("> " + text); transport.send(text); } catch (Exception e) { logger.info("IOEx: saving"); outputBuffer.add(text); } else { outputBuffer.add(text); } } private void invalidateTransport() { if (transport != null) transport.invalidate(); transport = null; } private synchronized void resetTimeout() { if (heartbeatTimeoutTask != null) { heartbeatTimeoutTask.cancel(); } if (getState() != STATE_INVALID) { heartbeatTimeoutTask = new HearbeatTimeoutTask(); backgroundTimer.schedule(heartbeatTimeoutTask, closingTimeout + heartbeatTimeout); } } private IOCallback findCallback(IOMessage message) throws SocketIOException { if ("".equals(message.getEndpoint())) return this; SocketIO socket = sockets.get(message.getEndpoint()); if (socket == null) { throw new SocketIOException("Cannot find socket for '" + message.getEndpoint() + "'"); } return socket.getCallback(); } public synchronized void transportConnected() { setState(STATE_READY); if (reconnectTask != null) { reconnectTask.cancel(); reconnectTask = null; } resetTimeout(); if (transport.canSendBulk()) { ConcurrentLinkedQueue<String> outputBuffer = this.outputBuffer; this.outputBuffer = new ConcurrentLinkedQueue<String>(); try { String[] texts = outputBuffer.toArray(new String[outputBuffer .size()]); logger.info("Bulk start:"); for (String text : texts) { logger.info("> " + text); } logger.info("Bulk end"); transport.sendBulk(texts); } catch (IOException e) { this.outputBuffer = outputBuffer; } } else { String text; while ((text = outputBuffer.poll()) != null) sendPlain(text); } this.keepAliveInQueue = false; } public void transportDisconnected() { this.lastException = null; setState(STATE_INTERRUPTED); reconnect(); } public void transportError(Exception error) { this.lastException = error; setState(STATE_INTERRUPTED); reconnect(); } public void transportData(String text) { if (!text.startsWith(FRAME_DELIMITER)) { transportMessage(text); return; } Iterator<String> fragments = Arrays.asList(text.split(FRAME_DELIMITER)) .listIterator(1); while (fragments.hasNext()) { int length = Integer.parseInt(fragments.next()); String string = (String) fragments.next(); if (length != string.length()) { error(new SocketIOException("Garbage from server: " + text)); return; } transportMessage(string); } } public void transportMessage(String text) { logger.info("< " + text); IOMessage message; try { message = new IOMessage(text); } catch (Exception e) { error(new SocketIOException("Garbage from server: " + text, e)); return; } resetTimeout(); switch (message.getType()) { case IOMessage.TYPE_DISCONNECT: try { findCallback(message).onDisconnect(); } catch (Exception e) { error(new SocketIOException( "Exception was thrown in onDisconnect()", e)); } break; case IOMessage.TYPE_CONNECT: try { if (firstSocket != null && "".equals(message.getEndpoint())) { if (firstSocket.getNamespace().equals("")) { firstSocket.getCallback().onConnect(); } else { IOMessage connect = new IOMessage( IOMessage.TYPE_CONNECT, firstSocket.getNamespace(), ""); sendPlain(connect.toString()); } } else { findCallback(message).onConnect(); } firstSocket = null; } catch (Exception e) { error(new SocketIOException( "Exception was thrown in onConnect()", e)); } break; case IOMessage.TYPE_HEARTBEAT: sendPlain("2::"); break; case IOMessage.TYPE_MESSAGE: try { findCallback(message).onMessage(message.getData(), remoteAcknowledge(message)); } catch (Exception e) { error(new SocketIOException( "Exception was thrown in onMessage(String).\n" + "Message was: " + message.toString(), e)); } break; case IOMessage.TYPE_JSON_MESSAGE: try { JSONObject obj = null; String data = message.getData(); if (data.trim().equals("null") == false) obj = new JSONObject(data); try { findCallback(message).onMessage(obj, remoteAcknowledge(message)); } catch (Exception e) { error(new SocketIOException( "Exception was thrown in onMessage(JSONObject).\n" + "Message was: " + message.toString(), e)); } } catch (JSONException e) { logger.warning("Malformated JSON received"); } break; case IOMessage.TYPE_EVENT: try { JSONObject event = new JSONObject(message.getData()); Object[] argsArray; if (event.has("args")) { JSONArray args = event.getJSONArray("args"); argsArray = new Object[args.length()]; for (int i = 0; i < args.length(); i++) { if (args.isNull(i) == false) argsArray[i] = args.get(i); } } else argsArray = new Object[0]; String eventName = event.getString("name"); try { findCallback(message).on(eventName, remoteAcknowledge(message), argsArray); } catch (Exception e) { error(new SocketIOException( "Exception was thrown in on(String, JSONObject[]).\n" + "Message was: " + message.toString(), e)); } } catch (JSONException e) { logger.warning("Malformated JSON received"); } break; case IOMessage.TYPE_ACK: String[] data = message.getData().split("\\+", 2); if (data.length == 2) { try { int id = Integer.parseInt(data[0]); IOAcknowledge ack = acknowledge.get(id); if (ack == null) logger.warning("Received unknown ack packet"); else { JSONArray array = new JSONArray(data[1]); Object[] args = new Object[array.length()]; for (int i = 0; i < args.length; i++) { args[i] = array.get(i); } ack.ack(args); } } catch (NumberFormatException e) { logger.warning("Received malformated Acknowledge! This is potentially filling up the acknowledges!"); } catch (JSONException e) { logger.warning("Received malformated Acknowledge data!"); } } else if (data.length == 1) { sendPlain("6:::" + data[0]); } break; case IOMessage.TYPE_ERROR: try { findCallback(message).onError( new SocketIOException(message.getData())); } catch (SocketIOException e) { error(e); } if (message.getData().endsWith("+0")) { cleanup(); } break; case IOMessage.TYPE_NOOP: break; default: logger.warning("Unkown type received" + message.getType()); break; } } public synchronized void reconnect() { if (getState() != STATE_INVALID) { invalidateTransport(); setState(STATE_INTERRUPTED); if (reconnectTask != null) { reconnectTask.cancel(); } reconnectTask = new ReconnectTask(); backgroundTimer.schedule(reconnectTask, 1000); } } public String getSessionId() { return sessionId; } public void send(SocketIO socket, IOAcknowledge ack, String text) { IOMessage message = new IOMessage(IOMessage.TYPE_MESSAGE, socket.getNamespace(), text); synthesizeAck(message, ack); sendPlain(message.toString()); } public void send(SocketIO socket, IOAcknowledge ack, JSONObject json) { IOMessage message = new IOMessage(IOMessage.TYPE_JSON_MESSAGE, socket.getNamespace(), json.toString()); synthesizeAck(message, ack); sendPlain(message.toString()); } public void emit(SocketIO socket, String event, IOAcknowledge ack, Object... args) { try { JSONObject json = new JSONObject().put("name", event).put("args", new JSONArray(Arrays.asList(args))); IOMessage message = new IOMessage(IOMessage.TYPE_EVENT, socket.getNamespace(), json.toString()); synthesizeAck(message, ack); sendPlain(message.toString()); } catch (JSONException e) { error(new SocketIOException( "Error while emitting an event. Make sure you only try to send arguments, which can be serialized into JSON.")); } } public void emitX(SocketIO socket, String event, IOAcknowledge ack, Object arg) { try { JSONObject json = new JSONObject().put("name", event).put("args", arg); String jsonStr = json.toString(); logger.warning("jsonStr=" + jsonStr); jsonStr = jsonStr.replace("\"[", "["); logger.warning("jsonStr1=" + jsonStr); jsonStr = jsonStr.replace("]\"", "]"); logger.warning("jsonStr2=" + jsonStr); jsonStr = jsonStr.replace("\\\"", "\""); logger.warning("jsonStr3=" + jsonStr); IOMessage message = new IOMessage(IOMessage.TYPE_EVENT, socket.getNamespace(), jsonStr); synthesizeAck(message, ack); String str = message.toString(); logger.warning("message str=" + str); sendPlain(str); } catch (JSONException e) { error(new SocketIOException( "Error while emitting an event. Make sure you only try to send arguments, which can be serialized into JSON.")); } } public boolean isConnected() { return getState() == STATE_READY; } private synchronized int getState() { return state; } private synchronized void setState(int state) { if (getState() != STATE_INVALID) this.state = state; } public IOTransport getTransport() { return transport; } @Override public void onDisconnect() { SocketIO socket = sockets.get(""); if (socket != null) socket.getCallback().onDisconnect(); } @Override public void onConnect() { SocketIO socket = sockets.get(""); if (socket != null) socket.getCallback().onConnect(); } @Override public void onMessage(String data, IOAcknowledge ack) { for (SocketIO socket : sockets.values()) socket.getCallback().onMessage(data, ack); } @Override public void onMessage(JSONObject json, IOAcknowledge ack) { for (SocketIO socket : sockets.values()) socket.getCallback().onMessage(json, ack); } @Override public void on(String event, IOAcknowledge ack, Object... args) { for (SocketIO socket : sockets.values()) socket.getCallback().on(event, ack, args); } @Override public void onError(SocketIOException socketIOException) { for (SocketIO socket : sockets.values()) socket.getCallback().onError(socketIOException); } }