/* * Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net> * Distributed under the terms of either: * - the common development and distribution license (CDDL), v1.0; or * - the GNU Lesser General Public License, v2.1 or later */ package winstone; import java.io.IOException; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.StringTokenizer; import java.util.TimeZone; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; /** * Response for servlet * * @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a> * @version $Id: WinstoneResponse.java,v 1.28 2005/04/19 07:33:41 rickknowles * Exp $ */ public class WinstoneResponse implements HttpServletResponse { private static final DateFormat HTTP_DF = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); private static final DateFormat VERSION0_DF = new SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss z", Locale.US); static { HTTP_DF.setTimeZone(TimeZone.getTimeZone("GMT")); VERSION0_DF.setTimeZone(TimeZone.getTimeZone("GMT")); } static final String CONTENT_LENGTH_HEADER = "Content-Length"; static final String CONTENT_TYPE_HEADER = "Content-Type"; // Response header constants private static final String CONTENT_LANGUAGE_HEADER = "Content-Language"; private static final String KEEP_ALIVE_HEADER = "Connection"; private static final String KEEP_ALIVE_OPEN = "Keep-Alive"; private static final String KEEP_ALIVE_CLOSE = "Close"; private static final String DATE_HEADER = "Date"; private static final String LOCATION_HEADER = "Location"; private static final String OUT_COOKIE_HEADER1 = "Set-Cookie"; private static final String X_POWERED_BY_HEADER = "X-Powered-By"; private static final String X_POWERED_BY_HEADER_VALUE = Launcher.RESOURCES.getString("PoweredByHeader"); private int statusCode; private WinstoneRequest req; private WebAppConfiguration webAppConfig; private WinstoneOutputStream outputStream; private PrintWriter outputWriter; private final List headers; private String explicitEncoding; private String implicitEncoding; private final List cookies; private Locale locale; private String protocol; private String reqKeepAliveHeader; private Integer errorStatusCode; /** * Constructor */ public WinstoneResponse() { this.headers = new ArrayList(); this.cookies = new ArrayList(); this.statusCode = SC_OK; this.locale = null; // Locale.getDefault(); this.explicitEncoding = null; this.protocol = null; this.reqKeepAliveHeader = null; } /** * Resets the request to be reused */ public void cleanUp() { this.req = null; this.webAppConfig = null; this.outputStream = null; this.outputWriter = null; this.headers.clear(); this.cookies.clear(); this.protocol = null; this.reqKeepAliveHeader = null; this.statusCode = SC_OK; this.errorStatusCode = null; this.locale = null; // Locale.getDefault(); this.explicitEncoding = null; this.implicitEncoding = null; } private String getEncodingFromLocale(Locale loc) { String localeString = loc.getLanguage() + "_" + loc.getCountry(); Map encMap = this.webAppConfig.getLocaleEncodingMap(); Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "WinstoneResponse.LookForLocaleEncoding", new String[] { localeString, encMap + "" }); String fullMatch = (String) encMap.get(localeString); if (fullMatch != null) { Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "WinstoneResponse.FoundLocaleEncoding", fullMatch); return fullMatch; } else { localeString = loc.getLanguage(); Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "WinstoneResponse.LookForLocaleEncoding", new String[] { localeString, encMap + "" }); String match = (String) encMap.get(localeString); if (match != null) { Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "WinstoneResponse.FoundLocaleEncoding", match); } return match; } } public void setErrorStatusCode(int statusCode) { this.errorStatusCode = new Integer(statusCode); this.statusCode = statusCode; } public WinstoneOutputStream getWinstoneOutputStream() { return this.outputStream; } public void setOutputStream(WinstoneOutputStream outData) { this.outputStream = outData; } public void setWebAppConfig(WebAppConfiguration webAppConfig) { this.webAppConfig = webAppConfig; } public String getProtocol() { return this.protocol; } public void setProtocol(String protocol) { this.protocol = protocol; } public void extractRequestKeepAliveHeader(WinstoneRequest req) { this.reqKeepAliveHeader = req.getHeader(KEEP_ALIVE_HEADER); } public List getHeaders() { return this.headers; } public List getCookies() { return this.cookies; } public WinstoneRequest getRequest() { return this.req; } public void setRequest(WinstoneRequest req) { this.req = req; } public void startIncludeBuffer() { this.outputStream.startIncludeBuffer(); } public void finishIncludeBuffer() throws IOException { if (isIncluding()) { if (this.outputWriter != null) { this.outputWriter.flush(); } this.outputStream.finishIncludeBuffer(); } } public void clearIncludeStackForForward() throws IOException { this.outputStream.clearIncludeStackForForward(); } protected static String getCharsetFromContentTypeHeader(String type, StringBuffer remainder) { if (type == null) { return null; } // Parse type to set encoding if needed StringTokenizer st = new StringTokenizer(type, ";"); String localEncoding = null; while (st.hasMoreTokens()) { String clause = st.nextToken().trim(); if (clause.startsWith("charset=")) localEncoding = clause.substring(8); else { if (remainder.length() > 0) { remainder.append(";"); } remainder.append(clause); } } if ((localEncoding == null) || !localEncoding.startsWith("\"") || !localEncoding.endsWith("\"")) { return localEncoding; } else { return localEncoding.substring(1, localEncoding.length() - 1); } } /** * This ensures the bare minimum correct http headers are present */ public void validateHeaders() { // Need this block for WebDAV support. "Connection:close" header is // ignored String lengthHeader = getHeader(CONTENT_LENGTH_HEADER); if ((lengthHeader == null) && (this.statusCode >= 300)) { int bodyBytes = this.outputStream.getOutputStreamLength(); if (getBufferSize() > bodyBytes) { Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "WinstoneResponse.ForcingContentLength", "" + bodyBytes); forceHeader(CONTENT_LENGTH_HEADER, "" + bodyBytes); lengthHeader = getHeader(CONTENT_LENGTH_HEADER); } } forceHeader(KEEP_ALIVE_HEADER, !closeAfterRequest() ? KEEP_ALIVE_OPEN : KEEP_ALIVE_CLOSE); String contentType = getHeader(CONTENT_TYPE_HEADER); if (this.statusCode != SC_MOVED_TEMPORARILY) { if (contentType == null) { // Bypass normal encoding forceHeader(CONTENT_TYPE_HEADER, "text/html;charset=" + getCharacterEncoding()); } else if (contentType.startsWith("text/")) { // replace charset in content StringBuffer remainder = new StringBuffer(); getCharsetFromContentTypeHeader(contentType, remainder); forceHeader(CONTENT_TYPE_HEADER, remainder.toString() + ";charset=" + getCharacterEncoding()); } } if (getHeader(DATE_HEADER) == null) { forceHeader(DATE_HEADER, formatHeaderDate(new Date())); } if (getHeader(X_POWERED_BY_HEADER) == null) { forceHeader(X_POWERED_BY_HEADER, X_POWERED_BY_HEADER_VALUE); } if (this.locale != null) { String lang = this.locale.getLanguage(); if ((this.locale.getCountry() != null) && !this.locale.getCountry().equals("")) { lang = lang + "-" + this.locale.getCountry(); } forceHeader(CONTENT_LANGUAGE_HEADER, lang); } // If we don't have a webappConfig, exit here, cause we definitely don't // have a session if (req.getWebAppConfig() == null) { return; } // Write out the new session cookie if it's present HostConfiguration hostConfig = req.getHostGroup().getHostByName(req.getServerName()); for (Iterator i = req.getCurrentSessionIds().keySet().iterator(); i.hasNext();) { String prefix = (String) i.next(); String sessionId = (String) req.getCurrentSessionIds().get(prefix); WebAppConfiguration ownerContext = hostConfig.getWebAppByURI(prefix); if (ownerContext != null) { WinstoneSession session = ownerContext.getSessionById(sessionId, true); if ((session != null) && session.isNew()) { session.setIsNew(false); Cookie cookie = new Cookie(WinstoneSession.SESSION_COOKIE_NAME, session.getId()); cookie.setMaxAge(-1); cookie.setSecure(req.isSecure()); cookie.setVersion(0); // req.isSecure() ? 1 : 0); cookie.setPath(req.getWebAppConfig().getContextPath().equals("") ? "/" : req.getWebAppConfig().getContextPath()); this.cookies.add(cookie); // don't call addCookie because we // might be including } } } // Look for expired sessions: ie ones where the requested and current // ids are different for (Iterator i = req.getRequestedSessionIds().keySet().iterator(); i.hasNext();) { String prefix = (String) i.next(); String sessionId = (String) req.getRequestedSessionIds().get(prefix); if (!req.getCurrentSessionIds().containsKey(prefix)) { Cookie cookie = new Cookie(WinstoneSession.SESSION_COOKIE_NAME, sessionId); cookie.setMaxAge(0); // explicitly expire this cookie cookie.setSecure(req.isSecure()); cookie.setVersion(0); // req.isSecure() ? 1 : 0); cookie.setPath(prefix.equals("") ? "/" : prefix); this.cookies.add(cookie); // don't call addCookie because we // might be including } } Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "WinstoneResponse.HeadersPreCommit", this.headers + ""); } /** * Writes out the http header for a single cookie */ public String writeCookie(Cookie cookie) throws IOException { Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "WinstoneResponse.WritingCookie", cookie + ""); StringBuffer out = new StringBuffer(); // Set-Cookie or Set-Cookie2 if (cookie.getVersion() >= 1) out.append(OUT_COOKIE_HEADER1).append(": "); // TCK doesn't like // set-cookie2 else out.append(OUT_COOKIE_HEADER1).append(": "); // name/value pair if (cookie.getVersion() == 0) out.append(cookie.getName()).append("=").append(cookie.getValue()); else { out.append(cookie.getName()).append("="); quote(cookie.getValue(), out); } if (cookie.getVersion() >= 1) { out.append("; Version=1"); if (cookie.getDomain() != null) { out.append("; Domain="); quote(cookie.getDomain(), out); } if (cookie.getSecure()) out.append("; Secure"); if (cookie.getMaxAge() >= 0) out.append("; Max-Age=").append(cookie.getMaxAge()); else out.append("; Discard"); if (cookie.getPath() != null) { out.append("; Path="); quote(cookie.getPath(), out); } } else { if (cookie.getDomain() != null) { out.append("; Domain="); out.append(cookie.getDomain()); } if (cookie.getMaxAge() > 0) { long expiryMS = System.currentTimeMillis() + (1000 * (long) cookie.getMaxAge()); String expiryDate = null; synchronized (VERSION0_DF) { expiryDate = VERSION0_DF.format(new Date(expiryMS)); } out.append("; Expires=").append(expiryDate); } else if (cookie.getMaxAge() == 0) { String expiryDate = null; synchronized (VERSION0_DF) { expiryDate = VERSION0_DF.format(new Date(5000)); } out.append("; Expires=").append(expiryDate); } if (cookie.getPath() != null) out.append("; Path=").append(cookie.getPath()); if (cookie.getSecure()) out.append("; Secure"); } return out.toString(); } private static String formatHeaderDate(Date dateIn) { String date = null; synchronized (HTTP_DF) { date = HTTP_DF.format(dateIn); } return date; } /** * Quotes the necessary strings in a cookie header. The quoting is only * applied if the string contains special characters. */ protected static void quote(String value, StringBuffer out) { if (value.startsWith("\"") && value.endsWith("\"")) { out.append(value); } else { boolean containsSpecial = false; for (int n = 0; n < value.length(); n++) { char thisChar = value.charAt(n); if ((thisChar < 32) || (thisChar >= 127) || (specialCharacters.indexOf(thisChar) != -1)) { containsSpecial = true; break; } } if (containsSpecial) out.append('"').append(value).append('"'); else out.append(value); } } private static final String specialCharacters = "()<>@,;:\\\"/[]?={} \t"; /** * Based on request/response headers and the protocol, determine whether or * not this connection should operate in keep-alive mode. */ public boolean closeAfterRequest() { String inKeepAliveHeader = this.reqKeepAliveHeader; String outKeepAliveHeader = getHeader(KEEP_ALIVE_HEADER); boolean hasContentLength = (getHeader(CONTENT_LENGTH_HEADER) != null); if (this.protocol.startsWith("HTTP/0")) return true; else if ((inKeepAliveHeader == null) && (outKeepAliveHeader == null)) return this.protocol.equals("HTTP/1.0") ? true : !hasContentLength; else if (outKeepAliveHeader != null) return outKeepAliveHeader.equalsIgnoreCase(KEEP_ALIVE_CLOSE) || !hasContentLength; else if (inKeepAliveHeader != null) return inKeepAliveHeader.equalsIgnoreCase(KEEP_ALIVE_CLOSE) || !hasContentLength; else return false; } // ServletResponse interface methods @Override public void flushBuffer() throws IOException { if (this.outputWriter != null) { this.outputWriter.flush(); } this.outputStream.flush(); } @Override public void setBufferSize(int size) { this.outputStream.setBufferSize(size); } @Override public int getBufferSize() { return this.outputStream.getBufferSize(); } @Override public String getCharacterEncoding() { String enc = getCurrentEncoding(); return (enc == null ? "ISO-8859-1" : enc); } @Override public void setCharacterEncoding(String encoding) { if ((this.outputWriter == null) && !isCommitted()) { Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "WinstoneResponse.SettingEncoding", encoding); this.explicitEncoding = encoding; correctContentTypeHeaderEncoding(encoding); } } private void correctContentTypeHeaderEncoding(String encoding) { String contentType = getContentType(); if (contentType != null) { StringBuffer remainderHeader = new StringBuffer(); getCharsetFromContentTypeHeader(contentType, remainderHeader); if (remainderHeader.length() != 0) { forceHeader(CONTENT_TYPE_HEADER, remainderHeader + ";charset=" + encoding); } } } @Override public String getContentType() { return getHeader(CONTENT_TYPE_HEADER); } @Override public void setContentType(String type) { setHeader(CONTENT_TYPE_HEADER, type); } @Override public Locale getLocale() { return this.locale == null ? Locale.getDefault() : this.locale; } private boolean isIncluding() { return this.outputStream.isIncluding(); } @Override public void setLocale(Locale loc) { if (isIncluding()) { return; } else if (isCommitted()) { Logger.log(Logger.WARNING, Launcher.RESOURCES, "WinstoneResponse.SetLocaleTooLate"); } else { if ((this.outputWriter == null) && (this.explicitEncoding == null)) { String localeEncoding = getEncodingFromLocale(loc); if (localeEncoding != null) { this.implicitEncoding = localeEncoding; correctContentTypeHeaderEncoding(localeEncoding); } } this.locale = loc; } } @Override public ServletOutputStream getOutputStream() throws IOException { Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "WinstoneResponse.GetOutputStream"); return this.outputStream; } @Override public PrintWriter getWriter() throws IOException { Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "WinstoneResponse.GetWriter"); if (this.outputWriter != null) return this.outputWriter; else { this.outputWriter = new WinstoneResponseWriter(this.outputStream, this); return this.outputWriter; } } @Override public boolean isCommitted() { return this.outputStream.isCommitted(); } @Override public void reset() { if (!isIncluding()) { resetBuffer(); this.statusCode = SC_OK; this.headers.clear(); this.cookies.clear(); } } @Override public void resetBuffer() { if (!isIncluding()) { if (isCommitted()) throw new IllegalStateException(Launcher.RESOURCES.getString("WinstoneResponse.ResponseCommitted")); // Disregard any output temporarily while we flush this.outputStream.setDisregardMode(true); if (this.outputWriter != null) { this.outputWriter.flush(); } this.outputStream.setDisregardMode(false); this.outputStream.reset(); } } @Override public void setContentLength(int len) { setIntHeader(CONTENT_LENGTH_HEADER, len); } // HttpServletResponse interface methods @Override public void addCookie(Cookie cookie) { if (!isIncluding()) { this.cookies.add(cookie); } } @Override public boolean containsHeader(String name) { for (int n = 0; n < this.headers.size(); n++) if (((String) this.headers.get(n)).startsWith(name)) return true; return false; } @Override public void addDateHeader(String name, long date) { addHeader(name, formatHeaderDate(new Date(date))); } // df.format(new Date(date)));} @Override public void addIntHeader(String name, int value) { addHeader(name, "" + value); } @Override public void addHeader(String name, String value) { if (isIncluding()) { Logger.log(Logger.DEBUG, Launcher.RESOURCES, "WinstoneResponse.HeaderInInclude", new String[] { name, value }); } else if (isCommitted()) { Logger.log(Logger.DEBUG, Launcher.RESOURCES, "WinstoneResponse.HeaderAfterCommitted", new String[] { name, value }); } else if (value != null) { if (name.equals(CONTENT_TYPE_HEADER)) { StringBuffer remainderHeader = new StringBuffer(); String headerEncoding = getCharsetFromContentTypeHeader(value, remainderHeader); if (this.outputWriter != null) { value = remainderHeader + ";charset=" + getCharacterEncoding(); } else if (headerEncoding != null) { this.explicitEncoding = headerEncoding; } } this.headers.add(name + ": " + value); } } @Override public void setDateHeader(String name, long date) { setHeader(name, formatHeaderDate(new Date(date))); } @Override public void setIntHeader(String name, int value) { setHeader(name, "" + value); } @Override public void setHeader(String name, String value) { if (isIncluding()) { Logger.log(Logger.DEBUG, Launcher.RESOURCES, "WinstoneResponse.HeaderInInclude", new String[] { name, value }); } else if (isCommitted()) { Logger.log(Logger.DEBUG, Launcher.RESOURCES, "WinstoneResponse.HeaderAfterCommitted", new String[] { name, value }); } else { boolean found = false; for (int n = 0; (n < this.headers.size()); n++) { String header = (String) this.headers.get(n); if (header.startsWith(name + ": ")) { if (found) { this.headers.remove(n); continue; } if (name.equals(CONTENT_TYPE_HEADER)) { if (value != null) { StringBuffer remainderHeader = new StringBuffer(); String headerEncoding = getCharsetFromContentTypeHeader(value, remainderHeader); if (this.outputWriter != null) { value = remainderHeader + ";charset=" + getCharacterEncoding(); } else if (headerEncoding != null) { this.explicitEncoding = headerEncoding; } } } if (value != null) { this.headers.set(n, name + ": " + value); } else { this.headers.remove(n); } found = true; } } if (!found) { addHeader(name, value); } } } private void forceHeader(String name, String value) { boolean found = false; for (int n = 0; (n < this.headers.size()); n++) { String header = (String) this.headers.get(n); if (header.startsWith(name + ": ")) { found = true; this.headers.set(n, name + ": " + value); } } if (!found) { this.headers.add(name + ": " + value); } } private String getCurrentEncoding() { if (this.explicitEncoding != null) { return this.explicitEncoding; } else if (this.implicitEncoding != null) { return this.implicitEncoding; } else if ((this.req != null) && (this.req.getCharacterEncoding() != null)) { try { "0".getBytes(this.req.getCharacterEncoding()); return this.req.getCharacterEncoding(); } catch (UnsupportedEncodingException err) { return null; } } else { return null; } } @Override public String getHeader(String name) { for (int n = 0; n < this.headers.size(); n++) { String header = (String) this.headers.get(n); if (header.startsWith(name + ": ")) return header.substring(name.length() + 2); } return null; } @Override public String encodeRedirectURL(String url) { return url; } @Override public String encodeURL(String url) { return url; } @Override public int getStatus() { return this.statusCode; } public Integer getErrorStatusCode() { return this.errorStatusCode; } @Override public void setStatus(int sc) { if (!isIncluding() && (this.errorStatusCode == null)) { // if (!isIncluding()) { this.statusCode = sc; // if (this.errorStatusCode != null) { // this.errorStatusCode = new Integer(sc); // } } } @Override public void sendRedirect(String location) throws IOException { if (isIncluding()) { Logger.log(Logger.ERROR, Launcher.RESOURCES, "IncludeResponse.Redirect", location); return; } else if (isCommitted()) { throw new IllegalStateException(Launcher.RESOURCES.getString("WinstoneOutputStream.AlreadyCommitted")); } resetBuffer(); // Build location StringBuffer fullLocation = new StringBuffer(); if (location.startsWith("http://") || location.startsWith("https://")) { fullLocation.append(location); } else { if (location.trim().equals(".")) { location = ""; } fullLocation.append(this.req.getScheme()).append("://"); fullLocation.append(this.req.getServerName()); if (!((this.req.getServerPort() == 80) && this.req.getScheme().equals("http")) && !((this.req.getServerPort() == 443) && this.req.getScheme().equals("https"))) fullLocation.append(':').append(this.req.getServerPort()); if (location.startsWith("/")) { fullLocation.append(location); } else { fullLocation.append(this.req.getRequestURI()); int questionPos = fullLocation.toString().indexOf("?"); if (questionPos != -1) { fullLocation.delete(questionPos, fullLocation.length()); } fullLocation.delete(fullLocation.toString().lastIndexOf("/") + 1, fullLocation.length()); fullLocation.append(location); } } if (this.req != null) { this.req.discardRequestBody(); } this.statusCode = HttpServletResponse.SC_MOVED_TEMPORARILY; setHeader(LOCATION_HEADER, fullLocation.toString()); setContentLength(0); getWriter().flush(); } @Override public void sendError(int sc) throws IOException { sendError(sc, null); } @Override public void sendError(int sc, String msg) throws IOException { if (isIncluding()) { Logger.log(Logger.ERROR, Launcher.RESOURCES, "IncludeResponse.Error", new String[] { "" + sc, msg }); return; } Logger.log(Logger.DEBUG, Launcher.RESOURCES, "WinstoneResponse.SendingError", new String[] { "" + sc, msg }); if ((this.webAppConfig != null) && (this.req != null)) { RequestDispatcher rd = this.webAppConfig.getErrorDispatcherByCode(sc, msg, null); if (rd != null) { try { rd.forward(this.req, this); return; } catch (IllegalStateException err) { throw err; } catch (IOException err) { throw err; } catch (Throwable err) { Logger.log(Logger.WARNING, Launcher.RESOURCES, "WinstoneResponse.ErrorInErrorPage", new String[] { rd.getName(), sc + "" }, err); return; } } } // If we are here there was no webapp and/or no request object, so // show the default error page if (this.errorStatusCode == null) { this.statusCode = sc; } String output = Launcher.RESOURCES.getString("WinstoneResponse.ErrorPage", new String[] { sc + "", (msg == null ? "" : msg), "", Launcher.RESOURCES.getString("ServerVersion"), "" + new Date() }); setContentLength(output.getBytes(getCharacterEncoding()).length); Writer out = getWriter(); out.write(output); out.flush(); } /** * @deprecated */ @Deprecated @Override public String encodeRedirectUrl(String url) { return encodeRedirectURL(url); } /** * @deprecated */ @Deprecated @Override public String encodeUrl(String url) { return encodeURL(url); } /** * @deprecated */ @Deprecated @Override public void setStatus(int sc, String sm) { setStatus(sc); } @Override public Collection<String> getHeaderNames() { // TODO Auto-generated method stub return null; } @Override public Collection<String> getHeaders(String arg0) { // TODO Auto-generated method stub return null; } }