| /* |
| * 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 static com.google.gwt.user.client.rpc.RpcRequestBuilder.MODULE_BASE_HEADER; |
| |
| import com.google.gwt.rpc.client.impl.RemoteException; |
| import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException; |
| import com.google.gwt.user.client.rpc.SerializationException; |
| import com.google.gwt.user.server.rpc.AbstractRemoteServiceServlet; |
| import com.google.gwt.user.server.rpc.RPCRequest; |
| import com.google.gwt.user.server.rpc.RPCServletUtils; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.lang.ref.SoftReference; |
| import java.net.InetAddress; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.net.UnknownHostException; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.zip.GZIPOutputStream; |
| |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| /** |
| * EXPERIMENTAL and subject to change. Do not use this in production code. |
| * <p> |
| * The servlet base class for your RPC service implementations that |
| * automatically deserializes incoming requests from the client and serializes |
| * outgoing responses for client/server RPCs. |
| */ |
| public class RpcServlet extends AbstractRemoteServiceServlet { |
| |
| protected static final String CLIENT_ORACLE_EXTENSION = ".gwt.rpc"; |
| private static final boolean DUMP_PAYLOAD = Boolean.getBoolean("gwt.rpc.dumpPayload"); |
| |
| private final Map<String, SoftReference<ClientOracle>> clientOracleCache = new HashMap<String, SoftReference<ClientOracle>>(); |
| |
| /** |
| * The implementation of the service. |
| */ |
| private final Object delegate; |
| |
| /** |
| * The default constructor used by service implementations that |
| * extend this class. The servlet will delegate AJAX requests to |
| * the appropriate method in the subclass. |
| */ |
| public RpcServlet() { |
| this.delegate = this; |
| } |
| |
| /** |
| * The wrapping constructor used by service implementations that are |
| * separate from this class. The servlet will delegate AJAX |
| * requests to the appropriate method in the given object. |
| */ |
| public RpcServlet(Object delegate) { |
| this.delegate = delegate; |
| } |
| |
| /** |
| * This method creates the ClientOracle that will provide data about the |
| * remote client. It delegates to |
| * {@link #findClientOracleData(String, String)} to obtain access to |
| * ClientOracle data emitted by the GWT compiler. |
| */ |
| public ClientOracle getClientOracle() throws SerializationException { |
| String permutationStrongName = getPermutationStrongName(); |
| if (permutationStrongName == null) { |
| throw new SecurityException( |
| "Blocked request without GWT permutation header (XSRF attack?)"); |
| } |
| String basePath = getRequestModuleBasePath(); |
| if (basePath == null) { |
| throw new SecurityException( |
| "Blocked request without GWT base path header (XSRF attack?)"); |
| } |
| |
| ClientOracle toReturn; |
| |
| synchronized (clientOracleCache) { |
| if (clientOracleCache.containsKey(permutationStrongName)) { |
| toReturn = clientOracleCache.get(permutationStrongName).get(); |
| if (toReturn != null) { |
| return toReturn; |
| } |
| } |
| |
| if ("HostedMode".equals(permutationStrongName)) { |
| if (!allowHostedModeConnections()) { |
| throw new SecurityException("Blocked hosted mode request"); |
| } |
| toReturn = new HostedModeClientOracle(); |
| } else { |
| InputStream in = findClientOracleData(basePath, permutationStrongName); |
| |
| try { |
| toReturn = WebModeClientOracle.load(in); |
| } catch (IOException e) { |
| throw new SerializationException( |
| "Could not load serialization policy for permutation " |
| + permutationStrongName, e); |
| } |
| } |
| clientOracleCache.put(permutationStrongName, |
| new SoftReference<ClientOracle>(toReturn)); |
| } |
| |
| return toReturn; |
| } |
| |
| /** |
| * Process a call originating from the given request. Uses the |
| * {@link RPC#invokeAndStreamResponse(Object, java.lang.reflect.Method, Object[], ClientOracle, OutputStream)} |
| * method to do the actual work. |
| * <p> |
| * Subclasses may optionally override this method to handle the payload in any |
| * way they desire (by routing the request to a framework component, for |
| * instance). The {@link HttpServletRequest} and {@link HttpServletResponse} |
| * can be accessed via the {@link #getThreadLocalRequest()} and |
| * {@link #getThreadLocalResponse()} methods. |
| * </p> |
| * This is public so that it can be unit tested easily without HTTP. |
| * |
| * @param clientOracle the ClientOracle that will be used to interpret the |
| * request |
| * @param payload the UTF-8 request payload |
| * @param stream the OutputStream that will receive the encoded response |
| * @throws SerializationException if we cannot serialize the response |
| */ |
| public void processCall(ClientOracle clientOracle, String payload, |
| OutputStream stream) throws SerializationException { |
| assert clientOracle != null : "clientOracle"; |
| assert payload != null : "payload"; |
| assert stream != null : "stream"; |
| |
| try { |
| RPCRequest rpcRequest = RPC.decodeRequest(payload, delegate.getClass(), |
| clientOracle); |
| onAfterRequestDeserialized(rpcRequest); |
| RPC.invokeAndStreamResponse(delegate, rpcRequest.getMethod(), |
| rpcRequest.getParameters(), clientOracle, stream); |
| } catch (RemoteException ex) { |
| throw new SerializationException("An exception was sent from the client", |
| ex.getCause()); |
| } catch (IncompatibleRemoteServiceException ex) { |
| log( |
| "An IncompatibleRemoteServiceException was thrown while processing this call.", |
| ex); |
| RPC.streamResponseForFailure(clientOracle, stream, ex); |
| } |
| } |
| |
| /** |
| * Standard HttpServlet method: handle the POST. |
| * |
| * This doPost method swallows ALL exceptions, logs them in the |
| * ServletContext, and returns a GENERIC_FAILURE_MSG response with status code |
| * 500. |
| * |
| * @throws IOException |
| * @throws ServletException |
| * @throws SerializationException |
| */ |
| @Override |
| public final void processPost(HttpServletRequest request, |
| HttpServletResponse response) throws ServletException, IOException, |
| SerializationException { |
| |
| /* |
| * Get the ClientOracle before doing anything else, so that if ClientOracle |
| * cannot be loaded, we haven't opened the response's OutputStream. |
| */ |
| ClientOracle clientOracle = getClientOracle(); |
| |
| // Read the request fully. |
| String requestPayload = readContent(request); |
| if (DUMP_PAYLOAD) { |
| System.out.println(requestPayload); |
| } |
| |
| response.setContentType("application/json"); |
| response.setCharacterEncoding("UTF-8"); |
| |
| // Configure the OutputStream based on configuration and capabilities |
| boolean canCompress = RPCServletUtils.acceptsGzipEncoding(request) |
| && shouldCompressResponse(request, response); |
| |
| OutputStream out; |
| if (DUMP_PAYLOAD) { |
| out = new ByteArrayOutputStream(); |
| |
| } else if (canCompress) { |
| RPCServletUtils.setGzipEncodingHeader(response); |
| out = new GZIPOutputStream(response.getOutputStream()); |
| |
| } else { |
| out = response.getOutputStream(); |
| } |
| |
| // Invoke the core dispatching logic, which returns the serialized result. |
| processCall(clientOracle, requestPayload, out); |
| |
| if (DUMP_PAYLOAD) { |
| byte[] bytes = ((ByteArrayOutputStream) out).toByteArray(); |
| System.out.println(new String(bytes, "UTF-8")); |
| response.getOutputStream().write(bytes); |
| } else if (canCompress) { |
| /* |
| * We want to write the end of the gzip data, but not close the underlying |
| * OutputStream in case there are servlet filters that want to write |
| * headers after processPost(). |
| */ |
| ((GZIPOutputStream) out).finish(); |
| } |
| } |
| |
| /** |
| * Indicates whether or not an RPC request from a hosted mode client should be |
| * serviced. Requests from hosted mode clients will expose unobfuscated |
| * identifiers in the payload. It is intended that developers override this |
| * method to restrict access based on installation-specific logic (such as a |
| * range of IP addresses, checking for certain cookies, etc.) |
| * <p> |
| * The default implementation allows hosted-mode connections from the local |
| * host, loopback addresses (127.*), site local (RFC 1918), link local |
| * (169.254/16) addresses, and their IPv6 equivalents. |
| * |
| * @return <code>true</code> if a hosted mode connection should be allowed |
| * @see #getThreadLocalRequest() |
| * @see InetAddress |
| */ |
| protected boolean allowHostedModeConnections() { |
| return isRequestFromLocalAddress(); |
| } |
| |
| /** |
| * Override this method to control access to permutation-specific data. For |
| * instance, the permutation-specific data may be stored in a database in |
| * order to support older clients. |
| * <p> |
| * The default implementation attempts to load the file from the |
| * ServletContext as |
| * |
| * <code>requestModuleBasePath + permutationStrongName + CLIENT_ORACLE_EXTENSION</code> |
| * |
| * @param requestModuleBasePath the module's base path, modulo protocol and |
| * host, as reported by {@link #getRequestModuleBasePath()} |
| * @param permutationStrongName the module's strong name as reported by |
| * {@link #getPermutationStrongName()} |
| */ |
| protected InputStream findClientOracleData(String requestModuleBasePath, |
| String permutationStrongName) throws SerializationException { |
| String resourcePath = requestModuleBasePath + permutationStrongName |
| + CLIENT_ORACLE_EXTENSION; |
| InputStream in = getServletContext().getResourceAsStream(resourcePath); |
| if (in == null) { |
| throw new SerializationException( |
| "Could not find ClientOracle data for permutation " |
| + permutationStrongName); |
| } |
| return in; |
| } |
| |
| /** |
| * Extract the module's base path from the current request. |
| * |
| * @return the module's base path, modulo protocol and host, as reported by |
| * {@link com.google.gwt.core.client.GWT#getModuleBaseURL()} or |
| * <code>null</code> if the request did not contain the |
| * {@value com.google.gwt.user.client.rpc.RpcRequestBuilder#MODULE_BASE_HEADER} header |
| */ |
| protected final String getRequestModuleBasePath() { |
| try { |
| String header = getThreadLocalRequest().getHeader(MODULE_BASE_HEADER); |
| if (header == null) { |
| return null; |
| } |
| String path = new URL(header).getPath(); |
| String contextPath = getThreadLocalRequest().getContextPath(); |
| if (!path.startsWith(contextPath)) { |
| return null; |
| } |
| return path.substring(contextPath.length()); |
| } catch (MalformedURLException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Determines whether the response to a given servlet request should or should |
| * not be GZIP compressed. This method is only called in cases where the |
| * requester accepts GZIP encoding. |
| * <p> |
| * This implementation currently returns <code>true</code> if the request |
| * originates from a non-local address. Subclasses can override this logic. |
| * </p> |
| * |
| * @param request the request being served |
| * @param response the response that will be written into |
| * @return <code>true</code> if responsePayload should be GZIP compressed, |
| * otherwise <code>false</code>. |
| */ |
| protected boolean shouldCompressResponse(HttpServletRequest request, |
| HttpServletResponse response) { |
| return !isRequestFromLocalAddress(); |
| } |
| |
| /** |
| * Utility function to determine if the thread-local request originates from a |
| * local address. |
| */ |
| private boolean isRequestFromLocalAddress() { |
| try { |
| InetAddress addr = InetAddress.getByName(getThreadLocalRequest().getRemoteAddr()); |
| |
| return InetAddress.getLocalHost().equals(addr) |
| || addr.isLoopbackAddress() || addr.isSiteLocalAddress() |
| || addr.isLinkLocalAddress(); |
| } catch (UnknownHostException e) { |
| return false; |
| } |
| } |
| } |