| /* |
| * Copyright 2008 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.user.server.rpc; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.reflect.Method; |
| import java.util.zip.GZIPOutputStream; |
| |
| import javax.servlet.ServletContext; |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| /** |
| * Utility class containing helper methods used by servlets that integrate with |
| * the RPC system. |
| */ |
| public class RPCServletUtils { |
| /** |
| * Package protected for use in tests. |
| */ |
| static final int BUFFER_SIZE = 4096; |
| |
| private static final String ACCEPT_ENCODING = "Accept-Encoding"; |
| |
| private static final String ATTACHMENT = "attachment"; |
| |
| /** |
| * Used both as expected request charset and encoded response charset. |
| */ |
| private static final String CHARSET_UTF8 = "UTF-8"; |
| |
| private static final String CONTENT_DISPOSITION = "Content-Disposition"; |
| |
| private static final String CONTENT_ENCODING = "Content-Encoding"; |
| |
| private static final String CONTENT_ENCODING_GZIP = "gzip"; |
| |
| private static final String CONTENT_TYPE_APPLICATION_JSON_UTF8 = "application/json; charset=utf-8"; |
| |
| private static final String GENERIC_FAILURE_MSG = "The call failed on the server; see server log for details"; |
| |
| private static final String GWT_RPC_CONTENT_TYPE = "text/x-gwt-rpc"; |
| |
| /** |
| * Controls the compression threshold at and below which no compression will |
| * take place. |
| */ |
| private static final int UNCOMPRESSED_BYTE_SIZE_LIMIT = 256; |
| |
| /** |
| * Returns <code>true</code> if the {@link HttpServletRequest} accepts Gzip |
| * encoding. This is done by checking that the accept-encoding header |
| * specifies gzip as a supported encoding. |
| * |
| * @param request the request instance to test for gzip encoding acceptance |
| * @return <code>true</code> if the {@link HttpServletRequest} accepts Gzip |
| * encoding |
| */ |
| public static boolean acceptsGzipEncoding(HttpServletRequest request) { |
| assert (request != null); |
| |
| String acceptEncoding = request.getHeader(ACCEPT_ENCODING); |
| if (null == acceptEncoding) { |
| return false; |
| } |
| |
| return (acceptEncoding.indexOf(CONTENT_ENCODING_GZIP) != -1); |
| } |
| |
| /** |
| * Returns <code>true</code> if the response content's estimated UTF-8 byte |
| * length exceeds 256 bytes. |
| * |
| * @param content the contents of the response |
| * @return <code>true</code> if the response content's estimated UTF-8 byte |
| * length exceeds 256 bytes |
| */ |
| public static boolean exceedsUncompressedContentLengthLimit(String content) { |
| return (content.length() * 2) > UNCOMPRESSED_BYTE_SIZE_LIMIT; |
| } |
| |
| /** |
| * Returns true if the {@link java.lang.reflect.Method Method} definition on |
| * the service is specified to throw the exception contained in the |
| * InvocationTargetException or false otherwise. NOTE we do not check that the |
| * type is serializable here. We assume that it must be otherwise the |
| * application would never have been allowed to run. |
| * |
| * @param serviceIntfMethod the method from the RPC request |
| * @param cause the exception that the method threw |
| * @return true if the exception's type is in the method's signature |
| */ |
| public static boolean isExpectedException(Method serviceIntfMethod, |
| Throwable cause) { |
| assert (serviceIntfMethod != null); |
| assert (cause != null); |
| |
| Class<?>[] exceptionsThrown = serviceIntfMethod.getExceptionTypes(); |
| if (exceptionsThrown.length <= 0) { |
| // The method is not specified to throw any exceptions |
| // |
| return false; |
| } |
| |
| Class<? extends Throwable> causeType = cause.getClass(); |
| |
| for (Class<?> exceptionThrown : exceptionsThrown) { |
| assert (exceptionThrown != null); |
| |
| if (exceptionThrown.isAssignableFrom(causeType)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Returns the content of an {@link HttpServletRequest} by decoding it using |
| * <code>expectedCharSet</code>, or <code>UTF-8</code> if |
| * <code>expectedCharSet</code> is <code>null</null>. |
| * |
| * @param request the servlet request whose content we want to read |
| * @param expectedContentType the expected content (i.e. 'type/subtype' only) |
| * in the Content-Type request header, or <code>null</code> if no |
| * validation is to be performed, and you are willing to allow for |
| * some types of cross type security attacks |
| * @param expectedCharSet the expected request charset, or <code>null</code> |
| * if no charset validation is to be performed and <code>UTF-8</code> |
| * should be assumed |
| * @return the content of an {@link HttpServletRequest} by decoding it using |
| * <code>expectedCharSet</code>, or <code>UTF-8</code> if |
| * <code>expectedCharSet</code> is <code>null</null> |
| * @throws IOException if the request's input stream cannot be accessed, read |
| * from or closed |
| * @throws ServletException if the request's content type does not |
| * equal the supplied <code>expectedContentType</code> or |
| * <code>expectedCharSet</code> |
| */ |
| public static String readContent(HttpServletRequest request, |
| String expectedContentType, String expectedCharSet) |
| throws IOException, ServletException { |
| if (expectedContentType != null) { |
| checkContentTypeIgnoreCase(request, expectedContentType); |
| } |
| if (expectedCharSet != null) { |
| checkCharacterEncodingIgnoreCase(request, expectedCharSet); |
| } |
| |
| /* |
| * Need to support 'Transfer-Encoding: chunked', so do not rely on |
| * presence of a 'Content-Length' request header. |
| */ |
| InputStream in = request.getInputStream(); |
| byte[] buffer = new byte[BUFFER_SIZE]; |
| ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE); |
| try { |
| while (true) { |
| int byteCount = in.read(buffer); |
| if (byteCount == -1) { |
| break; |
| } |
| out.write(buffer, 0, byteCount); |
| } |
| String contentCharSet = expectedCharSet != null |
| ? expectedCharSet : CHARSET_UTF8; |
| return out.toString(contentCharSet); |
| } finally { |
| if (in != null) { |
| in.close(); |
| } |
| } |
| } |
| |
| /** |
| * Returns the content of an {@link HttpServletRequest}, after verifying a |
| * <code>gwt/x-gwt-rpc; charset=utf-8</code> content type. |
| * |
| * @param request the servlet request whose content we want to read |
| * @return the content of an {@link HttpServletRequest} by decoding it using |
| * <code>UTF-8</code> |
| * @throws IOException if the request's input stream cannot be accessed, read |
| * from or closed |
| * @throws ServletException if the request's content type is not |
| * <code>gwt/x-gwt-rpc; charset=utf-8</code>, ignoring case |
| */ |
| public static String readContentAsGwtRpc(HttpServletRequest request) |
| throws IOException, ServletException { |
| return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8); |
| } |
| |
| /** |
| * Returns the content of an {@link HttpServletRequest} by decoding it using |
| * the UTF-8 charset. |
| * |
| * @param request the servlet request whose content we want to read |
| * @return the content of an {@link HttpServletRequest} by decoding it using |
| * the UTF-8 charset |
| * @throws IOException if the requests input stream cannot be accessed, read |
| * from or closed |
| * @throws ServletException if the content length of the request is not |
| * specified of if the request's content type is not |
| * 'text/x-gwt-rpc' and 'charset=utf-8' |
| * @deprecated Use {@link readContent} instead. |
| */ |
| @Deprecated |
| public static String readContentAsUtf8(HttpServletRequest request) |
| throws IOException, ServletException { |
| return readContent(request, null, null); |
| } |
| |
| /** |
| * Returns the content of an {@link HttpServletRequest} by decoding it using |
| * the UTF-8 charset. |
| * |
| * @param request the servlet request whose content we want to read |
| * @param checkHeaders Specify 'true' to check the Content-Type header to see |
| * that it matches the expected value 'text/x-gwt-rpc' and the |
| * content encoding is UTF-8. Disabling this check may allow some |
| * types of cross type security attacks. |
| * @return the content of an {@link HttpServletRequest} by decoding it using |
| * the UTF-8 charset |
| * @throws IOException if the requests input stream cannot be accessed, read |
| * from or closed |
| * @throws ServletException if the content length of the request is not |
| * specified of if the request's content type is not |
| * 'text/x-gwt-rpc' and 'charset=utf-8' |
| * @deprecated Use {@link readContent} instead. |
| */ |
| @Deprecated |
| public static String readContentAsUtf8(HttpServletRequest request, |
| boolean checkHeaders) throws IOException, ServletException { |
| return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8); |
| } |
| |
| /** |
| * Sets the correct header to indicate that a response is gzipped. |
| */ |
| public static void setGzipEncodingHeader(HttpServletResponse response) { |
| response.setHeader(CONTENT_ENCODING, CONTENT_ENCODING_GZIP); |
| } |
| |
| /** |
| * Returns <code>true</code> if the request accepts gzip encoding and the the |
| * response content's estimated UTF-8 byte length exceeds 256 bytes. |
| * |
| * @param request the request associated with the response content |
| * @param responseContent a string that will be |
| * @return <code>true</code> if the request accepts gzip encoding and the the |
| * response content's estimated UTF-8 byte length exceeds 256 bytes |
| */ |
| public static boolean shouldGzipResponseContent(HttpServletRequest request, |
| String responseContent) { |
| return acceptsGzipEncoding(request) |
| && exceedsUncompressedContentLengthLimit(responseContent); |
| } |
| |
| /** |
| * Write the response content into the {@link HttpServletResponse}. If |
| * <code>gzipResponse</code> is <code>true</code>, the response content will |
| * be gzipped prior to being written into the response. |
| * |
| * @param servletContext servlet context for this response |
| * @param response response instance |
| * @param responseContent a string containing the response content |
| * @param gzipResponse if <code>true</code> the response content will be gzip |
| * encoded before being written into the response |
| * @throws IOException if reading, writing, or closing the response's output |
| * stream fails |
| */ |
| public static void writeResponse(ServletContext servletContext, |
| HttpServletResponse response, String responseContent, boolean gzipResponse) |
| throws IOException { |
| |
| byte[] responseBytes = responseContent.getBytes(CHARSET_UTF8); |
| if (gzipResponse) { |
| // Compress the reply and adjust headers. |
| // |
| ByteArrayOutputStream output = null; |
| GZIPOutputStream gzipOutputStream = null; |
| Throwable caught = null; |
| try { |
| output = new ByteArrayOutputStream(responseBytes.length); |
| gzipOutputStream = new GZIPOutputStream(output); |
| gzipOutputStream.write(responseBytes); |
| gzipOutputStream.finish(); |
| gzipOutputStream.flush(); |
| setGzipEncodingHeader(response); |
| responseBytes = output.toByteArray(); |
| } catch (IOException e) { |
| caught = e; |
| } finally { |
| if (null != gzipOutputStream) { |
| gzipOutputStream.close(); |
| } |
| if (null != output) { |
| output.close(); |
| } |
| } |
| |
| if (caught != null) { |
| servletContext.log("Unable to compress response", caught); |
| response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); |
| return; |
| } |
| } |
| |
| // Send the reply. |
| // |
| response.setContentLength(responseBytes.length); |
| response.setContentType(CONTENT_TYPE_APPLICATION_JSON_UTF8); |
| response.setStatus(HttpServletResponse.SC_OK); |
| response.setHeader(CONTENT_DISPOSITION, ATTACHMENT); |
| response.getOutputStream().write(responseBytes); |
| } |
| |
| /** |
| * Called when the servlet itself has a problem, rather than the invoked |
| * third-party method. It writes a simple 500 message back to the client. |
| * |
| * @param servletContext |
| * @param response |
| * @param failure |
| */ |
| public static void writeResponseForUnexpectedFailure( |
| ServletContext servletContext, HttpServletResponse response, |
| Throwable failure) { |
| servletContext.log("Exception while dispatching incoming RPC call", failure); |
| |
| // Send GENERIC_FAILURE_MSG with 500 status. |
| // |
| try { |
| response.setContentType("text/plain"); |
| response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); |
| try { |
| response.getOutputStream().write(GENERIC_FAILURE_MSG.getBytes("UTF-8")); |
| } catch (IllegalStateException e) { |
| // Handle the (unexpected) case where getWriter() was previously used |
| response.getWriter().write(GENERIC_FAILURE_MSG); |
| } |
| } catch (IOException ex) { |
| servletContext.log( |
| "respondWithUnexpectedFailure failed while sending the previous failure to the client", |
| ex); |
| } |
| } |
| |
| /** |
| * Performs validation of the character encoding, ignoring case. |
| * |
| * @param request the incoming request |
| * @param expectedCharSet the expected charset of the request |
| * @throws ServletException if requests encoding is not <code>null</code> and |
| * does not equal, ignoring case, <code>expectedCharSet</code> |
| */ |
| private static void checkCharacterEncodingIgnoreCase( |
| HttpServletRequest request, String expectedCharSet) |
| throws ServletException { |
| boolean encodingOkay = false; |
| String characterEncoding = request.getCharacterEncoding(); |
| if (characterEncoding != null) { |
| /* |
| * TODO: It would seem that we should be able to use equalsIgnoreCase here |
| * instead of indexOf. Need to be sure that servlet engines return a |
| * properly parsed character encoding string if we decide to make this |
| * change. |
| */ |
| if (characterEncoding.toLowerCase().indexOf(expectedCharSet.toLowerCase()) |
| != -1) { |
| encodingOkay = true; |
| } |
| } |
| |
| if (!encodingOkay) { |
| throw new ServletException("Character Encoding is '" |
| + (characterEncoding == null ? "(null)" : characterEncoding) |
| + "'. Expected '" + expectedCharSet + "'"); |
| } |
| } |
| |
| /** |
| * Performs Content-Type validation of the incoming request, ignoring case |
| * and any <code>charset</code> parameter. |
| * |
| * @see #checkCharacterEncodingIgnoreCase(HttpServletRequest, String) |
| * @param request the incoming request |
| * @param expectedContentType the expected Content-Type for the incoming |
| * request |
| * @throws ServletException if the request's content type is not |
| * <code>null</code> and does not, ignoring case, equal |
| * <code>expectedContentType</code>, |
| */ |
| private static void checkContentTypeIgnoreCase( |
| HttpServletRequest request, String expectedContentType) |
| throws ServletException { |
| String contentType = request.getContentType(); |
| boolean contentTypeIsOkay = false; |
| |
| if (contentType != null) { |
| contentType = contentType.toLowerCase(); |
| /* |
| * NOTE:We use startsWith because some servlet engines, i.e. Tomcat, do |
| * not remove the charset component but others do. |
| */ |
| if (contentType.startsWith(expectedContentType.toLowerCase())) { |
| contentTypeIsOkay = true; |
| } |
| } |
| |
| if (!contentTypeIsOkay) { |
| throw new ServletException("Content-Type was '" |
| + (contentType == null ? "(null)" : contentType) + "'. Expected '" |
| + expectedContentType + "'."); |
| } |
| } |
| |
| private RPCServletUtils() { |
| // Not instantiable |
| } |
| } |