This patch addresses issues 1179 (provide access to read and write methods on RemoteServiceServlet) and issue 1613 (HttpServletRequest content type checking should be case insensitive). Basically, I moved the RSS methods into a utility class called RPCServletUtils and there added a case insensitive check for content type. Patch by: mmendez Review by: scottb git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1538 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java new file mode 100644 index 0000000..5053f73 --- /dev/null +++ b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
@@ -0,0 +1,252 @@ +/* + * Copyright 2007 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.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 { + private static final String ACCEPT_ENCODING = "Accept-Encoding"; + + private static final String CHARSET_UTF8 = "UTF-8"; + + private static final String CONTENT_ENCODING = "Content-Encoding"; + + private static final String CONTENT_ENCODING_GZIP = "gzip"; + + private static final String CONTENT_TYPE_TEXT_PLAIN_UTF8 = "text/plain; charset=utf-8"; + + private static final String GENERIC_FAILURE_MSG = "The call failed on the server; see server log for details"; + + /** + * 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 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/plain' or + * 'charset=utf-8' + */ + public static String readContentAsUtf8(HttpServletRequest request) + throws IOException, ServletException { + int contentLength = request.getContentLength(); + if (contentLength == -1) { + // Content length must be known. + throw new ServletException("Content-Length must be specified"); + } + + String contentType = request.getContentType(); + boolean contentTypeIsOkay = false; + // Content-Type must be specified. + if (contentType != null) { + contentType = contentType.toLowerCase(); + // The type must be plain text. + if (contentType.startsWith("text/plain")) { + // And it must be UTF-8 encoded (or unspecified, in which case we assume + // that it's either UTF-8 or ASCII). + if (contentType.indexOf("charset=") == -1) { + contentTypeIsOkay = true; + } else if (contentType.indexOf("charset=utf-8") != -1) { + contentTypeIsOkay = true; + } + } + } + + if (!contentTypeIsOkay) { + throw new ServletException( + "Content-Type must be 'text/plain' with 'charset=utf-8' (or unspecified charset)"); + } + + InputStream in = request.getInputStream(); + try { + byte[] payload = new byte[contentLength]; + int offset = 0; + int len = contentLength; + int byteCount; + while (offset < contentLength) { + byteCount = in.read(payload, offset, len); + if (byteCount == -1) { + throw new ServletException("Client did not send " + contentLength + + " bytes as expected"); + } + offset += byteCount; + len -= byteCount; + } + return new String(payload, CHARSET_UTF8); + } finally { + if (in != null) { + in.close(); + } + } + } + + /** + * 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(); + response.setHeader(CONTENT_ENCODING, CONTENT_ENCODING_GZIP); + 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_TEXT_PLAIN_UTF8); + response.setStatus(HttpServletResponse.SC_OK); + 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); + response.getWriter().write(GENERIC_FAILURE_MSG); + } catch (IOException ex) { + servletContext.log( + "respondWithUnexpectedFailure failed while sending the previous failure to the client", + ex); + } + } + + private RPCServletUtils() { + // Not instantiable + } +}
diff --git a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java index ce1d3c1..d4d0b2a 100644 --- a/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java +++ b/user/src/com/google/gwt/user/server/rpc/RemoteServiceServlet.java
@@ -18,7 +18,6 @@ import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException; import com.google.gwt.user.client.rpc.SerializationException; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; @@ -26,10 +25,8 @@ import java.text.ParseException; import java.util.HashMap; import java.util.Map; -import java.util.zip.GZIPOutputStream; import javax.servlet.ServletContext; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -41,106 +38,6 @@ */ public class RemoteServiceServlet extends HttpServlet implements SerializationPolicyProvider { - /* - * These members are used to get and set the different HttpServletResponse and - * HttpServletRequest headers. - */ - private static final String ACCEPT_ENCODING = "Accept-Encoding"; - - private static final String CHARSET_UTF8 = "UTF-8"; - private static final String CONTENT_ENCODING = "Content-Encoding"; - private static final String CONTENT_ENCODING_GZIP = "gzip"; - - private static final String CONTENT_TYPE_TEXT_PLAIN_UTF8 = "text/plain; charset=utf-8"; - private static final String GENERIC_FAILURE_MSG = "The call failed on the server; see server log for details"; - /** - * Controls the compression threshold at and below which no compression will - * take place. - */ - private static final int UNCOMPRESSED_BYTE_SIZE_LIMIT = 256; - - /** - * Return true if the response object accepts Gzip encoding. This is done by - * checking that the accept-encoding header specifies gzip as a supported - * encoding. - */ - private 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); - } - - /** - * This method attempts to estimate the number of bytes that a string will - * consume when it is sent out as part of an HttpServletResponse. This really - * a hack since we are assuming that every character will consume two bytes - * upon transmission. This is definitely not true since some characters - * actually consume more than two bytes and some consume less. This is even - * less accurate if the string is converted to UTF8. However, it does save us - * from converting every string that we plan on sending back to UTF8 just to - * determine that we should not compress it. - */ - private static int estimateByteSize(final String buffer) { - return (buffer.length() * 2); - } - - /** - * Read the payload as UTF-8 from the request stream. - */ - private static String readPayloadAsUtf8(HttpServletRequest request) - throws IOException, ServletException { - int contentLength = request.getContentLength(); - if (contentLength == -1) { - // Content length must be known. - throw new ServletException("Content-Length must be specified"); - } - - String contentType = request.getContentType(); - boolean contentTypeIsOkay = false; - // Content-Type must be specified. - if (contentType != null) { - // The type must be plain text. - if (contentType.startsWith("text/plain")) { - // And it must be UTF-8 encoded (or unspecified, in which case we assume - // that it's either UTF-8 or ASCII). - if (contentType.indexOf("charset=") == -1) { - contentTypeIsOkay = true; - } else if (contentType.indexOf("charset=utf-8") != -1) { - contentTypeIsOkay = true; - } - } - } - if (!contentTypeIsOkay) { - throw new ServletException( - "Content-Type must be 'text/plain' with 'charset=utf-8' (or unspecified charset)"); - } - InputStream in = request.getInputStream(); - try { - byte[] payload = new byte[contentLength]; - int offset = 0; - int len = contentLength; - int byteCount; - while (offset < contentLength) { - byteCount = in.read(payload, offset, len); - if (byteCount == -1) { - throw new ServletException("Client did not send " + contentLength - + " bytes as expected"); - } - offset += byteCount; - len -= byteCount; - } - return new String(payload, "UTF-8"); - } finally { - if (in != null) { - in.close(); - } - } - } private final ThreadLocal<HttpServletRequest> perThreadRequest = new ThreadLocal<HttpServletRequest>(); @@ -176,7 +73,7 @@ // Read the request fully. // - String requestPayload = readPayloadAsUtf8(request); + String requestPayload = RPCServletUtils.readContentAsUtf8(request); // Let subclasses see the serialized request. // @@ -389,11 +286,8 @@ */ protected void doUnexpectedFailure(Throwable e) { ServletContext servletContext = getServletContext(); - servletContext.log("Exception while dispatching incoming RPC call", e); - - // Send GENERIC_FAILURE_MSG with 500 status. - // - respondWithFailure(getThreadLocalResponse()); + RPCServletUtils.writeResponseForUnexpectedFailure(servletContext, + getThreadLocalResponse(), e); } /** @@ -448,7 +342,7 @@ */ protected boolean shouldCompressResponse(HttpServletRequest request, HttpServletResponse response, String responsePayload) { - return estimateByteSize(responsePayload) > UNCOMPRESSED_BYTE_SIZE_LIMIT; + return RPCServletUtils.exceedsUncompressedContentLengthLimit(responsePayload); } private SerializationPolicy getCachedSerializationPolicy( @@ -466,70 +360,12 @@ } } - /** - * Called when the machinery of this class itself has a problem, rather than - * the invoked third-party method. It writes a simple 500 message back to the - * client. - */ - private void respondWithFailure(HttpServletResponse response) { - try { - response.setContentType("text/plain"); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - response.getWriter().write(GENERIC_FAILURE_MSG); - } catch (IOException e) { - getServletContext().log( - "respondWithFailure failed while sending the previous failure to the client", - e); - } - } - - /** - * Write the response payload to the response stream. - */ private void writeResponse(HttpServletRequest request, HttpServletResponse response, String responsePayload) throws IOException { + boolean gzipEncode = RPCServletUtils.acceptsGzipEncoding(request) + && shouldCompressResponse(request, response, responsePayload); - byte[] reply = responsePayload.getBytes(CHARSET_UTF8); - String contentType = CONTENT_TYPE_TEXT_PLAIN_UTF8; - - if (acceptsGzipEncoding(request) - && shouldCompressResponse(request, response, responsePayload)) { - // Compress the reply and adjust headers. - // - ByteArrayOutputStream output = null; - GZIPOutputStream gzipOutputStream = null; - Throwable caught = null; - try { - output = new ByteArrayOutputStream(reply.length); - gzipOutputStream = new GZIPOutputStream(output); - gzipOutputStream.write(reply); - gzipOutputStream.finish(); - gzipOutputStream.flush(); - response.setHeader(CONTENT_ENCODING, CONTENT_ENCODING_GZIP); - reply = output.toByteArray(); - } catch (IOException e) { - caught = e; - } finally { - if (null != gzipOutputStream) { - gzipOutputStream.close(); - } - if (null != output) { - output.close(); - } - } - - if (caught != null) { - getServletContext().log("Unable to compress response", caught); - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - return; - } - } - - // Send the reply. - // - response.setContentLength(reply.length); - response.setContentType(contentType); - response.setStatus(HttpServletResponse.SC_OK); - response.getOutputStream().write(reply); + RPCServletUtils.writeResponse(getServletContext(), response, + responsePayload, gzipEncode); } }