/* * 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</code> * @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 } }