| /* |
| * 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.io.InterruptedIOException; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| |
| import jakarta.servlet.ServletConnection; |
| import jakarta.servlet.http.HttpServletResponse; |
| |
| import org.apache.coyote.AbstractProcessor; |
| import org.apache.coyote.ActionCode; |
| import org.apache.coyote.Adapter; |
| import org.apache.coyote.ContinueResponseTiming; |
| import org.apache.coyote.ErrorState; |
| import org.apache.coyote.Request; |
| import org.apache.coyote.RequestInfo; |
| import org.apache.coyote.UpgradeProtocol; |
| import org.apache.coyote.UpgradeToken; |
| 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.coyote.http11.upgrade.InternalHttpUpgradeHandler; |
| import org.apache.coyote.http11.upgrade.UpgradeApplicationBufferHandler; |
| import org.apache.juli.logging.Log; |
| import org.apache.juli.logging.LogFactory; |
| import org.apache.tomcat.util.ExceptionUtils; |
| 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.Method; |
| import org.apache.tomcat.util.http.MimeHeaders; |
| import org.apache.tomcat.util.http.parser.HttpParser; |
| import org.apache.tomcat.util.http.parser.TokenList; |
| import org.apache.tomcat.util.log.UserDataHelper; |
| import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState; |
| import org.apache.tomcat.util.net.ApplicationBufferHandler; |
| import org.apache.tomcat.util.net.SSLSupport; |
| import org.apache.tomcat.util.net.SendfileDataBase; |
| import org.apache.tomcat.util.net.SendfileKeepAliveState; |
| import org.apache.tomcat.util.net.SendfileState; |
| import org.apache.tomcat.util.net.SocketWrapperBase; |
| import org.apache.tomcat.util.res.StringManager; |
| |
| /** |
| * Processor for HTTP/1.1 requests and responses. |
| */ |
| public class Http11Processor extends AbstractProcessor { |
| |
| private static final Log log = LogFactory.getLog(Http11Processor.class); |
| |
| /** |
| * The string manager for this package. |
| */ |
| private static final StringManager sm = StringManager.getManager(Http11Processor.class); |
| |
| |
| private final AbstractHttp11Protocol<?> protocol; |
| |
| |
| /** |
| * Input. |
| */ |
| private final Http11InputBuffer inputBuffer; |
| |
| |
| /** |
| * Output. |
| */ |
| private final Http11OutputBuffer outputBuffer; |
| |
| |
| /** |
| * Tracks how many internal filters are in the filter library so they are skipped when looking for pluggable |
| * filters. |
| */ |
| private final int pluggableFilterIndex; |
| |
| |
| /** |
| * Keep-alive. |
| */ |
| private volatile boolean keepAlive = true; |
| |
| |
| /** |
| * Flag used to indicate that the socket should be kept open (e.g. for keep alive or send file). |
| */ |
| private volatile boolean openSocket = false; |
| |
| |
| /** |
| * Flag that indicates if the request headers have been completely read. |
| */ |
| private volatile boolean readComplete = true; |
| |
| /** |
| * HTTP/1.1 flag. |
| */ |
| private boolean http11 = true; |
| |
| |
| /** |
| * Content delimiter for the request (if false, the connection will be closed at the end of the request). |
| */ |
| private boolean contentDelimitation = true; |
| |
| |
| /** |
| * Instance of the new protocol to use after the HTTP connection has been upgraded. |
| */ |
| private UpgradeToken upgradeToken = null; |
| |
| |
| /** |
| * Sendfile data. |
| */ |
| private SendfileDataBase sendfileData = null; |
| |
| |
| /** |
| * Http parser. |
| */ |
| private final HttpParser httpParser; |
| |
| |
| /** |
| * Creates a new HTTP/1.1 processor. |
| * |
| * @param protocol the protocol handler |
| * @param adapter the adapter to pass requests to |
| */ |
| public Http11Processor(AbstractHttp11Protocol<?> protocol, Adapter adapter) { |
| super(adapter); |
| this.protocol = protocol; |
| |
| HttpParser httpParser = protocol.getHttpParser(); |
| if (httpParser == null) { |
| log.info(sm.getString("http11processor.noParser")); |
| httpParser = new HttpParser(protocol.getRelaxedPathChars(), protocol.getRelaxedQueryChars()); |
| } |
| this.httpParser = httpParser; |
| |
| inputBuffer = new Http11InputBuffer(request, protocol.getMaxHttpRequestHeaderSize(), httpParser); |
| request.setInputBuffer(inputBuffer); |
| |
| outputBuffer = new Http11OutputBuffer(response, protocol.getMaxHttpResponseHeaderSize()); |
| response.setOutputBuffer(outputBuffer); |
| |
| // Create and add the identity filters. |
| inputBuffer.addFilter(new IdentityInputFilter(protocol.getMaxSwallowSize())); |
| outputBuffer.addFilter(new IdentityOutputFilter()); |
| |
| // Create and add the chunked filters. |
| inputBuffer.addFilter(new ChunkedInputFilter(request, protocol.getMaxTrailerSize(), |
| protocol.getAllowedTrailerHeadersInternal(), protocol.getMaxExtensionSize(), |
| protocol.getMaxSwallowSize())); |
| outputBuffer.addFilter(new ChunkedOutputFilter()); |
| |
| // Create and add the void filters. |
| inputBuffer.addFilter(new VoidInputFilter()); |
| outputBuffer.addFilter(new VoidOutputFilter()); |
| |
| // Create and add buffered input filter |
| inputBuffer.addFilter(new BufferedInputFilter(protocol.getMaxSwallowSize())); |
| |
| // Create and add the gzip filters. |
| // inputBuffer.addFilter(new GzipInputFilter()); |
| outputBuffer.addFilter(new GzipOutputFilter()); |
| |
| pluggableFilterIndex = inputBuffer.getFilters().length; |
| } |
| |
| |
| /** |
| * Determine if we must drop the connection because of the HTTP status code. Use the same list of codes as |
| * Apache/httpd. |
| */ |
| private static 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 */; |
| } |
| |
| |
| /** |
| * Add an input filter to the current request. If the encoding is not supported, a 501 response will be returned to |
| * the client. |
| */ |
| private void addInputFilter(InputFilter[] inputFilters, String encodingName) { |
| if (contentDelimitation) { |
| // Chunked has already been specified and it must be the final |
| // encoding. |
| // 400 - Bad request |
| response.setStatus(400); |
| setErrorState(ErrorState.CLOSE_CLEAN, null); |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("http11processor.request.alreadyChunked", encodingName)); |
| } |
| return; |
| } |
| |
| // Parsing trims and converts to lower case. |
| if (encodingName.equals("chunked")) { |
| inputBuffer.addActiveFilter(inputFilters[Constants.CHUNKED_FILTER]); |
| contentDelimitation = true; |
| } else { |
| for (int i = pluggableFilterIndex; i < inputFilters.length; i++) { |
| if (inputFilters[i].getEncodingName().toString().equals(encodingName)) { |
| inputBuffer.addActiveFilter(inputFilters[i]); |
| return; |
| } |
| } |
| // Unsupported transfer encoding |
| // 501 - Unimplemented |
| response.setStatus(501); |
| setErrorState(ErrorState.CLOSE_CLEAN, null); |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("http11processor.request.unsupportedEncoding", encodingName)); |
| } |
| } |
| } |
| |
| |
| @Override |
| public SocketState service(SocketWrapperBase<?> socketWrapper) throws IOException { |
| RequestInfo rp = request.getRequestProcessor(); |
| rp.setStage(org.apache.coyote.Constants.STAGE_PARSE); |
| |
| // Setting up the I/O |
| setSocketWrapper(socketWrapper); |
| |
| // Flags |
| keepAlive = true; |
| openSocket = false; |
| readComplete = true; |
| boolean keptAlive = false; |
| SendfileState sendfileState = SendfileState.DONE; |
| |
| while (!getErrorState().isError() && keepAlive && !isAsync() && upgradeToken == null && |
| sendfileState == SendfileState.DONE && !protocol.isPaused()) { |
| |
| // Parsing the request header |
| try { |
| if (!inputBuffer.parseRequestLine(keptAlive, protocol.getConnectionTimeout(), |
| protocol.getKeepAliveTimeout())) { |
| if (inputBuffer.getParsingRequestLinePhase() == -1) { |
| return SocketState.UPGRADING; |
| } else if (handleIncompleteRequestLineRead()) { |
| break; |
| } |
| } |
| |
| // Process the Protocol component of the request line |
| // Need to know if this is an HTTP 0.9 request before trying to |
| // parse headers. |
| prepareRequestProtocol(); |
| |
| if (protocol.isPaused()) { |
| // 503 - Service unavailable |
| response.setStatus(503); |
| setErrorState(ErrorState.CLOSE_CLEAN, null); |
| } else { |
| keptAlive = true; |
| // Set this every time in case limit has been changed via JMX |
| request.getMimeHeaders().setLimit(protocol.getMaxHeaderCount()); |
| if (!inputBuffer.parseHeaders()) { |
| // We've read part of the request, don't recycle it |
| // instead associate it with the socket |
| openSocket = true; |
| readComplete = false; |
| break; |
| } |
| if (!protocol.getDisableUploadTimeout()) { |
| socketWrapper.setReadTimeout(protocol.getConnectionUploadTimeout()); |
| } |
| } |
| } catch (IOException ioe) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("http11processor.header.parse"), ioe); |
| } |
| setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); |
| break; |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| UserDataHelper.Mode logMode = userDataHelper.getNextMode(); |
| if (logMode != null) { |
| String message = sm.getString("http11processor.header.parse"); |
| switch (logMode) { |
| case INFO_THEN_DEBUG: |
| message += sm.getString("http11processor.fallToDebug"); |
| //$FALL-THROUGH$ |
| case INFO: |
| log.info(message, t); |
| break; |
| case DEBUG: |
| log.debug(message, t); |
| } |
| } |
| // 400 - Bad Request |
| response.setStatus(400); |
| setErrorState(ErrorState.CLOSE_CLEAN, t); |
| } |
| |
| // Has an upgrade been requested? |
| if (isConnectionToken(request.getMimeHeaders(), "upgrade")) { |
| // Check the protocol |
| String requestedProtocol = request.getHeader("Upgrade"); |
| |
| UpgradeProtocol upgradeProtocol = protocol.getUpgradeProtocol(requestedProtocol); |
| if (upgradeProtocol != null) { |
| if (upgradeProtocol.accept(request)) { |
| // Create clone of request for upgraded protocol |
| Request upgradeRequest = null; |
| try { |
| upgradeRequest = cloneRequest(request); |
| } catch (ByteChunk.BufferOverflowException ioe) { |
| response.setStatus(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE); |
| setErrorState(ErrorState.CLOSE_CLEAN, null); |
| } catch (IOException ioe) { |
| response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); |
| setErrorState(ErrorState.CLOSE_CLEAN, ioe); |
| } |
| |
| if (upgradeRequest != null) { |
| // Complete the HTTP/1.1 upgrade process |
| response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); |
| response.setHeader("Connection", "Upgrade"); |
| response.setHeader("Upgrade", requestedProtocol); |
| action(ActionCode.CLOSE, null); |
| getAdapter().log(request, response, 0); |
| |
| // Continue processing using new protocol |
| InternalHttpUpgradeHandler upgradeHandler = upgradeProtocol |
| .getInternalUpgradeHandler(socketWrapper, getAdapter(), upgradeRequest); |
| UpgradeToken upgradeToken = new UpgradeToken(upgradeHandler, null, null, requestedProtocol); |
| action(ActionCode.UPGRADE, upgradeToken); |
| return SocketState.UPGRADING; |
| } |
| } |
| } |
| } |
| |
| if (getErrorState().isIoAllowed()) { |
| // Setting up filters, and parse some request headers |
| rp.setStage(org.apache.coyote.Constants.STAGE_PREPARE); |
| try { |
| prepareRequest(); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("http11processor.request.prepare"), t); |
| } |
| // 500 - Internal Server Error |
| response.setStatus(500); |
| setErrorState(ErrorState.CLOSE_CLEAN, t); |
| } |
| } |
| |
| int maxKeepAliveRequests = protocol.getMaxKeepAliveRequests(); |
| if (maxKeepAliveRequests == 1) { |
| keepAlive = false; |
| } else if (maxKeepAliveRequests > 0 && socketWrapper.decrementKeepAlive() <= 0) { |
| keepAlive = false; |
| } |
| |
| // Process the request in the adapter |
| if (getErrorState().isIoAllowed()) { |
| try { |
| rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE); |
| getAdapter().service(request, response); |
| // Handle when the response was committed before a serious |
| // error occurred. Throwing a ServletException should both |
| // set the status to 500 and set the errorException. |
| // If we fail here, then the response is likely already |
| // committed, so we can't try and set headers. |
| if (keepAlive && !getErrorState().isError() && !isAsync() && |
| statusDropsConnection(response.getStatus())) { |
| setErrorState(ErrorState.CLOSE_CLEAN, null); |
| } |
| } catch (InterruptedIOException e) { |
| setErrorState(ErrorState.CLOSE_CONNECTION_NOW, e); |
| } catch (HeadersTooLargeException e) { |
| log.error(sm.getString("http11processor.request.process"), e); |
| // The response should not have been committed but check it |
| // anyway to be safe |
| if (response.isCommitted()) { |
| setErrorState(ErrorState.CLOSE_NOW, e); |
| } else { |
| response.reset(); |
| response.setStatus(500); |
| setErrorState(ErrorState.CLOSE_CLEAN, e); |
| response.setHeader("Connection", "close"); // TODO: Remove |
| } |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| log.error(sm.getString("http11processor.request.process"), t); |
| // 500 - Internal Server Error |
| response.setStatus(500); |
| setErrorState(ErrorState.CLOSE_CLEAN, t); |
| getAdapter().log(request, response, 0); |
| } |
| } |
| |
| // Finish the handling of the request |
| rp.setStage(org.apache.coyote.Constants.STAGE_ENDINPUT); |
| if (!isAsync()) { |
| // If this is an async request then the request ends when it has |
| // been completed. The AsyncContext is responsible for calling |
| // endRequest() in that case. |
| endRequest(); |
| } |
| rp.setStage(org.apache.coyote.Constants.STAGE_ENDOUTPUT); |
| |
| // If there was an error, make sure the request is counted as |
| // and error, and update the statistics counter |
| if (getErrorState().isError()) { |
| response.setStatus(500); |
| } |
| |
| if (!isAsync() || getErrorState().isError()) { |
| request.updateCounters(); |
| if (getErrorState().isIoAllowed()) { |
| inputBuffer.nextRequest(); |
| outputBuffer.nextRequest(); |
| } |
| } |
| |
| if (!protocol.getDisableUploadTimeout()) { |
| int connectionTimeout = protocol.getConnectionTimeout(); |
| socketWrapper.setReadTimeout(Math.max(connectionTimeout, 0)); |
| } |
| |
| rp.setStage(org.apache.coyote.Constants.STAGE_KEEPALIVE); |
| |
| sendfileState = processSendfile(socketWrapper); |
| } |
| |
| rp.setStage(org.apache.coyote.Constants.STAGE_ENDED); |
| |
| if (getErrorState().isError() || (protocol.isPaused() && !isAsync())) { |
| return SocketState.CLOSED; |
| } else if (isAsync()) { |
| return SocketState.LONG; |
| } else if (isUpgrade()) { |
| return SocketState.UPGRADING; |
| } else { |
| if (sendfileState == SendfileState.PENDING) { |
| return SocketState.SENDFILE; |
| } else { |
| if (openSocket) { |
| if (readComplete) { |
| return SocketState.OPEN; |
| } else { |
| return SocketState.LONG; |
| } |
| } else { |
| return SocketState.CLOSED; |
| } |
| } |
| } |
| } |
| |
| |
| @Override |
| protected final void setSocketWrapper(SocketWrapperBase<?> socketWrapper) { |
| super.setSocketWrapper(socketWrapper); |
| inputBuffer.init(socketWrapper); |
| outputBuffer.init(socketWrapper); |
| } |
| |
| |
| private Request cloneRequest(Request source) throws IOException { |
| Request dest = new Request(); |
| |
| // Transfer the minimal information required for the copy of the Request |
| // that is passed to the HTTP upgrade process |
| dest.decodedURI().duplicate(source.decodedURI()); |
| dest.setMethod(source.getMethod()); |
| dest.getMimeHeaders().duplicate(source.getMimeHeaders()); |
| dest.requestURI().duplicate(source.requestURI()); |
| dest.queryString().duplicate(source.queryString()); |
| |
| // Preparation for reading the request body |
| MimeHeaders headers = source.getMimeHeaders(); |
| prepareExpectation(headers); |
| prepareInputFilters(headers); |
| ack(ContinueResponseTiming.ALWAYS); |
| |
| // Need to read and buffer the request body, if any. RFC 7230 requires |
| // that the request is fully read before the upgrade takes place. |
| ByteChunk body = new ByteChunk(); |
| int maxSavePostSize = protocol.getMaxSavePostSize(); |
| if (maxSavePostSize != 0) { |
| body.setLimit(maxSavePostSize); |
| ApplicationBufferHandler buffer = new UpgradeApplicationBufferHandler(); |
| |
| while (source.getInputBuffer().doRead(buffer) >= 0) { |
| body.append(buffer.getByteBuffer()); |
| } |
| } |
| |
| // Make the buffered request body available to the upgraded protocol. |
| SavedRequestInputFilter srif = new SavedRequestInputFilter(body); |
| dest.setInputBuffer(srif); |
| |
| return dest; |
| } |
| |
| |
| private boolean handleIncompleteRequestLineRead() { |
| // Haven't finished reading the request so keep the socket |
| // open |
| openSocket = true; |
| // Check to see if we have read any of the request line yet |
| if (inputBuffer.getParsingRequestLinePhase() > 1) { |
| // Started to read request line. |
| if (protocol.isPaused()) { |
| // Partially processed the request so need to respond |
| response.setStatus(503); |
| setErrorState(ErrorState.CLOSE_CLEAN, null); |
| return false; |
| } else { |
| // Need to keep processor associated with socket |
| readComplete = false; |
| } |
| } |
| return true; |
| } |
| |
| |
| private void checkExpectationAndResponseStatus() { |
| if (request.hasExpectation() && !isRequestBodyFullyRead() && |
| (response.getStatus() < 200 || response.getStatus() > 299)) { |
| // Client sent Expect: 100-continue but received a |
| // non-2xx final response. Disable keep-alive (if enabled) |
| // to ensure that the connection is closed. Some clients may |
| // still send the body, some may send the next request. |
| // No way to differentiate, so close the connection to |
| // force the client to send the next request. |
| inputBuffer.setSwallowInput(false); |
| keepAlive = false; |
| } |
| } |
| |
| |
| private void checkMaxSwallowSize() { |
| // Parse content-length header |
| long contentLength = -1; |
| try { |
| contentLength = request.getContentLengthLong(); |
| } catch (Exception ignore) { |
| // Ignore, an error here is already processed in prepareRequest |
| // but is done again since the content length is still -1 |
| } |
| if (contentLength > 0 && protocol.getMaxSwallowSize() > -1 && |
| (contentLength - request.getBytesRead() > protocol.getMaxSwallowSize())) { |
| // There is more data to swallow than Tomcat will accept so the |
| // connection is going to be closed. Disable keep-alive which will |
| // trigger adding the "Connection: close" header if not already |
| // present. |
| keepAlive = false; |
| } |
| } |
| |
| |
| private void prepareRequestProtocol() { |
| |
| MessageBytes protocolMB = request.protocol(); |
| if (protocolMB.equals(Constants.HTTP_11)) { |
| http11 = true; |
| protocolMB.setString(Constants.HTTP_11); |
| } else if (protocolMB.equals(Constants.HTTP_10)) { |
| http11 = false; |
| keepAlive = false; |
| protocolMB.setString(Constants.HTTP_10); |
| } else { |
| // Unsupported protocol |
| http11 = false; |
| // Send 505; Unsupported HTTP version |
| response.setStatus(505); |
| setErrorState(ErrorState.CLOSE_CLEAN, null); |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("http11processor.request.unsupportedVersion", protocolMB)); |
| } |
| } |
| } |
| |
| |
| /** |
| * After reading the request headers, we have to set up the request filters. |
| */ |
| private void prepareRequest() throws IOException { |
| |
| if (protocol.isSSLEnabled()) { |
| request.scheme().setString("https"); |
| } |
| |
| MimeHeaders headers = request.getMimeHeaders(); |
| |
| // Check connection header |
| MessageBytes connectionValueMB = headers.getValue(Constants.CONNECTION); |
| if (connectionValueMB != null && !connectionValueMB.isNull()) { |
| Set<String> tokens = new HashSet<>(); |
| TokenList.parseTokenList(headers.values(Constants.CONNECTION), tokens); |
| if (tokens.contains(Constants.CLOSE)) { |
| keepAlive = false; |
| } else if (tokens.contains(Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN)) { |
| keepAlive = true; |
| } |
| } |
| |
| if (http11) { |
| prepareExpectation(headers); |
| } |
| |
| // Check user-agent header |
| Pattern restrictedUserAgents = protocol.getRestrictedUserAgentsPattern(); |
| if (restrictedUserAgents != null && (http11 || keepAlive)) { |
| MessageBytes userAgentValueMB = headers.getValue("user-agent"); |
| // Check in the restricted list, and adjust the http11 |
| // and keepAlive flags accordingly |
| if (userAgentValueMB != null && !userAgentValueMB.isNull()) { |
| String userAgentValue = userAgentValueMB.toString(); |
| if (restrictedUserAgents.matcher(userAgentValue).matches()) { |
| http11 = false; |
| keepAlive = false; |
| } |
| } |
| } |
| |
| |
| // Check host header |
| MessageBytes hostValueMB = null; |
| try { |
| hostValueMB = headers.getUniqueValue("host"); |
| } catch (IllegalArgumentException iae) { |
| // Multiple Host headers are not permitted |
| badRequest("http11processor.request.multipleHosts"); |
| } |
| if (http11 && hostValueMB == null) { |
| badRequest("http11processor.request.noHostHeader"); |
| } |
| |
| // Check for an absolute-URI less the query string which has already |
| // been removed during the parsing of the request line |
| ByteChunk uriBC = request.requestURI().getByteChunk(); |
| byte[] uriB = uriBC.getBytes(); |
| if (uriBC.startsWithIgnoreCase("http", 0)) { |
| int pos = 4; |
| // Check for https |
| if (uriBC.startsWithIgnoreCase("s", pos)) { |
| pos++; |
| } |
| // Next 3 characters must be "://" |
| if (uriBC.startsWith("://", pos)) { |
| pos += 3; |
| int uriBCStart = uriBC.getStart(); |
| |
| // '/' does not appear in the authority so use the first |
| // instance to split the authority and the path segments |
| int slashPos = uriBC.indexOf('/', pos); |
| // '@' in the authority delimits the userinfo |
| int atPos = uriBC.indexOf('@', pos); |
| if (slashPos > -1 && atPos > slashPos) { |
| // First '@' is in the path segments so no userinfo |
| atPos = -1; |
| } |
| |
| if (slashPos == -1) { |
| slashPos = uriBC.getLength(); |
| // Set URI as "/". Use 6 as it will always be a '/'. |
| // 01234567 |
| // http:// |
| // https:// |
| request.requestURI().setBytes(uriB, uriBCStart + 6, 1); |
| } else { |
| request.requestURI().setBytes(uriB, uriBCStart + slashPos, uriBC.getLength() - slashPos); |
| } |
| |
| // Skip any user info |
| if (atPos != -1) { |
| // Validate the userinfo |
| for (; pos < atPos; pos++) { |
| byte c = uriB[uriBCStart + pos]; |
| if (!HttpParser.isUserInfo(c)) { |
| // Strictly there needs to be a check for valid %nn |
| // encoding here but skip it since it will never be |
| // decoded because the userinfo is ignored |
| badRequest("http11processor.request.invalidUserInfo"); |
| break; |
| } |
| } |
| // Skip the '@' |
| pos = atPos + 1; |
| } |
| |
| if (http11) { |
| // Missing host header is illegal but handled above |
| if (hostValueMB != null) { |
| // Any host in the request line must be consistent with |
| // the Host header |
| if (!hostValueMB.getByteChunk().equalsIgnoreCase(uriB, uriBCStart + pos, slashPos - pos)) { |
| // The requirements of RFC 7230 are being |
| // applied. If the host header and the request |
| // line do not agree, trigger a 400 response. |
| badRequest("http11processor.request.inconsistentHosts"); |
| } |
| } |
| } else { |
| // Not HTTP/1.1 - no Host header so generate one since |
| // Tomcat internals assume it is set |
| try { |
| hostValueMB = headers.setValue("host"); |
| hostValueMB.setBytes(uriB, uriBCStart + pos, slashPos - pos); |
| } catch (IllegalStateException ignore) { |
| // Edge case |
| // If the request has too many headers it won't be |
| // possible to create the host header. Ignore this as |
| // processing won't reach the point where the Tomcat |
| // internals expect there to be a host header. |
| } |
| } |
| } else { |
| badRequest("http11processor.request.invalidScheme"); |
| } |
| } |
| |
| // Validate the characters in the URI. %nn decoding will be checked at |
| // the point of decoding. |
| for (int i = uriBC.getStart(); i < uriBC.getEnd(); i++) { |
| if (!httpParser.isAbsolutePathRelaxed(uriB[i])) { |
| badRequest("http11processor.request.invalidUri"); |
| break; |
| } |
| } |
| |
| // Input filter setup |
| prepareInputFilters(headers); |
| |
| // Validate host name and extract port if present |
| parseHost(hostValueMB); |
| |
| // Match host name with SNI if required |
| if (!protocol.checkSni(socketWrapper.getSniHostName(), request.serverName().toString())) { |
| badRequest("http11processor.request.sni"); |
| } |
| |
| if (!getErrorState().isIoAllowed()) { |
| getAdapter().log(request, response, 0); |
| } |
| } |
| |
| |
| private void prepareExpectation(MimeHeaders headers) { |
| MessageBytes expectMB = headers.getValue("expect"); |
| if (expectMB != null && !expectMB.isNull()) { |
| if (expectMB.toString().trim().equalsIgnoreCase("100-continue")) { |
| request.setExpectation(true); |
| } else { |
| response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); |
| setErrorState(ErrorState.CLOSE_CLEAN, null); |
| } |
| } |
| } |
| |
| private void prepareInputFilters(MimeHeaders headers) throws IOException { |
| |
| contentDelimitation = false; |
| |
| InputFilter[] inputFilters = inputBuffer.getFilters(); |
| |
| // Parse transfer-encoding header |
| // HTTP specs say an HTTP 1.1 server should accept any recognised |
| // HTTP 1.x header from a 1.x client unless the specs says otherwise. |
| MessageBytes transferEncodingValueMB = headers.getValue("transfer-encoding"); |
| if (transferEncodingValueMB != null) { |
| List<String> encodingNames = new ArrayList<>(); |
| if (TokenList.parseTokenList(headers.values("transfer-encoding"), encodingNames)) { |
| for (String encodingName : encodingNames) { |
| addInputFilter(inputFilters, encodingName); |
| } |
| } else { |
| // Invalid transfer encoding |
| badRequest("http11processor.request.invalidTransferEncoding"); |
| } |
| } |
| |
| // Parse content-length header |
| long contentLength = -1; |
| try { |
| contentLength = request.getContentLengthLong(); |
| } catch (NumberFormatException e) { |
| badRequest("http11processor.request.nonNumericContentLength"); |
| } catch (IllegalArgumentException e) { |
| badRequest("http11processor.request.multipleContentLength"); |
| } |
| if (contentLength >= 0) { |
| if (contentDelimitation) { |
| // contentDelimitation being true at this point indicates that |
| // chunked encoding is being used but chunked encoding should |
| // not be used with a content length. RFC 2616, section 4.4, |
| // bullet 3 states Content-Length must be ignored in this case - |
| // so remove it. |
| headers.removeHeader("content-length"); |
| request.setContentLength(-1); |
| keepAlive = false; |
| } else { |
| inputBuffer.addActiveFilter(inputFilters[Constants.IDENTITY_FILTER]); |
| contentDelimitation = true; |
| } |
| } |
| |
| if (!contentDelimitation) { |
| // If there's no content length |
| // (broken HTTP/1.0 or HTTP/1.1), assume |
| // the client is not broken and didn't send a body |
| inputBuffer.addActiveFilter(inputFilters[Constants.VOID_FILTER]); |
| contentDelimitation = true; |
| } |
| } |
| |
| |
| private void badRequest(String errorKey) { |
| response.setStatus(400); |
| setErrorState(ErrorState.CLOSE_CLEAN, null); |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString(errorKey)); |
| } |
| } |
| |
| |
| @Override |
| protected final void prepareResponse() throws IOException { |
| |
| boolean entityBody = true; |
| contentDelimitation = false; |
| |
| OutputFilter[] outputFilters = outputBuffer.getFilters(); |
| |
| int statusCode = response.getStatus(); |
| if (statusCode < 200 || statusCode == 204 || statusCode == 205 || statusCode == 304) { |
| // No entity body |
| outputBuffer.addActiveFilter(outputFilters[Constants.VOID_FILTER]); |
| entityBody = false; |
| contentDelimitation = true; |
| if (statusCode == 205) { |
| // RFC 7231 requires the server to explicitly signal an empty |
| // response in this case |
| response.setContentLength(0); |
| } else { |
| response.setContentLength(-1); |
| } |
| } |
| |
| boolean head = Method.HEAD.equals(request.getMethod()); |
| if (head) { |
| // Any entity body, if present, should not be sent |
| outputBuffer.addActiveFilter(outputFilters[Constants.VOID_FILTER]); |
| contentDelimitation = true; |
| } |
| |
| // Sendfile support |
| if (protocol.getUseSendfile()) { |
| prepareSendfile(outputFilters); |
| } |
| |
| // Check for compression |
| boolean useCompression = false; |
| if (entityBody && sendfileData == null) { |
| useCompression = protocol.useCompression(request, response); |
| } |
| |
| MimeHeaders headers = response.getMimeHeaders(); |
| // A SC_NO_CONTENT response may include entity headers |
| if (entityBody || statusCode == HttpServletResponse.SC_NO_CONTENT) { |
| 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(); |
| boolean connectionClosePresent = isConnectionToken(headers, Constants.CLOSE); |
| if (http11 && response.getTrailerFields() != null) { |
| // If trailer fields are set, always use chunking |
| outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]); |
| contentDelimitation = true; |
| headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED); |
| } else if (contentLength != -1) { |
| headers.setValue("Content-Length").setLong(contentLength); |
| outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]); |
| contentDelimitation = true; |
| } else if (head) { |
| /* |
| * The OutputBuffer can't differentiate between a zero length response because GET would have produced a |
| * zero length body and a zero length response because HEAD didn't write any content. Therefore skip setting |
| * the "transfer-encoding: chunked" header as that may not be consistent with what would be seen with the |
| * equivalent GET. |
| */ |
| } else { |
| // If the response code supports an entity body and we're on |
| // HTTP 1.1 then we chunk unless we have a Connection: close header |
| if (http11 && entityBody && !connectionClosePresent) { |
| outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]); |
| contentDelimitation = true; |
| headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED); |
| } else { |
| outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]); |
| } |
| } |
| |
| if (useCompression) { |
| outputBuffer.addActiveFilter(outputFilters[Constants.GZIP_FILTER]); |
| } |
| |
| // Add date header unless application has already set one (e.g. in a |
| // Caching Filter) |
| if (headers.getValue("Date") == null) { |
| headers.addValue("Date").setString(FastHttpDateFormat.getCurrentDate()); |
| } |
| |
| // Although using transfer-encoding for gzip would be doable and was |
| // the original intent (which means the compression would be from an |
| // endpoint to the next, so only for the current transmission), it |
| // has been found that using content-encoding (which is end to end |
| // compression) is more efficient and more reliable. |
| |
| if ((entityBody) && (!contentDelimitation) || connectionClosePresent) { |
| // Disable keep-alive if: |
| // - there is a response body but way for the client to determine |
| // the content length information; or |
| // - there is a "connection: close" header present |
| // This will cause the "connection: close" header to be added if it |
| // is not already present. |
| keepAlive = false; |
| } |
| |
| // This may disable keep-alive so check before working out the Connection header |
| checkExpectationAndResponseStatus(); |
| |
| // This may disable keep-alive if there is more body to swallow |
| // than the configuration allows |
| checkMaxSwallowSize(); |
| |
| // If we know that the request is bad this early, add the |
| // Connection: close header. |
| if (keepAlive && statusDropsConnection(statusCode)) { |
| keepAlive = false; |
| } |
| if (!keepAlive) { |
| // Avoid adding the close header twice |
| if (!connectionClosePresent) { |
| headers.addValue(Constants.CONNECTION).setString(Constants.CLOSE); |
| } |
| } else if (!getErrorState().isError()) { |
| if (!http11) { |
| headers.addValue(Constants.CONNECTION).setString(Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN); |
| } |
| |
| if (protocol.getUseKeepAliveResponseHeader()) { |
| boolean connectionKeepAlivePresent = |
| isConnectionToken(request.getMimeHeaders(), Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN); |
| |
| if (connectionKeepAlivePresent) { |
| int keepAliveTimeout = protocol.getKeepAliveTimeout(); |
| |
| if (keepAliveTimeout > 0) { |
| String value = "timeout=" + keepAliveTimeout / 1000L; |
| headers.setValue(Constants.KEEP_ALIVE_HEADER_NAME).setString(value); |
| |
| if (http11) { |
| // Append if there is already a Connection header, |
| // else create the header |
| MessageBytes connectionHeaderValue = headers.getValue(Constants.CONNECTION); |
| if (connectionHeaderValue == null) { |
| headers.addValue(Constants.CONNECTION) |
| .setString(Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN); |
| } else { |
| connectionHeaderValue.setString(connectionHeaderValue.getString() + ", " + |
| Constants.KEEP_ALIVE_HEADER_VALUE_TOKEN); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Add server header |
| String server = protocol.getServer(); |
| if (server == null) { |
| if (protocol.getServerRemoveAppProvidedValues()) { |
| headers.removeHeader("server"); |
| } |
| } else { |
| // server always overrides anything the app might set |
| headers.setValue("Server").setString(server); |
| } |
| |
| writeHeaders(response.getStatus(), headers); |
| |
| outputBuffer.commit(); |
| } |
| |
| |
| private void writeHeaders(int status, MimeHeaders headers) { |
| try { |
| outputBuffer.sendStatus(status); |
| |
| int size = headers.size(); |
| for (int i = 0; i < size; i++) { |
| try { |
| outputBuffer.sendHeader(headers.getName(i), headers.getValue(i)); |
| } catch (IllegalArgumentException iae) { |
| // Log the problematic header |
| log.warn(sm.getString("http11processor.response.invalidHeader", headers.getName(i), |
| headers.getValue(i)), iae); |
| // Remove the problematic header |
| headers.removeHeader(i); |
| size--; |
| // Header buffer is corrupted. Reset it and start again. |
| outputBuffer.resetHeaderBuffer(); |
| // -1 as it will be incremented at the start of the loop and header indexes start at 0. |
| i = -1; |
| outputBuffer.sendStatus(status); |
| } |
| } |
| outputBuffer.endHeaders(); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| // If something goes wrong, reset the header buffer so the error |
| // response can be written instead. |
| outputBuffer.resetHeaderBuffer(); |
| throw t; |
| } |
| } |
| |
| |
| private static boolean isConnectionToken(MimeHeaders headers, String token) throws IOException { |
| MessageBytes connection = headers.getValue(Constants.CONNECTION); |
| if (connection == null) { |
| return false; |
| } |
| |
| Set<String> tokens = new HashSet<>(); |
| TokenList.parseTokenList(headers.values(Constants.CONNECTION), tokens); |
| return tokens.contains(token); |
| } |
| |
| |
| private void prepareSendfile(OutputFilter[] outputFilters) { |
| String fileName = (String) request.getAttribute(org.apache.coyote.Constants.SENDFILE_FILENAME_ATTR); |
| if (fileName == null) { |
| sendfileData = null; |
| } else { |
| // No entity body sent here |
| outputBuffer.addActiveFilter(outputFilters[Constants.VOID_FILTER]); |
| contentDelimitation = true; |
| long pos = ((Long) request.getAttribute(org.apache.coyote.Constants.SENDFILE_FILE_START_ATTR)).longValue(); |
| long end = ((Long) request.getAttribute(org.apache.coyote.Constants.SENDFILE_FILE_END_ATTR)).longValue(); |
| sendfileData = socketWrapper.createSendfileData(fileName, pos, end - pos); |
| } |
| } |
| |
| |
| /* |
| * Note: populateHost() is not over-ridden. request.serverName() will be set to return the default host name by the |
| * Mapper. |
| */ |
| |
| |
| /** |
| * {@inheritDoc} |
| * <p> |
| * This implementation provides the server port from the local port. |
| */ |
| @Override |
| protected void populatePort() { |
| // Ensure the local port field is populated before using it. |
| request.action(ActionCode.REQ_LOCALPORT_ATTRIBUTE, request); |
| request.setServerPort(request.getLocalPort()); |
| } |
| |
| |
| @Override |
| protected boolean flushBufferedWrite() throws IOException { |
| if (outputBuffer.hasDataToWrite()) { |
| if (outputBuffer.flushBuffer(false)) { |
| // The buffer wasn't fully flushed so re-register the |
| // socket for write. Note this does not go via the |
| // Response since the write registration state at |
| // that level should remain unchanged. Once the buffer |
| // has been emptied then the code below will call |
| // Adaptor.asyncDispatch() which will enable the |
| // Response to respond to this event. |
| outputBuffer.registerWriteInterest(); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| |
| @Override |
| protected SocketState dispatchEndRequest() { |
| if (!keepAlive || protocol.isPaused()) { |
| return SocketState.CLOSED; |
| } else { |
| endRequest(); |
| inputBuffer.nextRequest(); |
| outputBuffer.nextRequest(); |
| // Set keep alive timeout for next request |
| socketWrapper.setReadTimeout(protocol.getKeepAliveTimeout()); |
| if (socketWrapper.isReadPending()) { |
| return SocketState.LONG; |
| } else { |
| return SocketState.OPEN; |
| } |
| } |
| } |
| |
| |
| @Override |
| protected Log getLog() { |
| return log; |
| } |
| |
| |
| @Override |
| protected ServletConnection getServletConnection() { |
| return socketWrapper.getServletConnection("http/1.1", ""); |
| } |
| |
| |
| /* |
| * No more input will be passed to the application. Remaining input will be swallowed or the connection dropped |
| * depending on the error and expectation status. |
| */ |
| private void endRequest() { |
| if (getErrorState().isError()) { |
| // If we know we are closing the connection, don't drain |
| // input. This way uploading a 100GB file doesn't tie up the |
| // thread if the servlet has rejected it. |
| inputBuffer.setSwallowInput(false); |
| } else { |
| // Need to check this again here in case the response was |
| // committed before the error that requires the connection |
| // to be closed occurred. |
| checkExpectationAndResponseStatus(); |
| } |
| |
| // Finish the handling of the request |
| if (getErrorState().isIoAllowed()) { |
| try { |
| inputBuffer.endRequest(); |
| } catch (IOException ioe) { |
| setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| // 500 - Internal Server Error |
| // Can't add a 500 to the access log since that has already been |
| // written in the Adapter.service method. |
| response.setStatus(500); |
| setErrorState(ErrorState.CLOSE_NOW, t); |
| log.error(sm.getString("http11processor.request.finish"), t); |
| } |
| } |
| if (getErrorState().isIoAllowed()) { |
| try { |
| action(ActionCode.COMMIT, null); |
| outputBuffer.end(); |
| } catch (IOException ioe) { |
| setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); |
| } catch (Throwable t) { |
| ExceptionUtils.handleThrowable(t); |
| setErrorState(ErrorState.CLOSE_NOW, t); |
| log.error(sm.getString("http11processor.response.finish"), t); |
| } |
| } |
| } |
| |
| |
| @Override |
| protected final void finishResponse() throws IOException { |
| outputBuffer.end(); |
| } |
| |
| |
| @Override |
| protected final void ack(ContinueResponseTiming continueResponseTiming) { |
| // Only try and send the ACK for ALWAYS or if the timing of the request |
| // to send the ACK matches the current configuration. |
| if (continueResponseTiming == ContinueResponseTiming.ALWAYS || |
| continueResponseTiming == protocol.getContinueResponseTimingInternal()) { |
| // 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() && request.hasExpectation()) { |
| try { |
| outputBuffer.sendAck(); |
| } catch (IOException ioe) { |
| setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); |
| } |
| } |
| } |
| } |
| |
| |
| @Override |
| protected void earlyHints() throws IOException { |
| writeHeaders(HttpServletResponse.SC_EARLY_HINTS, response.getMimeHeaders()); |
| outputBuffer.writeHeaders(); |
| outputBuffer.resetHeaderBuffer(); |
| } |
| |
| |
| @Override |
| protected final void flush() throws IOException { |
| outputBuffer.flush(); |
| } |
| |
| |
| @Override |
| protected final int available(boolean doRead) { |
| return inputBuffer.available(doRead); |
| } |
| |
| |
| @Override |
| protected final void setRequestBody(ByteChunk body) { |
| InputFilter savedBody = new SavedRequestInputFilter(body); |
| Http11InputBuffer internalBuffer = (Http11InputBuffer) request.getInputBuffer(); |
| internalBuffer.addActiveFilter(savedBody); |
| } |
| |
| |
| @Override |
| protected final void setSwallowResponse() { |
| outputBuffer.responseFinished = true; |
| } |
| |
| |
| @Override |
| protected final void disableSwallowRequest() { |
| inputBuffer.setSwallowInput(false); |
| } |
| |
| |
| @Override |
| protected final void sslReHandShake() throws IOException { |
| if (sslSupport != null) { |
| // Consume and buffer the request body, so that it does not |
| // interfere with the client's handshake messages |
| InputFilter[] inputFilters = inputBuffer.getFilters(); |
| ((BufferedInputFilter) inputFilters[Constants.BUFFERED_FILTER]).setLimit(protocol.getMaxSavePostSize()); |
| inputBuffer.addActiveFilter(inputFilters[Constants.BUFFERED_FILTER]); |
| |
| /* |
| * Outside the try/catch because we want I/O errors during renegotiation to be thrown for the caller to |
| * handle since they will be fatal to the connection. |
| */ |
| socketWrapper.doClientAuth(sslSupport); |
| try { |
| /* |
| * Errors processing the cert chain do not affect the client connection so they can be logged and |
| * swallowed here. |
| */ |
| Object sslO = sslSupport.getPeerCertificateChain(); |
| if (sslO != null) { |
| request.setAttribute(SSLSupport.CERTIFICATE_KEY, sslO); |
| } |
| } catch (IOException ioe) { |
| log.warn(sm.getString("http11processor.socket.ssl"), ioe); |
| } |
| } |
| } |
| |
| |
| @Override |
| protected final boolean isRequestBodyFullyRead() { |
| return inputBuffer.isFinished(); |
| } |
| |
| |
| @Override |
| protected final void registerReadInterest() { |
| socketWrapper.registerReadInterest(); |
| } |
| |
| |
| @Override |
| protected final boolean isReadyForWrite() { |
| return outputBuffer.isReady(); |
| } |
| |
| |
| @Override |
| public UpgradeToken getUpgradeToken() { |
| return upgradeToken; |
| } |
| |
| |
| @Override |
| protected final void doHttpUpgrade(UpgradeToken upgradeToken) { |
| this.upgradeToken = upgradeToken; |
| // Stop further HTTP output |
| outputBuffer.responseFinished = true; |
| } |
| |
| |
| @Override |
| public ByteBuffer getLeftoverInput() { |
| return inputBuffer.getLeftover(); |
| } |
| |
| |
| @Override |
| public boolean isUpgrade() { |
| return upgradeToken != null; |
| } |
| |
| |
| @Override |
| protected boolean isTrailerFieldsReady() { |
| if (inputBuffer.isChunking()) { |
| return inputBuffer.isFinished(); |
| } else { |
| return true; |
| } |
| } |
| |
| |
| @Override |
| protected boolean isTrailerFieldsSupported() { |
| // Request must be HTTP/1.1 to support trailer fields |
| if (!http11) { |
| return false; |
| } |
| |
| // If the response is not yet committed, chunked encoding can be used |
| // and the trailer fields sent |
| if (!response.isCommitted()) { |
| return true; |
| } |
| |
| // Response has been committed - need to see if chunked is being used |
| return outputBuffer.isChunking(); |
| } |
| |
| |
| /** |
| * Trigger sendfile processing if required. |
| * |
| * @return The state of send file processing |
| */ |
| private SendfileState processSendfile(SocketWrapperBase<?> socketWrapper) { |
| openSocket = keepAlive; |
| // Done is equivalent to sendfile not being used |
| SendfileState result = SendfileState.DONE; |
| // Do sendfile as needed: add socket to sendfile and end |
| if (sendfileData != null && !getErrorState().isError()) { |
| if (keepAlive) { |
| if (available(false) == 0) { |
| sendfileData.keepAliveState = SendfileKeepAliveState.OPEN; |
| } else { |
| sendfileData.keepAliveState = SendfileKeepAliveState.PIPELINED; |
| } |
| } else { |
| sendfileData.keepAliveState = SendfileKeepAliveState.NONE; |
| } |
| result = socketWrapper.processSendfile(sendfileData); |
| if (Objects.requireNonNull(result) == SendfileState.ERROR) { |
| // Write failed |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("http11processor.sendfile.error")); |
| } |
| setErrorState(ErrorState.CLOSE_CONNECTION_NOW, null); |
| } |
| sendfileData = null; |
| } |
| return result; |
| } |
| |
| |
| @Override |
| public final void recycle() { |
| getAdapter().checkRecycled(request, response); |
| super.recycle(); |
| inputBuffer.recycle(); |
| outputBuffer.recycle(); |
| upgradeToken = null; |
| socketWrapper = null; |
| sendfileData = null; |
| sslSupport = null; |
| } |
| |
| |
| @Override |
| public void pause() { |
| // NOOP for HTTP |
| } |
| } |