// ---------------------------------------------------------------------------
// 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.PluginConfiguration;
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.security.User;
import org.jwebsocket.token.BaseToken;
import org.jwebsocket.token.Token;
import org.jwebsocket.token.TokenFactory;
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_ECHO = "echo";
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";
private static boolean BROADCAST_OPEN = true;
private static final String BROADCAST_OPEN_KEY = "broadcastOpenEvent";
private static boolean BROADCAST_CLOSE = true;
private static final String BROADCAST_CLOSE_KEY = "broadcastCloseEvent";
private static boolean BROADCAST_LOGIN = true;
private static final String BROADCAST_LOGIN_KEY = "broadcastLoginEvent";
private static boolean BROADCAST_LOGOUT = true;
private static final String BROADCAST_LOGOUT_KEY = "broadcastLogoutEvent";
/**
* Default constructor
*/
public SystemPlugIn() {
this(null);
}
/**
* Constructor with configuration object
*/
public SystemPlugIn(PluginConfiguration aConfiguration) {
super(aConfiguration);
if (log.isDebugEnabled()) {
log.debug("Instantiating system plug-in...");
}
// specify default name space for system plugin
this.setNamespace(NS_SYSTEM_DEFAULT);
mGetSettings();
}
private void mGetSettings() {
// load global settings, default to "true"
BROADCAST_OPEN = "true".equals(getSetting(BROADCAST_OPEN_KEY, "true"));
BROADCAST_CLOSE = "true".equals(getSetting(BROADCAST_CLOSE_KEY, "true"));
BROADCAST_LOGIN = "true".equals(getSetting(BROADCAST_LOGIN_KEY, "true"));
BROADCAST_LOGOUT = "true".equals(getSetting(BROADCAST_LOGOUT_KEY, "true"));
}
@Override
public void processToken(PlugInResponse aResponse, WebSocketConnector aConnector, Token aToken) {
String lType = aToken.getType();
String lNS = aToken.getNS();
if (lType != null && getNamespace().equals(lNS)) {
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_ECHO)) {
echo(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 lRand = 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() + "." + lRand.nextInt()));
// 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) {
// 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) {
// only broadcast if corresponding global plugin setting is "true"
if (BROADCAST_OPEN) {
if (log.isDebugEnabled()) {
log.debug("Broadcasting connect...");
}
// broadcast connect event to other clients of the jWebSocket network
Token lConnect = TokenFactory.createToken(BaseToken.TT_EVENT);
lConnect.setString("name", "connect");
// lConnect.put("usid", getSessionId(aConnector));
lConnect.setString("sourceId", aConnector.getId());
// if a unique node id is specified for the client include that
String lNodeId = aConnector.getNodeId();
if (lNodeId != null) {
lConnect.setString("unid", lNodeId);
}
lConnect.setInteger("clientCount", getConnectorCount());
// broadcast to all except source
broadcastToken(aConnector, lConnect);
}
}
/**
*
*
* @param aConnector
*/
public void broadcastDisconnectEvent(WebSocketConnector aConnector) {
// only broadcast if corresponding global plugin setting is "true"
if (BROADCAST_CLOSE) {
if (log.isDebugEnabled()) {
log.debug("Broadcasting disconnect...");
}
// broadcast connect event to other clients of the jWebSocket network
Token lDisconnect = TokenFactory.createToken(BaseToken.TT_EVENT);
lDisconnect.setString("name", "disconnect");
// lDisconnect.put("usid", getSessionId(aConnector));
lDisconnect.setString("sourceId", aConnector.getId());
// if a unique node id is specified for the client include that
String lNodeId = aConnector.getNodeId();
if (lNodeId != null) {
lDisconnect.setString("unid", lNodeId);
}
lDisconnect.setInteger("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 = TokenFactory.createToken(TT_WELCOME);
lWelcome.setString("ns", getNamespace());
lWelcome.setString("vendor", JWebSocketCommonConstants.VENDOR);
lWelcome.setString("version", JWebSocketServerConstants.VERSION_STR);
// here the session id is MANDATORY! to pass to the client!
lWelcome.setString("usid", aConnector.getSession().getSessionId());
lWelcome.setString("sourceId", aConnector.getId());
// if a unique node id is specified for the client include that
String lNodeId = aConnector.getNodeId();
if (lNodeId != null) {
lWelcome.setString("unid", lNodeId);
}
lWelcome.setInteger("timeout", aConnector.getEngine().getConfiguration().getTimeout());
sendToken(aConnector, aConnector, lWelcome);
}
/**
*
*/
private void broadcastLoginEvent(WebSocketConnector aConnector) {
// only broadcast if corresponding global plugin setting is "true"
if (BROADCAST_LOGIN) {
if (log.isDebugEnabled()) {
log.debug("Broadcasting login event...");
}
// broadcast login event to other clients of the jWebSocket network
Token lLogin = TokenFactory.createToken(BaseToken.TT_EVENT);
lLogin.setString("name", "login");
lLogin.setString("username", getUsername(aConnector));
lLogin.setInteger("clientCount", getConnectorCount());
// do NEVER broadcast client's session id here!
// lLogin.put("usid", getSessionId(aConnector));
lLogin.setString("sourceId", aConnector.getId());
// if a unique node id is specified for the client include that
String lNodeId = aConnector.getNodeId();
if (lNodeId != null) {
lLogin.setString("unid", lNodeId);
}
// broadcast to all except source
broadcastToken(aConnector, lLogin);
}
}
/**
*
*/
private void broadcastLogoutEvent(WebSocketConnector aConnector) {
// only broadcast if corresponding global plugin setting is "true"
if (BROADCAST_LOGOUT) {
if (log.isDebugEnabled()) {
log.debug("Broadcasting logout event...");
}
// broadcast login event to other clients of the jWebSocket network
Token lLogout = TokenFactory.createToken(BaseToken.TT_EVENT);
lLogout.setString("ns", getNamespace());
lLogout.setString("name", "logout");
lLogout.setString("username", getUsername(aConnector));
lLogout.setInteger("clientCount", getConnectorCount());
// do NEVER broadcast client's session id here!
// lLogout.put("usid", getSessionId(aConnector));
lLogout.setString("sourceId", aConnector.getId());
// if a unique node id is specified for the client include that
String lNodeId = aConnector.getNodeId();
if (lNodeId != null) {
lLogout.setString("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 = TokenFactory.createToken(TT_GOODBYE);
lGoodBye.setString("ns", getNamespace());
lGoodBye.setString("vendor", JWebSocketCommonConstants.VENDOR);
lGoodBye.setString("version", JWebSocketServerConstants.VERSION_STR);
lGoodBye.setString("sourceId", aConnector.getId());
if (aCloseReason != null) {
lGoodBye.setString("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) {
// sendWelcome(aConnector);
Token lResponse = createResponse(aToken);
String lUsername = aToken.getString("username");
// TODO: Add authentication and password check
String lPassword = aToken.getString("password");
// optionally continue previous session
String lSessionId = aToken.getString("usid");
String lGroup = aToken.getString("group");
Boolean lReturnRoles = aToken.getBoolean("getRoles", Boolean.FALSE);
Boolean lReturnRights = aToken.getBoolean("getRights", Boolean.FALSE);
if (log.isDebugEnabled()) {
log.debug("Processing 'login' (username='" + lUsername
+ "', group='" + lGroup
+ "') from '" + aConnector + "'...");
}
if (lUsername != null) {
User lUser = SecurityFactory.getUser(lUsername);
// TODO: Here we need to check if the user is in the user data base at
// all.
lResponse.setString("username", lUsername);
// if previous session id was passed to continue an aborted session
// return the session-id to notify client about acceptance
if (lSessionId != null) {
lResponse.setString("usid", lSessionId);
}
lResponse.setString("sourceId", aConnector.getId());
// set shared variables
setUsername(aConnector, lUsername);
setGroup(aConnector, lGroup);
if (lUser != null) {
if (lReturnRoles) {
lResponse.setList("roles", new FastList(lUser.getRoleIdSet()));
}
if (lReturnRights) {
lResponse.setList("rights", new FastList(lUser.getRightIdSet()));
}
}
} else {
lResponse.setInteger("code", -1);
lResponse.setString("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 normal answer token, good bye is for close!
sendToken(aConnector, aConnector, lResponse);
// 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.setString("sourceId", aConnector.getId());
removeUsername(aConnector);
removeGroup(aConnector);
} else {
lResponse.setInteger("code", -1);
lResponse.setString("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.hasRight(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.setString("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.hasRight(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.setString("sourceId", aConnector.getId());
// don't distribute session id here!
aToken.remove("usid");
// keep senderIncluded beging false as default, apps rely on this!
Boolean lIsSenderIncluded = aToken.getBoolean("senderIncluded", false);
Boolean lIsResponseRequested = aToken.getBoolean("responseRequested", true);
// broadcast the token
broadcastToken(aConnector, aToken,
new BroadcastOptions(lIsSenderIncluded, lIsResponseRequested));
// check if response was requested
if (lIsResponseRequested) {
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);
// only send a good bye message if timeout is > 0
if (lTimeout > 0) {
sendGoodBye(aConnector, CloseReason.CLIENT);
}
// if logged in...
if (getUsername(aConnector) != null) {
// 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 lResponse = createResponse(aToken);
String lData = aToken.getString("data");
if (lData != null) {
if (log.isDebugEnabled()) {
log.debug("echo " + lData);
}
lResponse.setString("data", lData);
} else {
lResponse.setInteger("code", -1);
lResponse.setString("msg", "missing 'data' argument for 'echo' command");
}
sendToken(aConnector, aConnector, lResponse);
}
/**
*
* @param aConnector
* @param aToken
*/
public void ping(WebSocketConnector aConnector, Token aToken) {
Boolean lEcho = aToken.getBoolean("echo", Boolean.TRUE);
if (log.isDebugEnabled()) {
log.debug("Processing 'Ping' (echo='" + lEcho
+ "') from '" + aConnector + "'...");
}
if (lEcho) {
Token lResponse = createResponse(aToken);
// TODO: here could we optionally send a time stamp
// TODO: implement response time on client!
// lResponseToken.put("","");
sendToken(aConnector, aConnector, lResponse);
}
}
/**
*
* @param aConnector
* @param aToken
*/
public void getClients(WebSocketConnector aConnector, Token aToken) {
Token lResponse = 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());
}
lResponse.setList("clients", listOut);
lResponse.setInteger("count", listOut.size());
} else {
lResponse.setInteger("code", -1);
lResponse.setString("msg", "not logged in");
}
sendToken(aConnector, aConnector, lResponse);
}
/**
* allocates a "non-interruptable" communication channel between two clients.
*
* @param aConnector
* @param aToken
*/
public void allocChannel(WebSocketConnector aConnector, Token aToken) {
Token lResponse = 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 lResponse = createResponse(aToken);
if (log.isDebugEnabled()) {
log.debug("Processing 'deallocChannel' from '"
+ aConnector + "'...");
}
}
}