/* Https.java
Purpose:
Description:
History:
2001/11/29 13:53:05, Create, Tom M. Yeh.
Copyright (C) 2001 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.web.servlet.http;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zkoss.io.Files;
import org.zkoss.io.RepeatableInputStream;
import org.zkoss.io.RepeatableReader;
import org.zkoss.lang.Strings;
import org.zkoss.lang.SystemException;
import org.zkoss.util.media.Media;
import org.zkoss.web.Attributes;
import org.zkoss.web.servlet.Servlets;
import org.zkoss.web.util.resource.ExtendletContext;
/**
* The Servlet-related utilities.
*
* @author tomyeh
*/
public class Https extends Servlets {
private static final Logger log = LoggerFactory.getLogger(Https.class);
/** Compresses the content into an byte array, or null
* if the browser doesn't support the compression (accept-encoding).
*
* @param content1 the first part of the content to compress; null to ignore.
* If you have multiple input streams, use java.io.SequenceInputStream
* to concatenate them
* @param content2 the second part of the content to compress; null to ignore.
* @return the compressed result in an byte array,
* null if the browser doesn't support the compression.
* @since 2.4.1
*/
public static final byte[] gzip(HttpServletRequest request, HttpServletResponse response, InputStream content1,
byte[] content2) throws IOException {
//We check Content-Encoding first to avoid compressing twice
String ae = request.getHeader("accept-encoding");
if (ae != null && !response.containsHeader("Content-Encoding")) {
if (ae.indexOf("gzip") >= 0) {
response.addHeader("Content-Encoding", "gzip");
final ByteArrayOutputStream boas = new ByteArrayOutputStream(8192);
final GZIPOutputStream gzs = new GZIPOutputStream(boas);
if (content1 != null)
Files.copy(gzs, content1);
if (content2 != null)
gzs.write(content2);
gzs.finish();
return boas.toByteArray();
// } else if (ae.indexOf("deflate") >= 0) {
//Refer to http://www.gzip.org/zlib/zlib_faq.html#faq38
//It is not a good idea to zlib (i.e., deflate)
}
}
return null;
}
/**
* Gets the complete server name, including protocol, server, and ports.
* Example, http://mysite.com:8080
*/
public static final String getCompleteServerName(HttpServletRequest hreq) {
final StringBuffer sb = hreq.getRequestURL();
final String ctx = hreq.getContextPath();
final int j = sb.indexOf(ctx);
if (j < 0)
throw new SystemException("Unknown request: url=" + sb + ", ctx=" + ctx);
return sb.delete(j, sb.length()).toString();
}
/**
* Gets the complete context path, including protocol, server, ports, and
* context.
* Example, http://mysite.com:8080/we
*/
public static final String getCompleteContext(HttpServletRequest hreq) {
final StringBuffer sb = hreq.getRequestURL();
final String ctx = hreq.getContextPath();
final int j = sb.indexOf(ctx);
if (j < 0)
throw new SystemException("Unknown request: url=" + sb + ", ctx=" + ctx);
return sb.delete(j + ctx.length(), sb.length()).toString();
}
/** Gets the value of the specified cookie, or null if not found.
* @param name the cookie's name
*/
public static final String getCookieValue(HttpServletRequest request, String name) {
final Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (int j = cookies.length; --j >= 0;) {
if (cookies[j].getName().equals(name))
return cookies[j].getValue();
}
}
return null;
}
/**
* Returns the servlet uri of the request.
* A servlet uri is getServletPath() + getPathInfo().
* In other words, a servlet uri is a request uri without the context path.
* <p>However, HttpServletRequest.getRequestURI returns in encoded format,
* while this method returns in decode format (i.e., %nn is converted).
*/
public static final String getServletURI(HttpServletRequest request) {
final String sp = request.getServletPath();
final String pi = request.getPathInfo();
if (pi == null || pi.length() == 0)
return sp;
if (sp.length() == 0)
return pi;
return sp + pi;
}
/**
* Gets the context path of this page.
* Unlike getContextPath, it detects whether the current page is included.
*
* @return "/" if request is not a http request
*/
public static final String getThisContextPath(ServletRequest request) {
String path = (String) request.getAttribute(Attributes.INCLUDE_CONTEXT_PATH);
return path != null ? path
: request instanceof HttpServletRequest ? ((HttpServletRequest) request).getContextPath() : "";
}
/**
* Gets the servlet path of this page.
* Unlike getServletPath, it detects whether the current page is included.
*
* @return "/" if request is not a http request
*/
public static final String getThisServletPath(ServletRequest request) {
String path = (String) request.getAttribute(Attributes.INCLUDE_SERVLET_PATH);
return path != null ? path
: request instanceof HttpServletRequest ? ((HttpServletRequest) request).getServletPath() : "/";
}
/**
* Gets the request URI of this page.
* Unlike getRequestURI, it detects whether the current page is included.
*
* @return "/" if request is not a http request
*/
public static final String getThisRequestURI(ServletRequest request) {
String path = (String) request.getAttribute(Attributes.INCLUDE_REQUEST_URI);
return path != null ? path
: request instanceof HttpServletRequest ? ((HttpServletRequest) request).getRequestURI() : "/";
}
/**
* Gets the query string of this page.
* Unlike getQueryString, it detects whether the current page is included.
*
* @return null if request is not a http request
*/
public static final String getThisQueryString(ServletRequest request) {
String path = (String) request.getAttribute(Attributes.INCLUDE_QUERY_STRING);
return path != null || isIncluded(request) || !(request instanceof HttpServletRequest) ? path
: //null is valid even included
((HttpServletRequest) request).getQueryString();
}
/**
* Gets the path info of this page.
* Unlike getPathInfo, it detects whether the current page is included.
*
* @return null if request is not a http request
*/
public static final String getThisPathInfo(ServletRequest request) {
String path = (String) request.getAttribute(Attributes.INCLUDE_PATH_INFO);
return path != null || isIncluded(request) || !(request instanceof HttpServletRequest) ? path
: //null is valid even included
((HttpServletRequest) request).getPathInfo();
}
/**
* Gets the original context path regardless of being forwarded or not.
* Unlike getContextPath, it won't be affected by forwarding.
*/
public static final String getOriginContextPath(ServletRequest request) {
String path = (String) request.getAttribute(Attributes.FORWARD_CONTEXT_PATH);
return path != null ? path
: request instanceof HttpServletRequest ? ((HttpServletRequest) request).getContextPath() : "";
}
/**
* Gets the original servlet path regardless of being forwarded or not.
* Unlike getServletPath, it won't be affected by forwarding.
*/
public static final String getOriginServletPath(ServletRequest request) {
String path = (String) request.getAttribute(Attributes.FORWARD_SERVLET_PATH);
return path != null ? path
: request instanceof HttpServletRequest ? ((HttpServletRequest) request).getServletPath() : "/";
}
/**
* Gets the request URI regardless of being forwarded or not.
* Unlike HttpServletRequest.getRequestURI,
* it won't be affected by forwarding.
*/
public static final String getOriginRequestURI(ServletRequest request) {
String path = (String) request.getAttribute(Attributes.FORWARD_REQUEST_URI);
return path != null ? path
: request instanceof HttpServletRequest ? ((HttpServletRequest) request).getRequestURI() : "/";
}
/**
* Gets the path info regardless of being forwarded or not.
* Unlike getPathInfo, it won't be affected by forwarding.
*/
public static final String getOriginPathInfo(ServletRequest request) {
String path = (String) request.getAttribute(Attributes.FORWARD_PATH_INFO);
return path != null ? path : isForwarded(request) ? null
: //null is valid even included
request instanceof HttpServletRequest ? ((HttpServletRequest) request).getPathInfo() : null;
}
/**
* Gets the query string regardless of being forwarded or not.
* Unlike getQueryString, it won't be affected by forwarding.
*/
public static final String getOriginQueryString(ServletRequest request) {
String path = (String) request.getAttribute(Attributes.FORWARD_QUERY_STRING);
return path != null ? path : isForwarded(request) ? null
: //null is valid even included
request instanceof HttpServletRequest ? ((HttpServletRequest) request).getQueryString() : null;
}
/** Returns the servlet path + path info + query string.
* Because the path info is decoded, the return string can be considered
* as decoded. On the other hand {@link #getOriginFullRequest} is in
* the encoded form.
* @see #getOriginFullRequest
*/
public static final String getOriginFullServlet(ServletRequest request) {
final String qstr = getOriginQueryString(request);
final String pi = getOriginPathInfo(request);
if (qstr == null && pi == null)
return getOriginServletPath(request);
final StringBuffer sb = new StringBuffer(80).append(getOriginServletPath(request));
if (pi != null)
sb.append(pi);
if (qstr != null)
sb.append('?').append(qstr);
return sb.toString();
}
/** Returns the request uri + query string.
* Unlike {@link #getOriginFullServlet}, this is in the encoded form
* (e.g., %nn still exists, if any).
* Note: request uri = context path + servlet path + path info.
*/
public static final String getOriginFullRequest(ServletRequest request) {
final String qstr = getOriginQueryString(request);
return qstr != null ? getOriginRequestURI(request) + '?' + qstr : getOriginRequestURI(request);
}
/**
* Redirects to another URL by prefixing the context path and
* encoding with encodeRedirectURL.
*
* <p>It encodes the URI automatically (encodeRedirectURL).
* Parameters are encoded by
* {@link Encodes#setToQueryString(StringBuffer,Map)}.
*
* <p>Like {@link Encodes#encodeURL}, the servlet context is
* prefixed if uri starts with "/". In other words, to redirect other
* application, the complete URL must be used, e.g., http://host/other.
*
* <p>Also, HttpServletResponse.encodeRedirectURL is called automatically.
*
* @param request the request; used only if params is not null
* @param response the response
* @param uri the redirect uri (not encoded; not including context-path),
* or null to denote {@link #getOriginFullServlet}
* It is OK to relevant (without leading '/').
* If starts with "/", the context path of request is assumed.
* To reference to foreign context, use "~ctx/" where ctx is the
* context path of the foreign context (without leading '/').
* <br/>Notice that, since 3.6.3, <code>uri</code> could contain
* '*' (to denote locale and browser). Refer to {@link #locate}.
* @param params the attributes that will be set when the redirection
* is back; null to ignore; format: (String, Object)
* @param mode one of {@link #OVERWRITE_URI}, {@link #IGNORE_PARAM},
* and {@link #APPEND_PARAM}. It defines how to handle if both uri
* and params contains the same parameter.
*/
public static final void sendRedirect(ServletContext ctx, HttpServletRequest request, HttpServletResponse response,
String uri, Map params, int mode) throws IOException, ServletException {
uri = locate(ctx, request, uri, null);
final String encodedUrl = encodeRedirectURL(ctx, request, response, uri, params, mode);
//if (log.isDebugEnabled()) log.debug("redirect to " + encodedUrl);
response.sendRedirect(encodedUrl);
}
/** Encodes an URL such that it can be used with HttpServletResponse.sendRedirect.
*/
public static final String encodeRedirectURL(ServletContext ctx, HttpServletRequest request,
HttpServletResponse response, String uri, Map params, int mode) {
if (uri == null) {
uri = request.getContextPath() + getOriginFullServlet(request);
} else {
final int len = uri.length();
if (len == 0 || uri.charAt(0) == '/') {
uri = request.getContextPath() + uri;
} else if (uri.charAt(0) == '~') {
final int j = uri.indexOf('/', 1);
final String ctxroot = j >= 0 ? "/" + uri.substring(1, j) : "/" + uri.substring(1);
final ExtendletContext extctx = Servlets.getExtendletContext(ctx, ctxroot.substring(1));
if (extctx != null) {
uri = j >= 0 ? uri.substring(j) : "/";
return extctx.encodeRedirectURL(request, response, uri, params, mode);
} else {
uri = len >= 2 && uri.charAt(1) == '/' ? uri.substring(1) : '/' + uri.substring(1);
}
}
}
return response.encodeRedirectURL(generateURI(uri, params, mode));
}
/**
* Converts a date string to a Date instance.
* The format of the giving date string must be complaint
* to HTTP protocol.
*
* @exception ParseException if the string is not valid
*/
public static final Date toDate(String sdate) throws ParseException {
ParseException ex = null;
for (int j = 0; j < _dfs.length; ++j) {
try {
return new SimpleDateFormat(_dfs[j], Locale.US).parse(sdate);
} catch (ParseException t) {
if (ex == null)
ex = t;
}
}
throw ex;
}
/**
* Converts a data to a string complaint to HTTP protocol.
*/
public static final String toString(Date date) {
return new SimpleDateFormat(_dfs[0], Locale.US).format(date);
}
private static final String[] _dfs = { "EEE, dd MMM yyyy HH:mm:ss zzz", "EEEEEE, dd-MMM-yy HH:mm:ss zzz",
"EEE MMMM d HH:mm:ss yyyy" };
/** Write the specified media to HTTP response.
*
* @param response the HTTP response to write to
* @param media the content to be written
* @param download whether to cause the download to show at the client.
* If true, it sets the Content-Disposition header.
* @param repeatable whether to use {@link RepeatableInputStream}
* or {@link RepeatableReader} to read the media.
* It is better to specify true if the media might be read repeatedly.
* @since 3.5.0
*/
public static void write(HttpServletRequest request, HttpServletResponse response, Media media, boolean download,
boolean repeatable) throws IOException {
//2012/03/09 TonyQ: ZK-885 Iframe with PDF stop works in IE 8 when we have Accept-Ranges = bytes.
if (!Servlets.isBrowser(request, "ie")) {
response.setHeader("Accept-Ranges", "bytes");
}
final boolean headOnly = "HEAD".equalsIgnoreCase(request.getMethod());
final byte[] data;
int from = -1, to = -1;
synchronized (media) { //Bug 1896797: media might be accessed concurrently.
//reading an image and send it back to client
final String ctype = media.getContentType();
if (ctype != null)
response.setContentType(ctype);
if (download) {
String value = "attachment";
// Bug ZK-1257: Filedownload.save(media, filename) does not save the media as the specified filename
StringBuffer temp = request.getRequestURL();
final String update_uri = (String) request.getSession().getServletContext()
.getAttribute("org.zkoss.zk.ui.http.update-uri"); //B65-ZK-1619
String flnm = "";
if (update_uri != null && temp.toString().contains(update_uri + "/view")) {
// for Bug ZK-2350, we don't specify the filename when coming with ZK Fileupload, but invoke this directly as Bug ZK-1619
// final String saveAs = URLDecoder.decode(temp.substring(temp.lastIndexOf("/")+1), "UTF-8");
// flnm = ("".equals(saveAs)) ? media.getName() : saveAs;
} else
flnm = media.getName();
if (flnm != null && flnm.length() > 0)
value += ";filename=" + encodeFilename(request, flnm);
if (media.isContentDisposition())
response.setHeader("Content-Disposition", value);
//response.setHeader("Content-Transfer-Encoding", "binary");
}
final String rs = request.getHeader("Range");
if (rs != null && rs.length() > 0) {
final int[] range = parseRange(rs);
if (range != null) {
from = range[0];
to = range[1];
}
}
if (!media.inMemory()) {
final ServletOutputStream out = response.getOutputStream();
if (media.isBinary()) {
InputStream in = media.getStreamData();
if (repeatable)
in = RepeatableInputStream.getInstance(in);
try {
if (headOnly) {
int cnt = 0;
final byte[] buf = new byte[512];
for (int v; (v = in.read(buf)) >= 0;)
cnt += v;
response.setContentLength(cnt);
return;
}
if (from >= 0) { //partial
PartialByteStream pbs = new PartialByteStream(from, to);
Files.copy(pbs, in);
pbs.responseTo(response);
} else {
Files.copy(out, in);
}
} catch (IOException ex) {
//browser might close the connection
//and reread (test case: B30-1896797.zul)
//so, read it completely, since 2nd read counts on it
if (in instanceof org.zkoss.io.Repeatable) {
try {
final byte[] buf = new byte[1024 * 8];
for (int v; (v = in.read(buf)) >= 0;)
;
} catch (Throwable t) { //ignore it
}
}
throw ex;
} finally {
in.close();
}
} else {
final String charset = getCharset(ctype);
Reader in = media.getReaderData();
if (repeatable)
in = RepeatableReader.getInstance(in);
try {
if (headOnly) {
int cnt = 0;
final char[] buf = new char[256];
for (int v; (v = in.read(buf)) >= 0;)
cnt += new String(buf, 0, v).getBytes(charset).length;
response.setContentLength(cnt);
return;
}
if (from >= 0) { //partial
PartialByteStream pbs = new PartialByteStream(from, to);
OutputStreamWriter wt = new OutputStreamWriter(pbs, charset);
Files.copy(wt, in);
wt.close(); //flush to pbs
pbs.responseTo(response);
} else {
OutputStreamWriter wt = new OutputStreamWriter(out, charset);
Files.copy(wt, in);
wt.close(); //flush to out
}
} catch (IOException ex) {
//browser might close the connection and reread
//so, read it completely, since 2nd read counts on it
if (in instanceof org.zkoss.io.Repeatable) {
try {
final char[] buf = new char[1024 * 4];
for (int v; (v = in.read(buf)) >= 0;)
;
} catch (Throwable t) { //ignore it
}
}
throw ex;
} finally {
in.close();
}
}
out.flush();
return; //done;
}
data = media.isBinary() ? media.getByteData() : media.getStringData().getBytes(getCharset(ctype));
}
if (headOnly) {
response.setContentLength(data.length);
} else {
final ServletOutputStream out = response.getOutputStream();
if (from >= 0) { //partial
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
int f = from <= data.length ? from : data.length;
int t = to >= 0 && to < data.length ? to : data.length;
int cnt = t - f + 1;
response.setContentLength(cnt);
response.setHeader("Content-Range", "bytes " + f + "-" + t + "/" + data.length);
out.write(data, f, cnt);
} else {
response.setContentLength(data.length);
out.write(data);
}
out.flush();
}
}
/** Filename can be quoted-string.
* Refer to http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
* and http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html
*/
private static String encodeFilename(HttpServletRequest request, String filename) {
// ZK-2143: should access Chinese filename
String agent = request.getHeader("USER-AGENT");
if (agent != null) {
try {
if (agent.contains("Trident")) {
// as Bug ZK-2350, the space of the filename in IE will be encoded to a '+' word as described in the URLEncoder's JAVA doc
// and we have no smarter way to decode that precisely so leave it there when user invokes the Https.write() directly.
filename = URLEncoder.encode(filename, "UTF-8");
} else if (agent.contains("Mozilla")) {
byte[] bytes = filename.getBytes("UTF-8");
filename = "";
for (byte b : bytes) {
filename += (char) (b & 0xff);
}
}
} catch (UnsupportedEncodingException e) {
// ignore it, if not supported
log.warn("", e);
}
}
return '"' + Strings.escape(filename, "\"") + '"';
}
private static String getCharset(String contentType) {
if (contentType != null) {
int j = contentType.indexOf("charset=");
if (j >= 0) {
String cs = contentType.substring(j + 8).trim();
if (cs.length() > 0)
return cs;
}
}
return "UTF-8";
}
private static int[] parseRange(String range) {
range = range.toLowerCase(java.util.Locale.ENGLISH);
for (int j = 0, k, len = range.length(); (k = range.indexOf("bytes", j)) >= 0;) {
for (k += 5; k < len;) {
char cc = range.charAt(k++);
if (cc == ' ' || cc == '\t')
continue;
if (cc == '=') {
j = range.indexOf('-', k);
try {
int from = Integer.parseInt((j >= 0 ? range.substring(k, j) : range.substring(k)).trim());
if (from >= 0) {
if (j >= 0) {
String s = range.substring(j + 1).trim();
if (s.length() > 0) {
int to = Integer.parseInt(s);
if (to >= from)
return new int[] { from, to };
}
}
return new int[] { from, -1 };
}
} catch (Throwable ex) { //ignore
}
if (log.isDebugEnabled())
log.debug("Failed to parse Range: " + range);
return null;
}
}
j = k;
}
return null;
}
}
/*package*/ class PartialByteStream extends ByteArrayOutputStream {
private final int _from, _to;
private int _ofs, _cnt;
/*package*/ PartialByteStream(int from, int to) {
super(4096);
_from = from;
_to = to;
}
/*package*/ void responseTo(HttpServletResponse response) throws IOException {
//Note: after all content are written, _ofs is the total number
//while _cnt the number of bytes being written.
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setContentLength(_cnt);
int from = _from <= _ofs ? _from : _ofs;
int to = _to >= 0 && _to <= _ofs ? _to : _ofs;
response.setHeader("Content-Range", "bytes " + from + "-" + to + "/" + _ofs);
writeTo(response.getOutputStream());
}
public synchronized void write(int b) {
int ofs = _ofs++;
if (ofs >= _from && (_to < 0 || ofs <= _to)) {
++_cnt;
super.write(b);
}
}
public synchronized void write(byte[] b, int ofs, int len) {
while (--len >= 0)
write(b[ofs++]);
}
}