| /* |
| * 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, 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() { |
| } |
| } |