// --------------------------------------------------------------------------- // jWebSocket - RPC PlugIn // Copyright (c) 2010 Innotrade GmbH, jWebSocket.org // --------------------------------------------------------------------------- // 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.rpc; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Map.Entry; 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.api.WebSocketEngine; import org.jwebsocket.config.JWebSocketConfig; import org.jwebsocket.factory.JWebSocketJarClassLoader; import org.jwebsocket.kit.CloseReason; import org.jwebsocket.kit.PlugInResponse; import org.jwebsocket.logging.Logging; import org.jwebsocket.plugins.TokenPlugIn; import org.jwebsocket.plugins.rpc.RPCCallableClassLoader.MethodRightLink; import org.jwebsocket.plugins.rpc.rrpc.Rrpc; import org.jwebsocket.plugins.rpc.rrpc.RrpcRightNotGrantedException; import org.jwebsocket.plugins.rpc.util.RPCRightNotGrantedException; import org.jwebsocket.plugins.rpc.util.ServerMethodMatcher; import org.jwebsocket.plugins.rpc.util.TypeConverter; import org.jwebsocket.security.SecurityFactory; import org.jwebsocket.token.Token; /** * This plug-in provides all the functionality for remote procedure calls (RPC) * for client-to-server (C2S) apps, and reverse remote procedure calls (RRPC) * for server-to-client (S2C) or client-to-client apps (C2C). * * @author aschulze * @author Quentin Ambard */ // TODO: questions for Alex: // When doing a rrpc S2C, doing the following sendToken(null, lConnector, // lRRPC); log a warn error since the source connector is null. Do we have a // kind of ServerConnector ? public class RPCPlugIn extends TokenPlugIn { // keys to buil the rrpc call private static RPCPlugIn sRPCPlugIn = null; private static Logger mLog = Logging.getLogger(RPCPlugIn.class); // Store the parameters type allowed for rpc method. private Map<String, RPCCallableClassLoader> mRpcCallableClassLoader = new FastMap<String, RPCCallableClassLoader>(); // TODO: We need simple unique IDs to address a certain target, session id not // suitable here. // TODO: Show target(able) clients in a drop down box // TODO: RPC demo does not show other clients logging in /** * */ public RPCPlugIn() { this(null); } public RPCPlugIn(PluginConfiguration configuration) { super(configuration); if (mLog.isDebugEnabled()) { mLog.debug("Instantiating rpc plug-in..."); } // specify default name space this.setNamespace(CommonRpcPlugin.NS_RPC_DEFAULT); } @Override @SuppressWarnings("rawtypes") public void engineStarted(WebSocketEngine aEngine) { // we get the instance of the Plugin for the rrpc module: sRPCPlugIn = this; Class lClass = null; if (mLog.isDebugEnabled()) { mLog.debug("RPC Rights found in xml file: " + SecurityFactory.getGlobalRights(getNamespace()).getRightIdSet().toString()); } loadClassFromThirdJavaPart(); // Load map of granted procs Set<String> lPluginRights = SecurityFactory.getGlobalRights(getNamespace()).getRightIdSet(); for (String lRightId : lPluginRights) { // We remove the pluginId because we just want the name of the method: String lFullMethodName = lRightId.substring(getNamespace().length() + 1); // We don't care about global rpc and rrpc rights in this section. if (!lFullMethodName.equals(CommonRpcPlugin.RPC_RIGHT_ID) && !lFullMethodName.equals(CommonRpcPlugin.RRPC_RIGHT_ID)) { // setting with parameters type to handle java method overload String[] lParameterTypes = null; if (lFullMethodName.indexOf("(") != -1 && lFullMethodName.indexOf(")") != -1) { String lParameters = lFullMethodName.substring(lFullMethodName.indexOf("(") + 1, lFullMethodName.length() - 1); lParameters = lParameters.replace(" ", ""); lParameterTypes = lParameters.split(","); lFullMethodName = lFullMethodName.substring(0, lFullMethodName.indexOf("(")); } String lClassName = lFullMethodName.substring(0, lFullMethodName.lastIndexOf(".")); String lMethodName = lFullMethodName.substring(lFullMethodName.lastIndexOf(".") + 1); if (!mRpcCallableClassLoader.containsKey(lClassName)) { initRPCCallableClass(loadClassFromClassPath(lClassName), lClassName); } Method lMethod = getValidMethod(lClassName, lMethodName, lParameterTypes); if (lMethod != null) { // Add this method as a RPCCallable method. mRpcCallableClassLoader.get(lClassName).addMethod(lMethodName, lMethod, lRightId); } } } } /** * Load the class from classpath with logs of needed. * @param aClassName the class to be load * @return the class loaded, or null if not found. */ @SuppressWarnings("rawtypes") private Class loadClassFromClassPath(String aClassName) { try { if (mLog.isDebugEnabled()) { mLog.debug("Trying to load class '" + aClassName + "' from classpath..."); } return Class.forName(aClassName); } catch (Exception ex) { mLog.error(ex.getClass().getSimpleName() + " loading class from classpath: " + ex.getMessage() + ", hence trying to load from jar."); } return null; } /** * Try to load the classes which are not suposed to be on the classPath, so the right definition isn't enought; */ @SuppressWarnings("rawtypes") private void loadClassFromThirdJavaPart() { // TODO: move JWebSocketJarClassLoader into ServerAPI module ? JWebSocketJarClassLoader lClassLoader = new JWebSocketJarClassLoader(); Class lClass = null; Map<String, String> lSettings = getSettings(); // load map of RPC libraries first for (Entry<String, String> lSetting : lSettings.entrySet()) { String lKey = lSetting.getKey(); String lValue = lSetting.getValue(); if (lKey.startsWith("class:")) { String lClassName = lKey.substring(6); lClass = loadClassFromClassPath(lClassName); // if class could not be loaded from classpath... if (lClass == null) { String lJarFilePath = null; try { lJarFilePath = JWebSocketConfig.getLibraryFolderPath(lValue); if (mLog.isDebugEnabled()) { mLog.debug("Trying to load class '" + lClassName + "' from jar '" + lJarFilePath + "'..."); } lClassLoader.addFile(lJarFilePath); lClass = lClassLoader.loadClass(lClassName); if (mLog.isDebugEnabled()) { mLog.debug("Class '" + lClassName + "' successfully loaded from '" + lJarFilePath + "'."); } } catch (Exception ex) { mLog.error(ex.getClass().getSimpleName() + " loading jar '" + lJarFilePath + "': " + ex.getMessage()); } } // could the class be loaded? initRPCCallableClass(lClass, lClassName); } } } /** * Try to load an instance of the RPCCallable class in parameter. * Log an error if we can't loag this class. * RPCCallable class must have a default constructor or a constructor whith a single WebSocketConnector parameter * @param aClass * @param aClassName */ @SuppressWarnings({"rawtypes", "unchecked"}) private void initRPCCallableClass(Class aClass, String aClassName) { if (aClass != null) { try { RPCCallable lInstance = null; try { Constructor lConstructor = aClass.getConstructor(WebSocketConnector.class); lInstance = (RPCCallable) lConstructor.newInstance(new Object[]{null}); } catch (Exception ex) { lInstance = (RPCCallable) aClass.newInstance(); } mRpcCallableClassLoader.put(aClassName, new RPCCallableClassLoader(aClass, lInstance)); } catch (Exception ex) { mLog.error(ex.getClass().getSimpleName() + " creating '" + aClassName + "' instance : " + ex.getMessage() + ". RPCCallable class must have a default constructor or a constructor whith a single WebSocketConnector parameter."); } } } @Override public void connectorStarted(WebSocketConnector aConnector) { } @Override public void processToken(PlugInResponse aResponse, WebSocketConnector aConnector, Token aToken) { String lType = aToken.getType(); String lNS = aToken.getNS(); if (lType != null && getNamespace().equals(lNS)) { //Set the sourceId in the token. aToken.setString(CommonRpcPlugin.RRPC_KEY_SOURCE_ID, aConnector.getId()); // remote procedure call if (lType.equals("rpc")) { rpc(aConnector, aToken); // reverse remote procedure call } else if (lType.equals("rrpc")) { rrpc(aConnector, aToken); } } } /** * remote procedure call (RPC) * * @param aConnector * @param aToken */ @SuppressWarnings({"rawtypes", "unchecked"}) public void rpc(WebSocketConnector aConnector, Token aToken) { // check if user is allowed to run 'rpc' command if (!SecurityFactory.hasRight(getUsername(aConnector), CommonRpcPlugin.NS_RPC_DEFAULT + "." + CommonRpcPlugin.RPC_RIGHT_ID)) { sendToken(aConnector, aConnector, createAccessDenied(aToken)); return; } Token lResponseToken = createResponse(aToken); String lClassName = aToken.getString(CommonRpcPlugin.RRPC_KEY_CLASSNAME); String lMethod = aToken.getString(CommonRpcPlugin.RRPC_KEY_METHOD); List lArgs = aToken.getList(CommonRpcPlugin.RRPC_KEY_ARGS); // if it's not a List, but just a simple arg if (lArgs == null) { Object lArg = aToken.getObject(CommonRpcPlugin.RRPC_KEY_ARGS); if (lArg != null) { lArgs = new FastList(); lArgs.add(aToken.getObject(CommonRpcPlugin.RRPC_KEY_ARGS)); } else { lArgs = null; } } String lMsg = null; if (mLog.isDebugEnabled()) { mLog.debug("Processing RPC to class '" + lClassName + "', method '" + lMethod + "', args: '" + lArgs + "'..."); } // class is ignored until security restrictions are finished. try { // The class we try to call is not loaded if (mRpcCallableClassLoader.containsKey(lClassName)) { // the called method name is unexisting if (mRpcCallableClassLoader.get(lClassName).hasMethod(lMethod)) { RPCCallableClassLoader lRpcClassLoader = mRpcCallableClassLoader.get(lClassName); // We get the instance of the generator RPCCallable lInstanceGenerator = lRpcClassLoader.getRpcCallableInstanceGenerator(); // from this generator, we get an instance of the class we want to // call. This part is in the charge of the developper throw the // RPCCallable interface. RPCCallable lInstance = lInstanceGenerator.getInstance(aConnector); if (lInstance != null) { Object lObj = call(aConnector, lRpcClassLoader, lInstance, lMethod, lArgs); lResponseToken.setValidated("result", lObj); } else { lMsg = "Class '" + lClassName + "' found but get a null instance when calling the RPCCallable getInstance() method."; } } else { lMsg = "Class '" + lClassName + "' found but the method " + lMethod + " is not available. Right is missing, probably a typo (call are case sensitive)"; } } else { lMsg = "Class '" + lClassName + "' not found in the jwebsocket.xml file, or not properly loaded. probably a typo."; } } catch (NoSuchMethodException ex) { lMsg = "NoSuchMethodException calling '" + lMethod + "' for class " + lClassName + ": " + ex.getMessage(); } catch (IllegalAccessException ex) { lMsg = "IllegalAccessException calling '" + lMethod + "' for class " + lClassName + ": " + ex.getMessage(); } catch (InvocationTargetException ex) { lMsg = "InvocationTargetException calling '" + lMethod + "' for class " + lClassName + ": " + ex.getMessage(); } catch (RPCRightNotGrantedException ex) { lMsg = "RPCRightNotGrantedException calling '" + lMethod + "' for class " + lClassName + ": " + ex.getMessage(); } catch (ClassNotFoundException ex) { lMsg = "ClassNotFoundException (the method does probably not exist or is not defined in the jwebsocket.xml file) calling '" + lMethod + "' for class " + lClassName + ": " + ex.getMessage(); } if (lMsg != null) { lResponseToken.setInteger("code", -1); lResponseToken.setString("msg", lMsg); } /* * just for testing purposes of multi-threaded rpc's try { * Thread.sleep(3000); } catch (InterruptedException ex) { } */ sendToken(aConnector, aConnector, lResponseToken); } /** * Do a rrpc call from a rrpc-ready-to-use token * @param aConnectorFrom * @param aToken rrpc-ready-to-use */ public void rrpc(WebSocketConnector aConnectorFrom, Token aToken) { try { new Rrpc(aToken).call(); } catch (RrpcRightNotGrantedException e) { sendToken(aConnectorFrom, aConnectorFrom, createAccessDenied(aToken)); } } /** * reverse remote procedure call (RRPC) * * @param aConnector * @param aToken */ public static void processRrpc(WebSocketConnector aConnectorFrom, List<WebSocketConnector> aConnectorsTo, Token aToken) { if (mLog.isDebugEnabled()) { mLog.debug("Processing 'rrpc'..."); } if (sRPCPlugIn == null) { mLog.error("Try to make a rrpc call but the RPCPlugin doesn't seem to be load." + "Please make sure the plugin is correctly added to jWebsocket"); } else { // Send the rpc to every connector for (WebSocketConnector lConnector : aConnectorsTo) { if (lConnector != null) { sRPCPlugIn.sendToken(aConnectorFrom, lConnector, aToken); } } } } /** * * @param aClassName * @param aURL * @return */ @SuppressWarnings("rawtypes") public static Class loadClass(String aClassName, String aURL) { Class lClass = null; try { URLClassLoader lUCL = new URLClassLoader(new URL[]{new URL(aURL)}); // load class using previously defined class loader lClass = Class.forName(aClassName, true, lUCL); if (mLog.isDebugEnabled()) { mLog.debug("Class '" + lClass.getName() + "' loaded!"); } } catch (ClassNotFoundException ex) { mLog.error("Class not found exception: " + ex.getMessage()); } catch (MalformedURLException ex) { mLog.error("MalformesURL exception: " + ex.getMessage()); } return lClass; } /** * * @param aClass * @param aArgs * @return */ // public static Object createInstance(Class aClass, Object[] aArgs) { // Object lObj = null; // try { // Class[] lCA = new Class[aArgs != null ? aArgs.length : 0]; // for (int i = 0; i < lCA.length; i++) { // lCA[i] = aArgs[i].getClass(); // } // Constructor lConstructor = aClass.getConstructor(lCA); // lObj = lConstructor.newInstance(aArgs); // if (mLog.isDebugEnabled()) { // mLog.debug("Object '" + aClass.getName() + "' instantiated!"); // } // } catch (Exception ex) { // mLog.error("Exception instantiating class " + aClass.getName() + ": " + ex.getMessage()); // } // return lObj; // } /** * * @param aInstance * @param aName * @param aArgs * @return * @throws NoSuchMethodException * @throws IllegalAccessException * @throws InvocationTargetException * @throws RPCRightNotGrantedException * @throws ClassNotFoundException */ @SuppressWarnings("rawtypes") private Object call(WebSocketConnector aConnector, RPCCallableClassLoader aRpcClassLoader, Object aInstance, String aMethodName, List aArgs) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, RPCRightNotGrantedException, ClassNotFoundException { Class lClass = aRpcClassLoader.getRpcCallableClass(); Object[] lArg = null; // JSONArray lJsonArrayArgs = null; Method lMethodToInvoke = null; List<MethodRightLink> lListMethod = aRpcClassLoader.getMethods(aMethodName); //aConnector. // We look if one of the method we have loaded match for (MethodRightLink lMethodRight : lListMethod) { Method lMethod = lMethodRight.getMethod(); //We try to match each method against the parameter MethodMatcher lMethodMatcher = new ServerMethodMatcher(lMethod, aConnector); //If lArg is not null, means the method match if (lMethodMatcher.isMethodMatchingAgainstParameter(aArgs)) { //If the method match, we make sure the connector has the right to execute this method. if (SecurityFactory.hasRight(aConnector.getUsername(), lMethodRight.getRightId())) { lMethodToInvoke = lMethod; lArg = lMethodMatcher.getMethodParameters(); break; } //otherwise, that's the correct method but the connector hasn't the correct right. throw new RPCRightNotGrantedException(lMethodRight.getRightId(), aArgs.toString()); } } //If no method have been found. if (lMethodToInvoke == null) { throw new NoSuchMethodException(); } //We cast the intance to the correct class. aInstance = lClass.cast(aInstance); Object lObj = lMethodToInvoke.invoke(aInstance, lArg); return lObj; } /** * Check if a RPCCallable method has correct parameters type Store each class * methods inside mClassMethods to grant a faster access for each rpc call. * Log an error if on parameter is not valid Only called during the plugin * initialization * * @param aClassName class name * @param aMethodName method name * @param aXmlParametersType list of parameter type found in the xml file (for instance: "int, string, map") * @return true if the parameters are OK */ @SuppressWarnings("rawtypes") private Method getValidMethod(String aClassName, String aMethodName, String[] aXmlParametersType) { // check if the method own to a class which has been loaded. if (!mRpcCallableClassLoader.containsKey(aClassName)) { mLog.error("You try to grant access to a method which own to a class the server can't initialize. Make sure you didn't forget to declare it's jar file. " + aMethodName + " will *not* be loaded"); return null; } // Check if 2 methods have the same name and the same number of arguments // (this block just log an error if 2 method have the same number of // parameters without specific types) Class lClass = mRpcCallableClassLoader.get(aClassName).getRpcCallableClass(); Method[] lMethods = lClass.getMethods(); ArrayList<Integer> lMethodWithSameNameAndNumberOfArguments = new ArrayList<Integer>(); if (aXmlParametersType == null) { for (int i = 0; i < lMethods.length; i++) { Method lMethod = lMethods[i]; if (lMethod.getName().equals(aMethodName)) { if (lMethodWithSameNameAndNumberOfArguments.contains(lMethod.getParameterTypes().length)) { // 2 methods have the same name and number of parameters if (mLog.isDebugEnabled()) { mLog.debug("Two methods named " + aMethodName + " have the same number of argument. Can't know which one this setting concerns. Please use xml settings such as: MyClass.myMethod(int, boolean, string, double, map, array)"); } return null; } else { lMethodWithSameNameAndNumberOfArguments.add(lMethod.getParameterTypes().length); } } } } // if (aXmlParametersType != null && aXmlParametersType.length == 1 && "".equals(aXmlParametersType[0])) { // aXmlParametersType =null; // } //Make sure every parameter type is valid if (aXmlParametersType != null) { for (String parameterType : aXmlParametersType) { if (!"".equals(parameterType) && !TypeConverter.isValidProtocolType(parameterType)) { mLog.error(aXmlParametersType + " is not a valid parameter type. Valid parameter types are: " + TypeConverter.getValidParameterTypes()); return null; } } } // Check if on of the method match for (int i = 0; i < lMethods.length; i++) { // If we are on a method with the same name, we check it's parameters if (lMethods[i].getName().equals(aMethodName)) { if (checkMethodParameters(lMethods[i], aXmlParametersType, aClassName)) { return lMethods[i]; } } } mLog.error("The method " + aMethodName + " could not be loaded. " + "Probably a typo or invalid parameter (check previous error)."); return null; } /** * Check if the method aMethod match with aXmlParametersType. * * @param aMethod * @param aXmlParametersType list of parameter type found in the xml file (for instance: "int, string, map") * @param aClassName * @return true if the method match, false otherwise */ @SuppressWarnings("rawtypes") private boolean checkMethodParameters(Method aMethod, String[] aXmlParametersType, String aClassName) { Class[] lRealParametersType = aMethod.getParameterTypes(); List<Class> lParametersType = new FastList<Class>(); for (Class lClass : lRealParametersType) { if (lClass != null && lClass != WebSocketConnector.class) { lParametersType.add(lClass); } } //no parameters if (aXmlParametersType != null && aXmlParametersType.length == 1 && "".equals(aXmlParametersType[0])) { if (lParametersType.size() == 0) { if (mLog.isDebugEnabled()) { mLog.debug("Method " + aMethod.getName() + " loaded (expect 0 parameters)."); } return true; } else { return false; } } // Look for a method with the same arguments if they are defined in the xml // setting... if (aXmlParametersType != null) { //If it's not the same number of parameters as expected, that's not the correct method. if (aXmlParametersType.length != lParametersType.size()) { return false; } boolean methodMatch = true; for (int j = 0; j < aXmlParametersType.length; j++) { if (!TypeConverter.matchProtocolTypeToJavaType(aXmlParametersType[j], lParametersType.get(j).getName())) { methodMatch = false; break; } } if (methodMatch) { if (mLog.isDebugEnabled()) { StringBuilder lParametersList = new StringBuilder(); for (int k = 0; k < aXmlParametersType.length; k++) { lParametersList.append(aXmlParametersType[k] + ", "); } lParametersList.setLength(lParametersList.length() - 2); mLog.debug("Complex method " + aMethod.getName() + " loaded (expect " + lParametersType.size() + " parameters: " + lParametersList.toString() + ")."); } return true; } return false; } // without parameters, always true if (lParametersType.size() == 0) { mLog.debug("method " + aMethod.getName() + "() loaded."); return true; } // if parameters are not defined in the setting, means that we have a unique // method with this name. for (int j = 0; j < lParametersType.size(); j++) { Class lParameterType = lParametersType.get(j); if (!TypeConverter.isValidProtocolJavaType(lParameterType)) { mLog.error("The method " + aMethod.getName() + " has an invalid parameter: " + lParameterType.getName() + ". " + "This method will *not* be load." + "Suported parameters type are primitive, primitive's wrapper, List and Token."); return false; } } if (mLog.isDebugEnabled()) { mLog.debug("Method " + aMethod.getName() + " loaded (expect " + lParametersType.size() + " parameters)."); } // store the "complex" method in the Map. return true; } /** * Alert all the RpcCallableInstance that a connecter has stopped */ @Override public void connectorStopped(WebSocketConnector aConnector, CloseReason aCloseReason) { // We alert every instance of the generator that a connector stopped its // connection for (Entry<String, RPCCallableClassLoader> entry : mRpcCallableClassLoader.entrySet()) { entry.getValue().getRpcCallableInstanceGenerator().connectorStopped(aConnector, aCloseReason); } } /** * call the getConnector method of the instance of the server of the RpcPlugin * loaded * * @param aEngine * @param aConnectorId * @return */ public static WebSocketConnector getConnector(String aEngine, String aConnectorId) { if (sRPCPlugIn == null) { mLog.error("Try to make a rrpc call but the RPCPlugin doesn't seem to be load." + "Please make sure the plugin is correctly added to jWebsocket"); return null; } else { return sRPCPlugIn.getServer().getConnector(aEngine, aConnectorId); } } /** * call the getUsername method of the instance of the RpcPlugin * * @param aConnector * @return */ public static String getUsernameStatic(WebSocketConnector aConnector) { if (sRPCPlugIn == null) { mLog.error("Try to make a rrpc call but the RPCPlugin doesn't seem to be load." + "Please make sure the plugin is correctly added to jWebsocket"); return null; } else { return sRPCPlugIn.getUsername(aConnector); } } }