/* * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.rpc.server; import com.google.gwt.rpc.client.ast.CommandSink; import com.google.gwt.rpc.client.ast.HasValues; import com.google.gwt.rpc.client.ast.ReturnCommand; import com.google.gwt.rpc.client.ast.RpcCommand; import com.google.gwt.rpc.client.ast.ThrowCommand; import com.google.gwt.rpc.client.impl.HasValuesCommandSink; import com.google.gwt.rpc.client.impl.RemoteException; import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException; import com.google.gwt.user.client.rpc.RemoteService; import com.google.gwt.user.client.rpc.SerializationException; import com.google.gwt.user.server.rpc.RPCRequest; import com.google.gwt.user.server.rpc.RPCServletUtils; import com.google.gwt.user.server.rpc.UnexpectedException; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * EXPERIMENTAL and subject to change. Do not use this in production code. * <p> * Utility class for integrating with the RPC system. */ public class RPC { private static final HashMap<String, Class<?>> TYPE_NAMES = new HashMap<String, Class<?>>(); /** * Static map of classes to sets of interfaces (e.g. classes). Optimizes * lookup of interfaces for security. */ private static final Map<Class<?>, Set<String>> serviceToImplementedInterfacesMap = new HashMap<Class<?>, Set<String>>(); static { // The space is needed to prevent name collisions TYPE_NAMES.put(" Z", boolean.class); TYPE_NAMES.put(" B", byte.class); TYPE_NAMES.put(" C", char.class); TYPE_NAMES.put(" D", double.class); TYPE_NAMES.put(" F", float.class); TYPE_NAMES.put(" I", int.class); TYPE_NAMES.put(" J", long.class); TYPE_NAMES.put(" S", short.class); } public static RPCRequest decodeRequest(String encodedRequest, Class<?> type, ClientOracle clientOracle) throws RemoteException { if (encodedRequest == null) { throw new NullPointerException("encodedRequest cannot be null"); } if (encodedRequest.length() == 0) { throw new IllegalArgumentException("encodedRequest cannot be empty"); } ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try { SimplePayloadDecoder decoder; try { decoder = new SimplePayloadDecoder(clientOracle, encodedRequest); } catch (ClassNotFoundException e) { throw new IncompatibleRemoteServiceException( "Client does not have a type sent by the server", e); } CommandServerSerializationStreamReader streamReader = new CommandServerSerializationStreamReader(); if (decoder.getThrownValue() != null) { streamReader.prepareToRead(Collections.singletonList(decoder.getThrownValue())); try { throw new RemoteException((Throwable) streamReader.readObject()); } catch (ClassCastException e) { throw new SerializationException( "The remote end threw something other than a Throwable", e); } catch (SerializationException e) { throw new IncompatibleRemoteServiceException( "The remote end threw an exception which could not be deserialized", e); } } else { streamReader.prepareToRead(decoder.getValues()); } // Read the name of the RemoteService interface String serviceIntfName = streamReader.readString(); if (type != null) { if (!implementsInterface(type, serviceIntfName)) { // The service does not implement the requested interface throw new IncompatibleRemoteServiceException( "Blocked attempt to access interface '" + serviceIntfName + "', which is not implemented by '" + printTypeName(type) + "'; this is either misconfiguration or a hack attempt"); } } Class<?> serviceIntf; try { serviceIntf = getClassFromSerializedName(null, serviceIntfName, classLoader); if (!RemoteService.class.isAssignableFrom(serviceIntf)) { // The requested interface is not a RpcService interface throw new IncompatibleRemoteServiceException( "Blocked attempt to access interface '" + printTypeName(serviceIntf) + "', which doesn't extend RpcService; " + "this is either misconfiguration or a hack attempt"); } } catch (ClassNotFoundException e) { throw new IncompatibleRemoteServiceException( "Could not locate requested interface '" + serviceIntfName + "' in default classloader", e); } String serviceMethodName = streamReader.readString(); int paramCount = streamReader.readInt(); Class<?>[] parameterTypes = new Class[paramCount]; for (int i = 0; i < parameterTypes.length; i++) { String paramClassName = streamReader.readString(); try { parameterTypes[i] = getClassFromSerializedName(clientOracle, paramClassName, classLoader); } catch (ClassNotFoundException e) { throw new IncompatibleRemoteServiceException("Parameter " + i + " of is of an unknown type '" + paramClassName + "'", e); } } try { Method method = serviceIntf.getMethod(serviceMethodName, parameterTypes); Object[] parameterValues = new Object[parameterTypes.length]; for (int i = 0; i < parameterValues.length; i++) { Object o = CommandSerializationUtil.getAccessor(parameterTypes[i]).readNext( streamReader); parameterValues[i] = o; } return new RPCRequest(method, parameterValues, null, null, 0); } catch (NoSuchMethodException e) { throw new IncompatibleRemoteServiceException( formatMethodNotFoundErrorMessage(serviceIntf, serviceMethodName, parameterTypes)); } } catch (SerializationException ex) { throw new IncompatibleRemoteServiceException(ex.getMessage(), ex); } } public static void invokeAndStreamResponse(Object target, Method serviceMethod, Object[] args, ClientOracle clientOracle, OutputStream stream) throws SerializationException { if (serviceMethod == null) { throw new NullPointerException("serviceMethod"); } if (clientOracle == null) { throw new NullPointerException("clientOracle"); } CommandSink sink; try { sink = clientOracle.createCommandSink(stream); } catch (IOException e) { throw new SerializationException("Unable to initialize output", e); } try { Object result = serviceMethod.invoke(target, args); try { streamResponse(clientOracle, result, sink, false); } catch (SerializationException e) { streamResponse(clientOracle, e, sink, true); } } catch (IllegalAccessException e) { SecurityException securityException = new SecurityException( formatIllegalAccessErrorMessage(target, serviceMethod)); securityException.initCause(e); throw securityException; } catch (IllegalArgumentException e) { SecurityException securityException = new SecurityException( formatIllegalArgumentErrorMessage(target, serviceMethod, args)); securityException.initCause(e); throw securityException; } catch (InvocationTargetException e) { // Try to encode the caught exception Throwable cause = e.getCause(); // Don't allow random RuntimeExceptions to be thrown back to the client if (!RPCServletUtils.isExpectedException(serviceMethod, cause)) { throw new UnexpectedException("Service method '" + getSourceRepresentation(serviceMethod) + "' threw an unexpected exception: " + cause.toString(), cause); } streamResponse(clientOracle, cause, sink, true); } sink.finish(); } public static void streamResponseForFailure(ClientOracle clientOracle, OutputStream out, Throwable payload) throws SerializationException { CommandSink sink; try { sink = clientOracle.createCommandSink(out); } catch (IOException e) { throw new SerializationException("Unable to initialize output", e); } streamResponse(clientOracle, payload, sink, true); sink.finish(); } public static void streamResponseForSuccess(ClientOracle clientOracle, OutputStream out, Object payload) throws SerializationException { CommandSink sink; try { sink = clientOracle.createCommandSink(out); } catch (IOException e) { throw new SerializationException("Unable to initialize output", e); } streamResponse(clientOracle, payload, sink, false); sink.finish(); } private static String formatIllegalAccessErrorMessage(Object target, Method serviceMethod) { StringBuffer sb = new StringBuffer(); sb.append("Blocked attempt to access inaccessible method '"); sb.append(getSourceRepresentation(serviceMethod)); sb.append("'"); if (target != null) { sb.append(" on target '"); sb.append(printTypeName(target.getClass())); sb.append("'"); } sb.append("; this is either misconfiguration or a hack attempt"); return sb.toString(); } private static String formatIllegalArgumentErrorMessage(Object target, Method serviceMethod, Object[] args) { StringBuffer sb = new StringBuffer(); sb.append("Blocked attempt to invoke method '"); sb.append(getSourceRepresentation(serviceMethod)); sb.append("'"); if (target != null) { sb.append(" on target '"); sb.append(printTypeName(target.getClass())); sb.append("'"); } sb.append(" with invalid arguments"); if (args != null && args.length > 0) { sb.append(Arrays.asList(args)); } return sb.toString(); } private static String formatMethodNotFoundErrorMessage(Class<?> serviceIntf, String serviceMethodName, Class<?>[] parameterTypes) { StringBuffer sb = new StringBuffer(); sb.append("Could not locate requested method '"); sb.append(serviceMethodName); sb.append("("); for (int i = 0; i < parameterTypes.length; ++i) { if (i > 0) { sb.append(", "); } sb.append(printTypeName(parameterTypes[i])); } sb.append(")'"); sb.append(" in interface '"); sb.append(printTypeName(serviceIntf)); sb.append("'"); return sb.toString(); } /** * Returns the {@link Class} instance for the named class or primitive type. * * @param serializedName the serialized name of a class or primitive type * @param classLoader the classLoader used to load {@link Class}es * @return Class instance for the given type name * @throws ClassNotFoundException if the named type was not found */ private static Class<?> getClassFromSerializedName(ClientOracle clientOracle, String serializedName, ClassLoader classLoader) throws ClassNotFoundException { Class<?> value = TYPE_NAMES.get(serializedName); if (value != null) { return value; } // Interfaces don't exist in the client, so we use unobfuscated names if (serializedName.charAt(0) == ' ') { serializedName = serializedName.substring(1); } else if (clientOracle != null) { serializedName = clientOracle.getTypeName(serializedName); } assert serializedName != null; return Class.forName(serializedName, false, classLoader); } /** * Returns the source representation for a method signature. * * @param method method to get the source signature for * @return source representation for a method signature */ private static String getSourceRepresentation(Method method) { return method.toString().replace('$', '.'); } /** * Used to determine whether the specified interface name is implemented by * the service class. This is done without loading the class (for security). */ private static boolean implementsInterface(Class<?> service, String intfName) { synchronized (serviceToImplementedInterfacesMap) { // See if it's cached. // Set<String> interfaceSet = serviceToImplementedInterfacesMap.get(service); if (interfaceSet != null) { if (interfaceSet.contains(intfName)) { return true; } } else { interfaceSet = new HashSet<String>(); serviceToImplementedInterfacesMap.put(service, interfaceSet); } if (!service.isInterface()) { while ((service != null) && !RpcServlet.class.equals(service)) { Class<?>[] intfs = service.getInterfaces(); for (Class<?> intf : intfs) { if (implementsInterfaceRecursive(intf, intfName)) { interfaceSet.add(intfName); return true; } } // did not find the interface in this class so we look in the // superclass // service = service.getSuperclass(); } } else { if (implementsInterfaceRecursive(service, intfName)) { interfaceSet.add(intfName); return true; } } return false; } } /** * Recursive helper for implementsInterface(). */ private static boolean implementsInterfaceRecursive(Class<?> clazz, String intfName) { assert (clazz.isInterface()); if (clazz.getName().equals(intfName)) { return true; } // search implemented interfaces Class<?>[] intfs = clazz.getInterfaces(); for (Class<?> intf : intfs) { if (implementsInterfaceRecursive(intf, intfName)) { return true; } } return false; } /** * Straight copy from * {@link com.google.gwt.dev.util.TypeInfo#getSourceRepresentation(Class)} to * avoid runtime dependency on gwt-dev. */ private static String printTypeName(Class<?> type) { // Primitives // if (type.equals(Integer.TYPE)) { return "int"; } else if (type.equals(Long.TYPE)) { return "long"; } else if (type.equals(Short.TYPE)) { return "short"; } else if (type.equals(Byte.TYPE)) { return "byte"; } else if (type.equals(Character.TYPE)) { return "char"; } else if (type.equals(Boolean.TYPE)) { return "boolean"; } else if (type.equals(Float.TYPE)) { return "float"; } else if (type.equals(Double.TYPE)) { return "double"; } // Arrays // if (type.isArray()) { Class<?> componentType = type.getComponentType(); return printTypeName(componentType) + "[]"; } // Everything else // return type.getName().replace('$', '.'); } private static void streamResponse(ClientOracle clientOracle, Object payload, CommandSink sink, boolean asThrow) throws SerializationException { HasValues command; if (asThrow) { command = new ThrowCommand(); assert payload instanceof Throwable : "Trying to throw something other than a Throwable"; // payload = new RemoteException((Throwable) payload); } else { command = new ReturnCommand(); } CommandServerSerializationStreamWriter out = new CommandServerSerializationStreamWriter( clientOracle, new HasValuesCommandSink(command)); out.writeObject(payload); sink.accept((RpcCommand) command); } private RPC() { } }