Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / development / ResponseRewriterFilter.java
blobb7cb24cc032e8c2a2e791443cd6cb2ec2bc3aa77
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;
37 /**
38 * A filter that rewrites the response headers and body from the user's
39 * application.
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
44 * status.
46 * <p>This also strips out some request headers before passing the request to
47 * the application.
50 public class ResponseRewriterFilter implements Filter {
51 /**
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() {
74 super();
75 emulatedResponseTime = Long.MIN_VALUE;
78 /**
79 * Creates a ResponseRewriterFilter for testing purposes, which mocks the
80 * current time.
82 * @param mockTimestamp Indicates that the current time will be emulated with
83 * this timestamp.
85 public ResponseRewriterFilter(long mockTimestamp) {
86 super();
87 emulatedResponseTime = mockTimestamp;
90 @Override
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);
97 /**
98 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
99 * javax.servlet.ServletResponse,
100 * javax.servlet.FilterChain)
102 @Override
103 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
104 throws IOException, ServletException {
105 HttpServletRequest httprequest;
106 HttpServletResponse httpresponse;
107 try {
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");
121 long responseTime;
122 if (emulatedResponseTime == Long.MIN_VALUE) {
123 responseTime = System.currentTimeMillis();
124 } else {
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,
140 "Keep-Alive",
141 HttpHeaders.PROXY_AUTHORIZATION,
142 HttpHeaders.TE,
143 HttpHeaders.TRAILER,
144 HttpHeaders.TRANSFER_ENCODING,
147 private static final String[] IGNORE_RESPONSE_HEADERS = {
148 HttpHeaders.CONNECTION,
149 HttpHeaders.CONTENT_ENCODING,
150 HttpHeaders.DATE,
151 "Keep-Alive",
152 HttpHeaders.PROXY_AUTHENTICATE,
153 HttpHeaders.SERVER,
154 HttpHeaders.TRAILER,
155 HttpHeaders.TRANSFER_ENCODING,
156 HttpHeaders.UPGRADE,
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,
170 long responseTime) {
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,
186 long responseTime) {
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) {
199 if (status == s) {
200 return false;
203 return true;
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,
217 long responseTime) {
218 if (!responseMayHaveBody(response.getStatus())) {
219 return;
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,
264 long responseTime) {
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,
285 long responseTime) {
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();
294 } else {
295 response.reallySetHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(response.getBodyLength()));
296 logService.registerResponseSize(response.getBodyLength());
300 @Override
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<Object> {
312 private final Enumeration<?> allNames;
313 private String nextName;
315 HeaderFilterEnumeration(Enumeration<?> allNames) {
316 this.allNames = allNames;
317 getNextValidName();
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)) {
326 nextName = name;
327 return;
330 nextName = null;
333 @Override
334 public boolean hasMoreElements() {
335 return nextName != null;
338 @Override
339 public Object nextElement() {
340 if (nextName == null) {
341 throw new NoSuchElementException();
343 String result = nextName;
344 getNextValidName();
345 return result;
349 public RequestWrapper(HttpServletRequest request) {
350 super(request);
353 private static boolean validHeader(String name) {
354 for (String h : IGNORE_REQUEST_HEADERS) {
355 if (h.equalsIgnoreCase(name)) {
356 return false;
359 return true;
362 @Override
363 public long getDateHeader(String name) {
364 return validHeader(name) ? super.getIntHeader(name) : -1;
367 @Override
368 public String getHeader(String name) {
369 return validHeader(name) ? super.getHeader(name) : null;
372 @Override
373 public Enumeration<?> getHeaders(String name) {
374 if (validHeader(name)) {
375 return super.getHeaders(name);
376 } else {
377 return new Enumeration<Object>() {
378 @Override
379 public boolean hasMoreElements() {
380 return false;
382 @Override
383 public Object nextElement() {
384 throw new NoSuchElementException();
390 @Override
391 public Enumeration<?> getHeaderNames() {
392 return new HeaderFilterEnumeration(super.getHeaderNames());
395 @Override
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
420 * body.
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) {
445 super(response);
448 @Override
449 public ServletOutputStream getOutputStream() {
450 if (bodyServletStream != null) {
451 return bodyServletStream;
452 } else {
453 Preconditions.checkState(bodyPrintWriter == null, "getWriter has already been called");
454 bodyServletStream = new ServletOutputStreamWrapper(body);
455 return bodyServletStream;
459 @Override
460 public PrintWriter getWriter() throws UnsupportedEncodingException {
461 if (bodyPrintWriter != null) {
462 return bodyPrintWriter;
463 } else {
464 Preconditions.checkState(bodyServletStream == null,
465 "getOutputStream has already been called");
466 bodyPrintWriter = new PrintWriter(new OutputStreamWriter(body, getCharacterEncoding()));
467 return bodyPrintWriter;
471 @Override
472 public void setCharacterEncoding(String charset) {
473 if (bodyPrintWriter != null || isCommitted()) {
474 return;
476 super.setCharacterEncoding(charset);
479 @Override
480 public void setContentLength(int len) {
481 if (isCommitted()) {
482 return;
484 super.setContentLength(len);
487 @Override
488 public void setContentType(String type) {
489 if (isCommitted()) {
490 return;
492 if (type != null && nonAscii(type)) {
493 return;
495 if (bodyPrintWriter != null) {
496 type = stripCharsetFromMediaType(type);
498 super.setContentType(type);
501 @Override
502 public void setLocale(Locale loc) {
503 if (isCommitted()) {
504 return;
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);
517 @Override
518 public void setBufferSize(int size) {
519 checkNotCommitted();
520 super.setBufferSize(size);
523 @Override
524 public int getBufferSize() {
525 return 0;
528 @Override
529 public void flushBuffer() {
530 committed = true;
533 @Override
534 public void reset() {
535 checkNotCommitted();
536 super.reset();
539 @Override
540 public void resetBuffer() {
541 checkNotCommitted();
544 @Override
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");
557 @Override
558 public void addCookie(Cookie cookie) {
559 if (isCommitted()) {
560 return;
562 super.addCookie(cookie);
565 @Override
566 public void addDateHeader(String name, long date) {
567 if (isCommitted()) {
568 return;
570 if (nonAscii(name)) {
571 return;
573 super.addDateHeader(name, date);
574 if (name.equalsIgnoreCase(HttpHeaders.EXPIRES)) {
575 expires = date;
579 @Override
580 public void addHeader(String name, String value) {
581 if (isCommitted()) {
582 return;
584 if (value == null) {
585 return;
587 if (nonAscii(name) || nonAscii(value)) {
588 return;
590 if (name.equalsIgnoreCase(HttpHeaders.EXPIRES)) {
591 try {
592 parseExpires(value);
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);
605 @Override
606 public void addIntHeader(String name, int value) {
607 if (isCommitted()) {
608 return;
610 if (nonAscii(name)) {
611 return;
613 super.addIntHeader(name, value);
616 @Override
617 public void sendError(int sc) throws IOException {
618 checkNotCommitted();
619 setStatus(sc);
620 setErrorBody(Integer.toString(sc));
623 @Override
624 public void sendError(int sc, String msg) throws IOException {
625 checkNotCommitted();
626 setStatus(sc, msg);
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"
641 + "</html>";
642 body.write(bodyText.getBytes("iso-8859-1"));
645 @Override
646 public void sendRedirect(String location) {
647 checkNotCommitted();
648 setStatus(SC_FOUND);
649 resetBuffer();
650 setHeader(HttpHeaders.LOCATION, encodeRedirectURL(location));
651 status = SC_FOUND;
654 @Override
655 public void setDateHeader(String name, long date) {
656 if (isCommitted()) {
657 return;
659 if (nonAscii(name)) {
660 return;
662 reallySetDateHeader(name, date);
665 @Override
666 public void setHeader(String name, String value) {
667 if (isCommitted()) {
668 return;
670 if (nonAscii(name) || (value != null && nonAscii(value))) {
671 return;
673 if (name.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE)) {
674 if (bodyPrintWriter != null) {
675 value = stripCharsetFromMediaType(value);
678 reallySetHeader(name, value);
681 @Override
682 public void setIntHeader(String name, int value) {
683 if (isCommitted()) {
684 return;
686 if (nonAscii(name)) {
687 return;
689 if (name.equalsIgnoreCase(HttpHeaders.EXPIRES)) {
690 expires = Long.MIN_VALUE;
692 super.setIntHeader(name, value);
695 @Override
696 public void setStatus(int sc) {
697 if (isCommitted()) {
698 return;
700 super.setStatus(sc);
701 status = sc;
704 @Override
705 public void setStatus(int sc, String sm) {
706 if (isCommitted()) {
707 return;
709 super.setStatus(sc, sm);
710 status = sc;
714 * Gets the status code of the response.
716 int getStatus() {
717 return status;
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() {
726 return expires;
730 * Gets the value of the Cache-Control headers, parsed into separate directives.
732 public Vector<String> getCacheControl() {
733 return cacheControl;
737 * Gets the total number of bytes that have been written to the body without
738 * committing.
740 int getBodyLength() {
741 return body.size();
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 {
755 flushPrintWriter();
756 if (!isCommitted()) {
757 return;
759 OutputStream stream = super.getOutputStream();
760 stream.write(body.toByteArray());
761 body.reset();
765 * Reset the output buffer.
767 * This works even though {@link #isCommitted()} may return true.
769 void reallyResetBuffer() {
770 body.reset();
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)) {
783 if (value == null) {
784 expires = Long.MIN_VALUE;
785 } else {
786 try {
787 parseExpires(value);
788 } catch (ParseException e) {
789 expires = Long.MIN_VALUE;
792 } else if (name.equalsIgnoreCase(HttpHeaders.CACHE_CONTROL)) {
793 cacheControl.clear();
794 if (value != null) {
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)) {
808 expires = date;
813 * Flushes the {@link PrintWriter} returned by {@link #getWriter()}, if it
814 * exists.
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(";")) {
852 part = part.trim();
853 if (!(part.length() >= 8 &&
854 part.substring(0, 8).equalsIgnoreCase("charset="))) {
855 newMediaType = newMediaType == null ? "" : newMediaType + "; ";
856 newMediaType += part;
859 return newMediaType;
863 * Tests whether a string contains any non-ASCII characters.
865 private static boolean nonAscii(String string) {
866 for (char c : string.toCharArray()) {
867 if (c >= 0x80) {
868 return true;
871 return false;
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;
884 @Override
885 public void close() throws IOException {
886 stream.close();
889 @Override
890 public void flush() throws IOException {
891 stream.flush();
894 @Override
895 public void write(byte[] b) throws IOException {
896 stream.write(b);
899 @Override
900 public void write(byte[] b, int off, int len) throws IOException {
901 stream.write(b, off, len);
904 @Override
905 public void write(int b) throws IOException {
906 stream.write(b);