xref: /unit/src/java/nginx/unit/Response.java (revision 1652:856f4240beaa)
1 package nginx.unit;
2 
3 import java.io.IOException;
4 import java.io.OutputStreamWriter;
5 import java.io.PrintWriter;
6 
7 import java.lang.IllegalArgumentException;
8 import java.lang.String;
9 
10 import java.net.URI;
11 import java.net.URISyntaxException;
12 
13 import java.nio.charset.Charset;
14 import java.nio.charset.StandardCharsets;
15 
16 import java.text.SimpleDateFormat;
17 
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.Date;
21 import java.util.Enumeration;
22 import java.util.Locale;
23 import java.util.TimeZone;
24 import java.util.Vector;
25 
26 import javax.servlet.DispatcherType;
27 import javax.servlet.RequestDispatcher;
28 import javax.servlet.ServletOutputStream;
29 import javax.servlet.http.Cookie;
30 import javax.servlet.http.HttpServletResponse;
31 
32 import org.eclipse.jetty.http.MimeTypes;
33 import org.eclipse.jetty.util.StringUtil;
34 
35 public class Response implements HttpServletResponse {
36 
37     private long req_info_ptr;
38 
39     private static final String defaultCharacterEncoding = "iso-8859-1";
40     private String characterEncoding = defaultCharacterEncoding;
41     private String contentType = null;
42     private String contentTypeHeader = null;
43     private Locale locale = null;
44 
45     private static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1;
46     private static final Charset UTF_8 = StandardCharsets.UTF_8;
47 
48     private static final String CONTENT_TYPE = "Content-Type";
49     private static final byte[] CONTENT_LANGUAGE_BYTES = "Content-Language".getBytes(ISO_8859_1);
50     private static final byte[] SET_COOKIE_BYTES = "Set-Cookie".getBytes(ISO_8859_1);
51     private static final byte[] EXPIRES_BYTES = "Expires".getBytes(ISO_8859_1);
52 
53     /**
54      * The only date format permitted when generating HTTP headers.
55      */
56     public static final String RFC1123_DATE =
57             "EEE, dd MMM yyyy HH:mm:ss zzz";
58 
59     private static final SimpleDateFormat format =
60             new SimpleDateFormat(RFC1123_DATE, Locale.US);
61 
62     private static final String ZERO_DATE_STRING = dateToString(0);
63     private static final byte[] ZERO_DATE_BYTES = ZERO_DATE_STRING.getBytes(ISO_8859_1);
64 
65     /**
66      * If this string is found within the comment of a cookie added with {@link #addCookie(Cookie)}, then the cookie
67      * will be set as HTTP ONLY.
68      */
69     public final static String HTTP_ONLY_COMMENT = "__HTTP_ONLY__";
70 
71     private OutputStream outputStream = null;
72 
73     private PrintWriter writer = null;
74 
75 
Response(long ptr)76     public Response(long ptr) {
77         req_info_ptr = ptr;
78     }
79 
80     /**
81      * Format a set cookie value by RFC6265
82      *
83      * @param name the name
84      * @param value the value
85      * @param domain the domain
86      * @param path the path
87      * @param maxAge the maximum age
88      * @param isSecure true if secure cookie
89      * @param isHttpOnly true if for http only
90      */
addSetRFC6265Cookie( final String name, final String value, final String domain, final String path, final long maxAge, final boolean isSecure, final boolean isHttpOnly)91     public void addSetRFC6265Cookie(
92             final String name,
93             final String value,
94             final String domain,
95             final String path,
96             final long maxAge,
97             final boolean isSecure,
98             final boolean isHttpOnly)
99     {
100         // Check arguments
101         if (name == null || name.length() == 0) {
102             throw new IllegalArgumentException("Bad cookie name");
103         }
104 
105         // Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting
106         // Per RFC6265, Cookie.name follows RFC2616 Section 2.2 token rules
107         //Syntax.requireValidRFC2616Token(name, "RFC6265 Cookie name");
108         // Ensure that Per RFC6265, Cookie.value follows syntax rules
109         //Syntax.requireValidRFC6265CookieValue(value);
110 
111         // Format value and params
112         StringBuilder buf = new StringBuilder();
113         buf.append(name).append('=').append(value == null ? "" : value);
114 
115         // Append path
116         if (path != null && path.length() > 0) {
117             buf.append(";Path=").append(path);
118         }
119 
120         // Append domain
121         if (domain != null && domain.length() > 0) {
122             buf.append(";Domain=").append(domain);
123         }
124 
125         // Handle max-age and/or expires
126         if (maxAge >= 0) {
127             // Always use expires
128             // This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies
129             buf.append(";Expires=");
130             if (maxAge == 0)
131                 buf.append(ZERO_DATE_STRING);
132             else
133                 buf.append(dateToString(System.currentTimeMillis() + 1000L * maxAge));
134 
135             buf.append(";Max-Age=");
136             buf.append(maxAge);
137         }
138 
139         // add the other fields
140         if (isSecure)
141             buf.append(";Secure");
142         if (isHttpOnly)
143             buf.append(";HttpOnly");
144 
145         // add the set cookie
146         addHeader(req_info_ptr, SET_COOKIE_BYTES,
147             buf.toString().getBytes(ISO_8859_1));
148 
149         // Expire responses with set-cookie headers so they do not get cached.
150         setHeader(req_info_ptr, EXPIRES_BYTES, ZERO_DATE_BYTES);
151     }
152 
153     @Override
addCookie(Cookie cookie)154     public void addCookie(Cookie cookie)
155     {
156         trace("addCookie: " + cookie.getName() + "=" + cookie.getValue());
157 
158         if (StringUtil.isBlank(cookie.getName())) {
159             throw new IllegalArgumentException("Cookie.name cannot be blank/null");
160         }
161 
162         if (isCommitted()) {
163             return;
164         }
165 
166         addCookie_(cookie);
167     }
168 
addCookie_(Cookie cookie)169     private void addCookie_(Cookie cookie)
170     {
171         String comment = cookie.getComment();
172         boolean httpOnly = false;
173 
174         if (comment != null && comment.contains(HTTP_ONLY_COMMENT)) {
175             httpOnly = true;
176         }
177 
178         addSetRFC6265Cookie(cookie.getName(),
179             cookie.getValue(),
180             cookie.getDomain(),
181             cookie.getPath(),
182             cookie.getMaxAge(),
183             cookie.getSecure(),
184             httpOnly || cookie.isHttpOnly());
185     }
186 
addSessionIdCookie(Cookie cookie)187     public void addSessionIdCookie(Cookie cookie)
188     {
189         trace("addSessionIdCookie: " + cookie.getName() + "=" + cookie.getValue());
190 
191         if (isCommitted()) {
192             /*
193                 9.3 The Include Method
194 
195                 ... any call to HttpServletRequest.getSession() or
196                 HttpServletRequest.getSession(boolean) that would require
197                 adding a Cookie response header must throw an
198                 IllegalStateException if the response has been committed.
199              */
200             throw new IllegalStateException("Response already sent");
201         }
202 
203         addCookie_(cookie);
204     }
205 
206     @Override
addDateHeader(String name, long date)207     public void addDateHeader(String name, long date)
208     {
209         trace("addDateHeader: " + name + ": " + date);
210 
211         if (isCommitted()) {
212             return;
213         }
214 
215         String value = dateToString(date);
216 
217         addHeader(req_info_ptr, name.getBytes(ISO_8859_1),
218             value.getBytes(ISO_8859_1));
219     }
220 
dateToString(long date)221     private static String dateToString(long date)
222     {
223         Date dateValue = new Date(date);
224         format.setTimeZone(TimeZone.getTimeZone("GMT"));
225         return format.format(dateValue);
226     }
227 
228 
229     @Override
addHeader(String name, String value)230     public void addHeader(String name, String value)
231     {
232         trace("addHeader: " + name + ": " + value);
233 
234         if (value == null) {
235             return;
236         }
237 
238         if (isCommitted()) {
239             return;
240         }
241 
242         if (CONTENT_TYPE.equalsIgnoreCase(name)) {
243             setContentType(value);
244             return;
245         }
246 
247         addHeader(req_info_ptr, name.getBytes(ISO_8859_1),
248             value.getBytes(ISO_8859_1));
249     }
250 
addHeader(long req_info_ptr, byte[] name, byte[] value)251     private static native void addHeader(long req_info_ptr, byte[] name, byte[] value);
252 
253 
254     @Override
addIntHeader(String name, int value)255     public void addIntHeader(String name, int value)
256     {
257         trace("addIntHeader: " + name + ": " + value);
258 
259         if (isCommitted()) {
260             return;
261         }
262 
263         addIntHeader(req_info_ptr, name.getBytes(ISO_8859_1), value);
264     }
265 
addIntHeader(long req_info_ptr, byte[] name, int value)266     private static native void addIntHeader(long req_info_ptr, byte[] name, int value);
267 
268 
269     @Override
containsHeader(String name)270     public boolean containsHeader(String name)
271     {
272         trace("containsHeader: " + name);
273 
274         return containsHeader(req_info_ptr, name.getBytes(ISO_8859_1));
275     }
276 
containsHeader(long req_info_ptr, byte[] name)277     private static native boolean containsHeader(long req_info_ptr, byte[] name);
278 
279 
280     @Override
281     @Deprecated
encodeRedirectUrl(String url)282     public String encodeRedirectUrl(String url)
283     {
284         return encodeRedirectURL(url);
285     }
286 
287     @Override
encodeRedirectURL(String url)288     public String encodeRedirectURL(String url)
289     {
290         log("encodeRedirectURL: " + url);
291 
292         return url;
293     }
294 
295     @Override
296     @Deprecated
encodeUrl(String url)297     public String encodeUrl(String url)
298     {
299         return encodeURL(url);
300     }
301 
302     @Override
encodeURL(String url)303     public String encodeURL(String url)
304     {
305         log("encodeURL: " + url);
306 
307         return url;
308     }
309 
310     @Override
getHeader(String name)311     public String getHeader(String name)
312     {
313         trace("getHeader: " + name);
314 
315         return getHeader(req_info_ptr, name.getBytes(ISO_8859_1));
316     }
317 
getHeader(long req_info_ptr, byte[] name)318     private static native String getHeader(long req_info_ptr, byte[] name);
319 
320 
321     @Override
getHeaderNames()322     public Collection<String> getHeaderNames()
323     {
324         trace("getHeaderNames");
325 
326         Enumeration<String> e = getHeaderNames(req_info_ptr);
327         if (e == null) {
328             return Collections.emptyList();
329         }
330 
331         return Collections.list(e);
332     }
333 
getHeaderNames(long req_info_ptr)334     private static native Enumeration<String> getHeaderNames(long req_info_ptr);
335 
336 
337     @Override
getHeaders(String name)338     public Collection<String> getHeaders(String name)
339     {
340         trace("getHeaders: " + name);
341 
342         Enumeration<String> e = getHeaders(req_info_ptr, name.getBytes(ISO_8859_1));
343         if (e == null) {
344             return Collections.emptyList();
345         }
346 
347         return Collections.list(e);
348     }
349 
getHeaders(long req_info_ptr, byte[] name)350     private static native Enumeration<String> getHeaders(long req_info_ptr, byte[] name);
351 
352 
353     @Override
getStatus()354     public int getStatus()
355     {
356         trace("getStatus");
357 
358         return getStatus(req_info_ptr);
359     }
360 
getStatus(long req_info_ptr)361     private static native int getStatus(long req_info_ptr);
362 
363 
364     @Override
sendError(int sc)365     public void sendError(int sc) throws IOException
366     {
367         sendError(sc, null);
368     }
369 
370     @Override
sendError(int sc, String msg)371     public void sendError(int sc, String msg) throws IOException
372     {
373         trace("sendError: " + sc + ", " + msg);
374 
375         if (isCommitted()) {
376             throw new IllegalStateException("Response already sent");
377         }
378 
379         setStatus(sc);
380 
381         Request request = getRequest(req_info_ptr);
382 
383         // If we are allowed to have a body, then produce the error page.
384         if (sc != SC_NO_CONTENT && sc != SC_NOT_MODIFIED &&
385             sc != SC_PARTIAL_CONTENT && sc >= SC_OK)
386         {
387             request.setAttribute_(RequestDispatcher.ERROR_STATUS_CODE, sc);
388             request.setAttribute_(RequestDispatcher.ERROR_MESSAGE, msg);
389             request.setAttribute_(RequestDispatcher.ERROR_REQUEST_URI,
390                                   request.getRequestURI());
391 /*
392             request.setAttribute_(RequestDispatcher.ERROR_SERVLET_NAME,
393                                   request.getServletName());
394 */
395         }
396 
397 /*
398         Avoid commit and give chance for error handlers.
399 
400         if (!request.isAsyncStarted()) {
401             commit();
402         }
403 */
404     }
405 
getRequest(long req_info_ptr)406     private static native Request getRequest(long req_info_ptr);
407 
commit()408     private void commit()
409     {
410         if (writer != null) {
411             writer.close();
412 
413         } else if (outputStream != null) {
414             outputStream.close();
415 
416         } else {
417             commit(req_info_ptr);
418         }
419     }
420 
commit(long req_info_ptr)421     private static native void commit(long req_info_ptr);
422 
423 
424     @Override
sendRedirect(String location)425     public void sendRedirect(String location) throws IOException
426     {
427         trace("sendRedirect: " + location);
428 
429         if (isCommitted()) {
430             return;
431         }
432 
433         try {
434             URI uri = new URI(location);
435 
436             if (!uri.isAbsolute()) {
437                 URI req_uri = new URI(getRequest(req_info_ptr).getRequestURL().toString());
438                 uri = req_uri.resolve(uri);
439 
440                 location = uri.toString();
441             }
442         } catch (URISyntaxException e) {
443             log("sendRedirect: failed to send redirect: " + e);
444             return;
445         }
446 
447         sendRedirect(req_info_ptr, location.getBytes(ISO_8859_1));
448     }
449 
sendRedirect(long req_info_ptr, byte[] location)450     private static native void sendRedirect(long req_info_ptr, byte[] location);
451 
452 
453     @Override
setDateHeader(String name, long date)454     public void setDateHeader(String name, long date)
455     {
456         trace("setDateHeader: " + name + ": " + date);
457 
458         if (isCommitted()) {
459             return;
460         }
461 
462         String value = dateToString(date);
463 
464         setHeader(req_info_ptr, name.getBytes(ISO_8859_1),
465             value.getBytes(ISO_8859_1));
466     }
467 
468 
469     @Override
setHeader(String name, String value)470     public void setHeader(String name, String value)
471     {
472         trace("setHeader: " + name + ": " + value);
473 
474         if (isCommitted()) {
475             return;
476         }
477 
478         if (CONTENT_TYPE.equalsIgnoreCase(name)) {
479             setContentType(value);
480             return;
481         }
482 
483         /*
484          * When value is null container behaviour is undefined.
485          * - Tomcat ignores setHeader call;
486          * - Jetty & Resin acts as removeHeader;
487          */
488         if (value == null) {
489             removeHeader(req_info_ptr, name.getBytes(ISO_8859_1));
490             return;
491         }
492 
493         setHeader(req_info_ptr, name.getBytes(ISO_8859_1),
494             value.getBytes(ISO_8859_1));
495     }
496 
setHeader(long req_info_ptr, byte[] name, byte[] value)497     private static native void setHeader(long req_info_ptr, byte[] name, byte[] value);
498 
removeHeader(long req_info_ptr, byte[] name)499     private static native void removeHeader(long req_info_ptr, byte[] name);
500 
501     @Override
setIntHeader(String name, int value)502     public void setIntHeader(String name, int value)
503     {
504         trace("setIntHeader: " + name + ": " + value);
505 
506         if (isCommitted()) {
507             return;
508         }
509 
510         setIntHeader(req_info_ptr, name.getBytes(ISO_8859_1), value);
511     }
512 
setIntHeader(long req_info_ptr, byte[] name, int value)513     private static native void setIntHeader(long req_info_ptr, byte[] name, int value);
514 
515 
516     @Override
setStatus(int sc)517     public void setStatus(int sc)
518     {
519         trace("setStatus: " + sc);
520 
521         if (isCommitted()) {
522             return;
523         }
524 
525         setStatus(req_info_ptr, sc);
526     }
527 
setStatus(long req_info_ptr, int sc)528     private static native void setStatus(long req_info_ptr, int sc);
529 
530 
531     @Override
532     @Deprecated
setStatus(int sc, String sm)533     public void setStatus(int sc, String sm)
534     {
535         trace("setStatus: " + sc + "; " + sm);
536 
537         if (isCommitted()) {
538             return;
539         }
540 
541         setStatus(req_info_ptr, sc);
542     }
543 
544 
545     @Override
flushBuffer()546     public void flushBuffer() throws IOException
547     {
548         trace("flushBuffer");
549 
550         if (writer != null) {
551             writer.flush();
552         }
553 
554         if (outputStream != null) {
555             outputStream.flush();
556         }
557     }
558 
559     @Override
getBufferSize()560     public int getBufferSize()
561     {
562         trace("getBufferSize");
563 
564         return getBufferSize(req_info_ptr);
565     }
566 
getBufferSize(long req_info_ptr)567     public static native int getBufferSize(long req_info_ptr);
568 
569 
570     @Override
getCharacterEncoding()571     public String getCharacterEncoding()
572     {
573         trace("getCharacterEncoding");
574 
575         return characterEncoding;
576     }
577 
578     @Override
getContentType()579     public String getContentType()
580     {
581         /* In JIRA decorator get content type called after commit. */
582 
583         String res = contentTypeHeader;
584 
585         trace("getContentType: " + res);
586 
587         return res;
588     }
589 
getContentType(long req_info_ptr)590     private static native String getContentType(long req_info_ptr);
591 
592     @Override
getLocale()593     public Locale getLocale()
594     {
595         trace("getLocale");
596 
597         if (locale == null) {
598             return Locale.getDefault();
599         }
600 
601         return locale;
602     }
603 
604     @Override
getOutputStream()605     public ServletOutputStream getOutputStream() throws IOException
606     {
607         trace("getOutputStream");
608 
609         if (writer != null) {
610             throw new IllegalStateException("Writer already created");
611         }
612 
613         if (outputStream == null) {
614             outputStream = new OutputStream(req_info_ptr);
615         }
616 
617         return outputStream;
618     }
619 
620     @Override
getWriter()621     public PrintWriter getWriter() throws IOException
622     {
623         trace("getWriter ( characterEncoding = '" + characterEncoding + "' )");
624 
625         if (outputStream != null) {
626             throw new IllegalStateException("OutputStream already created");
627         }
628 
629         if (writer == null) {
630             ServletOutputStream stream = new OutputStream(req_info_ptr);
631 
632             writer = new PrintWriter(
633                 new OutputStreamWriter(stream, Charset.forName(characterEncoding)),
634                 false);
635         }
636 
637         return writer;
638     }
639 
640     @Override
isCommitted()641     public boolean isCommitted()
642     {
643         trace("isCommitted");
644 
645         return isCommitted(req_info_ptr);
646     }
647 
isCommitted(long req_info_ptr)648     public static native boolean isCommitted(long req_info_ptr);
649 
650     @Override
reset()651     public void reset()
652     {
653         trace("reset");
654 
655         if (isCommitted()) {
656             return;
657         }
658 
659         reset(req_info_ptr);
660 
661         writer = null;
662         outputStream = null;
663     }
664 
reset(long req_info_ptr)665     public static native void reset(long req_info_ptr);
666 
667     @Override
resetBuffer()668     public void resetBuffer()
669     {
670         trace("resetBuffer");
671 
672         resetBuffer(req_info_ptr);
673 
674         writer = null;
675         outputStream = null;
676     }
677 
resetBuffer(long req_info_ptr)678     public static native void resetBuffer(long req_info_ptr);
679 
680     @Override
setBufferSize(int size)681     public void setBufferSize(int size)
682     {
683         trace("setBufferSize: " + size);
684 
685         setBufferSize(req_info_ptr, size);
686     }
687 
setBufferSize(long req_info_ptr, int size)688     public static native void setBufferSize(long req_info_ptr, int size);
689 
690     @Override
setCharacterEncoding(String charset)691     public void setCharacterEncoding(String charset)
692     {
693         trace("setCharacterEncoding " + charset);
694 
695         if (isCommitted()) {
696             return;
697         }
698 
699         if (charset == null) {
700             if (writer != null
701                 && !characterEncoding.equalsIgnoreCase(defaultCharacterEncoding))
702             {
703                 /* TODO throw */
704                 return;
705             }
706 
707             characterEncoding = defaultCharacterEncoding;
708         } else {
709             if (writer != null
710                 && !characterEncoding.equalsIgnoreCase(charset))
711             {
712                 /* TODO throw */
713                 return;
714             }
715 
716             characterEncoding = charset;
717         }
718 
719         if (contentType != null) {
720             String type = contentType + ";charset=" + characterEncoding;
721 
722             contentTypeHeader = type;
723 
724             setContentType(req_info_ptr, type.getBytes(ISO_8859_1));
725         }
726     }
727 
728 
729     @Override
setContentLength(int len)730     public void setContentLength(int len)
731     {
732         trace("setContentLength: " + len);
733 
734         if (isCommitted()) {
735             return;
736         }
737 
738         setContentLength(req_info_ptr, len);
739     }
740 
741     @Override
setContentLengthLong(long len)742     public void setContentLengthLong(long len)
743     {
744         trace("setContentLengthLong: " + len);
745 
746         if (isCommitted()) {
747             return;
748         }
749 
750         setContentLength(req_info_ptr, len);
751     }
752 
setContentLength(long req_info_ptr, long len)753     private static native void setContentLength(long req_info_ptr, long len);
754 
755 
756     @Override
setContentType(String type)757     public void setContentType(String type)
758     {
759         trace("setContentType: " + type);
760 
761         if (isCommitted()) {
762             return;
763         }
764 
765         if (type == null) {
766             removeContentType(req_info_ptr);
767             contentType = null;
768             contentTypeHeader = null;
769             return;
770         }
771 
772         String charset = MimeTypes.getCharsetFromContentType(type);
773         String ctype = MimeTypes.getContentTypeWithoutCharset(type);
774 
775         if (writer != null
776             && charset != null
777             && !characterEncoding.equalsIgnoreCase(charset))
778         {
779             /* To late to change character encoding */
780             charset = characterEncoding;
781             type = ctype + ";charset=" + characterEncoding;
782         }
783 
784         if (charset == null) {
785             type = type + ";charset=" + characterEncoding;
786         } else {
787             characterEncoding = charset;
788         }
789 
790         contentType = ctype;
791         contentTypeHeader = type;
792 
793         setContentType(req_info_ptr, type.getBytes(ISO_8859_1));
794     }
795 
setContentType(long req_info_ptr, byte[] type)796     private static native void setContentType(long req_info_ptr, byte[] type);
797 
removeContentType(long req_info_ptr)798     private static native void removeContentType(long req_info_ptr);
799 
800 
801     @Override
setLocale(Locale loc)802     public void setLocale(Locale loc)
803     {
804         trace("setLocale: " + loc);
805 
806         if (loc == null || isCommitted()) {
807             return;
808         }
809 
810         locale = loc;
811         String lang = locale.toString().replace('_', '-');
812 
813         setHeader(req_info_ptr, CONTENT_LANGUAGE_BYTES, lang.getBytes(ISO_8859_1));
814     }
815 
log(String msg)816     private void log(String msg)
817     {
818         msg = "Response." + msg;
819         log(req_info_ptr, msg.getBytes(UTF_8));
820     }
821 
log(long req_info_ptr, byte[] msg)822     public static native void log(long req_info_ptr, byte[] msg);
823 
824 
trace(String msg)825     private void trace(String msg)
826     {
827         msg = "Response." + msg;
828         trace(req_info_ptr, msg.getBytes(UTF_8));
829     }
830 
trace(long req_info_ptr, byte[] msg)831     public static native void trace(long req_info_ptr, byte[] msg);
832 }
833