/*
* 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.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import org.apache.coyote.ProtocolException;
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.ByteBufferUtils;
import org.apache.tomcat.util.res.StringManager;
class Http2Parser {
private static final Log log = LogFactory.getLog(Http2Parser.class);
private static final StringManager sm = StringManager.getManager(Http2Parser.class);
static final byte[] CLIENT_PREFACE_START =
"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1);
private final String connectionId;
private final Input input;
private final Output output;
private final byte[] frameHeaderBuffer = new byte[9];
private volatile HpackDecoder hpackDecoder;
private volatile ByteBuffer headerReadBuffer =
ByteBuffer.allocate(Constants.DEFAULT_HEADER_READ_BUFFER_SIZE);
private volatile int headersCurrentStream = -1;
private volatile boolean headersEndStream = false;
private volatile boolean streamReset = false;
Http2Parser(String connectionId, Input input, Output output) {
this.connectionId = connectionId;
this.input = input;
this.output = output;
}
/**
* Read and process a single frame. Once the start of a frame is read, the
* remainder will be read using blocking IO.
*
* @param block Should this method block until a frame is available if no
* frame is available immediately?
*
* @return <code>true</code> if a frame was read otherwise
* <code>false</code>
*
* @throws IOException If an IO error occurs while trying to read a frame
*/
boolean readFrame(boolean block) throws Http2Exception, IOException {
return readFrame(block, null);
}
private boolean readFrame(boolean block, FrameType expected)
throws IOException, Http2Exception {
if (!input.fill(block, frameHeaderBuffer)) {
return false;
}
int payloadSize = ByteUtil.getThreeBytes(frameHeaderBuffer, 0);
FrameType frameType = FrameType.valueOf(ByteUtil.getOneByte(frameHeaderBuffer, 3));
int flags = ByteUtil.getOneByte(frameHeaderBuffer, 4);
int streamId = ByteUtil.get31Bits(frameHeaderBuffer, 5);
try {
validateFrame(expected, frameType, streamId, flags, payloadSize);
} catch (StreamException se) {
swallow(streamId, payloadSize, false);
throw se;
}
switch (frameType) {
case DATA:
readDataFrame(streamId, flags, payloadSize);
break;
case HEADERS:
readHeadersFrame(streamId, flags, payloadSize);
break;
case PRIORITY:
readPriorityFrame(streamId);
break;
case RST:
readRstFrame(streamId);
break;
case SETTINGS:
readSettingsFrame(flags, payloadSize);
break;
case PUSH_PROMISE:
readPushPromiseFrame(streamId);
break;
case PING:
readPingFrame(flags);
break;
case GOAWAY:
readGoawayFrame(payloadSize);
break;
case WINDOW_UPDATE:
readWindowUpdateFrame(streamId);
break;
case CONTINUATION:
readContinuationFrame(streamId, flags, payloadSize);
break;
case UNKNOWN:
readUnknownFrame(streamId, frameType, flags, payloadSize);
}
return true;
}
private void readDataFrame(int streamId, int flags, int payloadSize)
throws Http2Exception, IOException {
// Process the Stream
int padLength = 0;
boolean endOfStream = Flags.isEndOfStream(flags);
int dataLength;
if (Flags.hasPadding(flags)) {
byte[] b = new byte[1];
input.fill(true, b);
padLength = b[0] & 0xFF;
if (padLength >= payloadSize) {
throw new ConnectionException(
sm.getString("http2Parser.processFrame.tooMuchPadding", connectionId,
Integer.toString(streamId), Integer.toString(padLength),
Integer.toString(payloadSize)), Http2Error.PROTOCOL_ERROR);
}
// +1 is for the padding length byte we just read above
dataLength = payloadSize - (padLength + 1);
} else {
dataLength = payloadSize;
}
if (log.isDebugEnabled()) {
String padding;
if (Flags.hasPadding(flags)) {
padding = Integer.toString(padLength);
} else {
padding = "none";
}
log.debug(sm.getString("http2Parser.processFrameData.lengths", connectionId,
Integer.toString(streamId), Integer.toString(dataLength), padding));
}
ByteBuffer dest = output.startRequestBodyFrame(streamId, payloadSize);
if (dest == null) {
swallow(streamId, dataLength, false);
// Process padding before sending any notifications in case padding
// is invalid.
if (padLength > 0) {
swallow(streamId, padLength, true);
}
if (endOfStream) {
output.receivedEndOfStream(streamId);
}
} else {
synchronized (dest) {
if (dest.remaining() < dataLength) {
swallow(streamId, dataLength, false);
// Client has sent more data than permitted by Window size
throw new StreamException("Client sent more data than stream window allowed", Http2Error.FLOW_CONTROL_ERROR, streamId);
}
input.fill(true, dest, dataLength);
// Process padding before sending any notifications in case
// padding is invalid.
if (padLength > 0) {
swallow(streamId, padLength, true);
}
if (endOfStream) {
output.receivedEndOfStream(streamId);
}
output.endRequestBodyFrame(streamId);
}
}
if (padLength > 0) {
output.swallowedPadding(streamId, padLength);
}
}
private void readHeadersFrame(int streamId, int flags, int payloadSize)
throws Http2Exception, IOException {
headersEndStream = Flags.isEndOfStream(flags);
if (hpackDecoder == null) {
hpackDecoder = output.getHpackDecoder();
}
try {
hpackDecoder.setHeaderEmitter(output.headersStart(streamId, headersEndStream));
} catch (StreamException se) {
swallow(streamId, payloadSize, false);
throw se;
}
int padLength = 0;
boolean padding = Flags.hasPadding(flags);
boolean priority = Flags.hasPriority(flags);
int optionalLen = 0;
if (padding) {
optionalLen = 1;
}
if (priority) {
optionalLen += 5;
}
if (optionalLen > 0) {
byte[] optional = new byte[optionalLen];
input.fill(true, optional);
int optionalPos = 0;
if (padding) {
padLength = ByteUtil.getOneByte(optional, optionalPos++);
if (padLength >= payloadSize) {
throw new ConnectionException(
sm.getString("http2Parser.processFrame.tooMuchPadding", connectionId,
Integer.toString(streamId), Integer.toString(padLength),
Integer.toString(payloadSize)), Http2Error.PROTOCOL_ERROR);
}
}
if (priority) {
boolean exclusive = ByteUtil.isBit7Set(optional[optionalPos]);
int parentStreamId = ByteUtil.get31Bits(optional, optionalPos);
int weight = ByteUtil.getOneByte(optional, optionalPos + 4) + 1;
output.reprioritise(streamId, parentStreamId, exclusive, weight);
}
payloadSize -= optionalLen;
payloadSize -= padLength;
}
readHeaderPayload(streamId, payloadSize);
swallow(streamId, padLength, true);
if (Flags.isEndOfHeaders(flags)) {
onHeadersComplete(streamId);
} else {
headersCurrentStream = streamId;
}
}
private void readPriorityFrame(int streamId) throws Http2Exception, IOException {
byte[] payload = new byte[5];
input.fill(true, payload);
boolean exclusive = ByteUtil.isBit7Set(payload[0]);
int parentStreamId = ByteUtil.get31Bits(payload, 0);
int weight = ByteUtil.getOneByte(payload, 4) + 1;
if (streamId == parentStreamId) {
throw new StreamException(sm.getString("http2Parser.processFramePriority.invalidParent",
connectionId, Integer.valueOf(streamId)), Http2Error.PROTOCOL_ERROR, streamId);
}
output.reprioritise(streamId, parentStreamId, exclusive, weight);
}
private void readRstFrame(int streamId) throws Http2Exception, IOException {
byte[] payload = new byte[4];
input.fill(true, payload);
long errorCode = ByteUtil.getFourBytes(payload, 0);
output.reset(streamId, errorCode);
headersCurrentStream = -1;
headersEndStream = false;
}
private void readSettingsFrame(int flags, int payloadSize) throws Http2Exception, IOException {
boolean ack = Flags.isAck(flags);
if (payloadSize > 0 && ack) {
throw new ConnectionException(sm.getString(
"http2Parser.processFrameSettings.ackWithNonZeroPayload"),
Http2Error.FRAME_SIZE_ERROR);
}
if (payloadSize != 0) {
// Process the settings
byte[] setting = new byte[6];
for (int i = 0; i < payloadSize / 6; i++) {
input.fill(true, setting);
int id = ByteUtil.getTwoBytes(setting, 0);
long value = ByteUtil.getFourBytes(setting, 2);
output.setting(Setting.valueOf(id), value);
}
}
output.settingsEnd(ack);
}
private void readPushPromiseFrame(int streamId) throws Http2Exception {
throw new ConnectionException(sm.getString("http2Parser.processFramePushPromise",
connectionId, Integer.valueOf(streamId)), Http2Error.PROTOCOL_ERROR);
}
private void readPingFrame(int flags) throws IOException {
// Read the payload
byte[] payload = new byte[8];
input.fill(true, payload);
output.pingReceive(payload, Flags.isAck(flags));
}
private void readGoawayFrame(int payloadSize) throws IOException {
byte[] payload = new byte[payloadSize];
input.fill(true, payload);
int lastStreamId = ByteUtil.get31Bits(payload, 0);
long errorCode = ByteUtil.getFourBytes(payload, 4);
String debugData = null;
if (payloadSize > 8) {
debugData = new String(payload, 8, payloadSize - 8, StandardCharsets.UTF_8);
}
output.goaway(lastStreamId, errorCode, debugData);
}
private void readWindowUpdateFrame(int streamId) throws Http2Exception, IOException {
byte[] payload = new byte[4];
input.fill(true, payload);
int windowSizeIncrement = ByteUtil.get31Bits(payload, 0);
if (log.isDebugEnabled()) {
log.debug(sm.getString("http2Parser.processFrameWindowUpdate.debug", connectionId,
Integer.toString(streamId), Integer.toString(windowSizeIncrement)));
}
// Validate the data
if (windowSizeIncrement == 0) {
if (streamId == 0) {
throw new ConnectionException(
sm.getString("http2Parser.processFrameWindowUpdate.invalidIncrement"),
Http2Error.PROTOCOL_ERROR);
} else {
throw new StreamException(
sm.getString("http2Parser.processFrameWindowUpdate.invalidIncrement"),
Http2Error.PROTOCOL_ERROR, streamId);
}
}
output.incrementWindowSize(streamId, windowSizeIncrement);
}
private void readContinuationFrame(int streamId, int flags, int payloadSize)
throws Http2Exception, IOException {
if (headersCurrentStream == -1) {
// No headers to continue
throw new ConnectionException(sm.getString(
"http2Parser.processFrameContinuation.notExpected", connectionId,
Integer.toString(streamId)), Http2Error.PROTOCOL_ERROR);
}
readHeaderPayload(streamId, payloadSize);
if (Flags.isEndOfHeaders(flags)) {
onHeadersComplete(streamId);
headersCurrentStream = -1;
}
}
private void readHeaderPayload(int streamId, int payloadSize)
throws Http2Exception, IOException {
if (log.isDebugEnabled()) {
log.debug(sm.getString("http2Parser.processFrameHeaders.payload", connectionId,
Integer.valueOf(streamId), Integer.valueOf(payloadSize)));
}
int remaining = payloadSize;
while (remaining > 0) {
if (headerReadBuffer.remaining() == 0) {
// Buffer needs expansion
int newSize;
if (headerReadBuffer.capacity() < payloadSize) {
// First step, expand to the current payload. That should
// cover most cases.
newSize = payloadSize;
} else {
// Header must be spread over multiple frames. Keep doubling
// buffer size until the header can be read.
newSize = headerReadBuffer.capacity() * 2;
}
headerReadBuffer = ByteBufferUtils.expand(headerReadBuffer, newSize);
}
int toRead = Math.min(headerReadBuffer.remaining(), remaining);
// headerReadBuffer in write mode
input.fill(true, headerReadBuffer, toRead);
// switch to read mode
headerReadBuffer.flip();
try {
hpackDecoder.decode(headerReadBuffer);
} catch (HpackException hpe) {
throw new ConnectionException(
sm.getString("http2Parser.processFrameHeaders.decodingFailed"),
Http2Error.COMPRESSION_ERROR, hpe);
}
// switches to write mode
headerReadBuffer.compact();
remaining -= toRead;
if (hpackDecoder.isHeaderCountExceeded() && !streamReset) {
streamReset = true;
throw new StreamException(sm.getString("http2Parser.headerLimitCount", connectionId,
Integer.valueOf(streamId)), Http2Error.ENHANCE_YOUR_CALM, streamId);
}
if (hpackDecoder.isHeaderSizeExceeded(headerReadBuffer.position()) && !streamReset) {
streamReset = true;
throw new StreamException(sm.getString("http2Parser.headerLimitSize", connectionId,
Integer.valueOf(streamId)), Http2Error.ENHANCE_YOUR_CALM, streamId);
}
if (hpackDecoder.isHeaderSwallowSizeExceeded(headerReadBuffer.position())) {
throw new ConnectionException(sm.getString("http2Parser.headerLimitSize",
connectionId, Integer.valueOf(streamId)), Http2Error.ENHANCE_YOUR_CALM);
}
}
hpackDecoder.getHeaderEmitter().validateHeaders();
}
private void onHeadersComplete(int streamId) throws Http2Exception {
// Any left over data is a compression error
if (headerReadBuffer.position() > 0) {
throw new ConnectionException(
sm.getString("http2Parser.processFrameHeaders.decodingDataLeft"),
Http2Error.COMPRESSION_ERROR);
}
output.headersEnd(streamId);
if (headersEndStream) {
output.receivedEndOfStream(streamId);
headersEndStream = false;
}
// Reset size for new request if the buffer was previously expanded
if (headerReadBuffer.capacity() > Constants.DEFAULT_HEADER_READ_BUFFER_SIZE) {
headerReadBuffer = ByteBuffer.allocate(Constants.DEFAULT_HEADER_READ_BUFFER_SIZE);
}
// Clear the 'stream has been reset' flag, if set
if (streamReset) {
streamReset = false;
}
}
private void readUnknownFrame(int streamId, FrameType frameType, int flags, int payloadSize)
throws IOException {
try {
swallow(streamId, payloadSize, false);
} catch (ConnectionException e) {
// Will never happen because swallow() is called with mustBeZero set
// to false
}
output.swallowed(streamId, frameType, flags, payloadSize);
}
private void swallow(int streamId, int len, boolean mustBeZero)
throws IOException, ConnectionException {
if (log.isDebugEnabled()) {
log.debug(sm.getString("http2Parser.swallow.debug", connectionId,
Integer.toString(streamId), Integer.toString(len)));
}
if (len == 0) {
return;
}
int read = 0;
byte[] buffer = new byte[1024];
while (read < len) {
int thisTime = Math.min(buffer.length, len - read);
input.fill(true, buffer, 0, thisTime);
if (mustBeZero) {
// Validate the padding is zero since receiving non-zero padding
// is a strong indication of either a faulty client or a server
// side bug.
for (int i = 0; i < thisTime; i++) {
if (buffer[i] != 0) {
throw new ConnectionException(sm.getString("http2Parser.nonZeroPadding",
connectionId, Integer.toString(streamId)), Http2Error.PROTOCOL_ERROR);
}
}
}
read += thisTime;
}
}
/*
* Implementation note:
* Validation applicable to all incoming frames should be implemented here.
* Frame type specific validation should be performed in the appropriate
* readXxxFrame() method.
* For validation applicable to some but not all frame types, use your
* judgement.
*/
private void validateFrame(FrameType expected, FrameType frameType, int streamId, int flags,
int payloadSize) throws Http2Exception {
if (log.isDebugEnabled()) {
log.debug(sm.getString("http2Parser.processFrame", connectionId,
Integer.toString(streamId), frameType, Integer.toString(flags),
Integer.toString(payloadSize)));
}
if (expected != null && frameType != expected) {
throw new StreamException(sm.getString("http2Parser.processFrame.unexpectedType",
expected, frameType), Http2Error.PROTOCOL_ERROR, streamId);
}
int maxFrameSize = input.getMaxFrameSize();
if (payloadSize > maxFrameSize) {
throw new ConnectionException(sm.getString("http2Parser.payloadTooBig",
Integer.toString(payloadSize), Integer.toString(maxFrameSize)),
Http2Error.FRAME_SIZE_ERROR);
}
if (headersCurrentStream != -1) {
if (headersCurrentStream != streamId) {
throw new ConnectionException(sm.getString("http2Parser.headers.wrongStream",
connectionId, Integer.toString(headersCurrentStream),
Integer.toString(streamId)), Http2Error.COMPRESSION_ERROR);
}
if (frameType == FrameType.RST) {
// NO-OP: RST is OK here
} else if (frameType != FrameType.CONTINUATION) {
throw new ConnectionException(sm.getString("http2Parser.headers.wrongFrameType",
connectionId, Integer.toString(headersCurrentStream),
frameType), Http2Error.COMPRESSION_ERROR);
}
}
frameType.check(streamId, payloadSize);
}
/**
* Read and validate the connection preface from input using blocking IO.
*/
void readConnectionPreface() throws Http2Exception {
byte[] data = new byte[CLIENT_PREFACE_START.length];
try {
input.fill(true, data);
for (int i = 0; i < CLIENT_PREFACE_START.length; i++) {
if (CLIENT_PREFACE_START[i] != data[i]) {
throw new ProtocolException(sm.getString("http2Parser.preface.invalid"));
}
}
// Must always be followed by a settings frame
readFrame(true, FrameType.SETTINGS);
} catch (IOException ioe) {
throw new ProtocolException(sm.getString("http2Parser.preface.io"), ioe);
}
}
/**
* Interface that must be implemented by the source of data for the parser.
*/
static interface Input {
/**
* Fill the given array with data unless non-blocking is requested and
* no data is available. If any data is available then the buffer will
* be filled using blocking I/O.
*
* @param block Should the first read into the provided buffer be a
* blocking read or not.
* @param data Buffer to fill
* @param offset Position in buffer to start writing
* @param length Number of bytes to read
*
* @return <code>true</code> if the buffer was filled otherwise
* <code>false</code>
*
* @throws IOException If an I/O occurred while obtaining data with
* which to fill the buffer
*/
boolean fill(boolean block, byte[] data, int offset, int length) throws IOException;
default boolean fill(boolean block, byte[] data) throws IOException {
return fill(block, data, 0, data.length);
}
default boolean fill(boolean block, ByteBuffer data, int len) throws IOException {
boolean result = fill(block, data.array(), data.arrayOffset() + data.position(), len);
if (result) {
data.position(data.position() + len);
}
return result;
}
int getMaxFrameSize();
}
/**
* Interface that must be implemented to receive notifications from the
* parser as it processes incoming frames.
*/
static interface Output {
HpackDecoder getHpackDecoder();
// Data frames
ByteBuffer startRequestBodyFrame(int streamId, int payloadSize) throws Http2Exception;
void endRequestBodyFrame(int streamId) throws Http2Exception;
void receivedEndOfStream(int streamId) throws ConnectionException;
void swallowedPadding(int streamId, int paddingLength) throws ConnectionException, IOException;
// Header frames
HeaderEmitter headersStart(int streamId, boolean headersEndStream) throws Http2Exception;
void headersEnd(int streamId) throws ConnectionException;
// Priority frames (also headers)
void reprioritise(int streamId, int parentStreamId, boolean exclusive, int weight)
throws Http2Exception;
// Reset frames
void reset(int streamId, long errorCode) throws Http2Exception;
// Settings frames
void setting(Setting setting, long value) throws ConnectionException;
void settingsEnd(boolean ack) throws IOException;
// Ping frames
void pingReceive(byte[] payload, boolean ack) throws IOException;
// Goaway
void goaway(int lastStreamId, long errorCode, String debugData);
// Window size
void incrementWindowSize(int streamId, int increment) throws Http2Exception;
// Testing
void swallowed(int streamId, FrameType frameType, int flags, int size) throws IOException;
}
}