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