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
.Preconditions
;
7 import com
.google
.common
.net
.HttpHeaders
;
9 import java
.io
.ByteArrayOutputStream
;
10 import java
.io
.IOException
;
11 import java
.io
.OutputStream
;
12 import java
.io
.OutputStreamWriter
;
13 import java
.io
.PrintWriter
;
14 import java
.io
.UnsupportedEncodingException
;
15 import java
.text
.ParseException
;
16 import java
.text
.SimpleDateFormat
;
17 import java
.util
.Date
;
18 import java
.util
.Enumeration
;
19 import java
.util
.Locale
;
20 import java
.util
.NoSuchElementException
;
21 import java
.util
.TimeZone
;
22 import java
.util
.Vector
;
24 import javax
.servlet
.Filter
;
25 import javax
.servlet
.FilterChain
;
26 import javax
.servlet
.FilterConfig
;
27 import javax
.servlet
.ServletException
;
28 import javax
.servlet
.ServletOutputStream
;
29 import javax
.servlet
.ServletRequest
;
30 import javax
.servlet
.ServletResponse
;
31 import javax
.servlet
.http
.Cookie
;
32 import javax
.servlet
.http
.HttpServletRequest
;
33 import javax
.servlet
.http
.HttpServletRequestWrapper
;
34 import javax
.servlet
.http
.HttpServletResponse
;
35 import javax
.servlet
.http
.HttpServletResponseWrapper
;
38 * A filter that rewrites the response headers and body from the user's
41 * <p>This sanitises the headers to ensure that they are sensible and the user
42 * is not setting sensitive headers, such as Content-Length, incorrectly. It
43 * also deletes the body if the response status code indicates a non-body
46 * <p>This also strips out some request headers before passing the request to
50 public class ResponseRewriterFilter
implements Filter
{
52 * A mock timestamp to use as the response completion time, for testing.
54 * <p>Long.MIN_VALUE indicates that this should not be used, and instead, the
55 * current time should be taken.
57 private final long emulatedResponseTime
;
58 LocalLogService logService
;
60 private static final String BLOB_KEY_HEADER
= "X-AppEngine-BlobKey";
62 /** The value of the "Server" header output by the development server. */
63 private static final String DEVELOPMENT_SERVER
= "Development/1.0";
65 /** These statuses must not include a response body (RFC 2616). */
66 private static final int[] NO_BODY_RESPONSE_STATUSES
= {
67 HttpServletResponse
.SC_CONTINUE
,
68 HttpServletResponse
.SC_SWITCHING_PROTOCOLS
,
69 HttpServletResponse
.SC_NO_CONTENT
,
70 HttpServletResponse
.SC_NOT_MODIFIED
,
73 public ResponseRewriterFilter() {
75 emulatedResponseTime
= Long
.MIN_VALUE
;
79 * Creates a ResponseRewriterFilter for testing purposes, which mocks the
82 * @param mockTimestamp Indicates that the current time will be emulated with
85 public ResponseRewriterFilter(long mockTimestamp
) {
87 emulatedResponseTime
= mockTimestamp
;
91 public void init(FilterConfig filterConfig
) {
92 ApiProxyLocal apiProxyLocal
= (ApiProxyLocal
) filterConfig
.getServletContext().getAttribute(
93 "com.google.appengine.devappserver.ApiProxyLocal");
94 logService
= (LocalLogService
) apiProxyLocal
.getService(LocalLogService
.PACKAGE
);
98 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
99 * javax.servlet.ServletResponse,
100 * javax.servlet.FilterChain)
103 public void doFilter(ServletRequest request
, ServletResponse response
, FilterChain chain
)
104 throws IOException
, ServletException
{
105 HttpServletRequest httprequest
;
106 HttpServletResponse httpresponse
;
108 httprequest
= (HttpServletRequest
) request
;
109 httpresponse
= (HttpServletResponse
) response
;
110 } catch (ClassCastException e
) {
111 throw new ServletException(e
);
114 RequestWrapper wrappedRequest
= new RequestWrapper(httprequest
);
115 ResponseWrapper wrappedResponse
= new ResponseWrapper(httpresponse
);
117 chain
.doFilter(wrappedRequest
, wrappedResponse
);
119 Preconditions
.checkState(!response
.isCommitted(), "Response has already been committed");
122 if (emulatedResponseTime
== Long
.MIN_VALUE
) {
123 responseTime
= System
.currentTimeMillis();
125 responseTime
= emulatedResponseTime
;
128 ignoreHeadersRewriter(wrappedRequest
, wrappedResponse
, responseTime
);
129 serverDateRewriter(wrappedRequest
, wrappedResponse
, responseTime
);
130 cacheRewriter(wrappedRequest
, wrappedResponse
, responseTime
);
131 blobServeRewriter(wrappedRequest
, wrappedResponse
, responseTime
);
132 contentLengthRewriter(wrappedRequest
, wrappedResponse
, responseTime
);
134 wrappedResponse
.reallyCommit();
137 private static final String
[] IGNORE_REQUEST_HEADERS
= {
138 HttpHeaders
.ACCEPT_ENCODING
,
139 HttpHeaders
.CONNECTION
,
141 HttpHeaders
.PROXY_AUTHORIZATION
,
144 HttpHeaders
.TRANSFER_ENCODING
,
147 private static final String
[] IGNORE_RESPONSE_HEADERS
= {
148 HttpHeaders
.CONNECTION
,
149 HttpHeaders
.CONTENT_ENCODING
,
152 HttpHeaders
.PROXY_AUTHENTICATE
,
155 HttpHeaders
.TRANSFER_ENCODING
,
160 * Removes specific response headers.
162 * <p>Certain response headers cannot be modified by an Application. This
163 * rewriter simply removes those headers.
165 * @param request A request object, which is not modified.
166 * @param response A response object, which may be modified.
167 * @param responseTime The timestamp indicating when the response completed.
169 private void ignoreHeadersRewriter(HttpServletRequest request
, ResponseWrapper response
,
171 for (String h
: IGNORE_RESPONSE_HEADERS
) {
172 if (response
.containsHeader(h
)) {
173 response
.reallySetHeader(h
, null);
179 * Sets the Server and Date response headers to their correct value.
181 * @param request A request object, which is not modified.
182 * @param response A response object, which may be modified.
183 * @param responseTime The timestamp indicating when the response completed.
185 private void serverDateRewriter(HttpServletRequest request
, ResponseWrapper response
,
187 response
.reallySetHeader(HttpHeaders
.SERVER
, DEVELOPMENT_SERVER
);
188 response
.reallySetDateHeader(HttpHeaders
.DATE
, responseTime
);
192 * Determines whether the response may have a body, based on the status code.
194 * @param status The response status code.
195 * @return true if the response may have a body.
197 private static boolean responseMayHaveBody(int status
) {
198 for (int s
: NO_BODY_RESPONSE_STATUSES
) {
207 * Sets the default Cache-Control and Expires headers.
209 * These are only set if the response status allows a body, and only if the
210 * headers have not been explicitly set by the application.
212 * @param request A request object, which is not modified.
213 * @param response A response object, which may be modified.
214 * @param responseTime The timestamp indicating when the response completed.
216 private void cacheRewriter(HttpServletRequest request
, ResponseWrapper response
,
218 if (!responseMayHaveBody(response
.getStatus())) {
222 if (!response
.containsHeader(HttpHeaders
.CACHE_CONTROL
)) {
223 response
.reallySetHeader(HttpHeaders
.CACHE_CONTROL
, "no-cache");
224 if (!response
.containsHeader(HttpHeaders
.EXPIRES
)) {
225 response
.reallySetHeader(HttpHeaders
.EXPIRES
, "Fri, 01 Jan 1990 00:00:00 GMT");
229 if (response
.containsHeader(HttpHeaders
.SET_COOKIE
)) {
230 long expires
= response
.getExpires();
231 if (expires
== Long
.MIN_VALUE
|| expires
>= responseTime
) {
232 response
.reallySetDateHeader(HttpHeaders
.EXPIRES
, responseTime
);
235 Vector
<String
> cacheDirectives
= new Vector
<String
>(response
.getCacheControl());
236 while (cacheDirectives
.remove("public")) {
238 if (!cacheDirectives
.contains("private") && !cacheDirectives
.contains("no-cache") &&
239 !cacheDirectives
.contains("no-store")) {
240 cacheDirectives
.add("private");
242 StringBuilder newCacheControl
= new StringBuilder();
243 for (String directive
: cacheDirectives
) {
244 if (newCacheControl
.length() > 0) {
245 newCacheControl
.append(", ");
247 newCacheControl
.append(directive
);
249 response
.reallySetHeader(HttpHeaders
.CACHE_CONTROL
, newCacheControl
.toString());
254 * Deletes the response body, if X-AppEngine-BlobKey is present.
256 * Otherwise, it would be an error if we were to send text to the client and
257 * then attempt to rewrite the body to serve the blob.
259 * @param request A request object, which is not modified.
260 * @param response A response object, which may be modified.
261 * @param responseTime The timestamp indicating when the response completed.
263 private void blobServeRewriter(HttpServletRequest request
, ResponseWrapper response
,
265 if (response
.containsHeader(BLOB_KEY_HEADER
)) {
266 response
.reallyResetBuffer();
271 * Rewrites the Content-Length header.
273 * <p>Even though Content-Length is not a user modifiable header, App Engine
274 * sends a correct Content-Length to the user based on the actual response.
276 * <p>If the request method is HEAD or the response status indicates that the
277 * response should not have a body, the body is deleted instead. The existing
278 * Content-Length header is preserved for HEAD requests.
280 * @param request A request object, which is not modified.
281 * @param response A response object, which may be modified.
282 * @param responseTime The timestamp indicating when the response completed.
284 private void contentLengthRewriter(HttpServletRequest request
, ResponseWrapper response
,
286 response
.flushPrintWriter();
287 if (request
.getMethod().equals("HEAD")) {
288 response
.reallyResetBuffer();
289 logService
.clearResponseSize();
290 } else if (!responseMayHaveBody(response
.getStatus())) {
291 response
.reallySetHeader(HttpHeaders
.CONTENT_LENGTH
, null);
292 response
.reallyResetBuffer();
293 logService
.clearResponseSize();
295 response
.reallySetHeader(HttpHeaders
.CONTENT_LENGTH
, Long
.toString(response
.getBodyLength()));
296 logService
.registerResponseSize(response
.getBodyLength());
301 public void destroy() {
305 * Wraps a request to strip out some of the headers.
307 private static class RequestWrapper
extends HttpServletRequestWrapper
{
309 * An Enumeration that filters out ignored header names.
311 private static class HeaderFilterEnumeration
implements Enumeration
{
312 private final Enumeration allNames
;
313 private String nextName
;
315 HeaderFilterEnumeration(Enumeration allNames
) {
316 this.allNames
= allNames
;
320 /** Get the next non-ignored name from allNames and store it in nextName.
322 private void getNextValidName() {
323 while (allNames
.hasMoreElements()) {
324 String name
= (String
) allNames
.nextElement();
325 if (validHeader(name
)) {
334 public boolean hasMoreElements() {
335 return nextName
!= null;
339 public Object
nextElement() {
340 if (nextName
== null) {
341 throw new NoSuchElementException();
343 String result
= nextName
;
349 public RequestWrapper(HttpServletRequest request
) {
353 private static boolean validHeader(String name
) {
354 for (String h
: IGNORE_REQUEST_HEADERS
) {
355 if (h
.equalsIgnoreCase(name
)) {
363 public long getDateHeader(String name
) {
364 return validHeader(name
) ?
super.getIntHeader(name
) : -1;
368 public String
getHeader(String name
) {
369 return validHeader(name
) ?
super.getHeader(name
) : null;
373 public Enumeration
getHeaders(String name
) {
374 if (validHeader(name
)) {
375 return super.getHeaders(name
);
377 return new Enumeration() {
379 public boolean hasMoreElements() {
383 public Object
nextElement() {
384 throw new NoSuchElementException();
391 public Enumeration
getHeaderNames() {
392 return new HeaderFilterEnumeration(super.getHeaderNames());
396 public int getIntHeader(String name
) {
397 return validHeader(name
) ?
super.getIntHeader(name
) : -1;
402 * Wraps a response to buffer the entire body, and allow reading of the
403 * status, body and headers.
405 * <p>This buffers the entire body locally, so that the body is not streamed
406 * in chunks to the client, but instead all at the end.
408 * <p>This is necessary to calculate the correct Content-Length at the end,
409 * and also to modify headers after the application returns, but also matches
410 * production behaviour.
412 * <p>For the sake of compatibility, the class <em>pretends</em> not to buffer
413 * any data. (It behaves as if it has a buffer size of 0.) Therefore, as with
414 * a normal {@link HttpServletResponseWrapper}, you may not modify the status
415 * or headers after modifying the body. Note that the {@link PrintWriter}
416 * returned by {@link #getWriter()} does its own limited buffering.
418 * <p>This class also provides the ability to read the value of the status and
419 * some of the headers (which is not available before Servlet 3.0), and the
422 private static class ResponseWrapper
extends HttpServletResponseWrapper
{
423 private int status
= SC_OK
;
426 * The value of the Expires header, parsed as a Java timestamp.
428 * <p>Long.MIN_VALUE indicates that the Expires header is missing or invalid.
430 private long expires
= Long
.MIN_VALUE
;
431 /** The value of the Cache-Control headers, parsed into separate directives. */
432 private final Vector
<String
> cacheControl
= new Vector
<String
>();
434 /** A buffer to hold the body without sending it to the client. */
435 private final ByteArrayOutputStream body
= new ByteArrayOutputStream();
436 private ServletOutputStream bodyServletStream
= null;
437 private PrintWriter bodyPrintWriter
= null;
438 /** Indicates that flushBuffer() has been called. */
439 private boolean committed
= false;
441 private static final String DATE_FORMAT_STRING
=
442 "E, dd MMM yyyy HH:mm:ss 'GMT'";
444 ResponseWrapper(HttpServletResponse response
) {
449 public ServletOutputStream
getOutputStream() {
450 if (bodyServletStream
!= null) {
451 return bodyServletStream
;
453 Preconditions
.checkState(bodyPrintWriter
== null, "getWriter has already been called");
454 bodyServletStream
= new ServletOutputStreamWrapper(body
);
455 return bodyServletStream
;
460 public PrintWriter
getWriter() throws UnsupportedEncodingException
{
461 if (bodyPrintWriter
!= null) {
462 return bodyPrintWriter
;
464 Preconditions
.checkState(bodyServletStream
== null,
465 "getOutputStream has already been called");
466 bodyPrintWriter
= new PrintWriter(new OutputStreamWriter(body
, getCharacterEncoding()));
467 return bodyPrintWriter
;
472 public void setCharacterEncoding(String charset
) {
473 if (bodyPrintWriter
!= null || isCommitted()) {
476 super.setCharacterEncoding(charset
);
480 public void setContentLength(int len
) {
484 super.setContentLength(len
);
488 public void setContentType(String type
) {
492 if (type
!= null && nonAscii(type
)) {
495 if (bodyPrintWriter
!= null) {
496 type
= stripCharsetFromMediaType(type
);
498 super.setContentType(type
);
502 public void setLocale(Locale loc
) {
506 String oldCharacterEncoding
= getCharacterEncoding();
507 String oldContentType
= getContentType();
508 super.setLocale(loc
);
509 if (oldContentType
!= null || bodyPrintWriter
!= null) {
510 super.setCharacterEncoding(oldCharacterEncoding
);
512 if (oldContentType
!= null) {
513 super.setContentType(oldContentType
);
518 public void setBufferSize(int size
) {
520 super.setBufferSize(size
);
524 public int getBufferSize() {
529 public void flushBuffer() {
534 public void reset() {
540 public void resetBuffer() {
545 public boolean isCommitted() {
546 return committed
|| body
.size() > 0;
550 * Checks whether {@link #isCommitted()} is true, and if so, raises
551 * {@link IllegalStateException}.
553 void checkNotCommitted() {
554 Preconditions
.checkState(!isCommitted(), "Response has already been committed");
558 public void addCookie(Cookie cookie
) {
562 super.addCookie(cookie
);
566 public void addDateHeader(String name
, long date
) {
570 if (nonAscii(name
)) {
573 super.addDateHeader(name
, date
);
574 if (name
.equalsIgnoreCase(HttpHeaders
.EXPIRES
)) {
580 public void addHeader(String name
, String value
) {
587 if (nonAscii(name
) || nonAscii(value
)) {
590 if (name
.equalsIgnoreCase(HttpHeaders
.EXPIRES
)) {
593 } catch (ParseException e
) {
595 } else if (name
.equalsIgnoreCase(HttpHeaders
.CACHE_CONTROL
)) {
596 parseCacheControl(value
);
597 } else if (name
.equalsIgnoreCase(HttpHeaders
.CONTENT_TYPE
)) {
598 if (bodyPrintWriter
!= null) {
599 value
= stripCharsetFromMediaType(value
);
602 super.addHeader(name
, value
);
606 public void addIntHeader(String name
, int value
) {
610 if (nonAscii(name
)) {
613 super.addIntHeader(name
, value
);
617 public void sendError(int sc
) throws IOException
{
620 setErrorBody(Integer
.toString(sc
));
624 public void sendError(int sc
, String msg
) throws IOException
{
627 setErrorBody(sc
+ " " + msg
);
630 /** Sets the response body to an HTML page with an error message.
632 * This also sets the Content-Type header.
634 * @param errorText A message to display in the title and page contents.
635 * Should contain an HTTP status code and optional message.
637 private void setErrorBody(String errorText
) throws IOException
{
638 setHeader(HttpHeaders
.CONTENT_TYPE
, "text/html; charset=iso-8859-1");
639 String bodyText
= "<html><head><title>Error " + errorText
+ "</title></head>\n"
640 + "<body><h2>Error " + errorText
+ "</h2></body>\n"
642 body
.write(bodyText
.getBytes("iso-8859-1"));
646 public void sendRedirect(String location
) {
650 setHeader(HttpHeaders
.LOCATION
, encodeRedirectURL(location
));
655 public void setDateHeader(String name
, long date
) {
659 if (nonAscii(name
)) {
662 reallySetDateHeader(name
, date
);
666 public void setHeader(String name
, String value
) {
670 if (nonAscii(name
) || (value
!= null && nonAscii(value
))) {
673 if (name
.equalsIgnoreCase(HttpHeaders
.CONTENT_TYPE
)) {
674 if (bodyPrintWriter
!= null) {
675 value
= stripCharsetFromMediaType(value
);
678 reallySetHeader(name
, value
);
682 public void setIntHeader(String name
, int value
) {
686 if (nonAscii(name
)) {
689 if (name
.equalsIgnoreCase(HttpHeaders
.EXPIRES
)) {
690 expires
= Long
.MIN_VALUE
;
692 super.setIntHeader(name
, value
);
696 public void setStatus(int sc
) {
705 public void setStatus(int sc
, String sm
) {
709 super.setStatus(sc
, sm
);
714 * Gets the status code of the response.
721 * Gets the value of the Expires header, as a Java timestamp.
723 * <p>Long.MIN_VALUE indicates that the Expires header is missing or invalid.
725 public long getExpires() {
730 * Gets the value of the Cache-Control headers, parsed into separate directives.
732 public Vector
<String
> getCacheControl() {
737 * Gets the total number of bytes that have been written to the body without
740 int getBodyLength() {
745 * Writes the body to the wrapped response's output stream.
747 * <p>If the body is not empty, this causes the status and headers to be
748 * rewritten. This should not be called until all of the header and body
749 * rewriting is complete.
751 * <p>If the body is empty, this has no effect, so the response can be
752 * considered not committed.
754 void reallyCommit() throws IOException
{
756 if (!isCommitted()) {
759 OutputStream stream
= super.getOutputStream();
760 stream
.write(body
.toByteArray());
765 * Reset the output buffer.
767 * This works even though {@link #isCommitted()} may return true.
769 void reallyResetBuffer() {
771 bodyServletStream
= null;
772 bodyPrintWriter
= null;
776 * Sets a header in the response.
778 * This works even though {@link #isCommitted()} may return true.
780 void reallySetHeader(String name
, String value
) {
781 super.setHeader(name
, value
);
782 if (name
.equalsIgnoreCase(HttpHeaders
.EXPIRES
)) {
784 expires
= Long
.MIN_VALUE
;
788 } catch (ParseException e
) {
789 expires
= Long
.MIN_VALUE
;
792 } else if (name
.equalsIgnoreCase(HttpHeaders
.CACHE_CONTROL
)) {
793 cacheControl
.clear();
795 parseCacheControl(value
);
801 * Sets a date header in the response.
803 * This works even though {@link #isCommitted()} may return true.
805 void reallySetDateHeader(String name
, long date
) {
806 super.setDateHeader(name
, date
);
807 if (name
.equalsIgnoreCase(HttpHeaders
.EXPIRES
)) {
813 * Flushes the {@link PrintWriter} returned by {@link #getWriter()}, if it
816 void flushPrintWriter() {
817 if (bodyPrintWriter
!= null) {
818 bodyPrintWriter
.flush();
823 * Parse a date string and store the result in expires.
825 private void parseExpires(String date
) throws ParseException
{
826 SimpleDateFormat dateFormat
= new SimpleDateFormat(DATE_FORMAT_STRING
);
827 dateFormat
.setTimeZone(TimeZone
.getTimeZone("UTC"));
828 Date parsedDate
= dateFormat
.parse(date
);
829 expires
= parsedDate
.getTime();
833 * Parse a comma-separated list, and add the items to cacheControl.
835 private void parseCacheControl(String directives
) {
836 String
[] elements
= directives
.split(",");
837 for (String element
: elements
) {
838 cacheControl
.add(element
.trim());
843 * Removes the charset parameter from a media type string.
845 * @param mediaType A media type string, such as a Content-Type value.
846 * @return The media type with the charset parameter removed, if any
847 * existed. If not, returns the media type unchanged.
849 private static String
stripCharsetFromMediaType(String mediaType
) {
850 String newMediaType
= null;
851 for (String part
: mediaType
.split(";")) {
853 if (!(part
.length() >= 8 &&
854 part
.substring(0, 8).equalsIgnoreCase("charset="))) {
855 newMediaType
= newMediaType
== null ?
"" : newMediaType
+ "; ";
856 newMediaType
+= part
;
863 * Tests whether a string contains any non-ASCII characters.
865 private static boolean nonAscii(String string
) {
866 for (char c
: string
.toCharArray()) {
875 * A ServletOutputStream that wraps some other OutputStream.
877 private static class ServletOutputStreamWrapper
extends ServletOutputStream
{
878 private final OutputStream stream
;
880 ServletOutputStreamWrapper(OutputStream stream
) {
881 this.stream
= stream
;
885 public void close() throws IOException
{
890 public void flush() throws IOException
{
895 public void write(byte[] b
) throws IOException
{
900 public void write(byte[] b
, int off
, int len
) throws IOException
{
901 stream
.write(b
, off
, len
);
905 public void write(int b
) throws IOException
{