/* * 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.http2; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import org.apache.coyote.Adapter; import org.apache.coyote.CompressionConfig; import org.apache.coyote.Processor; import org.apache.coyote.Request; import org.apache.coyote.Response; import org.apache.coyote.UpgradeProtocol; import org.apache.coyote.UpgradeToken; import org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler; import org.apache.coyote.http11.upgrade.UpgradeProcessorInternal; import org.apache.tomcat.util.buf.StringUtils; import org.apache.tomcat.util.net.SocketWrapperBase; public class Http2Protocol implements UpgradeProtocol { static final long DEFAULT_READ_TIMEOUT = 10000; static final long DEFAULT_KEEP_ALIVE_TIMEOUT = -1; static final long DEFAULT_WRITE_TIMEOUT = 10000; // The HTTP/2 specification recommends a minimum default of 100 static final long DEFAULT_MAX_CONCURRENT_STREAMS = 200; // Maximum amount of streams which can be concurrently executed over // a single connection static final int DEFAULT_MAX_CONCURRENT_STREAM_EXECUTION = 20; // This default is defined by the HTTP/2 specification static final int DEFAULT_INITIAL_WINDOW_SIZE = (1 << 16) - 1; private static final String HTTP_UPGRADE_NAME = "h2c"; private static final String ALPN_NAME = "h2"; private static final byte[] ALPN_IDENTIFIER = ALPN_NAME.getBytes(StandardCharsets.UTF_8); // All timeouts in milliseconds private long readTimeout = DEFAULT_READ_TIMEOUT; private long keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT; private long writeTimeout = DEFAULT_WRITE_TIMEOUT; private long maxConcurrentStreams = DEFAULT_MAX_CONCURRENT_STREAMS; private int maxConcurrentStreamExecution = DEFAULT_MAX_CONCURRENT_STREAM_EXECUTION; // If a lower initial value is required, set it here but DO NOT change the // default defined above. private int initialWindowSize = DEFAULT_INITIAL_WINDOW_SIZE; // Limits private Set allowedTrailerHeaders = Collections.newSetFromMap(new ConcurrentHashMap()); private int maxHeaderCount = Constants.DEFAULT_MAX_HEADER_COUNT; private int maxHeaderSize = Constants.DEFAULT_MAX_HEADER_SIZE; private int maxTrailerCount = Constants.DEFAULT_MAX_TRAILER_COUNT; private int maxTrailerSize = Constants.DEFAULT_MAX_TRAILER_SIZE; private boolean initiatePingDisabled = false; private boolean useSendfile = true; // Compression private final CompressionConfig compressionConfig = new CompressionConfig(); @Override public String getHttpUpgradeName(boolean isSSLEnabled) { if (isSSLEnabled) { return null; } else { return HTTP_UPGRADE_NAME; } } @Override public byte[] getAlpnIdentifier() { return ALPN_IDENTIFIER; } @Override public String getAlpnName() { return ALPN_NAME; } @Override public Processor getProcessor(SocketWrapperBase socketWrapper, Adapter adapter) { UpgradeProcessorInternal processor = new UpgradeProcessorInternal(socketWrapper, new UpgradeToken(getInternalUpgradeHandler(socketWrapper, adapter, null), null, null)); return processor; } @Override public InternalHttpUpgradeHandler getInternalUpgradeHandler(SocketWrapperBase socketWrapper, Adapter adapter, Request coyoteRequest) { return socketWrapper.hasAsyncIO() ? new Http2AsyncUpgradeHandler(this, adapter, coyoteRequest) : new Http2UpgradeHandler(this, adapter, coyoteRequest); } @Override public boolean accept(Request request) { // Should only be one HTTP2-Settings header Enumeration settings = request.getMimeHeaders().values("HTTP2-Settings"); int count = 0; while (settings.hasMoreElements()) { count++; settings.nextElement(); } if (count != 1) { return false; } Enumeration connection = request.getMimeHeaders().values("Connection"); boolean found = false; while (connection.hasMoreElements() && !found) { found = connection.nextElement().contains("HTTP2-Settings"); } return found; } public long getReadTimeout() { return readTimeout; } public void setReadTimeout(long readTimeout) { this.readTimeout = readTimeout; } public long getKeepAliveTimeout() { return keepAliveTimeout; } public void setKeepAliveTimeout(long keepAliveTimeout) { this.keepAliveTimeout = keepAliveTimeout; } public long getWriteTimeout() { return writeTimeout; } public void setWriteTimeout(long writeTimeout) { this.writeTimeout = writeTimeout; } public long getMaxConcurrentStreams() { return maxConcurrentStreams; } public void setMaxConcurrentStreams(long maxConcurrentStreams) { this.maxConcurrentStreams = maxConcurrentStreams; } public int getMaxConcurrentStreamExecution() { return maxConcurrentStreamExecution; } public void setMaxConcurrentStreamExecution(int maxConcurrentStreamExecution) { this.maxConcurrentStreamExecution = maxConcurrentStreamExecution; } public int getInitialWindowSize() { return initialWindowSize; } public void setInitialWindowSize(int initialWindowSize) { this.initialWindowSize = initialWindowSize; } public boolean getUseSendfile() { return useSendfile; } public void setUseSendfile(boolean useSendfile) { this.useSendfile = useSendfile; } public void setAllowedTrailerHeaders(String commaSeparatedHeaders) { // Jump through some hoops so we don't end up with an empty set while // doing updates. Set toRemove = new HashSet<>(); toRemove.addAll(allowedTrailerHeaders); if (commaSeparatedHeaders != null) { String[] headers = commaSeparatedHeaders.split(","); for (String header : headers) { String trimmedHeader = header.trim().toLowerCase(Locale.ENGLISH); if (toRemove.contains(trimmedHeader)) { toRemove.remove(trimmedHeader); } else { allowedTrailerHeaders.add(trimmedHeader); } } allowedTrailerHeaders.removeAll(toRemove); } } public String getAllowedTrailerHeaders() { // Chances of a size change between these lines are small enough that a // sync is unnecessary. List copy = new ArrayList<>(allowedTrailerHeaders.size()); copy.addAll(allowedTrailerHeaders); return StringUtils.join(copy); } boolean isTrailerHeaderAllowed(String headerName) { return allowedTrailerHeaders.contains(headerName); } public void setMaxHeaderCount(int maxHeaderCount) { this.maxHeaderCount = maxHeaderCount; } public int getMaxHeaderCount() { return maxHeaderCount; } public void setMaxHeaderSize(int maxHeaderSize) { this.maxHeaderSize = maxHeaderSize; } public int getMaxHeaderSize() { return maxHeaderSize; } public void setMaxTrailerCount(int maxTrailerCount) { this.maxTrailerCount = maxTrailerCount; } public int getMaxTrailerCount() { return maxTrailerCount; } public void setMaxTrailerSize(int maxTrailerSize) { this.maxTrailerSize = maxTrailerSize; } public int getMaxTrailerSize() { return maxTrailerSize; } public void setInitiatePingDisabled(boolean initiatePingDisabled) { this.initiatePingDisabled = initiatePingDisabled; } public boolean getInitiatePingDisabled() { return initiatePingDisabled; } public void setCompression(String compression) { compressionConfig.setCompression(compression); } public String getCompression() { return compressionConfig.getCompression(); } protected int getCompressionLevel() { return compressionConfig.getCompressionLevel(); } public String getNoCompressionUserAgents() { return compressionConfig.getNoCompressionUserAgents(); } protected Pattern getNoCompressionUserAgentsPattern() { return compressionConfig.getNoCompressionUserAgentsPattern(); } public void setNoCompressionUserAgents(String noCompressionUserAgents) { compressionConfig.setNoCompressionUserAgents(noCompressionUserAgents); } public String getCompressibleMimeType() { return compressionConfig.getCompressibleMimeType(); } public void setCompressibleMimeType(String valueS) { compressionConfig.setCompressibleMimeType(valueS); } public String[] getCompressibleMimeTypes() { return compressionConfig.getCompressibleMimeTypes(); } public int getCompressionMinSize() { return compressionConfig.getCompressionMinSize(); } public void setCompressionMinSize(int compressionMinSize) { compressionConfig.setCompressionMinSize(compressionMinSize); } public boolean useCompression(Request request, Response response) { return compressionConfig.useCompression(request, response); } }