// --------------------------------------------------------------------------- // jWebSocket - The jWebSocket System Plug-In // Copyright (c) 2010 Alexander Schulze, Innotrade GmbH // --------------------------------------------------------------------------- // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published by the // Free Software Foundation; either version 3 of the License, or (at your // option) any later version. // 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 Lesser General Public License for // more details. // You should have received a copy of the GNU Lesser General Public License along // with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>. // --------------------------------------------------------------------------- package org.jwebsocket.plugins.system; import java.util.List; import java.util.Random; import javolution.util.FastList; import javolution.util.FastMap; import org.apache.log4j.Logger; import org.jwebsocket.api.WebSocketConnector; import org.jwebsocket.config.JWebSocketCommonConstants; import org.jwebsocket.config.JWebSocketServerConstants; import org.jwebsocket.connectors.BaseConnector; import org.jwebsocket.kit.BroadcastOptions; import org.jwebsocket.kit.CloseReason; import org.jwebsocket.logging.Logging; import org.jwebsocket.kit.PlugInResponse; import org.jwebsocket.plugins.TokenPlugIn; import org.jwebsocket.security.SecurityFactory; import org.jwebsocket.token.Token; import org.jwebsocket.util.Tools; /** * implements the jWebSocket system tokens like login, logout, send, * broadcast etc... * @author aschulze */ public class SystemPlugIn extends TokenPlugIn { private static Logger log = Logging.getLogger(SystemPlugIn.class); // specify name space for system plug-in private static final String NS_SYSTEM_DEFAULT = JWebSocketServerConstants.NS_BASE + ".plugins.system"; // specify token types processed by system plug-in private static final String TT_SEND = "send"; private static final String TT_BROADCAST = "broadcast"; private static final String TT_WELCOME = "welcome"; private static final String TT_GOODBYE = "goodBye"; private static final String TT_LOGIN = "login"; private static final String TT_LOGOUT = "logout"; private static final String TT_CLOSE = "close"; private static final String TT_GETCLIENTS = "getClients"; private static final String TT_PING = "ping"; private static final String TT_ALLOC_CHANNEL = "alloc"; private static final String TT_DEALLOC_CHANNEL = "dealloc"; // specify shared connector variables private static final String VAR_GROUP = NS_SYSTEM_DEFAULT + ".group"; /** * */ public SystemPlugIn() { if (log.isDebugEnabled()) { log.debug("Instantiating system plug-in..."); } // specify default name space for system plugin this.setNamespace(NS_SYSTEM_DEFAULT); } @Override public void processToken(PlugInResponse aResponse, WebSocketConnector aConnector, Token aToken) { String lType = aToken.getType(); String lNS = aToken.getNS(); if (lType != null && (lNS == null || lNS.equals(getNamespace()))) { if (lType.equals(TT_SEND)) { send(aConnector, aToken); aResponse.abortChain(); } else if (lType.equals(TT_BROADCAST)) { broadcast(aConnector, aToken); aResponse.abortChain(); } else if (lType.equals(TT_LOGIN)) { login(aConnector, aToken); aResponse.abortChain(); } else if (lType.equals(TT_LOGOUT)) { logout(aConnector, aToken); aResponse.abortChain(); } else if (lType.equals(TT_CLOSE)) { close(aConnector, aToken); aResponse.abortChain(); } else if (lType.equals(TT_GETCLIENTS)) { getClients(aConnector, aToken); aResponse.abortChain(); } else if (lType.equals(TT_PING)) { ping(aConnector, aToken); } else if (lType.equals(TT_ALLOC_CHANNEL)) { allocChannel(aConnector, aToken); } else if (lType.equals(TT_DEALLOC_CHANNEL)) { deallocChannel(aConnector, aToken); } } } @Override public void connectorStarted(WebSocketConnector aConnector) { // set session id first, so that it can be processed in the connectorStarted method Random rand = new Random(System.nanoTime()); // TODO: if unique node id is passed check if already assigned in the network and reject connect if so! aConnector.getSession().setSessionId(Tools.getMD5(aConnector.generateUID() + "." + rand.nextInt())); // call super connectorStarted super.connectorStarted(aConnector); // and send the welcome message incl. the session id sendWelcome(aConnector); // if new connector is active broadcast this event to then network broadcastConnectEvent(aConnector); } @Override public void connectorStopped(WebSocketConnector aConnector, CloseReason aCloseReason) { super.connectorStopped(aConnector, aCloseReason); // notify other clients that client disconnected broadcastDisconnectEvent(aConnector); } private String getGroup(WebSocketConnector aConnector) { return aConnector.getString(VAR_GROUP); } private void setGroup(WebSocketConnector aConnector, String aGroup) { aConnector.setString(VAR_GROUP, aGroup); } private void removeGroup(WebSocketConnector aConnector) { aConnector.removeVar(VAR_GROUP); } /** * * * @param aConnector */ public void broadcastConnectEvent(WebSocketConnector aConnector) { if (log.isDebugEnabled()) { log.debug("Broadcasting connect..."); } // broadcast connect event to other clients of the jWebSocket network Token lConnect = new Token(Token.TT_EVENT); lConnect.put("name", "connect"); // lConnect.put("usid", getSessionId(aConnector)); lConnect.put("sourceId", aConnector.getId()); // if a unique node id is specified for the client include that String lNodeId = aConnector.getNodeId(); if (lNodeId != null) { lConnect.put("unid", lNodeId); } lConnect.put("clientCount", getConnectorCount()); // broadcast to all except source broadcastToken(aConnector, lConnect); } /** * * * @param aConnector */ public void broadcastDisconnectEvent(WebSocketConnector aConnector) { if (log.isDebugEnabled()) { log.debug("Broadcasting disconnect..."); } // broadcast connect event to other clients of the jWebSocket network Token lDisconnect = new Token(Token.TT_EVENT); lDisconnect.put("name", "disconnect"); // lDisconnect.put("usid", getSessionId(aConnector)); lDisconnect.put("sourceId", aConnector.getId()); // if a unique node id is specified for the client include that String lNodeId = aConnector.getNodeId(); if (lNodeId != null) { lDisconnect.put("unid", lNodeId); } lDisconnect.put("clientCount", getConnectorCount()); // broadcast to all except source broadcastToken(aConnector, lDisconnect); } private void sendWelcome(WebSocketConnector aConnector) { if (log.isDebugEnabled()) { log.debug("Sending welcome..."); } // send "welcome" token to client Token lWelcome = new Token(TT_WELCOME); lWelcome.put("vendor", JWebSocketCommonConstants.VENDOR); lWelcome.put("version", JWebSocketServerConstants.VERSION_STR); // here the session id is MANDATORY! to pass to the client! lWelcome.put("usid", aConnector.getSession().getSessionId()); lWelcome.put("sourceId", aConnector.getId()); // if a unique node id is specified for the client include that String lNodeId = aConnector.getNodeId(); if (lNodeId != null) { lWelcome.put("unid", lNodeId); } lWelcome.put("timeout", aConnector.getEngine().getConfiguration().getTimeout()); sendToken(aConnector, aConnector, lWelcome); } /** * */ private void broadcastLoginEvent(WebSocketConnector aConnector) { if (log.isDebugEnabled()) { log.debug("Broadcasting login event..."); } // broadcast login event to other clients of the jWebSocket network Token lLogin = new Token(Token.TT_EVENT); lLogin.put("name", "login"); lLogin.put("username", getUsername(aConnector)); lLogin.put("clientCount", getConnectorCount()); // lLogin.put("usid", getSessionId(aConnector)); lLogin.put("sourceId", aConnector.getId()); // if a unique node id is specified for the client include that String lNodeId = aConnector.getNodeId(); if (lNodeId != null) { lLogin.put("unid", lNodeId); } // broadcast to all except source broadcastToken(aConnector, lLogin); } /** * */ private void broadcastLogoutEvent(WebSocketConnector aConnector) { if (log.isDebugEnabled()) { log.debug("Broadcasting logout event..."); } // broadcast login event to other clients of the jWebSocket network Token lLogout = new Token(Token.TT_EVENT); lLogout.put("name", "logout"); lLogout.put("username", getUsername(aConnector)); lLogout.put("clientCount", getConnectorCount()); // lLogout.put("usid", getSessionId(aConnector)); lLogout.put("sourceId", aConnector.getId()); // if a unique node id is specified for the client include that String lNodeId = aConnector.getNodeId(); if (lNodeId != null) { lLogout.put("unid", lNodeId); } // broadcast to all except source broadcastToken(aConnector, lLogout); } /** * * @param aConnector * @param aCloseReason */ private void sendGoodBye(WebSocketConnector aConnector, CloseReason aCloseReason) { if (log.isDebugEnabled()) { log.debug("Sending good bye..."); } // send "goodBye" token to client Token lGoodBye = new Token(TT_GOODBYE); lGoodBye.put("vendor", JWebSocketCommonConstants.VENDOR); lGoodBye.put("version", JWebSocketServerConstants.VERSION_STR); lGoodBye.put("sourceId", aConnector.getId()); if (aCloseReason != null) { lGoodBye.put("reason", aCloseReason.toString().toLowerCase()); } // don't send session-id on good bye, neither required nor desired sendToken(aConnector, aConnector, lGoodBye); } private void login(WebSocketConnector aConnector, Token aToken) { Token lResponse = createResponse(aToken); String lUsername = aToken.getString("username"); // TODO: Add authentication and password check String lPassword = aToken.getString("password"); String lGroup = aToken.getString("group"); if (log.isDebugEnabled()) { log.debug("Processing 'login' (username='" + lUsername + "', group='" + lGroup + "') from '" + aConnector + "'..."); } if (lUsername != null) { lResponse.put("username", lUsername); // lResponse.put("usid", getSessionId(aConnector)); lResponse.put("sourceId", aConnector.getId()); // set shared variables setUsername(aConnector, lUsername); setGroup(aConnector, lGroup); } else { lResponse.put("code", -1); lResponse.put("msg", "missing arguments for 'login' command"); } // send response to client sendToken(aConnector, aConnector, lResponse); // if successfully logged in... if (lUsername != null) { // broadcast "login event" to other clients broadcastLoginEvent(aConnector); } } private void logout(WebSocketConnector aConnector, Token aToken) { Token lResponse = createResponse(aToken); if (log.isDebugEnabled()) { log.debug("Processing 'logout' (username='" + getUsername(aConnector) + "') from '" + aConnector + "'..."); } if (getUsername(aConnector) != null) { // send good bye token as response to client sendGoodBye(aConnector, CloseReason.CLIENT); // and broadcast the logout event broadcastLogoutEvent(aConnector); // resetting the username is the only required signal for logout // lResponse.put("usid", getSessionId(aConnector)); lResponse.put("sourceId", aConnector.getId()); removeUsername(aConnector); removeGroup(aConnector); } else { lResponse.put("code", -1); lResponse.put("msg", "not logged in"); sendToken(aConnector, aConnector, lResponse); } } private void send(WebSocketConnector aConnector, Token aToken) { // check if user is allowed to run 'send' command if (!SecurityFactory.checkRight(getUsername(aConnector), NS_SYSTEM_DEFAULT + ".send")) { sendToken(aConnector, aConnector, createAccessDenied(aToken)); return; } Token lResponse = createResponse(aToken); WebSocketConnector lTargetConnector; String lTargetId = aToken.getString("unid"); if (lTargetId != null) { lTargetConnector = getNode(lTargetId); } else { // get the target lTargetId = aToken.getString("targetId"); lTargetConnector = getConnector(lTargetId); } /* if (getUsername(aConnector) != null) { */ if (lTargetConnector != null) { if (log.isDebugEnabled()) { log.debug("Processing 'send' (username='" + getUsername(aConnector) + "') from '" + aConnector + "' to " + lTargetId + "..."); } aToken.put("sourceId", aConnector.getId()); sendToken(aConnector, lTargetConnector, aToken); } else { log.warn("Target connector '" + lTargetId + "' not found."); } /* } else { lResponse.put("code", -1); lResponse.put("msg", "not logged in"); sendToken(aConnector, aConnector, lResponse); } */ } private void broadcast(WebSocketConnector aConnector, Token aToken) { // check if user is allowed to run 'broadcast' command if (!SecurityFactory.checkRight(getUsername(aConnector), NS_SYSTEM_DEFAULT + ".broadcast")) { sendToken(aConnector, aConnector, createAccessDenied(aToken)); return; } Token lResponse = createResponse(aToken); if (log.isDebugEnabled()) { log.debug("Processing 'broadcast' (username='" + getUsername(aConnector) + "') from '" + aConnector + "'..."); } /* if (getUsername(aConnector) != null) { */ aToken.put("sourceId", aConnector.getId()); // don't distribute session id here! aToken.remove("usid"); String lSenderIncluded = aToken.getString("senderIncluded"); String lResponseRequested = aToken.getString("responseRequested"); boolean bSenderIncluded = (lSenderIncluded != null && lSenderIncluded.equals("true")); boolean bResponseRequested = (lResponseRequested != null && lResponseRequested.equals("true")); // broadcast the token broadcastToken(aConnector, aToken, new BroadcastOptions(bSenderIncluded, bResponseRequested)); // check if response was requested if (bResponseRequested) { sendToken(aConnector, aConnector, lResponse); } /* } else { lResponse.put("code", -1); lResponse.put("msg", "not logged in"); sendToken(aConnector, lResponse); } */ } private void close(WebSocketConnector aConnector, Token aToken) { int lTimeout = aToken.getInteger("timeout", 0); // if logged in... if (getUsername(aConnector) != null) { // only send a good bye message if timeout is > 0 if (lTimeout > 0) { sendGoodBye(aConnector, CloseReason.CLIENT); } // broadcast the logout event. broadcastLogoutEvent(aConnector); } // reset the username, we're no longer logged in removeUsername(aConnector); if (log.isDebugEnabled()) { log.debug("Closing client " + (lTimeout > 0 ? "with timeout " + lTimeout + "ms" : "immediately") + "..."); } // don't send a response here! We're about to close the connection! // broadcasts disconnect event to other clients aConnector.stopConnector(CloseReason.CLIENT); } /** * * @param aToken */ private void echo(WebSocketConnector aConnector, Token aToken) { Token lResponseToken = createResponse(aToken); String lData = aToken.getString("data"); if (lData != null) { if (log.isDebugEnabled()) { log.debug("echo " + lData); } } else { lResponseToken.put("code", -1); lResponseToken.put("msg", "missing 'data' argument for 'echo' command"); } sendToken(aConnector, aConnector, lResponseToken); } /** * * @param aConnector * @param aToken */ public void ping(WebSocketConnector aConnector, Token aToken) { String lEcho = aToken.getString("echo"); if (log.isDebugEnabled()) { log.debug("Processing 'Ping' (echo='" + lEcho + "') from '" + aConnector + "'..."); } if (lEcho.equalsIgnoreCase("true")) { Token lResponseToken = createResponse(aToken); // TODO: here could optionally send a time stamp // TODO: implement response time on client! // lResponseToken.put("",""); sendToken(aConnector, aConnector, lResponseToken); } } /** * * @param aConnector * @param aToken */ public void getClients(WebSocketConnector aConnector, Token aToken) { Token lResponseToken = createResponse(aToken); if (log.isDebugEnabled()) { log.debug("Processing 'getClients' from '" + aConnector + "'..."); } if (getUsername(aConnector) != null) { String lGroup = aToken.getString("group"); Integer lMode = aToken.getInteger("mode", 0); FastMap lFilter = new FastMap(); lFilter.put(BaseConnector.VAR_USERNAME, ".*"); List<String> listOut = new FastList<String>(); for (WebSocketConnector lConnector : getServer().selectConnectors(lFilter).values()) { listOut.add(getUsername(lConnector) + "@" + lConnector.getRemotePort()); } lResponseToken.put("clients", listOut); lResponseToken.put("count", listOut.size()); } else { lResponseToken.put("code", -1); lResponseToken.put("msg", "not logged in"); } sendToken(aConnector, aConnector, lResponseToken); } /** * allocates a "non-interruptable" communication channel between two clients. * @param aConnector * @param aToken */ public void allocChannel(WebSocketConnector aConnector, Token aToken) { Token lResponseToken = createResponse(aToken); if (log.isDebugEnabled()) { log.debug("Processing 'allocChannel' from '" + aConnector + "'..."); } } /** * deallocates a "non-interruptable" communication channel between two clients. * @param aConnector * @param aToken */ public void deallocChannel(WebSocketConnector aConnector, Token aToken) { Token lResponseToken = createResponse(aToken); if (log.isDebugEnabled()) { log.debug("Processing 'deallocChannel' from '" + aConnector + "'..."); } } }