/*
* 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 org.apache.coyote.Adapter;
import org.apache.coyote.Processor;
import org.apache.coyote.Request;
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 = 200;
// 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<String> allowedTrailerHeaders =
Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
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;
@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) {
Http2UpgradeHandler result = (socketWrapper.hasAsyncIO())
? new Http2AsyncUpgradeHandler(adapter, coyoteRequest)
: new Http2UpgradeHandler(adapter, coyoteRequest);
result.setReadTimeout(getReadTimeout());
result.setKeepAliveTimeout(getKeepAliveTimeout());
result.setWriteTimeout(getWriteTimeout());
result.setMaxConcurrentStreams(getMaxConcurrentStreams());
result.setMaxConcurrentStreamExecution(getMaxConcurrentStreamExecution());
result.setInitialWindowSize(getInitialWindowSize());
result.setAllowedTrailerHeaders(allowedTrailerHeaders);
result.setMaxHeaderCount(getMaxHeaderCount());
result.setMaxHeaderSize(getMaxHeaderSize());
result.setMaxTrailerCount(getMaxTrailerCount());
result.setMaxTrailerSize(getMaxTrailerSize());
result.setInitiatePingDisabled(initiatePingDisabled);
return result;
}
@Override
public boolean accept(Request request) {
// Should only be one HTTP2-Settings header
Enumeration<String> settings = request.getMimeHeaders().values("HTTP2-Settings");
int count = 0;
while (settings.hasMoreElements()) {
count++;
settings.nextElement();
}
if (count != 1) {
return false;
}
Enumeration<String> 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 void setAllowedTrailerHeaders(String commaSeparatedHeaders) {
// Jump through some hoops so we don't end up with an empty set while
// doing updates.
Set<String> 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<String> copy = new ArrayList<>(allowedTrailerHeaders.size());
copy.addAll(allowedTrailerHeaders);
return StringUtils.join(copy);
}
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;
}
}