| /* |
| * 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.coyote.http11; |
| |
| import java.io.IOException; |
| import java.util.StringTokenizer; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.regex.Pattern; |
| |
| import org.apache.coyote.ActionCode; |
| import org.apache.coyote.ActionHook; |
| import org.apache.coyote.Adapter; |
| import org.apache.coyote.AsyncContextCallback; |
| import org.apache.coyote.AsyncStateMachine; |
| import org.apache.coyote.Processor; |
| import org.apache.coyote.Request; |
| import org.apache.coyote.Response; |
| import org.apache.coyote.http11.filters.BufferedInputFilter; |
| import org.apache.coyote.http11.filters.ChunkedInputFilter; |
| import org.apache.coyote.http11.filters.ChunkedOutputFilter; |
| import org.apache.coyote.http11.filters.GzipOutputFilter; |
| import org.apache.coyote.http11.filters.IdentityInputFilter; |
| import org.apache.coyote.http11.filters.IdentityOutputFilter; |
| import org.apache.coyote.http11.filters.SavedRequestInputFilter; |
| import org.apache.coyote.http11.filters.VoidInputFilter; |
| import org.apache.coyote.http11.filters.VoidOutputFilter; |
| import org.apache.juli.logging.Log; |
| import org.apache.tomcat.util.ExceptionUtils; |
| import org.apache.tomcat.util.buf.Ascii; |
| import org.apache.tomcat.util.buf.ByteChunk; |
| import org.apache.tomcat.util.buf.MessageBytes; |
| import org.apache.tomcat.util.http.FastHttpDateFormat; |
| import org.apache.tomcat.util.http.MimeHeaders; |
| import org.apache.tomcat.util.net.AbstractEndpoint; |
| import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; |
| import org.apache.tomcat.util.res.StringManager; |
| |
| public abstract class AbstractHttp11Processor implements ActionHook, Processor { |
| |
| protected abstract Log getLog(); |
| |
| /** |
| * The string manager for this package. |
| */ |
| protected static final StringManager sm = |
| StringManager.getManager(Constants.Package); |
| |
| /* |
| * Tracks how many internal filters are in the filter library so they |
| * are skipped when looking for pluggable filters. |
| */ |
| private int pluggableFilterIndex = Integer.MAX_VALUE; |
| |
| /** |
| * Associated adapter. |
| */ |
| protected Adapter adapter = null; |
| |
| |
| /** |
| * Request object. |
| */ |
| protected Request request = null; |
| |
| |
| /** |
| * Response object. |
| */ |
| protected Response response = null; |
| |
| |
| /** |
| * Error flag. |
| */ |
| protected boolean error = false; |
| |
| |
| /** |
| * Keep-alive. |
| */ |
| protected boolean keepAlive = true; |
| |
| |
| /** |
| * HTTP/1.1 flag. |
| */ |
| protected boolean http11 = true; |
| |
| |
| /** |
| * HTTP/0.9 flag. |
| */ |
| protected boolean http09 = false; |
| |
| |
| /** |
| * Content delimiter for the request (if false, the connection will |
| * be closed at the end of the request). |
| */ |
| protected boolean contentDelimitation = true; |
| |
| |
| /** |
| * Is there an expectation ? |
| */ |
| protected boolean expectation = false; |
| |
| |
| /** |
| * Regular expression that defines the restricted user agents. |
| */ |
| protected Pattern restrictedUserAgents = null; |
| |
| |
| /** |
| * Maximum number of Keep-Alive requests to honor. |
| */ |
| protected int maxKeepAliveRequests = -1; |
| |
| /** |
| * The number of seconds Tomcat will wait for a subsequent request |
| * before closing the connection. |
| */ |
| protected int keepAliveTimeout = -1; |
| |
| /** |
| * Remote Address associated with the current connection. |
| */ |
| protected String remoteAddr = null; |
| |
| |
| /** |
| * Remote Host associated with the current connection. |
| */ |
| protected String remoteHost = null; |
| |
| |
| /** |
| * Local Host associated with the current connection. |
| */ |
| protected String localName = null; |
| |
| |
| /** |
| * Local port to which the socket is connected |
| */ |
| protected int localPort = -1; |
| |
| |
| /** |
| * Remote port to which the socket is connected |
| */ |
| protected int remotePort = -1; |
| |
| |
| /** |
| * The local Host address. |
| */ |
| protected String localAddr = null; |
| |
| |
| /** |
| * Maximum timeout on uploads. 5 minutes as in Apache HTTPD server. |
| */ |
| protected int connectionUploadTimeout = 300000; |
| |
| |
| /** |
| * Flag to disable setting a different time-out on uploads. |
| */ |
| protected boolean disableUploadTimeout = false; |
| |
| |
| /** |
| * Allowed compression level. |
| */ |
| protected int compressionLevel = 0; |
| |
| |
| /** |
| * Minimum content size to make compression. |
| */ |
| protected int compressionMinSize = 2048; |
| |
| |
| /** |
| * Socket buffering. |
| */ |
| protected int socketBuffer = -1; |
| |
| |
| /** |
| * Max saved post size. |
| */ |
| protected int maxSavePostSize = 4 * 1024; |
| |
| |
| /** |
| * Regular expression that defines the user agents to not use gzip with |
| */ |
| protected Pattern noCompressionUserAgents = null; |
| |
| /** |
| * List of MIMES which could be gzipped |
| */ |
| protected String[] compressableMimeTypes = |
| { "text/html", "text/xml", "text/plain" }; |
| |
| |
| /** |
| * Host name (used to avoid useless B2C conversion on the host name). |
| */ |
| protected char[] hostNameC = new char[0]; |
| |
| |
| /** |
| * Allow a customized the server header for the tin-foil hat folks. |
| */ |
| protected String server = null; |
| |
| |
| /** |
| * Track changes in state for async requests. |
| */ |
| protected AsyncStateMachine asyncStateMachine = new AsyncStateMachine(this); |
| |
| |
| /** |
| * Set compression level. |
| */ |
| public void setCompression(String compression) { |
| if (compression.equals("on")) { |
| this.compressionLevel = 1; |
| } else if (compression.equals("force")) { |
| this.compressionLevel = 2; |
| } else if (compression.equals("off")) { |
| this.compressionLevel = 0; |
| } else { |
| try { |
| // Try to parse compression as an int, which would give the |
| // minimum compression size |
| compressionMinSize = Integer.parseInt(compression); |
| this.compressionLevel = 1; |
| } catch (Exception e) { |
| this.compressionLevel = 0; |
| } |
| } |
| } |
| |
| /** |
| * Set Minimum size to trigger compression. |
| */ |
| public void setCompressionMinSize(int compressionMinSize) { |
| this.compressionMinSize = compressionMinSize; |
| } |
| |
| |
| /** |
| * Set no compression user agent pattern. Regular expression as supported |
| * by {@link Pattern}. |
| * |
| * ie: "gorilla|desesplorer|tigrus" |
| */ |
| public void setNoCompressionUserAgents(String noCompressionUserAgents) { |
| if (noCompressionUserAgents == null || noCompressionUserAgents.length() == 0) { |
| this.noCompressionUserAgents = null; |
| } else { |
| this.noCompressionUserAgents = |
| Pattern.compile(noCompressionUserAgents); |
| } |
| } |
| |
| /** |
| * Add a mime-type which will be compressible |
| * The mime-type String will be exactly matched |
| * in the response mime-type header . |
| * |
| * @param mimeType mime-type string |
| */ |
| public void addCompressableMimeType(String mimeType) { |
| compressableMimeTypes = |
| addStringArray(compressableMimeTypes, mimeType); |
| } |
| |
| |
| /** |
| * Set compressible mime-type list (this method is best when used with |
| * a large number of connectors, where it would be better to have all of |
| * them referenced a single array). |
| */ |
| public void setCompressableMimeTypes(String[] compressableMimeTypes) { |
| this.compressableMimeTypes = compressableMimeTypes; |
| } |
| |
| |
| /** |
| * Set compressable mime-type list |
| * List contains users agents separated by ',' : |
| * |
| * ie: "text/html,text/xml,text/plain" |
| */ |
| public void setCompressableMimeTypes(String compressableMimeTypes) { |
| if (compressableMimeTypes != null) { |
| this.compressableMimeTypes = null; |
| StringTokenizer st = new StringTokenizer(compressableMimeTypes, ","); |
| |
| while (st.hasMoreTokens()) { |
| addCompressableMimeType(st.nextToken().trim()); |
| } |
| } |
| } |
| |
| |
| /** |
| * Return compression level. |
| */ |
| public String getCompression() { |
| switch (compressionLevel) { |
| case 0: |
| return "off"; |
| case 1: |
| return "on"; |
| case 2: |
| return "force"; |
| } |
| return "off"; |
| } |
| |
| |
| /** |
| * General use method |
| * |
| * @param sArray the StringArray |
| * @param value string |
| */ |
| private String[] addStringArray(String sArray[], String value) { |
| String[] result = null; |
| if (sArray == null) { |
| result = new String[1]; |
| result[0] = value; |
| } |
| else { |
| result = new String[sArray.length + 1]; |
| for (int i = 0; i < sArray.length; i++) |
| result[i] = sArray[i]; |
| result[sArray.length] = value; |
| } |
| return result; |
| } |
| |
| |
| /** |
| * Checks if any entry in the string array starts with the specified value |
| * |
| * @param sArray the StringArray |
| * @param value string |
| */ |
| private boolean startsWithStringArray(String sArray[], String value) { |
| if (value == null) |
| return false; |
| for (int i = 0; i < sArray.length; i++) { |
| if (value.startsWith(sArray[i])) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| |
| /** |
| * Set restricted user agent list (which will downgrade the connector |
| * to HTTP/1.0 mode). Regular expression as supported by {@link Pattern}. |
| * |
| * ie: "gorilla|desesplorer|tigrus" |
| */ |
| public void setRestrictedUserAgents(String restrictedUserAgents) { |
| if (restrictedUserAgents == null || |
| restrictedUserAgents.length() == 0) { |
| this.restrictedUserAgents = null; |
| } else { |
| this.restrictedUserAgents = Pattern.compile(restrictedUserAgents); |
| } |
| } |
| |
| |
| /** |
| * Set the maximum number of Keep-Alive requests to honor. |
| * This is to safeguard from DoS attacks. Setting to a negative |
| * value disables the check. |
| */ |
| public void setMaxKeepAliveRequests(int mkar) { |
| maxKeepAliveRequests = mkar; |
| } |
| |
| |
| /** |
| * Return the number of Keep-Alive requests that we will honor. |
| */ |
| public int getMaxKeepAliveRequests() { |
| return maxKeepAliveRequests; |
| } |
| |
| /** |
| * Set the Keep-Alive timeout. |
| */ |
| public void setKeepAliveTimeout(int timeout) { |
| keepAliveTimeout = timeout; |
| } |
| |
| |
| /** |
| * Return the number Keep-Alive timeout. |
| */ |
| public int getKeepAliveTimeout() { |
| return keepAliveTimeout; |
| } |
| |
| |
| /** |
| * Set the maximum size of a POST which will be buffered in SSL mode. |
| */ |
| public void setMaxSavePostSize(int msps) { |
| maxSavePostSize = msps; |
| } |
| |
| |
| /** |
| * Return the maximum size of a POST which will be buffered in SSL mode. |
| */ |
| public int getMaxSavePostSize() { |
| return maxSavePostSize; |
| } |
| |
| |
| /** |
| * Set the flag to control upload time-outs. |
| */ |
| public void setDisableUploadTimeout(boolean isDisabled) { |
| disableUploadTimeout = isDisabled; |
| } |
| |
| /** |
| * Get the flag that controls upload time-outs. |
| */ |
| public boolean getDisableUploadTimeout() { |
| return disableUploadTimeout; |
| } |
| |
| /** |
| * Set the socket buffer flag. |
| */ |
| public void setSocketBuffer(int socketBuffer) { |
| this.socketBuffer = socketBuffer; |
| } |
| |
| /** |
| * Get the socket buffer flag. |
| */ |
| public int getSocketBuffer() { |
| return socketBuffer; |
| } |
| |
| /** |
| * Set the upload timeout. |
| */ |
| public void setConnectionUploadTimeout(int timeout) { |
| connectionUploadTimeout = timeout ; |
| } |
| |
| /** |
| * Get the upload timeout. |
| */ |
| public int getConnectionUploadTimeout() { |
| return connectionUploadTimeout; |
| } |
| |
| |
| /** |
| * Set the server header name. |
| */ |
| public void setServer( String server ) { |
| if (server==null || server.equals("")) { |
| this.server = null; |
| } else { |
| this.server = server; |
| } |
| } |
| |
| /** |
| * Get the server header name. |
| */ |
| public String getServer() { |
| return server; |
| } |
| |
| |
| /** Get the request associated with this processor. |
| * |
| * @return The request |
| */ |
| public Request getRequest() { |
| return request; |
| } |
| |
| |
| /** |
| * Set the associated adapter. |
| * |
| * @param adapter the new adapter |
| */ |
| public void setAdapter(Adapter adapter) { |
| this.adapter = adapter; |
| } |
| |
| |
| /** |
| * Get the associated adapter. |
| * |
| * @return the associated adapter |
| */ |
| public Adapter getAdapter() { |
| return adapter; |
| } |
| |
| |
| /** |
| * Check if the resource could be compressed, if the client supports it. |
| */ |
| private boolean isCompressable() { |
| |
| // Check if content is not already gzipped |
| MessageBytes contentEncodingMB = |
| response.getMimeHeaders().getValue("Content-Encoding"); |
| |
| if ((contentEncodingMB != null) |
| && (contentEncodingMB.indexOf("gzip") != -1)) |
| return false; |
| |
| // If force mode, always compress (test purposes only) |
| if (compressionLevel == 2) |
| return true; |
| |
| // Check if sufficient length to trigger the compression |
| long contentLength = response.getContentLengthLong(); |
| if ((contentLength == -1) |
| || (contentLength > compressionMinSize)) { |
| // Check for compatible MIME-TYPE |
| if (compressableMimeTypes != null) { |
| return (startsWithStringArray(compressableMimeTypes, |
| response.getContentType())); |
| } |
| } |
| |
| return false; |
| } |
| |
| |
| /** |
| * Check if compression should be used for this resource. Already checked |
| * that the resource could be compressed if the client supports it. |
| */ |
| private boolean useCompression() { |
| |
| // Check if browser support gzip encoding |
| MessageBytes acceptEncodingMB = |
| request.getMimeHeaders().getValue("accept-encoding"); |
| |
| if ((acceptEncodingMB == null) |
| || (acceptEncodingMB.indexOf("gzip") == -1)) |
| return false; |
| |
| // If force mode, always compress (test purposes only) |
| if (compressionLevel == 2) |
| return true; |
| |
| // Check for incompatible Browser |
| if (noCompressionUserAgents != null) { |
| MessageBytes userAgentValueMB = |
| request.getMimeHeaders().getValue("user-agent"); |
| if(userAgentValueMB != null) { |
| String userAgentValue = userAgentValueMB.toString(); |
| |
| if (noCompressionUserAgents != null && |
| noCompressionUserAgents.matcher(userAgentValue).matches()) { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| |
| /** |
| * Specialized utility method: find a sequence of lower case bytes inside |
| * a ByteChunk. |
| */ |
| protected int findBytes(ByteChunk bc, byte[] b) { |
| |
| byte first = b[0]; |
| byte[] buff = bc.getBuffer(); |
| int start = bc.getStart(); |
| int end = bc.getEnd(); |
| |
| // Look for first char |
| int srcEnd = b.length; |
| |
| for (int i = start; i <= (end - srcEnd); i++) { |
| if (Ascii.toLower(buff[i]) != first) continue; |
| // found first char, now look for a match |
| int myPos = i+1; |
| for (int srcPos = 1; srcPos < srcEnd;) { |
| if (Ascii.toLower(buff[myPos++]) != b[srcPos++]) |
| break; |
| if (srcPos == srcEnd) return i - start; // found it |
| } |
| } |
| return -1; |
| } |
| |
| |
| /** |
| * Determine if we must drop the connection because of the HTTP status |
| * code. Use the same list of codes as Apache/httpd. |
| */ |
| protected boolean statusDropsConnection(int status) { |
| return status == 400 /* SC_BAD_REQUEST */ || |
| status == 408 /* SC_REQUEST_TIMEOUT */ || |
| status == 411 /* SC_LENGTH_REQUIRED */ || |
| status == 413 /* SC_REQUEST_ENTITY_TOO_LARGE */ || |
| status == 414 /* SC_REQUEST_URI_TOO_LONG */ || |
| status == 500 /* SC_INTERNAL_SERVER_ERROR */ || |
| status == 503 /* SC_SERVICE_UNAVAILABLE */ || |
| status == 501 /* SC_NOT_IMPLEMENTED */; |
| } |
| |
| |
| /** |
| * Exposes input buffer to super class to allow better code re-use. |
| * @return The input buffer used by the processor. |
| */ |
| protected abstract AbstractInputBuffer getInputBuffer(); |
| |
| |
| /** |
| * Exposes output buffer to super class to allow better code re-use. |
| * @return The output buffer used by the processor. |
| */ |
| protected abstract AbstractOutputBuffer getOutputBuffer(); |
| |
| |
| /** |
| * Initialize standard input and output filters. |
| */ |
| protected void initializeFilters(int maxTrailerSize) { |
| // Create and add the identity filters. |
| getInputBuffer().addFilter(new IdentityInputFilter()); |
| getOutputBuffer().addFilter(new IdentityOutputFilter()); |
| |
| // Create and add the chunked filters. |
| getInputBuffer().addFilter(new ChunkedInputFilter(maxTrailerSize)); |
| getOutputBuffer().addFilter(new ChunkedOutputFilter()); |
| |
| // Create and add the void filters. |
| getInputBuffer().addFilter(new VoidInputFilter()); |
| getOutputBuffer().addFilter(new VoidOutputFilter()); |
| |
| // Create and add buffered input filter |
| getInputBuffer().addFilter(new BufferedInputFilter()); |
| |
| // Create and add the chunked filters. |
| //getInputBuffer().addFilter(new GzipInputFilter()); |
| getOutputBuffer().addFilter(new GzipOutputFilter()); |
| |
| pluggableFilterIndex = getInputBuffer().getFilters().length; |
| } |
| |
| |
| /** |
| * Add an input filter to the current request. |
| * |
| * @return false if the encoding was not found (which would mean it is |
| * unsupported) |
| */ |
| protected boolean addInputFilter(InputFilter[] inputFilters, |
| String encodingName) { |
| if (encodingName.equals("identity")) { |
| // Skip |
| } else if (encodingName.equals("chunked")) { |
| getInputBuffer().addActiveFilter |
| (inputFilters[Constants.CHUNKED_FILTER]); |
| contentDelimitation = true; |
| } else { |
| for (int i = pluggableFilterIndex; i < inputFilters.length; i++) { |
| if (inputFilters[i].getEncodingName() |
| .toString().equals(encodingName)) { |
| getInputBuffer().addActiveFilter(inputFilters[i]); |
| return true; |
| } |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| |
| /** |
| * Send an action to the connector. |
| * |
| * @param actionCode Type of the action |
| * @param param Action parameter |
| */ |
| @Override |
| public final void action(ActionCode actionCode, Object param) { |
| |
| if (actionCode == ActionCode.COMMIT) { |
| // Commit current response |
| |
| if (response.isCommitted()) |
| return; |
| |
| // Validate and write response headers |
| try { |
| prepareResponse(); |
| getOutputBuffer().commit(); |
| } catch (IOException e) { |
| // Set error flag |
| error = true; |
| } |
| } else if (actionCode == ActionCode.ACK) { |
| // Acknowledge request |
| // Send a 100 status back if it makes sense (response not committed |
| // yet, and client specified an expectation for 100-continue) |
| |
| if ((response.isCommitted()) || !expectation) |
| return; |
| |
| getInputBuffer().setSwallowInput(true); |
| try { |
| getOutputBuffer().sendAck(); |
| } catch (IOException e) { |
| // Set error flag |
| error = true; |
| } |
| } else if (actionCode == ActionCode.CLIENT_FLUSH) { |
| |
| try { |
| getOutputBuffer().flush(); |
| } catch (IOException e) { |
| // Set error flag |
| error = true; |
| response.setErrorException(e); |
| } |
| |
| } else if (actionCode == ActionCode.DISABLE_SWALLOW_INPUT) { |
| // Do not swallow request input but |
| // make sure we are closing the connection |
| error = true; |
| getInputBuffer().setSwallowInput(false); |
| |
| } else if (actionCode == ActionCode.RESET) { |
| // Reset response |
| // Note: This must be called before the response is committed |
| |
| getOutputBuffer().reset(); |
| |
| } else if (actionCode == ActionCode.CUSTOM) { |
| // Do nothing |
| // TODO Remove this action |
| |
| } else if (actionCode == ActionCode.REQ_SET_BODY_REPLAY) { |
| ByteChunk body = (ByteChunk) param; |
| |
| InputFilter savedBody = new SavedRequestInputFilter(body); |
| savedBody.setRequest(request); |
| |
| AbstractInputBuffer internalBuffer = (AbstractInputBuffer) |
| request.getInputBuffer(); |
| internalBuffer.addActiveFilter(savedBody); |
| } else if (actionCode == ActionCode.ASYNC_START) { |
| asyncStateMachine.asyncStart((AsyncContextCallback) param); |
| } else if (actionCode == ActionCode.ASYNC_DISPATCHED) { |
| asyncStateMachine.asyncDispatched(); |
| } else if (actionCode == ActionCode.ASYNC_TIMEOUT) { |
| AtomicBoolean result = (AtomicBoolean) param; |
| result.set(asyncStateMachine.asyncTimeout()); |
| } else if (actionCode == ActionCode.ASYNC_RUN) { |
| asyncStateMachine.asyncRun((Runnable) param); |
| } else if (actionCode == ActionCode.ASYNC_ERROR) { |
| asyncStateMachine.asyncError(); |
| } else if (actionCode == ActionCode.ASYNC_IS_STARTED) { |
| ((AtomicBoolean) param).set(asyncStateMachine.isAsyncStarted()); |
| } else if (actionCode == ActionCode.ASYNC_IS_DISPATCHING) { |
| ((AtomicBoolean) param).set(asyncStateMachine.isAsyncDispatching()); |
| } else if (actionCode == ActionCode.ASYNC_IS_ASYNC) { |
| ((AtomicBoolean) param).set(asyncStateMachine.isAsync()); |
| } else if (actionCode == ActionCode.ASYNC_IS_TIMINGOUT) { |
| ((AtomicBoolean) param).set(asyncStateMachine.isAsyncTimingOut()); |
| } else { |
| actionInternal(actionCode, param); |
| } |
| } |
| |
| abstract void actionInternal(ActionCode actionCode, Object param); |
| |
| |
| /** |
| * When committing the response, we have to validate the set of headers, as |
| * well as setup the response filters. |
| */ |
| private void prepareResponse() { |
| |
| boolean entityBody = true; |
| contentDelimitation = false; |
| |
| OutputFilter[] outputFilters = getOutputBuffer().getFilters(); |
| |
| if (http09 == true) { |
| // HTTP/0.9 |
| getOutputBuffer().addActiveFilter |
| (outputFilters[Constants.IDENTITY_FILTER]); |
| return; |
| } |
| |
| int statusCode = response.getStatus(); |
| if ((statusCode == 204) || (statusCode == 205) |
| || (statusCode == 304)) { |
| // No entity body |
| getOutputBuffer().addActiveFilter |
| (outputFilters[Constants.VOID_FILTER]); |
| entityBody = false; |
| contentDelimitation = true; |
| } |
| |
| MessageBytes methodMB = request.method(); |
| if (methodMB.equals("HEAD")) { |
| // No entity body |
| getOutputBuffer().addActiveFilter |
| (outputFilters[Constants.VOID_FILTER]); |
| contentDelimitation = true; |
| } |
| |
| // Sendfile support |
| boolean sendingWithSendfile = false; |
| if (getEndpoint().getUseSendfile()) { |
| sendingWithSendfile = prepareSendfile(outputFilters); |
| } |
| |
| // Check for compression |
| boolean isCompressable = false; |
| boolean useCompression = false; |
| if (entityBody && (compressionLevel > 0) && !sendingWithSendfile) { |
| isCompressable = isCompressable(); |
| if (isCompressable) { |
| useCompression = useCompression(); |
| } |
| // Change content-length to -1 to force chunking |
| if (useCompression) { |
| response.setContentLength(-1); |
| } |
| } |
| |
| MimeHeaders headers = response.getMimeHeaders(); |
| if (!entityBody) { |
| response.setContentLength(-1); |
| } else { |
| String contentType = response.getContentType(); |
| if (contentType != null) { |
| headers.setValue("Content-Type").setString(contentType); |
| } |
| String contentLanguage = response.getContentLanguage(); |
| if (contentLanguage != null) { |
| headers.setValue("Content-Language") |
| .setString(contentLanguage); |
| } |
| } |
| |
| long contentLength = response.getContentLengthLong(); |
| if (contentLength != -1) { |
| headers.setValue("Content-Length").setLong(contentLength); |
| getOutputBuffer().addActiveFilter |
| (outputFilters[Constants.IDENTITY_FILTER]); |
| contentDelimitation = true; |
| } else { |
| if (entityBody && http11) { |
| getOutputBuffer().addActiveFilter |
| (outputFilters[Constants.CHUNKED_FILTER]); |
| contentDelimitation = true; |
| headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED); |
| } else { |
| getOutputBuffer().addActiveFilter |
| (outputFilters[Constants.IDENTITY_FILTER]); |
| } |
| } |
| |
| if (useCompression) { |
| getOutputBuffer().addActiveFilter(outputFilters[Constants.GZIP_FILTER]); |
| headers.setValue("Content-Encoding").setString("gzip"); |
| } |
| // If it might be compressed, set the Vary header |
| if (isCompressable) { |
| // Make Proxies happy via Vary (from mod_deflate) |
| MessageBytes vary = headers.getValue("Vary"); |
| if (vary == null) { |
| // Add a new Vary header |
| headers.setValue("Vary").setString("Accept-Encoding"); |
| } else if (vary.equals("*")) { |
| // No action required |
| } else { |
| // Merge into current header |
| headers.setValue("Vary").setString( |
| vary.getString() + ",Accept-Encoding"); |
| } |
| } |
| |
| // Add date header |
| headers.setValue("Date").setString(FastHttpDateFormat.getCurrentDate()); |
| |
| // FIXME: Add transfer encoding header |
| |
| if ((entityBody) && (!contentDelimitation)) { |
| // Mark as close the connection after the request, and add the |
| // connection: close header |
| keepAlive = false; |
| } |
| |
| // If we know that the request is bad this early, add the |
| // Connection: close header. |
| keepAlive = keepAlive && !statusDropsConnection(statusCode); |
| if (!keepAlive) { |
| headers.addValue(Constants.CONNECTION).setString(Constants.CLOSE); |
| } else if (!http11 && !error) { |
| headers.addValue(Constants.CONNECTION).setString(Constants.KEEPALIVE); |
| } |
| |
| // Build the response header |
| getOutputBuffer().sendStatus(); |
| |
| // Add server header |
| if (server != null) { |
| // Always overrides anything the app might set |
| headers.setValue("Server").setString(server); |
| } else if (headers.getValue("Server") == null) { |
| // If app didn't set the header, use the default |
| getOutputBuffer().write(Constants.SERVER_BYTES); |
| } |
| |
| int size = headers.size(); |
| for (int i = 0; i < size; i++) { |
| getOutputBuffer().sendHeader(headers.getName(i), headers.getValue(i)); |
| } |
| getOutputBuffer().endHeaders(); |
| |
| } |
| |
| abstract AbstractEndpoint getEndpoint(); |
| abstract boolean prepareSendfile(OutputFilter[] outputFilters); |
| |
| public void endRequest() { |
| |
| // Finish the handling of the request |
| try { |
| getInputBuffer().endRequest(); |
| } catch (IOException e) { |
| error = true; |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| getLog().error(sm.getString("http11processor.request.finish"), t); |
| // 500 - Internal Server Error |
| response.setStatus(500); |
| adapter.log(request, response, 0); |
| error = true; |
| } |
| try { |
| getOutputBuffer().endRequest(); |
| } catch (IOException e) { |
| error = true; |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| getLog().error(sm.getString("http11processor.response.finish"), t); |
| error = true; |
| } |
| |
| } |
| |
| public final void recycle() { |
| getInputBuffer().recycle(); |
| getOutputBuffer().recycle(); |
| asyncStateMachine.recycle(); |
| recycleInternal(); |
| } |
| |
| protected abstract void recycleInternal(); |
| |
| @Override |
| public abstract Executor getExecutor(); |
| |
| protected boolean isAsync() { |
| return asyncStateMachine.isAsync(); |
| } |
| |
| protected SocketState asyncPostProcess() { |
| return asyncStateMachine.asyncPostProcess(); |
| } |
| } |