From 097be68dab8bc8a41158363db230b0f59d35f437 Mon Sep 17 00:00:00 2001 From: Takahiro Nagao Date: Wed, 1 Jun 2022 20:29:29 +0900 Subject: [PATCH] Fix absence of date header --- .../grizzly/http/HttpServerFilter.java | 1267 +++++++++++++++++ .../grizzly/http/util/MimeHeaders.java | 845 +++++++++++ 2 files changed, 2112 insertions(+) create mode 100644 launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http/HttpServerFilter.java create mode 100644 launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http/util/MimeHeaders.java diff --git a/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http/HttpServerFilter.java b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http/HttpServerFilter.java new file mode 100644 index 0000000..3f07f45 --- /dev/null +++ b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http/HttpServerFilter.java @@ -0,0 +1,1267 @@ +/* + * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022 Fujitsu Limited. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.grizzly.http; + +import static org.glassfish.grizzly.http.util.HttpCodecUtils.findEOL; +import static org.glassfish.grizzly.http.util.HttpCodecUtils.findSpace; +import static org.glassfish.grizzly.http.util.HttpCodecUtils.put; +import static org.glassfish.grizzly.http.util.HttpCodecUtils.skipSpaces; +import static org.glassfish.grizzly.http.util.HttpCodecUtils.toCheckedByteArray; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import org.glassfish.grizzly.Buffer; +import org.glassfish.grizzly.Connection; +import org.glassfish.grizzly.Grizzly; +import org.glassfish.grizzly.ThreadCache; +import org.glassfish.grizzly.attributes.Attribute; +import org.glassfish.grizzly.filterchain.FilterChainContext; +import org.glassfish.grizzly.filterchain.FilterChainEvent; +import org.glassfish.grizzly.filterchain.NextAction; +import org.glassfish.grizzly.http.Method.PayloadExpectation; +import org.glassfish.grizzly.http.util.Constants; +import org.glassfish.grizzly.http.util.ContentType; +import org.glassfish.grizzly.http.util.DataChunk; +import org.glassfish.grizzly.http.util.FastHttpDateFormat; +import org.glassfish.grizzly.http.util.Header; +import org.glassfish.grizzly.http.util.HttpStatus; +import org.glassfish.grizzly.http.util.HttpUtils; +import org.glassfish.grizzly.http.util.MimeHeaders; +import org.glassfish.grizzly.memory.MemoryManager; +import org.glassfish.grizzly.utils.DelayedExecutor; + +/** + * Server side {@link HttpCodecFilter} implementation, which is responsible for decoding {@link HttpRequestPacket} and + * encoding {@link HttpResponsePacket} messages. + * + * This Filter is usually used, when we build an asynchronous HTTP server connection. + * + * @see HttpCodecFilter + * @see HttpClientFilter + * + * @author Alexey Stashok + */ +public class HttpServerFilter extends HttpCodecFilter { + public static final String HTTP_SERVER_REQUEST_ATTR_NAME = HttpServerFilter.class.getName() + ".HttpRequest"; + + public static final FilterChainEvent RESPONSE_COMPLETE_EVENT = new HttpEvents.ResponseCompleteEvent(); + + private final Attribute httpRequestInProcessAttr; + private final Attribute keepAliveContextAttr; + + private final DelayedExecutor.DelayQueue keepAliveQueue; + + private final KeepAlive keepAlive; + + private String defaultResponseContentType; + private byte[] defaultResponseContentTypeBytes; + private byte[] defaultResponseContentTypeBytesNoCharset; + + private final boolean allowKeepAlive; + private final int maxRequestHeaders; + private final int maxResponseHeaders; + + // flag, which enables/disables payload support for HTTP methods, + // for which HTTP spec doesn't clearly state whether they support payload. + // Known "undefined" methods are: GET, HEAD, DELETE + private boolean allowPayloadForUndefinedHttpMethods; + + /** + * Constructor, which creates HttpServerFilter instance + * + * @deprecated Next major release will include builders for filters requiring configuration. Constructors will be + * hidden. + */ + @Deprecated + public HttpServerFilter() { + this(true, DEFAULT_MAX_HTTP_PACKET_HEADER_SIZE, null, null); + } + + /** + * Constructor, which creates HttpServerFilter instance, with the specific max header size parameter. + * + * @param chunkingEnabled flag indicating whether or not chunking should be allowed or not. + * @param maxHeadersSize the maximum size of an inbound HTTP message header. + * @param keepAlive keep-alive configuration for this filter instance. + * @param executor {@link DelayedExecutor} for handling keep-alive. + * + * @deprecated Next major release will include builders for filters requiring configuration. Constructors will be + * hidden. + */ + @Deprecated + public HttpServerFilter(boolean chunkingEnabled, int maxHeadersSize, KeepAlive keepAlive, DelayedExecutor executor) { + this(chunkingEnabled, maxHeadersSize, Constants.DEFAULT_RESPONSE_TYPE, keepAlive, executor); + } + + /** + * Constructor, which creates HttpServerFilter instance, with the specific max header size parameter. + * + * @param chunkingEnabled flag indicating whether or not chunking should be allowed or not. + * @param maxHeadersSize the maximum size of an inbound HTTP message header. + * @param defaultResponseContentType the content type that the response should use if no content had been specified at + * the time the response is committed. + * @param keepAlive keep-alive configuration for this filter instance. + * @param executor {@link DelayedExecutor} for handling keep-alive. If null - keep-alive idle connections + * should be managed outside HttpServerFilter. + * + * @deprecated Next major release will include builders for filters requiring configuration. Constructors will be + * hidden. + */ + @Deprecated + public HttpServerFilter(boolean chunkingEnabled, int maxHeadersSize, String defaultResponseContentType, KeepAlive keepAlive, DelayedExecutor executor) { + this(chunkingEnabled, maxHeadersSize, defaultResponseContentType, keepAlive, executor, MimeHeaders.MAX_NUM_HEADERS_DEFAULT, + MimeHeaders.MAX_NUM_HEADERS_DEFAULT); + } + + /** + * Constructor, which creates HttpServerFilter instance, with the specific max header size parameter. + * + * @param chunkingEnabled flag indicating whether or not chunking should be allowed or not. + * @param maxHeadersSize the maximum size of an inbound HTTP message header. + * @param defaultResponseContentType the content type that the response should use if no content had been specified at + * the time the response is committed. + * @param keepAlive keep-alive configuration for this filter instance. + * @param executor {@link DelayedExecutor} for handling keep-alive. If null - keep-alive idle connections + * should be managed outside HttpServerFilter. + * @param maxRequestHeaders maximum number of request headers allowed for a single request. + * @param maxResponseHeaders maximum number of response headers allowed for a single response. + * + * @since 2.2.11 + * + * @deprecated Next major release will include builders for filters requiring configuration. Constructors will be + * hidden. + */ + @Deprecated + public HttpServerFilter(boolean chunkingEnabled, int maxHeadersSize, String defaultResponseContentType, KeepAlive keepAlive, DelayedExecutor executor, + int maxRequestHeaders, int maxResponseHeaders) { + super(chunkingEnabled, maxHeadersSize); + + this.httpRequestInProcessAttr = Grizzly.DEFAULT_ATTRIBUTE_BUILDER.createAttribute(HTTP_SERVER_REQUEST_ATTR_NAME); + this.keepAliveContextAttr = Grizzly.DEFAULT_ATTRIBUTE_BUILDER.createAttribute("HttpServerFilter.KeepAliveContext"); + + keepAliveQueue = executor != null ? executor.createDelayQueue(new KeepAliveWorker(keepAlive), new KeepAliveResolver()) : null; + + this.allowKeepAlive = keepAlive != null; + this.keepAlive = allowKeepAlive ? new KeepAlive(keepAlive) : null; + + if (defaultResponseContentType != null && !defaultResponseContentType.isEmpty()) { + setDefaultResponseContentType(defaultResponseContentType); + } + this.maxRequestHeaders = maxRequestHeaders; + this.maxResponseHeaders = maxResponseHeaders; + } + + // ----------------------------------------------------------- Configuration + + @SuppressWarnings("UnusedDeclaration") + public String getDefaultResponseContentType() { + return defaultResponseContentType; + } + + public final void setDefaultResponseContentType(final String contentType) { + this.defaultResponseContentType = contentType; + if (contentType != null) { + defaultResponseContentTypeBytes = toCheckedByteArray(contentType); + defaultResponseContentTypeBytesNoCharset = ContentType.removeCharset(defaultResponseContentTypeBytes); + } else { + defaultResponseContentTypeBytes = defaultResponseContentTypeBytesNoCharset = null; + } + } + + /** + * The flag, which enables/disables payload support for HTTP methods, for which HTTP spec doesn't clearly state whether + * they support payload. Known "undefined" methods are: GET, HEAD, DELETE. + * + * @return true if "undefined" methods support payload, or false otherwise + * @since 2.3.12 + */ + public boolean isAllowPayloadForUndefinedHttpMethods() { + return allowPayloadForUndefinedHttpMethods; + } + + /** + * The flag, which enables/disables payload support for HTTP methods, for which HTTP spec doesn't clearly state whether + * they support payload. Known "undefined" methods are: GET, HEAD, DELETE. + * + * @param allowPayloadForUndefinedHttpMethods true if "undefined" methods support payload, or false + * otherwise + * @since 2.3.12 + */ + public void setAllowPayloadForUndefinedHttpMethods(boolean allowPayloadForUndefinedHttpMethods) { + this.allowPayloadForUndefinedHttpMethods = allowPayloadForUndefinedHttpMethods; + } + + // ----------------------------------------------------------- Parsing + + /** + * The method is called, once we have received a {@link Buffer}, which has to be transformed into HTTP request packet + * part. + * + * Filter gets {@link Buffer}, which represents a part or complete HTTP request message. As the result of "read" + * transformation - we will get {@link HttpContent} message, which will represent HTTP request packet content (might be + * zero length content) and reference to a {@link HttpHeader}, which contains HTTP request message header. + * + * @param ctx Request processing context + * + * @return {@link NextAction} + * @throws IOException + */ + @Override + public NextAction handleRead(final FilterChainContext ctx) throws IOException { + final Buffer input = ctx.getMessage(); + final Connection connection = ctx.getConnection(); + ServerHttpRequestImpl httpRequest = httpRequestInProcessAttr.get(connection); + + if (httpRequest == null) { + final boolean isSecureLocal = isSecure(connection); + httpRequest = ServerHttpRequestImpl.create(); + httpRequest.initialize(connection, this, input.position(), maxHeadersSize, maxRequestHeaders); + httpRequest.setSecure(isSecureLocal); + final HttpResponsePacket response = httpRequest.getResponse(); + response.setSecure(isSecureLocal); + response.getHeaders().setMaxNumHeaders(maxResponseHeaders); + httpRequest.setResponse(response); + response.setRequest(httpRequest); + + final HttpContext httpContext = HttpContext.newInstance(connection, connection, connection, httpRequest).attach(ctx); + + httpRequest.getProcessingState().setHttpContext(httpContext); + + if (allowKeepAlive) { + KeepAliveContext keepAliveContext = keepAliveContextAttr.get(httpContext); + if (keepAliveContext == null) { + keepAliveContext = new KeepAliveContext(connection); + keepAliveContextAttr.set(httpContext, keepAliveContext); + } else if (keepAliveQueue != null) { + keepAliveQueue.remove(keepAliveContext); + } + + final int requestsProcessed = keepAliveContext.requestsProcessed; + if (requestsProcessed > 0) { + KeepAlive.notifyProbesHit(keepAlive, connection, requestsProcessed); + } + + } + httpRequestInProcessAttr.set(httpContext, httpRequest); + } else if (httpRequest.isContentBroken()) { + // if payload of the current/last HTTP request associated with the + // Connection is broken - stop processing here + return ctx.getStopAction(); + } else { + httpRequest.getProcessingState().getHttpContext().attach(ctx); + } + + return handleRead(ctx, httpRequest); + } + + @Override + final boolean decodeInitialLineFromBytes(final FilterChainContext ctx, final HttpPacketParsing httpPacket, final HeaderParsingState parsingState, + final byte[] input, final int end) { + + final ServerHttpRequestImpl httpRequest = (ServerHttpRequestImpl) httpPacket; + + final int arrayOffs = parsingState.arrayOffset; + final int reqLimit = arrayOffs + parsingState.packetLimit; + + // noinspection LoopStatementThatDoesntLoop + while (true) { + int subState = parsingState.subState; + + switch (subState) { + case 0: { // parse the method name + final int spaceIdx = findSpace(input, arrayOffs + parsingState.offset, end, reqLimit); + if (spaceIdx == -1) { + parsingState.offset = end - arrayOffs; + return false; + } + + httpRequest.getMethodDC().setBytes(input, arrayOffs + parsingState.start, spaceIdx); + + parsingState.start = -1; + parsingState.offset = spaceIdx - arrayOffs; + + parsingState.subState++; + } + + case 1: { // skip spaces after the method name + final int nonSpaceIdx = skipSpaces(input, arrayOffs + parsingState.offset, end, reqLimit) - arrayOffs; + if (nonSpaceIdx < 0) { + parsingState.offset = end - arrayOffs; + return false; + } + + parsingState.start = nonSpaceIdx; + parsingState.offset = nonSpaceIdx + 1; + parsingState.subState++; + } + + case 2: { // parse the requestURI + if (!parseRequestURI(httpRequest, parsingState, input, end)) { + return false; + } + } + + case 3: { // skip spaces after requestURI + final int nonSpaceIdx = skipSpaces(input, arrayOffs + parsingState.offset, end, reqLimit) - arrayOffs; + if (nonSpaceIdx < 0) { + parsingState.offset = end - arrayOffs; + return false; + } + + parsingState.start = nonSpaceIdx; + parsingState.offset = nonSpaceIdx; + parsingState.subState++; + } + + case 4: { // HTTP protocol + if (!findEOL(parsingState, input, end)) { + parsingState.offset = end - arrayOffs; + return false; + } + + if (parsingState.checkpoint > parsingState.start) { + httpRequest.getProtocolDC().setBytes(input, arrayOffs + parsingState.start, arrayOffs + parsingState.checkpoint); + } else { + httpRequest.getProtocolDC().setString(""); + } + + parsingState.subState = 0; + parsingState.start = -1; + parsingState.checkpoint = -1; + onInitialLineParsed(httpRequest, ctx); + return true; + } + + default: + throw new IllegalStateException(); + } + } + } + + private static boolean parseRequestURI(final ServerHttpRequestImpl httpRequest, final HeaderParsingState state, final byte[] input, final int end) { + + final int arrayOffs = state.arrayOffset; + final int limit = Math.min(end, arrayOffs + state.packetLimit); + + int offset = arrayOffs + state.offset; + + boolean found = false; + + while (offset < limit) { + final byte b = input[offset]; + if (b == Constants.SP || b == Constants.HT) { + found = true; + break; + } else if (b == Constants.CR || b == Constants.LF) { + // HTTP/0.9 style request + found = true; + break; + } else if (b == Constants.QUESTION && state.checkpoint == -1) { + state.checkpoint = offset - arrayOffs; + } + + offset++; + } + + if (found) { + int requestURIEnd = offset; + if (state.checkpoint != -1) { + // cut RequestURI to not include query string + requestURIEnd = arrayOffs + state.checkpoint; + + httpRequest.getQueryStringDC().setBytes(input, requestURIEnd + 1, offset); + } + + httpRequest.getRequestURIRef().init(input, arrayOffs + state.start, requestURIEnd); + + state.start = -1; + state.checkpoint = -1; + state.subState++; + } + + state.offset = offset - arrayOffs; + return found; + } + + @Override + final boolean decodeInitialLineFromBuffer(final FilterChainContext ctx, final HttpPacketParsing httpPacket, final HeaderParsingState parsingState, + final Buffer input) { + + final ServerHttpRequestImpl httpRequest = (ServerHttpRequestImpl) httpPacket; + + final int reqLimit = parsingState.packetLimit; + + // noinspection LoopStatementThatDoesntLoop + while (true) { + int subState = parsingState.subState; + + switch (subState) { + case 0: { // parse the method name + final int spaceIdx = findSpace(input, parsingState.offset, reqLimit); + if (spaceIdx == -1) { + parsingState.offset = input.limit(); + return false; + } + + httpRequest.getMethodDC().setBuffer(input, parsingState.start, spaceIdx); + + parsingState.start = -1; + parsingState.offset = spaceIdx; + + parsingState.subState++; + } + + case 1: { // skip spaces after the method name + final int nonSpaceIdx = skipSpaces(input, parsingState.offset, reqLimit); + if (nonSpaceIdx == -1) { + parsingState.offset = input.limit(); + return false; + } + + parsingState.start = nonSpaceIdx; + parsingState.offset = nonSpaceIdx + 1; + parsingState.subState++; + } + + case 2: { // parse the requestURI + if (!parseRequestURI(httpRequest, parsingState, input)) { + return false; + } + } + + case 3: { // skip spaces after requestURI + final int nonSpaceIdx = skipSpaces(input, parsingState.offset, reqLimit); + if (nonSpaceIdx == -1) { + parsingState.offset = input.limit(); + return false; + } + + parsingState.start = nonSpaceIdx; + parsingState.offset = nonSpaceIdx; + parsingState.subState++; + } + + case 4: { // HTTP protocol + if (!findEOL(parsingState, input)) { + parsingState.offset = input.limit(); + return false; + } + + if (parsingState.checkpoint > parsingState.start) { + httpRequest.getProtocolDC().setBuffer(input, parsingState.start, parsingState.checkpoint); + } else { + httpRequest.getProtocolDC().setString(""); + } + + parsingState.subState = 0; + parsingState.start = -1; + parsingState.checkpoint = -1; + onInitialLineParsed(httpRequest, ctx); + return true; + } + + default: + throw new IllegalStateException(); + } + } + } + + private static boolean parseRequestURI(ServerHttpRequestImpl httpRequest, HeaderParsingState state, Buffer input) { + + final int limit = Math.min(input.limit(), state.packetLimit); + + int offset = state.offset; + + boolean found = false; + + while (offset < limit) { + final byte b = input.get(offset); + if (b == Constants.SP || b == Constants.HT) { + found = true; + break; + } else if (b == Constants.CR || b == Constants.LF) { + // HTTP/0.9 style request + found = true; + break; + } else if (b == Constants.QUESTION && state.checkpoint == -1) { + state.checkpoint = offset; + } + + offset++; + } + + if (found) { + int requestURIEnd = offset; + if (state.checkpoint != -1) { + // cut RequestURI to not include query string + requestURIEnd = state.checkpoint; + + httpRequest.getQueryStringDC().setBuffer(input, state.checkpoint + 1, offset); + } + + httpRequest.getRequestURIRef().init(input, state.start, requestURIEnd); + + state.start = -1; + state.checkpoint = -1; + state.subState++; + } + + state.offset = offset; + return found; + } + + @Override + protected boolean onHttpHeaderParsed(final HttpHeader httpHeader, final Buffer buffer, final FilterChainContext ctx) { + + final ServerHttpRequestImpl request = (ServerHttpRequestImpl) httpHeader; + + prepareRequest(request, buffer.hasRemaining()); + return request.getProcessingState().error; + } + + private void prepareRequest(final ServerHttpRequestImpl request, final boolean hasReadyContent) { + + final ProcessingState state = request.getProcessingState(); + final HttpResponsePacket response = request.getResponse(); + + Protocol protocol; + try { + protocol = request.getProtocol(); + } catch (IllegalStateException e) { + state.error = true; + // Send 505; Unsupported HTTP version + HttpStatus.HTTP_VERSION_NOT_SUPPORTED_505.setValues(response); + protocol = Protocol.HTTP_1_1; + request.setProtocol(protocol); + + return; + } + + // set the default chunking mode + request.getResponse().setChunkingAllowed(isChunkingEnabled()); + + if (request.getHeaderParsingState().contentLengthsDiffer) { + request.getProcessingState().error = true; + return; + } + + final MimeHeaders headers = request.getHeaders(); + + DataChunk hostDC = null; + + // Check for a full URI (including protocol://host:port/) + + final DataChunk uriBC = request.getRequestURIRef().getRequestURIBC(); + + if (uriBC.startsWithIgnoreCase("http", 0)) { + + int pos = uriBC.indexOf("://", 4); + int uriBCStart = uriBC.getStart(); + int slashPos; + if (pos != -1) { +// final Buffer uriB = uriBC.getBuffer(); + slashPos = uriBC.indexOf('/', pos + 3); + if (slashPos == -1) { + slashPos = uriBC.getLength(); + // Set URI as "/" + uriBC.setStart(uriBCStart + pos + 1); + uriBC.setEnd(uriBCStart + pos + 2); + } else { + uriBC.setStart(uriBCStart + slashPos); + uriBC.setEnd(uriBC.getEnd()); + } + hostDC = headers.setValue(Header.Host); + hostDC.set(uriBC, uriBCStart + pos + 3, uriBCStart + slashPos); + } + + } + + // -------------------------- + + if (hostDC == null) { + hostDC = headers.getValue(Header.Host); + } + + final boolean isHttp11 = protocol == Protocol.HTTP_1_1; + + // Check host header + if (isHttp11 && (hostDC == null || hostDC.isNull())) { + state.error = true; + return; + } + request.unparsedHostC = hostDC; + + // Check if we have to ignore content modifiers like HTTP method, + // Transfer and Content encoding + if (request.isIgnoreContentModifiers()) { + return; + } + + final Method method = request.getMethod(); + + final PayloadExpectation payloadExpectation = method.getPayloadExpectation(); + if (payloadExpectation != PayloadExpectation.NOT_ALLOWED) { + final boolean hasPayload = request.getContentLength() > 0 || request.isChunked(); + + if (hasPayload && payloadExpectation == PayloadExpectation.UNDEFINED && !allowPayloadForUndefinedHttpMethods) { + // if payload is not allowed for the "undefined" methods + state.error = true; + // Send 400; Bad Request + HttpStatus.BAD_REQUEST_400.setValues(response); + return; + } + + request.setExpectContent(hasPayload); + } else { + request.setExpectContent(method == Method.CONNECT || method == Method.PRI); + } + + // ------ Set keep-alive flag + if (method == Method.CONNECT) { + state.keepAlive = false; + } else { + final DataChunk connectionValueDC = headers.getValue(Header.Connection); + final boolean isConnectionClose = connectionValueDC != null && connectionValueDC.equalsIgnoreCaseLowerCase(CLOSE_BYTES); + + if (!isConnectionClose) { + state.keepAlive = allowKeepAlive && (isHttp11 || connectionValueDC != null && connectionValueDC.equalsIgnoreCaseLowerCase(KEEPALIVE_BYTES)); + } + } + + if (request.requiresAcknowledgement()) { + // if we have any request content, we can ignore the Expect + // request + request.requiresAcknowledgement(isHttp11 && !hasReadyContent); + } + } + + @Override + protected final boolean onHttpPacketParsed(final HttpHeader httpHeader, final FilterChainContext ctx) { + final ServerHttpRequestImpl request = (ServerHttpRequestImpl) httpHeader; + + final boolean error = request.getProcessingState().error; + if (!error) { + // remove the Connection -> HttpRequestPacket association + httpRequestInProcessAttr.remove(ctx.getConnection()); + } + return error; + } + + @Override + protected void onInitialLineParsed(final HttpHeader httpHeader, final FilterChainContext ctx) { + // no-op + + } + + @Override + protected void onHttpHeadersParsed(final HttpHeader httpHeader, final MimeHeaders headers, final FilterChainContext ctx) { + // no-op + + } + + @Override + protected void onHttpContentParsed(HttpContent content, FilterChainContext ctx) { + + // no-op + + } + + @Override + protected void onHttpHeaderError(final HttpHeader httpHeader, final FilterChainContext ctx, final Throwable t) throws IOException { + + final ServerHttpRequestImpl request = (ServerHttpRequestImpl) httpHeader; + final HttpResponsePacket response = request.getResponse(); + + sendBadRequestResponse(ctx, response); + } + + @Override + protected void onHttpContentError(final HttpHeader httpHeader, final FilterChainContext ctx, final Throwable t) throws IOException { + final ServerHttpRequestImpl request = (ServerHttpRequestImpl) httpHeader; + final HttpResponsePacket response = request.getResponse(); + if (!response.isCommitted()) { + sendBadRequestResponse(ctx, response); + } + httpHeader.setContentBroken(true); + + } + + // ----------------------------------------------------------- Serializing + + @Override + protected Buffer encodeHttpPacket(final FilterChainContext ctx, final HttpPacket input) { + final HttpHeader header; + HttpContent content; + + final boolean isHeaderPacket = input.isHeader(); + if (isHeaderPacket) { + header = (HttpHeader) input; + content = null; + } else { + content = (HttpContent) input; + header = content.getHttpHeader(); + } + + boolean wasContentAlreadyEncoded = false; + final HttpResponsePacket response = (HttpResponsePacket) header; + if (!response.isCommitted()) { + final HttpContent encodedHttpContent = prepareResponse(ctx, response.getRequest(), response, content); + + if (encodedHttpContent != null) { + content = encodedHttpContent; + wasContentAlreadyEncoded = true; + } + } + + final Buffer encoded = super.encodeHttpPacket(ctx, header, content, wasContentAlreadyEncoded); + if (!isHeaderPacket) { + input.recycle(); + } + return encoded; + } + + /** + * Prepare Http response + * + * @return encoded HttpContent, if content encoders have been applied, or null, if HttpContent wasn't changed. + */ + private HttpContent prepareResponse(final FilterChainContext ctx, final HttpRequestPacket request, final HttpResponsePacket response, + final HttpContent httpContent) { + + // If it's upgraded HTTP - don't check semantics + if (request.isIgnoreContentModifiers() || response.isIgnoreContentModifiers()) { + return httpContent; + } + + final Protocol requestProtocol = request.getProtocol(); + + if (requestProtocol == Protocol.HTTP_0_9) { + return null; + } + + boolean entityBody = true; + final int statusCode = response.getStatus(); + + final boolean is204 = HttpStatus.NO_CONTENT_204.statusMatches(statusCode); + if (is204 || HttpStatus.RESET_CONTENT_205.statusMatches(statusCode) || HttpStatus.NOT_MODIFIED_304.statusMatches(statusCode)) { + // No entity body + entityBody = false; + response.setExpectContent(false); + if (is204) { + response.setTransferEncoding(null); + response.getHeaders().removeHeader(Header.TransferEncoding); + } + } + + final boolean isHttp11OrHigher = requestProtocol.compareTo(Protocol.HTTP_1_1) >= 0; + + HttpContent encodedHttpContent = null; + + final Method method = request.getMethod(); + + if (!Method.CONNECT.equals(method)) { + // @TODO consider moving underlying "if"-logic to HttpCodecFilter + // to make it common for client and server sides. + if (entityBody) { + // Check if any compression would be applied + setContentEncodingsOnSerializing(response); + + if (response.getContentLength() == -1L && !response.isChunked()) { + // If neither content-length not chunking is explicitly set - + // try to apply one of those depending on headers and content + if (httpContent != null && httpContent.isLast()) { + // if this is first and last data chunk - set the content-length + if (!response.getContentEncodings(true).isEmpty()) { + // optimization... + // if content encodings have to be applied - apply them here + // to be able to set correct content-length + encodedHttpContent = encodeContent(ctx.getConnection(), httpContent); + } + + response.setContentLength(httpContent.getContent().remaining()); + } else if (chunkingEnabled && isHttp11OrHigher) { + // otherwise use chunking if possible + response.setChunked(true); + } + } + } + + if (Method.HEAD.equals(method)) { + // No entity body + response.setExpectContent(false); + setContentEncodingsOnSerializing(response); + setTransferEncodingOnSerializing(ctx, response, httpContent); + } + } else { // Method.CONNECT + // Disable all encodings + response.setContentEncodingsSelected(true); + response.setContentLength(-1); + response.setChunked(false); + } + + final MimeHeaders headers = response.getHeaders(); + + if (!entityBody) { + response.setContentLength(-1); + } else { + String contentLanguage = response.getContentLanguage(); + if (contentLanguage != null) { + headers.setValue(Header.ContentLanguage).setString(contentLanguage); + } + + // Optimize content-type serialization depending on its state + final ContentType contentType = response.getContentTypeHolder(); + if (contentType.isMimeTypeSet()) { + final DataChunk contentTypeValue = headers.setValue(Header.ContentType); + if (contentTypeValue.isNull()) { + contentType.serializeToDataChunk(contentTypeValue); + } + } else if (defaultResponseContentType != null) { + final DataChunk contenTypeValue = headers.setValue(Header.ContentType); + if (contenTypeValue.isNull()) { + final String ce = response.getCharacterEncoding(); + if (ce == null) { + contenTypeValue.setBytes(defaultResponseContentTypeBytes); + } else { + final byte[] array = ContentType.compose(defaultResponseContentTypeBytesNoCharset, ce); + contenTypeValue.setBytes(array); + } + } + } + } + + if (!response.containsHeader(Header.Date)) { + response.getHeaders().addValueWithoutValidation(Header.Date).setBytes(FastHttpDateFormat.getCurrentDateBytes()); + } + + final ProcessingState state = response.getProcessingState(); + final boolean isHttp11 = requestProtocol == Protocol.HTTP_1_1; + + if (state.keepAlive) { + if (entityBody && !isHttp11 && response.getContentLength() == -1) { + // HTTP 1.0 response with no content-length having been set. + // Close the connection to signal the response as being complete. + state.keepAlive = false; + } else if (entityBody && !response.isChunked() && response.getContentLength() == -1) { + // HTTP 1.1 response with chunking disabled and no content-length having been set. + // Close the connection to signal the response as being complete. + state.keepAlive = false; + } else if (!checkKeepAliveRequestsCount(state.getHttpContext())) { + // We processed max allowed HTTP requests over the keep alive connection + state.keepAlive = false; + } else { + final DataChunk dc = headers.getValue(Header.Connection); + if (dc != null && !dc.isNull() && dc.equalsIgnoreCase(CLOSE_BYTES)) { + state.keepAlive = false; + } + } + + // If we know that the request is bad this early, add the + // Connection: close header. + state.keepAlive = state.keepAlive && !statusDropsConnection(response.getStatus()); + } + + if (!state.keepAlive) { + headers.setValue(Header.Connection).setBytes(CLOSE_BYTES); + } else if (!isHttp11 && !state.error) { + headers.setValue(Header.Connection).setBytes(KEEPALIVE_BYTES); + } + + return encodedHttpContent; + } + + @Override + Buffer encodeInitialLine(HttpPacket httpPacket, Buffer output, MemoryManager memoryManager) { + final HttpResponsePacket httpResponse = (HttpResponsePacket) httpPacket; + output = put(memoryManager, output, httpResponse.getProtocol().getProtocolBytes()); + output = put(memoryManager, output, Constants.SP); + output = put(memoryManager, output, httpResponse.getHttpStatus().getStatusBytes()); + output = put(memoryManager, output, Constants.SP); + if (httpResponse.isCustomReasonPhraseSet()) { + + final DataChunk customReasonPhrase = httpResponse.isHtmlEncodingCustomReasonPhrase() ? HttpUtils.filter(httpResponse.getReasonPhraseDC()) + : HttpUtils.filterNonPrintableCharacters(httpResponse.getReasonPhraseDC()); + + output = put(memoryManager, output, httpResponse.getTempHeaderEncodingBuffer(), customReasonPhrase); + } else { + output = put(memoryManager, output, httpResponse.getHttpStatus().getReasonPhraseBytes()); + } + + return output; + } + + @Override + protected void onInitialLineEncoded(HttpHeader header, FilterChainContext ctx) { + + // no-op + + } + + @Override + protected void onHttpHeadersEncoded(HttpHeader httpHeader, FilterChainContext ctx) { + + // no-op + + } + + @Override + protected void onHttpContentEncoded(HttpContent content, FilterChainContext ctx) { + + // no-op + + } + + @Override + public NextAction handleEvent(final FilterChainContext ctx, final FilterChainEvent event) throws IOException { + + if (event.type() == HttpEvents.ResponseCompleteEvent.TYPE) { + + if (ctx.getConnection().isOpen()) { + final HttpContext context = HttpContext.get(ctx); + final HttpRequestPacket httpRequest = context.getRequest(); + + if (allowKeepAlive) { + if (keepAliveQueue != null) { + final KeepAliveContext keepAliveContext = keepAliveContextAttr.get(context); + + if (keepAliveContext != null) { + keepAliveQueue.add(keepAliveContext, keepAlive.getIdleTimeoutInSeconds(), TimeUnit.SECONDS); + } + } + + final boolean isStayAlive = httpRequest.getProcessingState().isKeepAlive(); + + processResponseComplete(ctx, httpRequest, isStayAlive); + } else { + processResponseComplete(ctx, httpRequest, false); + } + } + + return ctx.getStopAction(); + } + + return ctx.getInvokeAction(); + } + + @Override + public NextAction handleClose(final FilterChainContext ctx) throws IOException { + final ServerHttpRequestImpl httpRequest = httpRequestInProcessAttr.get(ctx.getConnection()); + if (httpRequest != null && !httpRequest.isContentBroken()) { + // if we still have HTTP request in progress and this HTTP request + // doesn't have specified TransferEncoder - it means we parse + // it till EOF... so now it's time to notify that this packet has + // been parsed completely + if (httpRequest.isExpectContent() && httpRequest.getTransferEncoding() == null) { + + httpRequest.setExpectContent(false); + // notify processed. If packet has transfer encoding - the notification should be called elsewhere + onHttpPacketParsed(httpRequest, ctx); + } + } + + return ctx.getInvokeAction(); + } + + private void processResponseComplete(final FilterChainContext ctx, final HttpRequestPacket httpRequest, final boolean isStayAlive) throws IOException { + + // if this is upgraded HTTP connection - close it + if (httpRequest.isUpgrade()) { + httpRequest.getProcessingState().getHttpContext().close(); + return; + } + + if (httpRequest.isExpectContent()) { + if (!httpRequest.isContentBroken() && checkContentLengthRemainder(httpRequest)) { + // If transfer encoding is defined and we can determine the message body length + // we will check HTTP keep-alive settings once remainder is fully read + httpRequest.setSkipRemainder(true); + } else { + // if the packet is broken notify it's been parsed and + // close the connection + httpRequest.setExpectContent(false); + onHttpPacketParsed(httpRequest, ctx); + // no matter it's keep-alive or not - we close the connection + httpRequest.getProcessingState().getHttpContext().close(); +// flushAndClose(ctx); + } + } else if (!isStayAlive) { + // if we don't expect more data on the request and it's not in keep-alive mode + // close it + httpRequest.getProcessingState().getHttpContext().close(); +// flushAndClose(ctx); + } /* + * else { we don't expect more data on the request, but it's keep-alive } + */ + + } + + protected HttpContent customizeErrorResponse(final HttpResponsePacket response) { + + response.setContentLength(0); + return HttpContent.builder(response).last(true).build(); + } + + private boolean checkKeepAliveRequestsCount(final HttpContext httpContext) { + if (!allowKeepAlive) { + return false; + } + + final KeepAliveContext keepAliveContext = keepAliveContextAttr.get(httpContext); + final int requestsProcessed = keepAliveContext.requestsProcessed++; + final int maxRequestCount = keepAlive.getMaxRequestsCount(); + final boolean isKeepAlive = maxRequestCount == -1 || keepAliveContext.requestsProcessed < maxRequestCount; + + if (requestsProcessed == 0) { + if (isKeepAlive) { // New keep-alive connection + KeepAlive.notifyProbesConnectionAccepted(keepAlive, keepAliveContext.connection); + } else { // Refused keep-alive connection + KeepAlive.notifyProbesRefused(keepAlive, keepAliveContext.connection); + } + } + + return isKeepAlive; + } + + private void sendBadRequestResponse(final FilterChainContext ctx, final HttpResponsePacket response) { + if (response.getHttpStatus().getStatusCode() < 400) { + // 400 - Bad request + HttpStatus.BAD_REQUEST_400.setValues(response); + } + commitAndCloseAsError(ctx, response); + } + + /* + * caller has the responsibility to set the status of th response. + */ + private void commitAndCloseAsError(FilterChainContext ctx, HttpResponsePacket response) { + final HttpContent errorHttpResponse = customizeErrorResponse(response); + final Buffer resBuf = encodeHttpPacket(ctx, errorHttpResponse); + ctx.write(resBuf); + response.getProcessingState().getHttpContext().close(); + } + + /** + * @param httpRequest + * @return false if the request payload size is specified by the content-length header and according to the + * request parsing state the remaining payload size is larger than {@link #getMaxPayloadRemainderToSkip()}. Otherwise + * return true + */ + private boolean checkContentLengthRemainder(final HttpRequestPacket httpRequest) { + return maxPayloadRemainderToSkip < 0 || httpRequest.getContentLength() <= 0 + || ((HttpPacketParsing) httpRequest).getContentParsingState().chunkRemainder <= maxPayloadRemainderToSkip; + } + + // ---------------------------------------------------------- Nested Classes + + private static class KeepAliveContext { + private final Connection connection; + + public KeepAliveContext(Connection connection) { + this.connection = connection; + } + + private volatile long keepAliveTimeoutMillis = DelayedExecutor.UNSET_TIMEOUT; + private int requestsProcessed; + } // END KeepAliveContext + + private static class KeepAliveWorker implements DelayedExecutor.Worker { + + private final KeepAlive keepAlive; + + public KeepAliveWorker(final KeepAlive keepAlive) { + this.keepAlive = keepAlive; + } + + @Override + public boolean doWork(final KeepAliveContext context) { + KeepAlive.notifyProbesTimeout(keepAlive, context.connection); + context.connection.closeSilently(); + + return true; + } + + } // END KeepAliveWorker + + private static class KeepAliveResolver implements DelayedExecutor.Resolver { + + @Override + public boolean removeTimeout(KeepAliveContext context) { + if (context.keepAliveTimeoutMillis != DelayedExecutor.UNSET_TIMEOUT) { + context.keepAliveTimeoutMillis = DelayedExecutor.UNSET_TIMEOUT; + return true; + } + + return false; + } + + @Override + public long getTimeoutMillis(KeepAliveContext element) { + return element.keepAliveTimeoutMillis; + } + + @Override + public void setTimeoutMillis(KeepAliveContext element, long timeoutMillis) { + element.keepAliveTimeoutMillis = timeoutMillis; + } + + } // END KeepAliveResolver + + private static final class ServerHttpRequestImpl extends HttpRequestPacket implements HttpPacketParsing { + + private static final ThreadCache.CachedTypeIndex CACHE_IDX = ThreadCache.obtainIndex(ServerHttpRequestImpl.class, 16); + + public static ServerHttpRequestImpl create() { + final ServerHttpRequestImpl httpRequestImpl = ThreadCache.takeFromCache(CACHE_IDX); + if (httpRequestImpl != null) { + return httpRequestImpl; + } + + return new ServerHttpRequestImpl(); + } + + /** + * Char encoding parsed flag. + */ + private boolean contentTypeParsed; + + private boolean isHeaderParsed; + private final HttpCodecFilter.HeaderParsingState headerParsingState; + private final HttpCodecFilter.ContentParsingState contentParsingState; + private final ProcessingState processingState; + + private final HttpResponsePacket finalHttpResponse; + + private ServerHttpRequestImpl() { + this.headerParsingState = new HttpCodecFilter.HeaderParsingState(); + this.contentParsingState = new HttpCodecFilter.ContentParsingState(); + this.processingState = new ProcessingState(); + finalHttpResponse = new HttpResponsePacketImpl(); + isExpectContent = true; + } + + public void initialize(final Connection connection, final HttpCodecFilter filter, final int initialOffset, final int maxHeaderSize, + final int maxNumberOfHeaders) { + headerParsingState.initialize(filter, initialOffset, maxHeaderSize); + contentParsingState.trailerHeaders.setMaxNumHeaders(maxNumberOfHeaders); + headers.setMaxNumHeaders(maxNumberOfHeaders); + finalHttpResponse.setProtocol(Protocol.HTTP_1_1); + setResponse(finalHttpResponse); + setConnection(connection); + } + + @Override + public String getCharacterEncoding() { + if (!contentTypeParsed) { + parseContentTypeHeader(); + } + + return super.getCharacterEncoding(); + } + + @Override + public void setCharacterEncoding(final String charset) { + if (!contentTypeParsed) { + parseContentTypeHeader(); + } + + super.setCharacterEncoding(charset); + } + + @Override + public String getContentType() { + if (!contentTypeParsed) { + parseContentTypeHeader(); + } + + return super.getContentType(); + } + + private void parseContentTypeHeader() { + contentTypeParsed = true; + + if (!contentType.isSet()) { + final DataChunk dc = headers.getValue(Header.ContentType); + + if (dc != null && !dc.isNull()) { + setContentType(dc.toString()); + } + } + } + + @Override + public ProcessingState getProcessingState() { + return processingState; + } + + @Override + public HttpCodecFilter.HeaderParsingState getHeaderParsingState() { + return headerParsingState; + } + + @Override + public ContentParsingState getContentParsingState() { + return contentParsingState; + } + + @Override + public boolean isHeaderParsed() { + return isHeaderParsed; + } + + @Override + public void setHeaderParsed(final boolean isHeaderParsed) { + if (isHeaderParsed && isExpectContent() && !isChunked) { + contentParsingState.chunkRemainder = getContentLength(); + } + + this.isHeaderParsed = isHeaderParsed; + } + + @Override + protected HttpPacketParsing getParsingState() { + return this; + } + + /** + * {@inheritDoc} + */ + @Override + protected void reset() { + contentTypeParsed = false; + isHeaderParsed = false; + headerParsingState.recycle(); + contentParsingState.recycle(); + processingState.recycle(); + + super.reset(); + } + + /** + * {@inheritDoc} + */ + @Override + public void recycle() { + if (isExpectContent()) { + return; + } + reset(); + ThreadCache.putToCache(CACHE_IDX, this); + } + } +} diff --git a/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http/util/MimeHeaders.java b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http/util/MimeHeaders.java new file mode 100644 index 0000000..b5b21f5 --- /dev/null +++ b/launcher-impl/glassfish/src/main/java/org/glassfish/grizzly/http/util/MimeHeaders.java @@ -0,0 +1,845 @@ +/* + * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright 2004 The Apache Software Foundation + * Copyright (c) 2022 Fujitsu Limited. + * + * Licensed 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.glassfish.grizzly.http.util; + +import java.util.Arrays; +import java.util.Iterator; + +import org.glassfish.grizzly.Buffer; + +/* XXX XXX XXX Need a major rewrite !!!! + */ +/* + * This class is used to contain standard Internet message headers, + * used for SMTP (RFC822) and HTTP (RFC2068) messages as well as for + * MIME (RFC 2045) applications such as transferring typed data and + * grouping related items in multipart message bodies. + * + *

Message headers, as specified in RFC822, include a field name + * and a field body. Order has no semantic significance, and several + * fields with the same name may exist. However, most fields do not + * (and should not) exist more than once in a header. + * + *

Many kinds of field body must conform to a specified syntax, + * including the standard parenthesized comment syntax. This class + * supports only two simple syntaxes, for dates and integers. + * + *

When processing headers, care must be taken to handle the case of + * multiple same-name fields correctly. The values of such fields are + * only available as strings. They may be accessed by index (treating + * the header as an array of fields), or by name (returning an array + * of string values). + */ + +/* Headers are first parsed and stored in the order they are +received. This is based on the fact that most servlets will not +directly access all headers, and most headers are single-valued. +( the alternative - a hash or similar data structure - will add +an overhead that is not needed in most cases ) + +Apache seems to be using a similar method for storing and manipulating +headers. + +Future enhancements: +- hash the headers the first time a header is requested ( i.e. if the +servlet needs direct access to headers). +- scan "common" values ( length, cookies, etc ) during the parse +( addHeader hook ) + + */ +/** + * Memory-efficient repository for Mime Headers. When the object is recycled, it will keep the allocated headers[] and + * all the MimeHeaderField - no GC is generated. + * + * For input headers it is possible to use the DataChunk for Fields - so no GC will be generated. + * + * The only garbage is generated when using the String for header names/values - this can't be avoided when the servlet + * calls header methods, but is easy to avoid inside tomcat. The goal is to use _only_ DataChunk-based Fields, and + * reduce to 0 the memory overhead of tomcat. + * + * TODO: XXX one-buffer parsing - for HTTP ( other protocols don't need that ) XXX remove unused methods XXX External + * enumerations, with 0 GC. XXX use HeaderName ID + * + * + * @author dac@eng.sun.com + * @author James Todd [gonzo@eng.sun.com] + * @author Costin Manolache + * @author kevin seguin + */ +public class MimeHeaders { + + private static final String[] INVALID_TRAILER_NAMES = { Header.CacheControl.getLowerCase(), Header.Expect.getLowerCase(), Header.Host.getLowerCase(), + Header.MaxForwards.getLowerCase(), Header.Pragma.getLowerCase(), Header.Range.getLowerCase(), Header.TE.getLowerCase(), + Header.SetCookie.getLowerCase(), Header.Authorization.getLowerCase(), Header.WWWAuthenticate.getLowerCase(), + Header.ProxyAuthenticate.getLowerCase(), Header.ProxyAuthorization.getLowerCase(), Header.Age.getLowerCase(), Header.Date.getLowerCase(), + Header.Location.getLowerCase(), Header.RetryAfter.getLowerCase(), Header.Vary.getLowerCase(), Header.Warnings.getLowerCase(), + Header.IfMatch.getLowerCase(), Header.IfNoneMatch.getLowerCase(), Header.IfModifiedSince.getLowerCase(), Header.IfUnmodifiedSince.getLowerCase(), + Header.IfRange.getLowerCase() }; + static { + Arrays.sort(INVALID_TRAILER_NAMES); + } + + public static final int MAX_NUM_HEADERS_UNBOUNDED = -1; + + public static final int MAX_NUM_HEADERS_DEFAULT = 100; + + /** + * Initial size - should be == average number of headers per request XXX make it configurable ( fine-tuning of web-apps + * ) + */ + public static final int DEFAULT_HEADER_SIZE = 8; + + public static DataChunk NOOP_CHUNK = new DataChunk.Immutable(null); + + /** + * The header fields. + */ + private MimeHeaderField[] headers = new MimeHeaderField[DEFAULT_HEADER_SIZE]; + /** + * The current number of header fields. + */ + private int count; + private boolean marked; + protected int mark; + + private int maxNumHeaders = MAX_NUM_HEADERS_DEFAULT; + + /** + * The header names {@link Iterable}. + */ + private final Iterable namesIterable = new Iterable() { + + @Override + public Iterator iterator() { + return new NamesIterator(MimeHeaders.this, false); + } + }; + + /** + * Creates a new MimeHeaders object using a default buffer size. + */ + public MimeHeaders() { + } + + public void mark() { + if (!marked) { + marked = true; + mark = count; + } + } + + /** + * Clears all header fields. + */ + // [seguin] added for consistency -- most other objects have recycle(). + public void recycle() { + clear(); + } + + /** + * Clears all header fields. + */ + public void clear() { + for (int i = 0; i < count; i++) { + headers[i].recycle(); + } + count = 0; + mark = 0; + marked = false; + + } + + /** + * EXPENSIVE!!! only for debugging. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append("=== MimeHeaders ===\n"); + + for (int i = 0; i < count; i++) { + sb.append(headers[i].nameB).append(" = ").append(headers[i].valueB).append('\n'); + } + + return sb.toString(); + } + + public void copyFrom(final MimeHeaders source) { + if (source == null || source.size() == 0) { + return; + } + this.maxNumHeaders = source.maxNumHeaders; + this.count = source.count; + if (headers.length < count) { + MimeHeaderField tmp[] = new MimeHeaderField[count * 2]; + System.arraycopy(headers, 0, tmp, 0, headers.length); + headers = tmp; + } + + for (int i = 0, len = source.count; i < len; i++) { + MimeHeaderField sourceField = source.headers[i]; + if (!isValidName(sourceField.getName().toString())) { + continue; + } + + MimeHeaderField f = headers[i]; + if (f == null) { + f = new MimeHeaderField(); + headers[i] = f; + } + if (sourceField.nameB.type == DataChunk.Type.Buffer) { + copyBufferChunk(sourceField.nameB, f.nameB); + } else { + f.nameB.set(sourceField.nameB); + } + if (sourceField.valueB.type == DataChunk.Type.Buffer) { + copyBufferChunk(sourceField.valueB, f.valueB); + } else { + f.valueB.set(sourceField.valueB); + } + } + + } + + private static void copyBufferChunk(DataChunk source, DataChunk dest) { + final BufferChunk bc = source.getBufferChunk(); + int l = bc.getLength(); + byte[] bytes = new byte[l]; + final Buffer b = bc.getBuffer(); + int oldPos = b.position(); + try { + b.position(bc.getStart()); + bc.getBuffer().get(bytes, 0, l); + dest.setBytes(bytes); + } finally { + b.position(oldPos); + } + } + + // -------------------- Idx access to headers ---------- + /** + * Returns the current number of header fields. + */ + public int size() { + return count; + } + + public int trailerSize() { + return marked ? count - mark : 0; + } + + /** + * Returns the Nth header name, or null if there is no such header. This may be used to iterate through all header + * fields. + */ + public DataChunk getName(int n) { + return n >= 0 && n < count ? headers[n].getName() : null; + } + + /** + * Returns the Nth header value, or null if there is no such header. This may be used to iterate through all header + * fields. + */ + public DataChunk getValue(int n) { + return n >= 0 && n < count ? headers[n].getValue() : null; + } + + /** + * Get the header's "serialized" flag. + * + * @param n the header index + * @return the header's "serialized" flag value. + */ + public boolean isSerialized(int n) { + if (n >= 0 && n < count) { + final MimeHeaderField field = headers[n]; + return field.isSerialized(); + } + + return false; + } + + /** + * Set the header's "serialized" flag. + * + * @param n the header index + * @param newValue the new value + * @return the old header "serialized" flag value. + */ + public boolean setSerialized(int n, boolean newValue) { + final boolean value; + if (n >= 0 && n < count) { + final MimeHeaderField field = headers[n]; + value = field.isSerialized(); + field.setSerialized(newValue); + } else { + value = true; + } + + return value; + } + + /** + * Find the index of a header with the given name. + */ + public int indexOf(String name, int fromIndex) { + // We can use a hash - but it's not clear how much + // benefit you can get - there is an overhead + // and the number of headers is small (4-5 ?) + // Another problem is that we'll pay the overhead + // of constructing the hashtable + + // A custom search tree may be better + for (int i = fromIndex; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name)) { + return i; + } + } + return -1; + } + + /** + * Find the index of a header with the given name. + */ + public int indexOf(final Header header, final int fromIndex) { + // We can use a hash - but it's not clear how much + // benefit you can get - there is an overhead + // and the number of headers is small (4-5 ?) + // Another problem is that we'll pay the overhead + // of constructing the hashtable + + // A custom search tree may be better + final byte[] bytes = header.getLowerCaseBytes(); + for (int i = fromIndex; i < count; i++) { + if (headers[i].getName().equalsIgnoreCaseLowerCase(bytes)) { + return i; + } + } + return -1; + } + + public boolean contains(final Header header) { + return indexOf(header, 0) >= 0; + } + + public boolean contains(final String header) { + return indexOf(header, 0) >= 0; + } + + // -------------------- -------------------- + /** + * Returns an enumeration of strings representing the header field names. Field names may appear multiple times in this + * enumeration, indicating that multiple fields with that name exist in this header. + */ + public Iterable names() { + return namesIterable; + } + + public Iterable trailerNames() { + return new Iterable() { + + @Override + public Iterator iterator() { + return new NamesIterator(MimeHeaders.this, true); + } + }; + } + + public Iterable values(final String name) { + return new Iterable() { + + @Override + public Iterator iterator() { + return new ValuesIterator(MimeHeaders.this, name, false); + } + }; + } + + public Iterable values(final Header name) { + return values(name.toString()); + } + + public Iterable trailerValues(final String name) { + return new Iterable() { + + @Override + public Iterator iterator() { + return new ValuesIterator(MimeHeaders.this, name, true); + } + }; + } + + @SuppressWarnings("unused") + public Iterable trailerValues(final Header name) { + return trailerValues(name.toString()); + } + + // -------------------- Adding headers -------------------- + /** + * Adds a partially constructed field to the header. This field has not had its name or value initialized. + */ + private MimeHeaderField createHeader() { + if (maxNumHeaders >= 0 && count == maxNumHeaders) { + throw new MaxHeaderCountExceededException(); + } + MimeHeaderField mh; + int len = headers.length; + + if (count >= len) { + // expand header list array + int newCount = count * 2; + if (maxNumHeaders >= 0 && newCount > maxNumHeaders) { + newCount = maxNumHeaders; + } + MimeHeaderField tmp[] = new MimeHeaderField[newCount]; + System.arraycopy(headers, 0, tmp, 0, len); + headers = tmp; + } + if ((mh = headers[count]) == null) { + headers[count] = mh = new MimeHeaderField(); + } + count++; + return mh; + } + + /** + * Create a new named header , return the MessageBytes container for the new value + */ + public DataChunk addValue(String name) { + if (!isValidName(name)) { + return NOOP_CHUNK; + } + MimeHeaderField mh = createHeader(); + mh.getName().setString(name); + return mh.getValue(); + } + + /** + * Create a new named header , return the MessageBytes container for the new value + */ + public DataChunk addValue(final Header header) { + if (!isValidName(header)) { + return NOOP_CHUNK; + } + MimeHeaderField mh = createHeader(); + mh.getName().setBytes(header.toByteArray()); + return mh.getValue(); + } + + /** + * Create a new named header using un-translated byte[]. The conversion to chars can be delayed until encoding is known. + */ + public DataChunk addValue(final byte[] buffer, final int startN, final int len) { + if (!isValidName(buffer)) { + return NOOP_CHUNK; + } + MimeHeaderField mhf = createHeader(); + mhf.getName().setBytes(buffer, startN, startN + len); + return mhf.getValue(); + } + + /** + * Create a new named header using un-translated Buffer. The conversion to chars can be delayed until encoding is known. + */ + public DataChunk addValue(final Buffer buffer, final int startN, final int len) { + if (!isValidName(buffer)) { + return NOOP_CHUNK; + } + MimeHeaderField mhf = createHeader(); + mhf.getName().setBuffer(buffer, startN, startN + len); + return mhf.getValue(); + } + + /** + * Create a new named header, return the MessageBytes container for the new value. Add a Date header that is no longer + * added due to trailer support. + */ + public DataChunk addValueWithoutValidation(final Header header) { + MimeHeaderField mh = createHeader(); + mh.getName().setBytes(header.toByteArray()); + return mh.getValue(); + } + + /** + * Allow "set" operations - return a DataChunk container for the header value ( existing header or new if this . + */ + public DataChunk setValue(final String name) { + if (!isValidName(name)) { + return NOOP_CHUNK; + } + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name)) { + for (int j = i + 1; j < count; j++) { + if (headers[j].getName().equalsIgnoreCase(name)) { + removeHeader(j--); + } + } + return headers[i].getValue(); + } + } + MimeHeaderField mh = createHeader(); + mh.getName().setString(name); + return mh.getValue(); + } + + /** + * Allow "set" operations - return a DataChunk container for the header value ( existing header or new if this . + */ + public DataChunk setValue(final Header header) { + if (!isValidName(header)) { + return NOOP_CHUNK; + } + final byte[] bytes = header.getLowerCaseBytes(); + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCaseLowerCase(bytes)) { + for (int j = i + 1; j < count; j++) { + if (headers[j].getName().equalsIgnoreCaseLowerCase(bytes)) { + removeHeader(j--); + } + } + return headers[i].getValue(); + } + } + MimeHeaderField mh = createHeader(); + mh.getName().setBytes(header.toByteArray()); + + return mh.getValue(); + } + + // -------------------- Getting headers -------------------- + /** + * Finds and returns a header field with the given name. If no such field exists, null is returned. If more than one + * such field is in the header, an arbitrary one is returned. + */ + public DataChunk getValue(String name) { + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name)) { + return headers[i].getValue(); + } + } + return null; + } + + /** + * Finds and returns a header field with the given name. If no such field exists, null is returned. If more than one + * such field is in the header, an arbitrary one is returned. + */ + public DataChunk getValue(final Header header) { + final byte[] bytes = header.getLowerCaseBytes(); + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCaseLowerCase(bytes)) { + return headers[i].getValue(); + } + } + return null; + } + + // bad shortcut - it'll convert to string ( too early probably, + // encoding is guessed very late ) + public String getHeader(String name) { + DataChunk mh = getValue(name); + return mh != null ? mh.toString() : null; + } + + public String getHeader(final Header header) { + DataChunk mh = getValue(header); + return mh != null ? mh.toString() : null; + } + + // -------------------- Removing -------------------- + /** + * Removes a header field with the specified name. Does nothing if such a field could not be found. + * + * @param name the name of the header field to be removed + */ + public void removeHeader(String name) { + // XXX + // warning: rather sticky code; heavily tuned + + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name)) { + removeHeader(i--); + } + } + } + + public void removeHeader(final Header header) { + + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(header.getBytes())) { + removeHeader(i--); + } + } + + } + + /** + * Removes the headers with the given name whose values contain the given string. + * + * @param name The name of the headers to be removed + * @param str The string to check the header values against + */ + @SuppressWarnings("UnusedDeclaration") + public void removeHeader(final String name, final String str) { + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name) && getValue(i) != null && getValue(i).toString() != null && getValue(i).toString().contains(str)) { + removeHeader(i--); + } + } + } + + /** + * Removes the headers with the given name whose values contain the given string. + * + * @param name The name of the headers to be removed + * @param regex The regex string to check the header values against + */ + @SuppressWarnings("UnusedDeclaration") + public void removeHeaderMatches(final String name, final String regex) { + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCase(name) && getValue(i) != null && getValue(i).toString() != null && getValue(i).toString().matches(regex)) { + removeHeader(i--); + } + } + } + + /** + * Removes the headers with the given name whose values contain the given string. + * + * @param header The name of the {@link Header}s to be removed + * @param regex The regex string to check the header values against + */ + public void removeHeaderMatches(final Header header, final String regex) { + for (int i = 0; i < count; i++) { + if (headers[i].getName().equalsIgnoreCaseLowerCase(header.getLowerCaseBytes()) && getValue(i) != null && getValue(i).toString() != null + && getValue(i).toString().matches(regex)) { + removeHeader(i--); + } + } + } + + /** + * reset and swap with last header + * + * @param idx the index of the header to remove. + */ + void removeHeader(int idx) { + MimeHeaderField mh = headers[idx]; + + mh.recycle(); + headers[idx] = headers[count - 1]; + headers[count - 1] = mh; + count--; + } + + // ----------------------------------------------------- Max Header Handling + + public void setMaxNumHeaders(int maxNumHeaders) { + this.maxNumHeaders = maxNumHeaders; + } + + @SuppressWarnings("unused") + public int getMaxNumHeaders() { + return maxNumHeaders; + } + + public class MaxHeaderCountExceededException extends IllegalStateException { + + public MaxHeaderCountExceededException() { + super("Illegal attempt to exceed the configured maximum number of headers: " + maxNumHeaders); + } + + } // END MaxHeaderCountExceededException + + private boolean isValidName(final String name) { + return !marked || Arrays.binarySearch(INVALID_TRAILER_NAMES, name.toLowerCase()) < 0; + } + + private boolean isValidName(final Header name) { + return !marked || Arrays.binarySearch(INVALID_TRAILER_NAMES, name.getLowerCase()) < 0; + } + + private boolean isValidName(final byte[] name) { + return !marked || Arrays.binarySearch(INVALID_TRAILER_NAMES, new String(name).toLowerCase()) < 0; + } + + private boolean isValidName(final Buffer name) { + return !marked || Arrays.binarySearch(INVALID_TRAILER_NAMES, name.toStringContent().toLowerCase()) < 0; + } + +} + +abstract class BaseIterator implements Iterator { + int pos; + int size; + int currentPos; + + protected final MimeHeaders headers; + + public BaseIterator(final MimeHeaders headers) { + this.headers = headers; + } + + protected abstract void findNext(); + + @Override + public void remove() { + if (currentPos < 0) { + throw new IllegalStateException("No current element"); + } + headers.removeHeader(currentPos); + pos = currentPos; + currentPos = -1; + size--; + findNext(); + } +} + +/** + * Enumerate the distinct header names. Each nextElement() is O(n) ( a comparison is done with all previous elements ). + * This is less frequent than add() - we want to keep add O(1). + */ +class NamesIterator extends BaseIterator { + + String next; + + NamesIterator(MimeHeaders headers, final boolean trailersOnly) { + super(headers); + pos = trailersOnly ? headers.mark : 0; + size = headers.size(); + findNext(); + } + + @Override + protected void findNext() { + next = null; + for (; pos < size; pos++) { + next = headers.getName(pos).toString(); + for (int j = 0; j < pos; j++) { + if (headers.getName(j).equalsIgnoreCase(next)) { + // duplicate. + next = null; + break; + } + } + if (next != null) { + // it's not a duplicate + break; + } + } + // next time findNext is called it will try the + // next element + pos++; + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public String next() { + currentPos = pos - 1; + final String current = next; + findNext(); + return current; + } + +} // END NamesIterator + +/** + * Enumerate the values for a (possibly ) multiple value element. + */ +final class ValuesIterator extends BaseIterator { + + DataChunk next; + final String name; + + ValuesIterator(final MimeHeaders headers, final String name, final boolean trailersOnly) { + super(headers); + this.name = name; + pos = trailersOnly ? headers.mark : 0; + size = headers.size(); + findNext(); + } + + @Override + protected void findNext() { + next = null; + for (; pos < size; pos++) { + final DataChunk n1 = headers.getName(pos); + if (n1.equalsIgnoreCase(name)) { + next = headers.getValue(pos); + break; + } + } + pos++; + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public String next() { + currentPos = pos - 1; + final String current = next.toString(); + findNext(); + return current; + } + +} // END ValuesIterator + +final class MimeHeaderField { + + protected final DataChunk nameB = DataChunk.newInstance(); + protected final DataChunk valueB = DataChunk.newInstance(); + + private boolean isSerialized; + + /** + * Creates a new, uninitialized header field. + */ + public MimeHeaderField() { + } + + public void recycle() { + isSerialized = false; + nameB.recycle(); + valueB.recycle(); + } + + public DataChunk getName() { + return nameB; + } + + public DataChunk getValue() { + return valueB; + } + + public boolean isSerialized() { + return isSerialized; + } + + public void setSerialized(boolean isSerialized) { + this.isSerialized = isSerialized; + } +} // END MimeHeadersField