| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.catalina.connector; |
| |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.net.MalformedURLException; |
| import java.nio.charset.Charset; |
| import java.security.AccessController; |
| import java.security.PrivilegedAction; |
| import java.security.PrivilegedActionException; |
| import java.security.PrivilegedExceptionAction; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Enumeration; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.TimeZone; |
| import java.util.Vector; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import javax.servlet.ServletOutputStream; |
| import javax.servlet.SessionTrackingMode; |
| import javax.servlet.http.Cookie; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.apache.catalina.Context; |
| import org.apache.catalina.Globals; |
| import org.apache.catalina.Session; |
| import org.apache.catalina.Wrapper; |
| import org.apache.catalina.security.SecurityUtil; |
| import org.apache.catalina.util.RequestUtil; |
| import org.apache.catalina.util.SessionConfig; |
| import org.apache.coyote.ActionCode; |
| import org.apache.tomcat.util.buf.CharChunk; |
| import org.apache.tomcat.util.buf.UEncoder; |
| import org.apache.tomcat.util.http.FastHttpDateFormat; |
| import org.apache.tomcat.util.http.MimeHeaders; |
| import org.apache.tomcat.util.http.parser.MediaTypeCache; |
| import org.apache.tomcat.util.net.URL; |
| import org.apache.tomcat.util.res.StringManager; |
| |
| /** |
| * Wrapper object for the Coyote response. |
| * |
| * @author Remy Maucherat |
| * @author Craig R. McClanahan |
| */ |
| public class Response |
| implements HttpServletResponse { |
| |
| |
| // ----------------------------------------------------------- Constructors |
| |
| private static final MediaTypeCache MEDIA_TYPE_CACHE = |
| new MediaTypeCache(100); |
| |
| /** |
| * Compliance with SRV.15.2.22.1. A call to Response.getWriter() if no |
| * character encoding has been specified will result in subsequent calls to |
| * Response.getCharacterEncoding() returning ISO-8859-1 and the Content-Type |
| * response header will include a charset=ISO-8859-1 component. |
| */ |
| private static final boolean ENFORCE_ENCODING_IN_GET_WRITER; |
| |
| static { |
| // Ensure that URL is loaded for SM |
| URL.isSchemeChar('c'); |
| |
| ENFORCE_ENCODING_IN_GET_WRITER = Boolean.valueOf( |
| System.getProperty("org.apache.catalina.connector.Response.ENFORCE_ENCODING_IN_GET_WRITER", |
| "true")).booleanValue(); |
| } |
| |
| public Response() { |
| urlEncoder.addSafeCharacter('/'); |
| } |
| |
| |
| // ----------------------------------------------------- Class Variables |
| |
| /** |
| * The string manager for this package. |
| */ |
| protected static final StringManager sm = StringManager.getManager(Response.class); |
| |
| |
| // ----------------------------------------------------- Instance Variables |
| |
| /** |
| * The date format we will use for creating date headers. |
| */ |
| protected SimpleDateFormat format = null; |
| |
| |
| // ------------------------------------------------------------- Properties |
| |
| /** |
| * Set the Connector through which this Request was received. |
| * |
| * @param connector The new connector |
| */ |
| public void setConnector(Connector connector) { |
| if("AJP/1.3".equals(connector.getProtocol())) { |
| // default size to size of one ajp-packet |
| outputBuffer = new OutputBuffer(8184); |
| } else { |
| outputBuffer = new OutputBuffer(); |
| } |
| outputStream = new CoyoteOutputStream(outputBuffer); |
| writer = new CoyoteWriter(outputBuffer); |
| } |
| |
| |
| /** |
| * Coyote response. |
| */ |
| protected org.apache.coyote.Response coyoteResponse; |
| |
| /** |
| * Set the Coyote response. |
| * |
| * @param coyoteResponse The Coyote response |
| */ |
| public void setCoyoteResponse(org.apache.coyote.Response coyoteResponse) { |
| this.coyoteResponse = coyoteResponse; |
| outputBuffer.setResponse(coyoteResponse); |
| } |
| |
| /** |
| * Get the Coyote response. |
| */ |
| public org.apache.coyote.Response getCoyoteResponse() { |
| return this.coyoteResponse; |
| } |
| |
| |
| /** |
| * Return the Context within which this Request is being processed. |
| */ |
| public Context getContext() { |
| return (request.getContext()); |
| } |
| |
| |
| /** |
| * The associated output buffer. |
| */ |
| protected OutputBuffer outputBuffer; |
| |
| |
| /** |
| * The associated output stream. |
| */ |
| protected CoyoteOutputStream outputStream; |
| |
| |
| /** |
| * The associated writer. |
| */ |
| protected CoyoteWriter writer; |
| |
| |
| /** |
| * The application commit flag. |
| */ |
| protected boolean appCommitted = false; |
| |
| |
| /** |
| * The included flag. |
| */ |
| protected boolean included = false; |
| |
| |
| /** |
| * The characterEncoding flag |
| */ |
| private boolean isCharacterEncodingSet = false; |
| |
| /** |
| * With the introduction of async processing and the possibility of |
| * non-container threads calling sendError() tracking the current error |
| * state and ensuring that the correct error page is called becomes more |
| * complicated. This state attribute helps by tracking the current error |
| * state and informing callers that attempt to change state if the change |
| * was successful or if another thread got there first. |
| * |
| * <pre> |
| * The state machine is very simple: |
| * |
| * 0 - NONE |
| * 1 - NOT_REPORTED |
| * 2 - REPORTED |
| * |
| * |
| * -->---->-- >NONE |
| * | | | |
| * | | | setError() |
| * ^ ^ | |
| * | | \|/ |
| * | |-<-NOT_REPORTED |
| * | | |
| * ^ | report() |
| * | | |
| * | \|/ |
| * |----<----REPORTED |
| * </pre> |
| */ |
| private final AtomicInteger errorState = new AtomicInteger(0); |
| |
| |
| /** |
| * Using output stream flag. |
| */ |
| protected boolean usingOutputStream = false; |
| |
| |
| /** |
| * Using writer flag. |
| */ |
| protected boolean usingWriter = false; |
| |
| |
| /** |
| * URL encoder. |
| */ |
| protected final UEncoder urlEncoder = new UEncoder(); |
| |
| |
| /** |
| * Recyclable buffer to hold the redirect URL. |
| */ |
| protected final CharChunk redirectURLCC = new CharChunk(); |
| |
| |
| // --------------------------------------------------------- Public Methods |
| |
| |
| /** |
| * Release all object references, and initialize instance variables, in |
| * preparation for reuse of this object. |
| */ |
| public void recycle() { |
| |
| outputBuffer.recycle(); |
| usingOutputStream = false; |
| usingWriter = false; |
| appCommitted = false; |
| included = false; |
| errorState.set(0); |
| isCharacterEncodingSet = false; |
| |
| if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) { |
| if (facade != null) { |
| facade.clear(); |
| facade = null; |
| } |
| if (outputStream != null) { |
| outputStream.clear(); |
| outputStream = null; |
| } |
| if (writer != null) { |
| writer.clear(); |
| writer = null; |
| } |
| } else { |
| writer.recycle(); |
| } |
| |
| } |
| |
| |
| /** |
| * Clear cached encoders (to save memory for async requests). |
| */ |
| public void clearEncoders() { |
| outputBuffer.clearEncoders(); |
| } |
| |
| |
| // ------------------------------------------------------- Response Methods |
| |
| |
| /** |
| * Return the number of bytes the application has actually written to the |
| * output stream. This excludes chunking, compression, etc. as well as |
| * headers. |
| */ |
| public long getContentWritten() { |
| return outputBuffer.getContentWritten(); |
| } |
| |
| |
| /** |
| * Return the number of bytes the actually written to the socket. This |
| * includes chunking, compression, etc. but excludes headers. |
| */ |
| public long getBytesWritten(boolean flush) { |
| if (flush) { |
| try { |
| outputBuffer.flush(); |
| } catch (IOException ioe) { |
| // Ignore - the client has probably closed the connection |
| } |
| } |
| return coyoteResponse.getBytesWritten(flush); |
| } |
| |
| /** |
| * Set the application commit flag. |
| * |
| * @param appCommitted The new application committed flag value |
| */ |
| public void setAppCommitted(boolean appCommitted) { |
| this.appCommitted = appCommitted; |
| } |
| |
| |
| /** |
| * Application commit flag accessor. |
| */ |
| public boolean isAppCommitted() { |
| return (this.appCommitted || isCommitted() || isSuspended() |
| || ((getContentLength() > 0) |
| && (getContentWritten() >= getContentLength()))); |
| } |
| |
| |
| /** |
| * The request with which this response is associated. |
| */ |
| protected Request request = null; |
| |
| /** |
| * Return the Request with which this Response is associated. |
| */ |
| public org.apache.catalina.connector.Request getRequest() { |
| return (this.request); |
| } |
| |
| /** |
| * Set the Request with which this Response is associated. |
| * |
| * @param request The new associated request |
| */ |
| public void setRequest(org.apache.catalina.connector.Request request) { |
| this.request = request; |
| } |
| |
| |
| /** |
| * The facade associated with this response. |
| */ |
| protected ResponseFacade facade = null; |
| |
| /** |
| * Return the <code>ServletResponse</code> for which this object |
| * is the facade. |
| */ |
| public HttpServletResponse getResponse() { |
| if (facade == null) { |
| facade = new ResponseFacade(this); |
| } |
| return (facade); |
| } |
| |
| |
| /** |
| * Set the suspended flag. |
| * |
| * @param suspended The new suspended flag value |
| */ |
| public void setSuspended(boolean suspended) { |
| outputBuffer.setSuspended(suspended); |
| } |
| |
| |
| /** |
| * Suspended flag accessor. |
| */ |
| public boolean isSuspended() { |
| return outputBuffer.isSuspended(); |
| } |
| |
| |
| /** |
| * Closed flag accessor. |
| */ |
| public boolean isClosed() { |
| return outputBuffer.isClosed(); |
| } |
| |
| |
| /** |
| * Set the error flag. |
| */ |
| public boolean setError() { |
| boolean result = errorState.compareAndSet(0, 1); |
| if (result) { |
| Wrapper wrapper = getRequest().getWrapper(); |
| if (wrapper != null) { |
| wrapper.incrementErrorCount(); |
| } |
| } |
| return result; |
| } |
| |
| |
| /** |
| * Error flag accessor. |
| */ |
| public boolean isError() { |
| return errorState.get() > 0; |
| } |
| |
| |
| public boolean isErrorReportRequired() { |
| return errorState.get() == 1; |
| } |
| |
| |
| public boolean setErrorReported() { |
| return errorState.compareAndSet(1, 2); |
| } |
| |
| |
| /** |
| * Perform whatever actions are required to flush and close the output |
| * stream or writer, in a single operation. |
| * |
| * @exception IOException if an input/output error occurs |
| */ |
| public void finishResponse() throws IOException { |
| // Writing leftover bytes |
| outputBuffer.close(); |
| } |
| |
| |
| /** |
| * Return the content length that was set or calculated for this Response. |
| */ |
| public int getContentLength() { |
| return coyoteResponse.getContentLength(); |
| } |
| |
| |
| /** |
| * Return the content type that was set or calculated for this response, |
| * or <code>null</code> if no content type was set. |
| */ |
| @Override |
| public String getContentType() { |
| return coyoteResponse.getContentType(); |
| } |
| |
| |
| /** |
| * Return a PrintWriter that can be used to render error messages, |
| * regardless of whether a stream or writer has already been acquired. |
| * |
| * @return Writer which can be used for error reports. If the response is |
| * not an error report returned using sendError or triggered by an |
| * unexpected exception thrown during the servlet processing |
| * (and only in that case), null will be returned if the response stream |
| * has already been used. |
| * |
| * @exception IOException if an input/output error occurs |
| */ |
| public PrintWriter getReporter() throws IOException { |
| if (outputBuffer.isNew()) { |
| outputBuffer.checkConverter(); |
| if (writer == null) { |
| writer = new CoyoteWriter(outputBuffer); |
| } |
| return writer; |
| } else { |
| return null; |
| } |
| } |
| |
| |
| // ------------------------------------------------ ServletResponse Methods |
| |
| |
| /** |
| * Flush the buffer and commit this response. |
| * |
| * @exception IOException if an input/output error occurs |
| */ |
| @Override |
| public void flushBuffer() throws IOException { |
| outputBuffer.flush(); |
| } |
| |
| |
| /** |
| * Return the actual buffer size used for this Response. |
| */ |
| @Override |
| public int getBufferSize() { |
| return outputBuffer.getBufferSize(); |
| } |
| |
| |
| /** |
| * Return the character encoding used for this Response. |
| */ |
| @Override |
| public String getCharacterEncoding() { |
| return (coyoteResponse.getCharacterEncoding()); |
| } |
| |
| |
| /** |
| * Return the servlet output stream associated with this Response. |
| * |
| * @exception IllegalStateException if <code>getWriter</code> has |
| * already been called for this response |
| * @exception IOException if an input/output error occurs |
| */ |
| @Override |
| public ServletOutputStream getOutputStream() |
| throws IOException { |
| |
| if (usingWriter) { |
| throw new IllegalStateException |
| (sm.getString("coyoteResponse.getOutputStream.ise")); |
| } |
| |
| usingOutputStream = true; |
| if (outputStream == null) { |
| outputStream = new CoyoteOutputStream(outputBuffer); |
| } |
| return outputStream; |
| |
| } |
| |
| |
| /** |
| * Return the Locale assigned to this response. |
| */ |
| @Override |
| public Locale getLocale() { |
| return (coyoteResponse.getLocale()); |
| } |
| |
| |
| /** |
| * Return the writer associated with this Response. |
| * |
| * @exception IllegalStateException if <code>getOutputStream</code> has |
| * already been called for this response |
| * @exception IOException if an input/output error occurs |
| */ |
| @Override |
| public PrintWriter getWriter() |
| throws IOException { |
| |
| if (usingOutputStream) { |
| throw new IllegalStateException |
| (sm.getString("coyoteResponse.getWriter.ise")); |
| } |
| |
| if (ENFORCE_ENCODING_IN_GET_WRITER) { |
| /* |
| * If the response's character encoding has not been specified as |
| * described in <code>getCharacterEncoding</code> (i.e., the method |
| * just returns the default value <code>ISO-8859-1</code>), |
| * <code>getWriter</code> updates it to <code>ISO-8859-1</code> |
| * (with the effect that a subsequent call to getContentType() will |
| * include a charset=ISO-8859-1 component which will also be |
| * reflected in the Content-Type response header, thereby satisfying |
| * the Servlet spec requirement that containers must communicate the |
| * character encoding used for the servlet response's writer to the |
| * client). |
| */ |
| setCharacterEncoding(getCharacterEncoding()); |
| } |
| |
| usingWriter = true; |
| outputBuffer.checkConverter(); |
| if (writer == null) { |
| writer = new CoyoteWriter(outputBuffer); |
| } |
| return writer; |
| } |
| |
| |
| /** |
| * Has the output of this response already been committed? |
| */ |
| @Override |
| public boolean isCommitted() { |
| return coyoteResponse.isCommitted(); |
| } |
| |
| |
| /** |
| * Clear any content written to the buffer. |
| * |
| * @exception IllegalStateException if this response has already |
| * been committed |
| */ |
| @Override |
| public void reset() { |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| coyoteResponse.reset(); |
| outputBuffer.reset(); |
| usingOutputStream = false; |
| usingWriter = false; |
| isCharacterEncodingSet = false; |
| } |
| |
| |
| /** |
| * Reset the data buffer but not any status or header information. |
| * |
| * @exception IllegalStateException if the response has already |
| * been committed |
| */ |
| @Override |
| public void resetBuffer() { |
| resetBuffer(false); |
| } |
| |
| |
| /** |
| * Reset the data buffer and the using Writer/Stream flags but not any |
| * status or header information. |
| * |
| * @param resetWriterStreamFlags <code>true</code> if the internal |
| * <code>usingWriter</code>, <code>usingOutputStream</code>, |
| * <code>isCharacterEncodingSet</code> flags should also be reset |
| * |
| * @exception IllegalStateException if the response has already |
| * been committed |
| */ |
| public void resetBuffer(boolean resetWriterStreamFlags) { |
| |
| if (isCommitted()) { |
| throw new IllegalStateException |
| (sm.getString("coyoteResponse.resetBuffer.ise")); |
| } |
| |
| outputBuffer.reset(resetWriterStreamFlags); |
| |
| if(resetWriterStreamFlags) { |
| usingOutputStream = false; |
| usingWriter = false; |
| isCharacterEncodingSet = false; |
| } |
| |
| } |
| |
| |
| /** |
| * Set the buffer size to be used for this Response. |
| * |
| * @param size The new buffer size |
| * |
| * @exception IllegalStateException if this method is called after |
| * output has been committed for this response |
| */ |
| @Override |
| public void setBufferSize(int size) { |
| |
| if (isCommitted() || !outputBuffer.isNew()) { |
| throw new IllegalStateException |
| (sm.getString("coyoteResponse.setBufferSize.ise")); |
| } |
| |
| outputBuffer.setBufferSize(size); |
| |
| } |
| |
| |
| /** |
| * Set the content length (in bytes) for this Response. |
| * |
| * @param length The new content length |
| */ |
| @Override |
| public void setContentLength(int length) { |
| |
| setContentLengthLong(length); |
| } |
| |
| |
| |
| /** |
| * TODO SERVLET 3.1 |
| */ |
| @Override |
| public void setContentLengthLong(long length) { |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| coyoteResponse.setContentLength(length); |
| |
| } |
| |
| |
| /** |
| * Set the content type for this Response. |
| * |
| * @param type The new content type |
| */ |
| @Override |
| public void setContentType(String type) { |
| |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| if (type == null) { |
| coyoteResponse.setContentType(null); |
| return; |
| } |
| |
| String[] m = MEDIA_TYPE_CACHE.parse(type); |
| if (m == null) { |
| // Invalid - Assume no charset and just pass through whatever |
| // the user provided. |
| coyoteResponse.setContentTypeNoCharset(type); |
| return; |
| } |
| |
| coyoteResponse.setContentTypeNoCharset(m[0]); |
| |
| if (m[1] != null) { |
| // Ignore charset if getWriter() has already been called |
| if (!usingWriter) { |
| coyoteResponse.setCharacterEncoding(m[1]); |
| isCharacterEncodingSet = true; |
| } |
| } |
| } |
| |
| |
| /* |
| * Overrides the name of the character encoding used in the body |
| * of the request. This method must be called prior to reading |
| * request parameters or reading input using getReader(). |
| * |
| * @param charset String containing the name of the character encoding. |
| */ |
| @Override |
| public void setCharacterEncoding(String charset) { |
| |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| // Ignore any call made after the getWriter has been invoked |
| // The default should be used |
| if (usingWriter) { |
| return; |
| } |
| |
| coyoteResponse.setCharacterEncoding(charset); |
| isCharacterEncodingSet = true; |
| } |
| |
| |
| /** |
| * Set the Locale that is appropriate for this response, including |
| * setting the appropriate character encoding. |
| * |
| * @param locale The new locale |
| */ |
| @Override |
| public void setLocale(Locale locale) { |
| |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| coyoteResponse.setLocale(locale); |
| |
| // Ignore any call made after the getWriter has been invoked. |
| // The default should be used |
| if (usingWriter) { |
| return; |
| } |
| |
| if (isCharacterEncodingSet) { |
| return; |
| } |
| |
| String charset = getContext().getCharset(locale); |
| if (charset != null) { |
| coyoteResponse.setCharacterEncoding(charset); |
| } |
| } |
| |
| |
| // --------------------------------------------------- HttpResponse Methods |
| |
| |
| @Override |
| public String getHeader(String name) { |
| return coyoteResponse.getMimeHeaders().getHeader(name); |
| } |
| |
| |
| @Override |
| public Collection<String> getHeaderNames() { |
| |
| MimeHeaders headers = coyoteResponse.getMimeHeaders(); |
| int n = headers.size(); |
| List<String> result = new ArrayList<>(n); |
| for (int i = 0; i < n; i++) { |
| result.add(headers.getName(i).toString()); |
| } |
| return result; |
| |
| } |
| |
| |
| @Override |
| public Collection<String> getHeaders(String name) { |
| |
| Enumeration<String> enumeration = |
| coyoteResponse.getMimeHeaders().values(name); |
| Vector<String> result = new Vector<>(); |
| while (enumeration.hasMoreElements()) { |
| result.addElement(enumeration.nextElement()); |
| } |
| return result; |
| } |
| |
| |
| /** |
| * Return the error message that was set with <code>sendError()</code> |
| * for this Response. |
| */ |
| public String getMessage() { |
| return coyoteResponse.getMessage(); |
| } |
| |
| |
| @Override |
| public int getStatus() { |
| return coyoteResponse.getStatus(); |
| } |
| |
| |
| // -------------------------------------------- HttpServletResponse Methods |
| |
| /** |
| * Add the specified Cookie to those that will be included with |
| * this Response. |
| * |
| * @param cookie Cookie to be added |
| */ |
| @Override |
| public void addCookie(final Cookie cookie) { |
| |
| // Ignore any call from an included servlet |
| if (included || isCommitted()) { |
| return; |
| } |
| |
| String header = generateCookieString(cookie); |
| //if we reached here, no exception, cookie is valid |
| // the header name is Set-Cookie for both "old" and v.1 ( RFC2109 ) |
| // RFC2965 is not supported by browsers and the Servlet spec |
| // asks for 2109. |
| addHeader("Set-Cookie", header, getContext().getCookieProcessor().getCharset()); |
| } |
| |
| /** |
| * Special method for adding a session cookie as we should be overriding |
| * any previous. |
| * |
| * @param cookie The new session cookie to add the response |
| */ |
| public void addSessionCookieInternal(final Cookie cookie) { |
| if (isCommitted()) { |
| return; |
| } |
| |
| String name = cookie.getName(); |
| final String headername = "Set-Cookie"; |
| final String startsWith = name + "="; |
| String header = generateCookieString(cookie); |
| boolean set = false; |
| MimeHeaders headers = coyoteResponse.getMimeHeaders(); |
| int n = headers.size(); |
| for (int i = 0; i < n; i++) { |
| if (headers.getName(i).toString().equals(headername)) { |
| if (headers.getValue(i).toString().startsWith(startsWith)) { |
| headers.getValue(i).setString(header); |
| set = true; |
| } |
| } |
| } |
| if (!set) { |
| addHeader(headername, header); |
| } |
| |
| |
| } |
| |
| public String generateCookieString(final Cookie cookie) { |
| // Web application code can receive a IllegalArgumentException |
| // from the generateHeader() invocation |
| if (SecurityUtil.isPackageProtectionEnabled()) { |
| return AccessController.doPrivileged(new PrivilegedAction<String>() { |
| @Override |
| public String run(){ |
| return getContext().getCookieProcessor().generateHeader(cookie); |
| } |
| }); |
| } else { |
| return getContext().getCookieProcessor().generateHeader(cookie); |
| } |
| } |
| |
| |
| /** |
| * Add the specified date header to the specified value. |
| * |
| * @param name Name of the header to set |
| * @param value Date value to be set |
| */ |
| @Override |
| public void addDateHeader(String name, long value) { |
| |
| if (name == null || name.length() == 0) { |
| return; |
| } |
| |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| if (format == null) { |
| format = new SimpleDateFormat(FastHttpDateFormat.RFC1123_DATE, |
| Locale.US); |
| format.setTimeZone(TimeZone.getTimeZone("GMT")); |
| } |
| |
| addHeader(name, FastHttpDateFormat.formatDate(value, format)); |
| |
| } |
| |
| |
| /** |
| * Add the specified header to the specified value. |
| * |
| * @param name Name of the header to set |
| * @param value Value to be set |
| */ |
| @Override |
| public void addHeader(String name, String value) { |
| addHeader(name, value, null); |
| } |
| |
| |
| private void addHeader(String name, String value, Charset charset) { |
| |
| if (name == null || name.length() == 0 || value == null) { |
| return; |
| } |
| |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| char cc=name.charAt(0); |
| if (cc=='C' || cc=='c') { |
| if (checkSpecialHeader(name, value)) |
| return; |
| } |
| |
| coyoteResponse.addHeader(name, value, charset); |
| } |
| |
| |
| /** |
| * An extended version of this exists in {@link org.apache.coyote.Response}. |
| * This check is required here to ensure that the usingWriter check in |
| * {@link #setContentType(String)} is applied since usingWriter is not |
| * visible to {@link org.apache.coyote.Response} |
| * |
| * Called from set/addHeader. |
| * Return true if the header is special, no need to set the header. |
| */ |
| private boolean checkSpecialHeader(String name, String value) { |
| if (name.equalsIgnoreCase("Content-Type")) { |
| setContentType(value); |
| return true; |
| } |
| return false; |
| } |
| |
| |
| /** |
| * Add the specified integer header to the specified value. |
| * |
| * @param name Name of the header to set |
| * @param value Integer value to be set |
| */ |
| @Override |
| public void addIntHeader(String name, int value) { |
| |
| if (name == null || name.length() == 0) { |
| return; |
| } |
| |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| addHeader(name, "" + value); |
| |
| } |
| |
| |
| /** |
| * Has the specified header been set already in this response? |
| * |
| * @param name Name of the header to check |
| */ |
| @Override |
| public boolean containsHeader(String name) { |
| // Need special handling for Content-Type and Content-Length due to |
| // special handling of these in coyoteResponse |
| char cc=name.charAt(0); |
| if(cc=='C' || cc=='c') { |
| if(name.equalsIgnoreCase("Content-Type")) { |
| // Will return null if this has not been set |
| return (coyoteResponse.getContentType() != null); |
| } |
| if(name.equalsIgnoreCase("Content-Length")) { |
| // -1 means not known and is not sent to client |
| return (coyoteResponse.getContentLengthLong() != -1); |
| } |
| } |
| |
| return coyoteResponse.containsHeader(name); |
| } |
| |
| |
| /** |
| * Encode the session identifier associated with this response |
| * into the specified redirect URL, if necessary. |
| * |
| * @param url URL to be encoded |
| */ |
| @Override |
| public String encodeRedirectURL(String url) { |
| |
| if (isEncodeable(toAbsolute(url))) { |
| return (toEncoded(url, request.getSessionInternal().getIdInternal())); |
| } else { |
| return (url); |
| } |
| |
| } |
| |
| |
| /** |
| * Encode the session identifier associated with this response |
| * into the specified redirect URL, if necessary. |
| * |
| * @param url URL to be encoded |
| * |
| * @deprecated As of Version 2.1 of the Java Servlet API, use |
| * <code>encodeRedirectURL()</code> instead. |
| */ |
| @Override |
| @Deprecated |
| public String encodeRedirectUrl(String url) { |
| return (encodeRedirectURL(url)); |
| } |
| |
| |
| /** |
| * Encode the session identifier associated with this response |
| * into the specified URL, if necessary. |
| * |
| * @param url URL to be encoded |
| */ |
| @Override |
| public String encodeURL(String url) { |
| |
| String absolute; |
| try { |
| absolute = toAbsolute(url); |
| } catch (IllegalArgumentException iae) { |
| // Relative URL |
| return url; |
| } |
| |
| if (isEncodeable(absolute)) { |
| // W3c spec clearly said |
| if (url.equalsIgnoreCase("")) { |
| url = absolute; |
| } else if (url.equals(absolute) && !hasPath(url)) { |
| url += '/'; |
| } |
| return (toEncoded(url, request.getSessionInternal().getIdInternal())); |
| } else { |
| return (url); |
| } |
| |
| } |
| |
| |
| /** |
| * Encode the session identifier associated with this response |
| * into the specified URL, if necessary. |
| * |
| * @param url URL to be encoded |
| * |
| * @deprecated As of Version 2.1 of the Java Servlet API, use |
| * <code>encodeURL()</code> instead. |
| */ |
| @Override |
| @Deprecated |
| public String encodeUrl(String url) { |
| return (encodeURL(url)); |
| } |
| |
| |
| /** |
| * Send an acknowledgement of a request. |
| * |
| * @exception IOException if an input/output error occurs |
| */ |
| public void sendAcknowledgement() |
| throws IOException { |
| |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| coyoteResponse.action(ActionCode.ACK, null); |
| } |
| |
| |
| /** |
| * Send an error response with the specified status and a |
| * default message. |
| * |
| * @param status HTTP status code to send |
| * |
| * @exception IllegalStateException if this response has |
| * already been committed |
| * @exception IOException if an input/output error occurs |
| */ |
| @Override |
| public void sendError(int status) throws IOException { |
| sendError(status, null); |
| } |
| |
| |
| /** |
| * Send an error response with the specified status and message. |
| * |
| * @param status HTTP status code to send |
| * @param message Corresponding message to send |
| * |
| * @exception IllegalStateException if this response has |
| * already been committed |
| * @exception IOException if an input/output error occurs |
| */ |
| @Override |
| public void sendError(int status, String message) throws IOException { |
| |
| if (isCommitted()) { |
| throw new IllegalStateException |
| (sm.getString("coyoteResponse.sendError.ise")); |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| setError(); |
| |
| coyoteResponse.setStatus(status); |
| coyoteResponse.setMessage(message); |
| |
| // Clear any data content that has been buffered |
| resetBuffer(); |
| |
| // Cause the response to be finished (from the application perspective) |
| setSuspended(true); |
| } |
| |
| |
| /** |
| * Send a temporary redirect to the specified redirect location URL. |
| * |
| * @param location Location URL to redirect to |
| * |
| * @exception IllegalStateException if this response has |
| * already been committed |
| * @exception IOException if an input/output error occurs |
| */ |
| @Override |
| public void sendRedirect(String location) throws IOException { |
| sendRedirect(location, SC_FOUND); |
| } |
| |
| /** |
| * Internal method that allows a redirect to be sent with a status other |
| * than {@link HttpServletResponse#SC_FOUND} (302). No attempt is made to |
| * validate the status code. |
| */ |
| public void sendRedirect(String location, int status) throws IOException { |
| if (isCommitted()) { |
| throw new IllegalStateException |
| (sm.getString("coyoteResponse.sendRedirect.ise")); |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| // Clear any data content that has been buffered |
| resetBuffer(true); |
| |
| // Generate a temporary redirect to the specified location |
| try { |
| String absolute = toAbsolute(location); |
| setStatus(status); |
| setHeader("Location", absolute); |
| if (getContext().getSendRedirectBody()) { |
| PrintWriter writer = getWriter(); |
| writer.print(sm.getString("coyoteResponse.sendRedirect.note", |
| RequestUtil.filter(absolute))); |
| flushBuffer(); |
| } |
| } catch (IllegalArgumentException e) { |
| setStatus(SC_NOT_FOUND); |
| } |
| |
| // Cause the response to be finished (from the application perspective) |
| setSuspended(true); |
| } |
| |
| |
| /** |
| * Set the specified date header to the specified value. |
| * |
| * @param name Name of the header to set |
| * @param value Date value to be set |
| */ |
| @Override |
| public void setDateHeader(String name, long value) { |
| |
| if (name == null || name.length() == 0) { |
| return; |
| } |
| |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| if (format == null) { |
| format = new SimpleDateFormat(FastHttpDateFormat.RFC1123_DATE, |
| Locale.US); |
| format.setTimeZone(TimeZone.getTimeZone("GMT")); |
| } |
| |
| setHeader(name, FastHttpDateFormat.formatDate(value, format)); |
| } |
| |
| |
| /** |
| * Set the specified header to the specified value. |
| * |
| * @param name Name of the header to set |
| * @param value Value to be set |
| */ |
| @Override |
| public void setHeader(String name, String value) { |
| |
| if (name == null || name.length() == 0 || value == null) { |
| return; |
| } |
| |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| char cc=name.charAt(0); |
| if (cc=='C' || cc=='c') { |
| if (checkSpecialHeader(name, value)) |
| return; |
| } |
| |
| coyoteResponse.setHeader(name, value); |
| } |
| |
| |
| /** |
| * Set the specified integer header to the specified value. |
| * |
| * @param name Name of the header to set |
| * @param value Integer value to be set |
| */ |
| @Override |
| public void setIntHeader(String name, int value) { |
| |
| if (name == null || name.length() == 0) { |
| return; |
| } |
| |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| setHeader(name, "" + value); |
| |
| } |
| |
| |
| /** |
| * Set the HTTP status to be returned with this response. |
| * |
| * @param status The new HTTP status |
| */ |
| @Override |
| public void setStatus(int status) { |
| setStatus(status, null); |
| } |
| |
| |
| /** |
| * Set the HTTP status and message to be returned with this response. |
| * |
| * @param status The new HTTP status |
| * @param message The associated text message |
| * |
| * @deprecated As of Version 2.1 of the Java Servlet API, this method |
| * has been deprecated due to the ambiguous meaning of the message |
| * parameter. |
| */ |
| @Override |
| @Deprecated |
| public void setStatus(int status, String message) { |
| |
| if (isCommitted()) { |
| return; |
| } |
| |
| // Ignore any call from an included servlet |
| if (included) { |
| return; |
| } |
| |
| coyoteResponse.setStatus(status); |
| coyoteResponse.setMessage(message); |
| |
| } |
| |
| |
| // ------------------------------------------------------ Protected Methods |
| |
| |
| /** |
| * Return <code>true</code> if the specified URL should be encoded with |
| * a session identifier. This will be true if all of the following |
| * conditions are met: |
| * <ul> |
| * <li>The request we are responding to asked for a valid session |
| * <li>The requested session ID was not received via a cookie |
| * <li>The specified URL points back to somewhere within the web |
| * application that is responding to this request |
| * </ul> |
| * |
| * @param location Absolute URL to be validated |
| */ |
| protected boolean isEncodeable(final String location) { |
| |
| if (location == null) { |
| return (false); |
| } |
| |
| // Is this an intra-document reference? |
| if (location.startsWith("#")) { |
| return (false); |
| } |
| |
| // Are we in a valid session that is not using cookies? |
| final Request hreq = request; |
| final Session session = hreq.getSessionInternal(false); |
| if (session == null) { |
| return (false); |
| } |
| if (hreq.isRequestedSessionIdFromCookie()) { |
| return (false); |
| } |
| |
| // Is URL encoding permitted |
| if (!hreq.getServletContext().getEffectiveSessionTrackingModes(). |
| contains(SessionTrackingMode.URL)) { |
| return false; |
| } |
| |
| if (SecurityUtil.isPackageProtectionEnabled()) { |
| return ( |
| AccessController.doPrivileged(new PrivilegedAction<Boolean>() { |
| |
| @Override |
| public Boolean run(){ |
| return Boolean.valueOf(doIsEncodeable(hreq, session, location)); |
| } |
| })).booleanValue(); |
| } else { |
| return doIsEncodeable(hreq, session, location); |
| } |
| } |
| |
| private boolean doIsEncodeable(Request hreq, Session session, |
| String location) { |
| // Is this a valid absolute URL? |
| URL url = null; |
| try { |
| url = new URL(location); |
| } catch (MalformedURLException e) { |
| return (false); |
| } |
| |
| // Does this URL match down to (and including) the context path? |
| if (!hreq.getScheme().equalsIgnoreCase(url.getProtocol())) { |
| return (false); |
| } |
| if (!hreq.getServerName().equalsIgnoreCase(url.getHost())) { |
| return (false); |
| } |
| int serverPort = hreq.getServerPort(); |
| if (serverPort == -1) { |
| if ("https".equals(hreq.getScheme())) { |
| serverPort = 443; |
| } else { |
| serverPort = 80; |
| } |
| } |
| int urlPort = url.getPort(); |
| if (urlPort == -1) { |
| if ("https".equals(url.getProtocol())) { |
| urlPort = 443; |
| } else { |
| urlPort = 80; |
| } |
| } |
| if (serverPort != urlPort) { |
| return (false); |
| } |
| |
| String contextPath = getContext().getPath(); |
| if (contextPath != null) { |
| String file = url.getFile(); |
| if ((file == null) || !file.startsWith(contextPath)) { |
| return (false); |
| } |
| String tok = ";" + |
| SessionConfig.getSessionUriParamName(request.getContext()) + |
| "=" + session.getIdInternal(); |
| if( file.indexOf(tok, contextPath.length()) >= 0 ) { |
| return (false); |
| } |
| } |
| |
| // This URL belongs to our web application, so it is encodeable |
| return (true); |
| |
| } |
| |
| |
| /** |
| * Convert (if necessary) and return the absolute URL that represents the |
| * resource referenced by this possibly relative URL. If this URL is |
| * already absolute, return it unchanged. |
| * |
| * @param location URL to be (possibly) converted and then returned |
| * |
| * @exception IllegalArgumentException if a MalformedURLException is |
| * thrown when converting the relative URL to an absolute one |
| */ |
| protected String toAbsolute(String location) { |
| |
| if (location == null) { |
| return (location); |
| } |
| |
| boolean leadingSlash = location.startsWith("/"); |
| |
| if (location.startsWith("//")) { |
| // Scheme relative |
| redirectURLCC.recycle(); |
| // Add the scheme |
| String scheme = request.getScheme(); |
| try { |
| redirectURLCC.append(scheme, 0, scheme.length()); |
| redirectURLCC.append(':'); |
| redirectURLCC.append(location, 0, location.length()); |
| return redirectURLCC.toString(); |
| } catch (IOException e) { |
| IllegalArgumentException iae = |
| new IllegalArgumentException(location); |
| iae.initCause(e); |
| throw iae; |
| } |
| |
| } else if (leadingSlash || !hasScheme(location)) { |
| |
| redirectURLCC.recycle(); |
| |
| String scheme = request.getScheme(); |
| String name = request.getServerName(); |
| int port = request.getServerPort(); |
| |
| try { |
| redirectURLCC.append(scheme, 0, scheme.length()); |
| redirectURLCC.append("://", 0, 3); |
| redirectURLCC.append(name, 0, name.length()); |
| if ((scheme.equals("http") && port != 80) |
| || (scheme.equals("https") && port != 443)) { |
| redirectURLCC.append(':'); |
| String portS = port + ""; |
| redirectURLCC.append(portS, 0, portS.length()); |
| } |
| if (!leadingSlash) { |
| String relativePath = request.getDecodedRequestURI(); |
| int pos = relativePath.lastIndexOf('/'); |
| CharChunk encodedURI = null; |
| final String frelativePath = relativePath; |
| final int fend = pos; |
| if (SecurityUtil.isPackageProtectionEnabled() ){ |
| try{ |
| encodedURI = AccessController.doPrivileged( |
| new PrivilegedExceptionAction<CharChunk>(){ |
| @Override |
| public CharChunk run() throws IOException{ |
| return urlEncoder.encodeURL(frelativePath, 0, fend); |
| } |
| }); |
| } catch (PrivilegedActionException pae){ |
| IllegalArgumentException iae = |
| new IllegalArgumentException(location); |
| iae.initCause(pae.getException()); |
| throw iae; |
| } |
| } else { |
| encodedURI = urlEncoder.encodeURL(relativePath, 0, pos); |
| } |
| redirectURLCC.append(encodedURI); |
| encodedURI.recycle(); |
| redirectURLCC.append('/'); |
| } |
| redirectURLCC.append(location, 0, location.length()); |
| |
| normalize(redirectURLCC); |
| } catch (IOException e) { |
| IllegalArgumentException iae = |
| new IllegalArgumentException(location); |
| iae.initCause(e); |
| throw iae; |
| } |
| |
| return redirectURLCC.toString(); |
| |
| } else { |
| |
| return (location); |
| |
| } |
| |
| } |
| |
| /* |
| * Removes /./ and /../ sequences from absolute URLs. |
| * Code borrowed heavily from CoyoteAdapter.normalize() |
| */ |
| private void normalize(CharChunk cc) { |
| // Strip query string and/or fragment first as doing it this way makes |
| // the normalization logic a lot simpler |
| int truncate = cc.indexOf('?'); |
| if (truncate == -1) { |
| truncate = cc.indexOf('#'); |
| } |
| char[] truncateCC = null; |
| if (truncate > -1) { |
| truncateCC = Arrays.copyOfRange(cc.getBuffer(), |
| cc.getStart() + truncate, cc.getEnd()); |
| cc.setEnd(cc.getStart() + truncate); |
| } |
| |
| if (cc.endsWith("/.") || cc.endsWith("/..")) { |
| try { |
| cc.append('/'); |
| } catch (IOException e) { |
| throw new IllegalArgumentException(cc.toString(), e); |
| } |
| } |
| |
| char[] c = cc.getChars(); |
| int start = cc.getStart(); |
| int end = cc.getEnd(); |
| int index = 0; |
| int startIndex = 0; |
| |
| // Advance past the first three / characters (should place index just |
| // scheme://host[:port] |
| |
| for (int i = 0; i < 3; i++) { |
| startIndex = cc.indexOf('/', startIndex + 1); |
| } |
| |
| // Remove /./ |
| index = startIndex; |
| while (true) { |
| index = cc.indexOf("/./", 0, 3, index); |
| if (index < 0) { |
| break; |
| } |
| copyChars(c, start + index, start + index + 2, |
| end - start - index - 2); |
| end = end - 2; |
| cc.setEnd(end); |
| } |
| |
| // Remove /../ |
| index = startIndex; |
| int pos; |
| while (true) { |
| index = cc.indexOf("/../", 0, 4, index); |
| if (index < 0) { |
| break; |
| } |
| // Can't go above the server root |
| if (index == startIndex) { |
| throw new IllegalArgumentException(); |
| } |
| int index2 = -1; |
| for (pos = start + index - 1; (pos >= 0) && (index2 < 0); pos --) { |
| if (c[pos] == (byte) '/') { |
| index2 = pos; |
| } |
| } |
| copyChars(c, start + index2, start + index + 3, |
| end - start - index - 3); |
| end = end + index2 - index - 3; |
| cc.setEnd(end); |
| index = index2; |
| } |
| |
| // Add the query string and/or fragment (if present) back in |
| if (truncateCC != null) { |
| try { |
| cc.append(truncateCC, 0, truncateCC.length); |
| } catch (IOException ioe) { |
| throw new IllegalArgumentException(ioe); |
| } |
| } |
| } |
| |
| private void copyChars(char[] c, int dest, int src, int len) { |
| for (int pos = 0; pos < len; pos++) { |
| c[pos + dest] = c[pos + src]; |
| } |
| } |
| |
| |
| /** |
| * Determine if an absolute URL has a path component |
| */ |
| private boolean hasPath(String uri) { |
| int pos = uri.indexOf("://"); |
| if (pos < 0) { |
| return false; |
| } |
| pos = uri.indexOf('/', pos + 3); |
| if (pos < 0) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Determine if a URI string has a <code>scheme</code> component. |
| */ |
| private boolean hasScheme(String uri) { |
| int len = uri.length(); |
| for(int i=0; i < len ; i++) { |
| char c = uri.charAt(i); |
| if(c == ':') { |
| return i > 0; |
| } else if(!URL.isSchemeChar(c)) { |
| return false; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return the specified URL with the specified session identifier |
| * suitably encoded. |
| * |
| * @param url URL to be encoded with the session id |
| * @param sessionId Session id to be included in the encoded URL |
| */ |
| protected String toEncoded(String url, String sessionId) { |
| |
| if ((url == null) || (sessionId == null)) { |
| return (url); |
| } |
| |
| String path = url; |
| String query = ""; |
| String anchor = ""; |
| int question = url.indexOf('?'); |
| if (question >= 0) { |
| path = url.substring(0, question); |
| query = url.substring(question); |
| } |
| int pound = path.indexOf('#'); |
| if (pound >= 0) { |
| anchor = path.substring(pound); |
| path = path.substring(0, pound); |
| } |
| StringBuilder sb = new StringBuilder(path); |
| if( sb.length() > 0 ) { // jsessionid can't be first. |
| sb.append(";"); |
| sb.append(SessionConfig.getSessionUriParamName( |
| request.getContext())); |
| sb.append("="); |
| sb.append(sessionId); |
| } |
| sb.append(anchor); |
| sb.append(query); |
| return (sb.toString()); |
| |
| } |
| } |