1 // Copyright 2012 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.tools
.development
;
5 import com
.google
.appengine
.api
.log
.dev
.LocalLogService
;
6 import com
.google
.common
.base
.Optional
;
7 import com
.google
.common
.base
.Preconditions
;
8 import com
.google
.common
.net
.HttpHeaders
;
10 import java
.io
.ByteArrayOutputStream
;
11 import java
.io
.IOException
;
12 import java
.io
.OutputStream
;
13 import java
.io
.OutputStreamWriter
;
14 import java
.io
.PrintWriter
;
15 import java
.io
.UnsupportedEncodingException
;
16 import java
.text
.ParseException
;
17 import java
.text
.SimpleDateFormat
;
18 import java
.util
.Date
;
19 import java
.util
.Enumeration
;
20 import java
.util
.Locale
;
21 import java
.util
.NoSuchElementException
;
22 import java
.util
.TimeZone
;
23 import java
.util
.Vector
;
25 import javax
.servlet
.Filter
;
26 import javax
.servlet
.FilterChain
;
27 import javax
.servlet
.FilterConfig
;
28 import javax
.servlet
.ServletException
;
29 import javax
.servlet
.ServletOutputStream
;
30 import javax
.servlet
.ServletRequest
;
31 import javax
.servlet
.ServletResponse
;
32 import javax
.servlet
.http
.Cookie
;
33 import javax
.servlet
.http
.HttpServletRequest
;
34 import javax
.servlet
.http
.HttpServletRequestWrapper
;
35 import javax
.servlet
.http
.HttpServletResponse
;
36 import javax
.servlet
.http
.HttpServletResponseWrapper
;
39 * A filter that rewrites the response headers and body from the user's
42 * <p>This sanitises the headers to ensure that they are sensible and the user
43 * is not setting sensitive headers, such as Content-Length, incorrectly. It
44 * also deletes the body if the response status code indicates a non-body
47 * <p>This also strips out some request headers before passing the request to
51 public class ResponseRewriterFilter
implements Filter
{
53 * A mock timestamp to use as the response completion time, for testing.
55 * <p>Long.MIN_VALUE indicates that this should not be used, and instead, the
56 * current time should be taken.
58 private final long emulatedResponseTime
;
59 private LocalLogService logService
;
61 private static final String BLOB_KEY_HEADER
= "X-AppEngine-BlobKey";
63 /** The value of the "Server" header output by the development server. */
64 private static final String DEVELOPMENT_SERVER
= "Development/1.0";
66 /** These statuses must not include a response body (RFC 2616). */
67 private static final int[] NO_BODY_RESPONSE_STATUSES
= {
68 HttpServletResponse
.SC_CONTINUE
,
69 HttpServletResponse
.SC_SWITCHING_PROTOCOLS
,
70 HttpServletResponse
.SC_NO_CONTENT
,
71 HttpServletResponse
.SC_NOT_MODIFIED
,
74 public ResponseRewriterFilter() {
76 emulatedResponseTime
= Long
.MIN_VALUE
;
80 * Creates a ResponseRewriterFilter for testing purposes, which mocks the
83 * @param mockTimestamp Indicates that the current time will be emulated with
86 public ResponseRewriterFilter(long mockTimestamp
) {
88 emulatedResponseTime
= mockTimestamp
;
92 public void init(FilterConfig filterConfig
) {
93 Object apiProxyDelegate
= filterConfig
.getServletContext().getAttribute(
94 "com.google.appengine.devappserver.ApiProxyLocal");
95 if (apiProxyDelegate
instanceof ApiProxyLocal
) {
96 ApiProxyLocal apiProxyLocal
= (ApiProxyLocal
) apiProxyDelegate
;
97 logService
= (LocalLogService
) apiProxyLocal
.getService(LocalLogService
.PACKAGE
);
102 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
103 * javax.servlet.ServletResponse,
104 * javax.servlet.FilterChain)
107 public void doFilter(ServletRequest request
, ServletResponse response
, FilterChain chain
)
108 throws IOException
, ServletException
{
109 HttpServletRequest httprequest
;
110 HttpServletResponse httpresponse
;
112 httprequest
= (HttpServletRequest
) request
;
113 httpresponse
= (HttpServletResponse
) response
;
114 } catch (ClassCastException e
) {
115 throw new ServletException(e
);
118 RequestWrapper wrappedRequest
= new RequestWrapper(httprequest
);
119 ResponseWrapper wrappedResponse
= new ResponseWrapper(httpresponse
);
121 chain
.doFilter(wrappedRequest
, wrappedResponse
);
123 Preconditions
.checkState(!response
.isCommitted(), "Response has already been committed");
126 if (emulatedResponseTime
== Long
.MIN_VALUE
) {
127 responseTime
= System
.currentTimeMillis();
129 responseTime
= emulatedResponseTime
;
132 ignoreHeadersRewriter(wrappedRequest
, wrappedResponse
, responseTime
);
133 serverDateRewriter(wrappedRequest
, wrappedResponse
, responseTime
);
134 cacheRewriter(wrappedRequest
, wrappedResponse
, responseTime
);
135 blobServeRewriter(wrappedRequest
, wrappedResponse
, responseTime
);
136 contentLengthRewriter(wrappedRequest
, wrappedResponse
, responseTime
);
138 wrappedResponse
.reallyCommit();
141 private static final String
[] IGNORE_REQUEST_HEADERS
= {
142 HttpHeaders
.ACCEPT_ENCODING
,
143 HttpHeaders
.CONNECTION
,
145 HttpHeaders
.PROXY_AUTHORIZATION
,
148 HttpHeaders
.TRANSFER_ENCODING
,
151 private static final String
[] IGNORE_RESPONSE_HEADERS
= {
152 HttpHeaders
.CONNECTION
,
153 HttpHeaders
.CONTENT_ENCODING
,
156 HttpHeaders
.PROXY_AUTHENTICATE
,
159 HttpHeaders
.TRANSFER_ENCODING
,
164 * Removes specific response headers.
166 * <p>Certain response headers cannot be modified by an Application. This
167 * rewriter simply removes those headers.
169 * @param request A request object, which is not modified.
170 * @param response A response object, which may be modified.
171 * @param responseTime The timestamp indicating when the response completed.
173 private void ignoreHeadersRewriter(HttpServletRequest request
, ResponseWrapper response
,
175 for (String h
: IGNORE_RESPONSE_HEADERS
) {
176 if (response
.containsHeader(h
)) {
177 response
.reallySetHeader(h
, null);
183 * Sets the Server and Date response headers to their correct value.
185 * @param request A request object, which is not modified.
186 * @param response A response object, which may be modified.
187 * @param responseTime The timestamp indicating when the response completed.
189 private void serverDateRewriter(HttpServletRequest request
, ResponseWrapper response
,
191 response
.reallySetHeader(HttpHeaders
.SERVER
, DEVELOPMENT_SERVER
);
192 response
.reallySetDateHeader(HttpHeaders
.DATE
, responseTime
);
196 * Determines whether the response may have a body, based on the status code.
198 * @param status The response status code.
199 * @return true if the response may have a body.
201 private static boolean responseMayHaveBody(int status
) {
202 for (int s
: NO_BODY_RESPONSE_STATUSES
) {
211 * Sets the default Cache-Control and Expires headers.
213 * These are only set if the response status allows a body, and only if the
214 * headers have not been explicitly set by the application.
216 * @param request A request object, which is not modified.
217 * @param response A response object, which may be modified.
218 * @param responseTime The timestamp indicating when the response completed.
220 private void cacheRewriter(HttpServletRequest request
, ResponseWrapper response
,
222 if (!responseMayHaveBody(response
.getStatus())) {
226 if (!response
.containsHeader(HttpHeaders
.CACHE_CONTROL
)) {
227 response
.reallySetHeader(HttpHeaders
.CACHE_CONTROL
, "no-cache");
228 if (!response
.containsHeader(HttpHeaders
.EXPIRES
)) {
229 response
.reallySetHeader(HttpHeaders
.EXPIRES
, "Fri, 01 Jan 1990 00:00:00 GMT");
233 if (response
.containsHeader(HttpHeaders
.SET_COOKIE
)) {
234 long expires
= response
.getExpires();
235 if (expires
== Long
.MIN_VALUE
|| expires
>= responseTime
) {
236 response
.reallySetDateHeader(HttpHeaders
.EXPIRES
, responseTime
);
239 Vector
<String
> cacheDirectives
= new Vector
<String
>(response
.getCacheControl());
240 while (cacheDirectives
.remove("public")) {
242 if (!cacheDirectives
.contains("private") && !cacheDirectives
.contains("no-cache") &&
243 !cacheDirectives
.contains("no-store")) {
244 cacheDirectives
.add("private");
246 StringBuilder newCacheControl
= new StringBuilder();
247 for (String directive
: cacheDirectives
) {
248 if (newCacheControl
.length() > 0) {
249 newCacheControl
.append(", ");
251 newCacheControl
.append(directive
);
253 response
.reallySetHeader(HttpHeaders
.CACHE_CONTROL
, newCacheControl
.toString());
258 * Deletes the response body, if X-AppEngine-BlobKey is present.
260 * Otherwise, it would be an error if we were to send text to the client and
261 * then attempt to rewrite the body to serve the blob.
263 * @param request A request object, which is not modified.
264 * @param response A response object, which may be modified.
265 * @param responseTime The timestamp indicating when the response completed.
267 private void blobServeRewriter(HttpServletRequest request
, ResponseWrapper response
,
269 if (response
.containsHeader(BLOB_KEY_HEADER
)) {
270 response
.reallyResetBuffer();
275 * Rewrites the Content-Length header.
277 * <p>Even though Content-Length is not a user modifiable header, App Engine
278 * sends a correct Content-Length to the user based on the actual response.
280 * <p>If the request method is HEAD or the response status indicates that the
281 * response should not have a body, the body is deleted instead. The existing
282 * Content-Length header is preserved for HEAD requests.
284 * @param request A request object, which is not modified.
285 * @param response A response object, which may be modified.
286 * @param responseTime The timestamp indicating when the response completed.
288 private void contentLengthRewriter(HttpServletRequest request
, ResponseWrapper response
,
290 response
.flushPrintWriter();
291 Optional
<Integer
> responseSize
;
292 if (request
.getMethod().equals("HEAD")) {
293 response
.reallyResetBuffer();
294 responseSize
= Optional
.absent();
295 } else if (!responseMayHaveBody(response
.getStatus())) {
296 response
.reallySetHeader(HttpHeaders
.CONTENT_LENGTH
, null);
297 response
.reallyResetBuffer();
298 responseSize
= Optional
.absent();
300 response
.reallySetHeader(HttpHeaders
.CONTENT_LENGTH
, Long
.toString(response
.getBodyLength()));
301 responseSize
= Optional
.of(response
.getBodyLength());
303 if (logService
!= null) {
304 if (responseSize
.isPresent()) {
305 logService
.registerResponseSize(responseSize
.get());
307 logService
.clearResponseSize();
313 public void destroy() {
317 * Wraps a request to strip out some of the headers.
319 private static class RequestWrapper
extends HttpServletRequestWrapper
{
321 * An Enumeration that filters out ignored header names.
323 private static class HeaderFilterEnumeration
implements Enumeration
<Object
> {
324 private final Enumeration
<?
> allNames
;
325 private String nextName
;
327 HeaderFilterEnumeration(Enumeration
<?
> allNames
) {
328 this.allNames
= allNames
;
332 /** Get the next non-ignored name from allNames and store it in nextName.
334 private void getNextValidName() {
335 while (allNames
.hasMoreElements()) {
336 String name
= (String
) allNames
.nextElement();
337 if (validHeader(name
)) {
346 public boolean hasMoreElements() {
347 return nextName
!= null;
351 public Object
nextElement() {
352 if (nextName
== null) {
353 throw new NoSuchElementException();
355 String result
= nextName
;
361 public RequestWrapper(HttpServletRequest request
) {
365 private static boolean validHeader(String name
) {
366 for (String h
: IGNORE_REQUEST_HEADERS
) {
367 if (h
.equalsIgnoreCase(name
)) {
375 public long getDateHeader(String name
) {
376 return validHeader(name
) ?
super.getDateHeader(name
) : -1;
380 public String
getHeader(String name
) {
381 return validHeader(name
) ?
super.getHeader(name
) : null;
385 public Enumeration
<?
> getHeaders(String name
) {
386 if (validHeader(name
)) {
387 return super.getHeaders(name
);
389 return new Enumeration
<Object
>() {
391 public boolean hasMoreElements() {
395 public Object
nextElement() {
396 throw new NoSuchElementException();
403 public Enumeration
<?
> getHeaderNames() {
404 return new HeaderFilterEnumeration(super.getHeaderNames());
408 public int getIntHeader(String name
) {
409 return validHeader(name
) ?
super.getIntHeader(name
) : -1;
414 * Wraps a response to buffer the entire body, and allow reading of the
415 * status, body and headers.
417 * <p>This buffers the entire body locally, so that the body is not streamed
418 * in chunks to the client, but instead all at the end.
420 * <p>This is necessary to calculate the correct Content-Length at the end,
421 * and also to modify headers after the application returns, but also matches
422 * production behaviour.
424 * <p>For the sake of compatibility, the class <em>pretends</em> not to buffer
425 * any data. (It behaves as if it has a buffer size of 0.) Therefore, as with
426 * a normal {@link HttpServletResponseWrapper}, you may not modify the status
427 * or headers after modifying the body. Note that the {@link PrintWriter}
428 * returned by {@link #getWriter()} does its own limited buffering.
430 * <p>This class also provides the ability to read the value of the status and
431 * some of the headers (which is not available before Servlet 3.0), and the
434 private static class ResponseWrapper
extends HttpServletResponseWrapper
{
435 private int status
= SC_OK
;
438 * The value of the Expires header, parsed as a Java timestamp.
440 * <p>Long.MIN_VALUE indicates that the Expires header is missing or invalid.
442 private long expires
= Long
.MIN_VALUE
;
443 /** The value of the Cache-Control headers, parsed into separate directives. */
444 private final Vector
<String
> cacheControl
= new Vector
<String
>();
446 /** A buffer to hold the body without sending it to the client. */
447 private final ByteArrayOutputStream body
= new ByteArrayOutputStream();
448 private ServletOutputStream bodyServletStream
= null;
449 private PrintWriter bodyPrintWriter
= null;
450 /** Indicates that flushBuffer() has been called. */
451 private boolean committed
= false;
453 private static final String DATE_FORMAT_STRING
=
454 "E, dd MMM yyyy HH:mm:ss 'GMT'";
456 ResponseWrapper(HttpServletResponse response
) {
461 public ServletOutputStream
getOutputStream() {
462 if (bodyServletStream
!= null) {
463 return bodyServletStream
;
465 Preconditions
.checkState(bodyPrintWriter
== null, "getWriter has already been called");
466 bodyServletStream
= new ServletOutputStreamWrapper(body
);
467 return bodyServletStream
;
472 public PrintWriter
getWriter() throws UnsupportedEncodingException
{
473 if (bodyPrintWriter
!= null) {
474 return bodyPrintWriter
;
476 Preconditions
.checkState(bodyServletStream
== null,
477 "getOutputStream has already been called");
478 bodyPrintWriter
= new PrintWriter(new OutputStreamWriter(body
, getCharacterEncoding()));
479 return bodyPrintWriter
;
484 public void setCharacterEncoding(String charset
) {
485 if (bodyPrintWriter
!= null || isCommitted()) {
488 super.setCharacterEncoding(charset
);
492 public void setContentLength(int len
) {
496 super.setContentLength(len
);
500 public void setContentType(String type
) {
504 if (type
!= null && nonAscii(type
)) {
507 if (bodyPrintWriter
!= null) {
508 type
= stripCharsetFromMediaType(type
);
510 super.setContentType(type
);
514 public void setLocale(Locale loc
) {
518 String oldCharacterEncoding
= getCharacterEncoding();
519 String oldContentType
= getContentType();
520 super.setLocale(loc
);
521 if (oldContentType
!= null || bodyPrintWriter
!= null) {
522 super.setCharacterEncoding(oldCharacterEncoding
);
524 if (oldContentType
!= null) {
525 super.setContentType(oldContentType
);
530 public void setBufferSize(int size
) {
532 super.setBufferSize(size
);
536 public int getBufferSize() {
541 public void flushBuffer() {
546 public void reset() {
552 public void resetBuffer() {
557 public boolean isCommitted() {
558 return committed
|| body
.size() > 0;
562 * Checks whether {@link #isCommitted()} is true, and if so, raises
563 * {@link IllegalStateException}.
565 void checkNotCommitted() {
566 Preconditions
.checkState(!isCommitted(), "Response has already been committed");
570 public void addCookie(Cookie cookie
) {
574 super.addCookie(cookie
);
578 public void addDateHeader(String name
, long date
) {
582 if (nonAscii(name
)) {
585 super.addDateHeader(name
, date
);
586 if (name
.equalsIgnoreCase(HttpHeaders
.EXPIRES
)) {
592 public void addHeader(String name
, String value
) {
599 if (nonAscii(name
) || nonAscii(value
)) {
602 if (name
.equalsIgnoreCase(HttpHeaders
.EXPIRES
)) {
605 } catch (ParseException e
) {
607 } else if (name
.equalsIgnoreCase(HttpHeaders
.CACHE_CONTROL
)) {
608 parseCacheControl(value
);
609 } else if (name
.equalsIgnoreCase(HttpHeaders
.CONTENT_TYPE
)) {
610 if (bodyPrintWriter
!= null) {
611 value
= stripCharsetFromMediaType(value
);
614 super.addHeader(name
, value
);
618 public void addIntHeader(String name
, int value
) {
622 if (nonAscii(name
)) {
625 super.addIntHeader(name
, value
);
629 public void sendError(int sc
) throws IOException
{
632 setErrorBody(Integer
.toString(sc
));
636 public void sendError(int sc
, String msg
) throws IOException
{
639 setErrorBody(sc
+ " " + msg
);
642 /** Sets the response body to an HTML page with an error message.
644 * This also sets the Content-Type header.
646 * @param errorText A message to display in the title and page contents.
647 * Should contain an HTTP status code and optional message.
649 private void setErrorBody(String errorText
) throws IOException
{
650 setHeader(HttpHeaders
.CONTENT_TYPE
, "text/html; charset=iso-8859-1");
651 String bodyText
= "<html><head><title>Error " + errorText
+ "</title></head>\n"
652 + "<body><h2>Error " + errorText
+ "</h2></body>\n"
654 body
.write(bodyText
.getBytes("iso-8859-1"));
658 public void sendRedirect(String location
) {
662 setHeader(HttpHeaders
.LOCATION
, encodeRedirectURL(location
));
667 public void setDateHeader(String name
, long date
) {
671 if (nonAscii(name
)) {
674 reallySetDateHeader(name
, date
);
678 public void setHeader(String name
, String value
) {
682 if (nonAscii(name
) || (value
!= null && nonAscii(value
))) {
685 if (name
.equalsIgnoreCase(HttpHeaders
.CONTENT_TYPE
)) {
686 if (bodyPrintWriter
!= null) {
687 value
= stripCharsetFromMediaType(value
);
690 reallySetHeader(name
, value
);
694 public void setIntHeader(String name
, int value
) {
698 if (nonAscii(name
)) {
701 if (name
.equalsIgnoreCase(HttpHeaders
.EXPIRES
)) {
702 expires
= Long
.MIN_VALUE
;
704 super.setIntHeader(name
, value
);
708 public void setStatus(int sc
) {
717 public void setStatus(int sc
, String sm
) {
721 super.setStatus(sc
, sm
);
726 * Gets the status code of the response.
733 * Gets the value of the Expires header, as a Java timestamp.
735 * <p>Long.MIN_VALUE indicates that the Expires header is missing or invalid.
737 public long getExpires() {
742 * Gets the value of the Cache-Control headers, parsed into separate directives.
744 public Vector
<String
> getCacheControl() {
749 * Gets the total number of bytes that have been written to the body without
752 int getBodyLength() {
757 * Writes the body to the wrapped response's output stream.
759 * <p>If the body is not empty, this causes the status and headers to be
760 * rewritten. This should not be called until all of the header and body
761 * rewriting is complete.
763 * <p>If the body is empty, this has no effect, so the response can be
764 * considered not committed.
766 void reallyCommit() throws IOException
{
768 if (!isCommitted()) {
771 OutputStream stream
= super.getOutputStream();
772 stream
.write(body
.toByteArray());
777 * Reset the output buffer.
779 * This works even though {@link #isCommitted()} may return true.
781 void reallyResetBuffer() {
783 bodyServletStream
= null;
784 bodyPrintWriter
= null;
788 * Sets a header in the response.
790 * This works even though {@link #isCommitted()} may return true.
792 void reallySetHeader(String name
, String value
) {
793 super.setHeader(name
, value
);
794 if (name
.equalsIgnoreCase(HttpHeaders
.EXPIRES
)) {
796 expires
= Long
.MIN_VALUE
;
800 } catch (ParseException e
) {
801 expires
= Long
.MIN_VALUE
;
804 } else if (name
.equalsIgnoreCase(HttpHeaders
.CACHE_CONTROL
)) {
805 cacheControl
.clear();
807 parseCacheControl(value
);
813 * Sets a date header in the response.
815 * This works even though {@link #isCommitted()} may return true.
817 void reallySetDateHeader(String name
, long date
) {
818 super.setDateHeader(name
, date
);
819 if (name
.equalsIgnoreCase(HttpHeaders
.EXPIRES
)) {
825 * Flushes the {@link PrintWriter} returned by {@link #getWriter()}, if it
828 void flushPrintWriter() {
829 if (bodyPrintWriter
!= null) {
830 bodyPrintWriter
.flush();
835 * Parse a date string and store the result in expires.
837 private void parseExpires(String date
) throws ParseException
{
838 SimpleDateFormat dateFormat
= new SimpleDateFormat(DATE_FORMAT_STRING
);
839 dateFormat
.setTimeZone(TimeZone
.getTimeZone("UTC"));
840 Date parsedDate
= dateFormat
.parse(date
);
841 expires
= parsedDate
.getTime();
845 * Parse a comma-separated list, and add the items to cacheControl.
847 private void parseCacheControl(String directives
) {
848 String
[] elements
= directives
.split(",");
849 for (String element
: elements
) {
850 cacheControl
.add(element
.trim());
855 * Removes the charset parameter from a media type string.
857 * @param mediaType A media type string, such as a Content-Type value.
858 * @return The media type with the charset parameter removed, if any
859 * existed. If not, returns the media type unchanged.
861 private static String
stripCharsetFromMediaType(String mediaType
) {
862 String newMediaType
= null;
863 for (String part
: mediaType
.split(";")) {
865 if (!(part
.length() >= 8 &&
866 part
.substring(0, 8).equalsIgnoreCase("charset="))) {
867 newMediaType
= newMediaType
== null ?
"" : newMediaType
+ "; ";
868 newMediaType
+= part
;
875 * Tests whether a string contains any non-ASCII characters.
877 private static boolean nonAscii(String string
) {
878 for (char c
: string
.toCharArray()) {
887 * A ServletOutputStream that wraps some other OutputStream.
889 private static class ServletOutputStreamWrapper
extends ServletOutputStream
{
890 private final OutputStream stream
;
892 ServletOutputStreamWrapper(OutputStream stream
) {
893 this.stream
= stream
;
897 public void close() throws IOException
{
902 public void flush() throws IOException
{
907 public void write(byte[] b
) throws IOException
{
912 public void write(byte[] b
, int off
, int len
) throws IOException
{
913 stream
.write(b
, off
, len
);
917 public void write(int b
) throws IOException
{