org.apache.coyote.http2.Stream Maven / Gradle / Ivy
/*
* 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.io.IOException;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import jakarta.servlet.RequestDispatcher;
import org.apache.coyote.ActionCode;
import org.apache.coyote.CloseNowException;
import org.apache.coyote.InputBuffer;
import org.apache.coyote.Request;
import org.apache.coyote.Response;
import org.apache.coyote.http11.HttpOutputBuffer;
import org.apache.coyote.http11.OutputFilter;
import org.apache.coyote.http11.filters.SavedRequestInputFilter;
import org.apache.coyote.http11.filters.VoidOutputFilter;
import org.apache.coyote.http2.HpackDecoder.HeaderEmitter;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.http.MimeHeaders;
import org.apache.tomcat.util.http.parser.Host;
import org.apache.tomcat.util.http.parser.Priority;
import org.apache.tomcat.util.net.ApplicationBufferHandler;
import org.apache.tomcat.util.net.WriteBuffer;
import org.apache.tomcat.util.res.StringManager;
class Stream extends AbstractNonZeroStream implements HeaderEmitter {
private static final Log log = LogFactory.getLog(Stream.class);
private static final StringManager sm = StringManager.getManager(Stream.class);
private static final int HEADER_STATE_START = 0;
private static final int HEADER_STATE_PSEUDO = 1;
private static final int HEADER_STATE_REGULAR = 2;
private static final int HEADER_STATE_TRAILER = 3;
private static final MimeHeaders ACK_HEADERS;
private static final Integer HTTP_UPGRADE_STREAM = Integer.valueOf(1);
private static final Set HTTP_CONNECTION_SPECIFIC_HEADERS = new HashSet<>();
static {
Response response = new Response();
response.setStatus(100);
StreamProcessor.prepareHeaders(null, response, true, null, null);
ACK_HEADERS = response.getMimeHeaders();
HTTP_CONNECTION_SPECIFIC_HEADERS.add("connection");
HTTP_CONNECTION_SPECIFIC_HEADERS.add("proxy-connection");
HTTP_CONNECTION_SPECIFIC_HEADERS.add("keep-alive");
HTTP_CONNECTION_SPECIFIC_HEADERS.add("transfer-encoding");
HTTP_CONNECTION_SPECIFIC_HEADERS.add("upgrade");
}
private volatile long contentLengthReceived = 0;
private final Http2UpgradeHandler handler;
private final WindowAllocationManager allocationManager = new WindowAllocationManager(this);
private final Request coyoteRequest;
private final Response coyoteResponse;
private final StreamInputBuffer inputBuffer;
private final StreamOutputBuffer streamOutputBuffer = new StreamOutputBuffer();
private final Http2OutputBuffer http2OutputBuffer;
private final AtomicBoolean removedFromActiveCount = new AtomicBoolean(false);
// State machine would be too much overhead
private int headerState = HEADER_STATE_START;
private StreamException headerException = null;
private volatile StringBuilder cookieHeader = null;
private volatile boolean hostHeaderSeen = false;
private Object pendingWindowUpdateForStreamLock = new Object();
private int pendingWindowUpdateForStream = 0;
private volatile int urgency = Priority.DEFAULT_URGENCY;
private volatile boolean incremental = Priority.DEFAULT_INCREMENTAL;
private final Object recycledLock = new Object();
private volatile boolean recycled = false;
Stream(Integer identifier, Http2UpgradeHandler handler) {
this(identifier, handler, null);
}
@SuppressWarnings("deprecation")
Stream(Integer identifier, Http2UpgradeHandler handler, Request coyoteRequest) {
super(handler.getConnectionId(), identifier);
this.handler = handler;
setWindowSize(handler.getRemoteSettings().getInitialWindowSize());
if (coyoteRequest == null) {
// HTTP/2 new request
this.coyoteRequest = handler.getProtocol().popRequestAndResponse();
this.coyoteResponse = this.coyoteRequest.getResponse();
this.inputBuffer = new StandardStreamInputBuffer();
this.coyoteRequest.setInputBuffer(inputBuffer);
} else {
// HTTP/2 Push or HTTP/1.1 upgrade
/*
* Implementation note. The request passed in is always newly created so it is safe to recycle it for re-use
* in the Stream.recyle() method. Need to create a matching, new response.
*/
this.coyoteRequest = coyoteRequest;
this.coyoteResponse = new Response();
this.coyoteRequest.setResponse(coyoteResponse);
this.inputBuffer =
new SavedRequestStreamInputBuffer((SavedRequestInputFilter) this.coyoteRequest.getInputBuffer());
// Headers have been read by this point
state.receivedStartOfHeaders();
if (HTTP_UPGRADE_STREAM.equals(identifier)) {
// Populate coyoteRequest from headers (HTTP/1.1 only)
try {
prepareRequest();
} catch (IllegalArgumentException iae) {
// Something in the headers is invalid
// Set correct return status
coyoteResponse.setStatus(400);
// Set error flag. This triggers error processing rather than
// the normal mapping
coyoteResponse.setError();
}
}
// Request body, if any, has been read and buffered
state.receivedEndOfStream();
}
this.coyoteRequest.setSendfile(handler.hasAsyncIO() && handler.getProtocol().getUseSendfile());
http2OutputBuffer = new Http2OutputBuffer(this.coyoteResponse, streamOutputBuffer);
this.coyoteResponse.setOutputBuffer(http2OutputBuffer);
this.coyoteRequest.setResponse(coyoteResponse);
this.coyoteRequest.protocol().setString("HTTP/2.0");
this.coyoteRequest.setStartTimeNanos(System.nanoTime());
}
private void prepareRequest() {
if (coyoteRequest.scheme().isNull()) {
if (handler.getProtocol().getHttp11Protocol().isSSLEnabled()) {
coyoteRequest.scheme().setString("https");
} else {
coyoteRequest.scheme().setString("http");
}
}
MessageBytes hostValueMB = coyoteRequest.getMimeHeaders().getUniqueValue("host");
if (hostValueMB == null) {
throw new IllegalArgumentException();
}
// This processing expects bytes. Server push will have used a String
// so trigger a conversion if required.
hostValueMB.toBytes();
ByteChunk valueBC = hostValueMB.getByteChunk();
byte[] valueB = valueBC.getBytes();
int valueL = valueBC.getLength();
int valueS = valueBC.getStart();
int colonPos = Host.parse(hostValueMB);
if (colonPos != -1) {
int port = 0;
for (int i = colonPos + 1; i < valueL; i++) {
char c = (char) valueB[i + valueS];
if (c < '0' || c > '9') {
throw new IllegalArgumentException();
}
port = port * 10 + c - '0';
}
coyoteRequest.setServerPort(port);
// Only need to copy the host name up to the :
valueL = colonPos;
}
// Extract the host name
char[] hostNameC = new char[valueL];
for (int i = 0; i < valueL; i++) {
hostNameC[i] = (char) valueB[i + valueS];
}
coyoteRequest.serverName().setChars(hostNameC, 0, valueL);
}
final void receiveReset(long errorCode) {
if (log.isTraceEnabled()) {
log.trace(
sm.getString("stream.reset.receive", getConnectionId(), getIdAsString(), Long.toString(errorCode)));
}
// Set the new state first since read and write both check this
state.receivedReset();
// Reads wait internally so need to call a method to break the wait()
inputBuffer.receiveReset();
cancelAllocationRequests();
}
final void cancelAllocationRequests() {
allocationManager.notifyAny();
}
@Override
final void incrementWindowSize(int windowSizeIncrement) throws Http2Exception {
windowAllocationLock.lock();
try {
// If this is zero then any thread that has been trying to write for
// this stream will be waiting. Notify that thread it can continue. Use
// notify all even though only one thread is waiting to be on the safe
// side.
boolean notify = getWindowSize() < 1;
super.incrementWindowSize(windowSizeIncrement);
if (notify && getWindowSize() > 0) {
allocationManager.notifyStream();
}
} finally {
windowAllocationLock.unlock();
}
}
final int reserveWindowSize(int reservation, boolean block) throws IOException {
windowAllocationLock.lock();
try {
long windowSize = getWindowSize();
while (windowSize < 1) {
if (!canWrite()) {
throw new CloseNowException(sm.getString("stream.notWritable", getConnectionId(), getIdAsString()));
}
if (block) {
try {
long writeTimeout = handler.getProtocol().getStreamWriteTimeout();
allocationManager.waitForStream(writeTimeout);
windowSize = getWindowSize();
if (windowSize == 0) {
doStreamCancel(sm.getString("stream.writeTimeout"), Http2Error.ENHANCE_YOUR_CALM);
}
} catch (InterruptedException e) {
// Possible shutdown / rst or similar. Use an IOException to
// signal to the client that further I/O isn't possible for this
// Stream.
throw new IOException(e);
}
} else {
allocationManager.waitForStreamNonBlocking();
return 0;
}
}
int allocation;
if (windowSize < reservation) {
allocation = (int) windowSize;
} else {
allocation = reservation;
}
decrementWindowSize(allocation);
return allocation;
} finally {
windowAllocationLock.unlock();
}
}
@SuppressWarnings("deprecation")
void doStreamCancel(String msg, Http2Error error) throws CloseNowException {
StreamException se = new StreamException(msg, error, getIdAsInt());
// Prevent the application making further writes
streamOutputBuffer.closed = true;
// Prevent Tomcat's error handling trying to write
coyoteResponse.setError();
coyoteResponse.setErrorReported();
// Trigger a reset once control returns to Tomcat
streamOutputBuffer.reset = se;
throw new CloseNowException(msg, se);
}
void waitForConnectionAllocation(long timeout) throws InterruptedException {
allocationManager.waitForConnection(timeout);
}
void waitForConnectionAllocationNonBlocking() {
allocationManager.waitForConnectionNonBlocking();
}
void notifyConnection() {
allocationManager.notifyConnection();
}
@Override
public final void emitHeader(String name, String value) throws HpackException {
if (log.isTraceEnabled()) {
log.trace(sm.getString("stream.header.debug", getConnectionId(), getIdAsString(), name, value));
}
// Header names must be lower case
if (!name.toLowerCase(Locale.US).equals(name)) {
throw new HpackException(sm.getString("stream.header.case", getConnectionId(), getIdAsString(), name));
}
if (HTTP_CONNECTION_SPECIFIC_HEADERS.contains(name)) {
throw new HpackException(
sm.getString("stream.header.connection", getConnectionId(), getIdAsString(), name));
}
if ("te".equals(name)) {
if (!"trailers".equals(value)) {
throw new HpackException(sm.getString("stream.header.te", getConnectionId(), getIdAsString(), value));
}
}
if (headerException != null) {
// Don't bother processing the header since the stream is going to
// be reset anyway
return;
}
if (name.length() == 0) {
throw new HpackException(sm.getString("stream.header.empty", getConnectionId(), getIdAsString()));
}
boolean pseudoHeader = name.charAt(0) == ':';
if (pseudoHeader && headerState != HEADER_STATE_PSEUDO) {
headerException = new StreamException(
sm.getString("stream.header.unexpectedPseudoHeader", getConnectionId(), getIdAsString(), name),
Http2Error.PROTOCOL_ERROR, getIdAsInt());
// No need for further processing. The stream will be reset.
return;
}
if (headerState == HEADER_STATE_PSEUDO && !pseudoHeader) {
headerState = HEADER_STATE_REGULAR;
}
switch (name) {
case ":method": {
if (coyoteRequest.method().isNull()) {
coyoteRequest.method().setString(value);
if ("HEAD".equals(value)) {
configureVoidOutputFilter();
}
} else {
throw new HpackException(
sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":method"));
}
break;
}
case ":scheme": {
if (coyoteRequest.scheme().isNull()) {
coyoteRequest.scheme().setString(value);
} else {
throw new HpackException(
sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":scheme"));
}
break;
}
case ":path": {
if (!coyoteRequest.requestURI().isNull()) {
throw new HpackException(
sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":path"));
}
if (value.length() == 0) {
throw new HpackException(sm.getString("stream.header.noPath", getConnectionId(), getIdAsString()));
}
int queryStart = value.indexOf('?');
String uri;
if (queryStart == -1) {
uri = value;
} else {
uri = value.substring(0, queryStart);
String query = value.substring(queryStart + 1);
coyoteRequest.queryString().setString(query);
}
// Bug 61120. Set the URI as bytes rather than String so:
// - any path parameters are correctly processed
// - the normalization security checks are performed that prevent
// directory traversal attacks
byte[] uriBytes = uri.getBytes(StandardCharsets.ISO_8859_1);
coyoteRequest.requestURI().setBytes(uriBytes, 0, uriBytes.length);
break;
}
case ":authority": {
if (coyoteRequest.serverName().isNull()) {
parseAuthority(value, false);
} else {
throw new HpackException(
sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), ":authority"));
}
break;
}
case "cookie": {
// Cookie headers need to be concatenated into a single header
// See RFC 7540 8.1.2.5
if (cookieHeader == null) {
cookieHeader = new StringBuilder();
} else {
cookieHeader.append("; ");
}
cookieHeader.append(value);
break;
}
case "host": {
if (coyoteRequest.serverName().isNull()) {
// No :authority header. This is first host header. Use it.
hostHeaderSeen = true;
parseAuthority(value, true);
} else if (!hostHeaderSeen) {
// First host header - must be consistent with :authority
hostHeaderSeen = true;
compareAuthority(value);
} else {
// Multiple hosts headers - illegal
throw new HpackException(
sm.getString("stream.header.duplicate", getConnectionId(), getIdAsString(), "host"));
}
break;
}
case "priority": {
try {
Priority p = Priority.parsePriority(new StringReader(value));
setUrgency(p.getUrgency());
setIncremental(p.getIncremental());
} catch (IOException ioe) {
// Not possible with StringReader
}
break;
}
default: {
if (headerState == HEADER_STATE_TRAILER && !handler.getProtocol().isTrailerHeaderAllowed(name)) {
break;
}
if ("expect".equals(name) && "100-continue".equals(value)) {
coyoteRequest.setExpectation(true);
}
if (pseudoHeader) {
headerException = new StreamException(
sm.getString("stream.header.unknownPseudoHeader", getConnectionId(), getIdAsString(), name),
Http2Error.PROTOCOL_ERROR, getIdAsInt());
}
if (headerState == HEADER_STATE_TRAILER) {
// HTTP/2 headers are already always lower case
coyoteRequest.getMimeTrailerFields().addValue(name).setString(value);
} else {
coyoteRequest.getMimeHeaders().addValue(name).setString(value);
}
}
}
}
void configureVoidOutputFilter() {
addOutputFilter(new VoidOutputFilter());
// Prevent further writes by the application
streamOutputBuffer.closed = true;
}
private void parseAuthority(String value, boolean host) throws HpackException {
int i;
try {
i = Host.parse(value);
} catch (IllegalArgumentException iae) {
// Host value invalid
throw new HpackException(sm.getString("stream.header.invalid", getConnectionId(), getIdAsString(),
host ? "host" : ":authority", value));
}
if (i > -1) {
coyoteRequest.serverName().setString(value.substring(0, i));
coyoteRequest.setServerPort(Integer.parseInt(value.substring(i + 1)));
} else {
coyoteRequest.serverName().setString(value);
}
}
private void compareAuthority(String value) throws HpackException {
int i;
try {
i = Host.parse(value);
} catch (IllegalArgumentException iae) {
// Host value invalid
throw new HpackException(
sm.getString("stream.header.invalid", getConnectionId(), getIdAsString(), "host", value));
}
if (i == -1 && (!value.equals(coyoteRequest.serverName().getString()) || coyoteRequest.getServerPort() != -1) ||
i > -1 && ((!value.substring(0, i).equals(coyoteRequest.serverName().getString()) ||
Integer.parseInt(value.substring(i + 1)) != coyoteRequest.getServerPort()))) {
// Host value inconsistent
throw new HpackException(sm.getString("stream.host.inconsistent", getConnectionId(), getIdAsString(), value,
coyoteRequest.serverName().getString(), Integer.toString(coyoteRequest.getServerPort())));
}
}
@Override
public void setHeaderException(StreamException streamException) {
if (headerException == null) {
headerException = streamException;
}
}
@Override
public void validateHeaders() throws StreamException {
if (headerException == null) {
return;
}
handler.getHpackDecoder().setHeaderEmitter(Http2UpgradeHandler.HEADER_SINK);
throw headerException;
}
final boolean receivedEndOfHeaders() throws ConnectionException {
if (coyoteRequest.method().isNull() || coyoteRequest.scheme().isNull() ||
!coyoteRequest.method().equals("CONNECT") && coyoteRequest.requestURI().isNull()) {
throw new ConnectionException(sm.getString("stream.header.required", getConnectionId(), getIdAsString()),
Http2Error.PROTOCOL_ERROR);
}
// Cookie headers need to be concatenated into a single header
// See RFC 7540 8.1.2.5
// Can only do this once the headers are fully received
if (cookieHeader != null) {
coyoteRequest.getMimeHeaders().addValue("cookie").setString(cookieHeader.toString());
}
return headerState == HEADER_STATE_REGULAR || headerState == HEADER_STATE_PSEUDO;
}
final void writeHeaders() throws IOException {
boolean endOfStream = streamOutputBuffer.hasNoBody() && coyoteResponse.getTrailerFields() == null;
handler.writeHeaders(this, 0, coyoteResponse.getMimeHeaders(), endOfStream,
Constants.DEFAULT_HEADERS_FRAME_SIZE);
}
final void addOutputFilter(OutputFilter filter) {
http2OutputBuffer.addFilter(filter);
}
final void writeTrailers() throws IOException {
Supplier
© 2015 - 2024 Weber Informatics LLC | Privacy Policy