io.servicetalk.http.netty.HttpResponseDecoder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of servicetalk-http-netty Show documentation
Show all versions of servicetalk-http-netty Show documentation
A networking framework that evolves with your application
The newest version!
/*
* Copyright © 2018-2020 Apple Inc. and the ServiceTalk project authors
*
* 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 io.servicetalk.http.netty;
import io.servicetalk.http.api.HttpHeadersFactory;
import io.servicetalk.http.api.HttpRequestMethod;
import io.servicetalk.http.api.HttpResponseMetaData;
import io.servicetalk.http.api.HttpResponseStatus;
import io.servicetalk.transport.netty.internal.CloseHandler;
import io.servicetalk.transport.netty.internal.DefaultNettyConnection.CancelWriteUserEvent;
import io.servicetalk.transport.netty.internal.DefaultNettyConnection.ContinueUserEvent;
import io.servicetalk.utils.internal.IllegalCharacterException;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.DecoderException;
import io.netty.util.ByteProcessor;
import java.util.Deque;
import java.util.Queue;
import static io.netty.handler.codec.http.HttpConstants.HT;
import static io.netty.util.internal.StringUtil.EMPTY_STRING;
import static io.servicetalk.http.api.HttpHeaderNames.SEC_WEBSOCKET_ACCEPT;
import static io.servicetalk.http.api.HttpHeaderNames.UPGRADE;
import static io.servicetalk.http.api.HttpHeaderValues.WEBSOCKET;
import static io.servicetalk.http.api.HttpRequestMethod.CONNECT;
import static io.servicetalk.http.api.HttpRequestMethod.HEAD;
import static io.servicetalk.http.api.HttpResponseMetaDataFactory.newResponseMetaData;
import static io.servicetalk.http.api.HttpResponseStatus.CONTINUE;
import static io.servicetalk.http.api.HttpResponseStatus.NOT_MODIFIED;
import static io.servicetalk.http.api.HttpResponseStatus.NO_CONTENT;
import static io.servicetalk.http.api.HttpResponseStatus.SWITCHING_PROTOCOLS;
import static io.servicetalk.http.api.HttpResponseStatus.StatusClass.INFORMATIONAL_1XX;
import static io.servicetalk.http.api.HttpResponseStatus.StatusClass.SUCCESSFUL_2XX;
import static io.servicetalk.http.netty.HttpResponseDecoder.Signal.REQUEST_SIGNAL;
import static io.servicetalk.http.netty.HttpResponseDecoder.Signal.REQUEST_WITH_EXPECT_CONTINUE_SIGNAL;
import static java.lang.Math.min;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.util.Objects.requireNonNull;
final class HttpResponseDecoder extends HttpObjectDecoder {
enum Signal {
REQUEST_SIGNAL, REQUEST_WITH_EXPECT_CONTINUE_SIGNAL
}
private static final byte[] FIRST_BYTES = "HTTP/".getBytes(US_ASCII);
private static final byte DEL = 127;
private static final ByteProcessor ENSURE_REASON_PHRASE = value -> {
// Any control character (0x00-0x1F) except HT
if (((value & 0xE0) == 0 && value != HT) || value == DEL) {
throw new IllegalCharacterException(value, "HTAB / SP / VCHAR / obs-text");
}
return true;
};
private final Queue methodQueue;
private final Deque signalsQueue;
HttpResponseDecoder(final Queue methodQueue, final Deque signalsQueue,
final ByteBufAllocator alloc,
final HttpHeadersFactory headersFactory, final int maxStartLineLength, int maxHeaderFieldLength,
final boolean allowPrematureClosureBeforePayloadBody, final boolean allowLFWithoutCR,
final CloseHandler closeHandler) {
super(alloc, headersFactory, maxStartLineLength, maxHeaderFieldLength, allowPrematureClosureBeforePayloadBody,
allowLFWithoutCR, closeHandler);
this.methodQueue = requireNonNull(methodQueue);
this.signalsQueue = requireNonNull(signalsQueue);
}
@Override
protected boolean isDecodingRequest() {
return false;
}
@Override
protected void handlePartialInitialLine(final ChannelHandlerContext ctx, final ByteBuf buffer) {
final int len = min(FIRST_BYTES.length, buffer.readableBytes());
final int rIdx = buffer.readerIndex();
for (int i = 0; i < len; ++i) {
final byte b = buffer.getByte(rIdx + i);
if (b != FIRST_BYTES[i]) {
throw new StacklessDecoderException(
"Invalid start-line: HTTP response must start with HTTP-version: HTTP/1.x",
new IllegalCharacterException(b));
}
}
}
@Override
protected HttpResponseMetaData createMessage(final ByteBuf buffer, final int firstStart, final int firstLength,
final int secondStart, final int secondLength,
final int thirdStart, final int thirdLength) {
return newResponseMetaData(nettyBufferToHttpVersion(buffer, firstStart, firstLength),
HttpResponseStatus.of(nettyBufferToStatusCode(buffer, secondStart, secondLength),
reasonPhrase(buffer, thirdStart, thirdLength)),
headersFactory().newHeaders());
}
private static String reasonPhrase(final ByteBuf buffer, final int start, final int length) {
if (length <= 0) {
return EMPTY_STRING;
}
final String reasonPhrase = buffer.toString(start, length, US_ASCII);
try {
buffer.forEachByte(start, length, ENSURE_REASON_PHRASE);
} catch (IllegalCharacterException cause) {
throw new StacklessDecoderException("Invalid start-line: HTTP reason-phrase contains an illegal character",
cause);
}
return reasonPhrase;
}
@Override
protected boolean isContentAlwaysEmpty(final HttpResponseMetaData msg) {
// Don't poll from the queue for informational responses, because the real response is expected next.
// https://tools.ietf.org/html/rfc7230#section-3.3.3 (1): any response with a 1xx (Informational) status code
// cannot contain a message body:
if (msg.status().statusClass() == INFORMATIONAL_1XX) {
// One exception: Hixie 76 websocket handshake response
return !(msg.status().code() == SWITCHING_PROTOCOLS.code() &&
!msg.headers().contains(SEC_WEBSOCKET_ACCEPT) &&
msg.headers().containsIgnoreCase(UPGRADE, WEBSOCKET));
}
// This method has side effects on the methodQueue for the following reasons:
// - createMessage will not necessary fire a message up the pipeline.
// - the trigger points on the queue are currently symmetric for the request/response decoder and
// request/response encoder. We may use header information on the response decoder side, and the queue
// interaction is conditional (1xx responses don't touch the queue).
// - unit tests exist which verify these side effects occur, so if behavior of the internal classes changes the
// unit test should catch it.
// - this is the rough equivalent of what is done in Netty in terms of sequencing. Instead of trying to
// iterate a decoded list it makes some assumptions about the base class ordering of events.
HttpRequestMethod method = methodQueue.poll();
// https://tools.ietf.org/html/rfc7230#section-3.3.3 (2): Any 2xx (Successful) response to a CONNECT request
// implies that the connection will become a tunnel immediately after the empty line that concludes the header
// fields:
if (CONNECT.equals(method) && msg.status().statusClass() == SUCCESSFUL_2XX) {
return true;
}
// https://tools.ietf.org/html/rfc7230#section-3.3.3 (1): Any response to a HEAD request and any response with
// a 204 (No Content), or 304 (Not Modified) status code cannot contain a message body:
return HEAD.equals(method)
|| msg.status().code() == NO_CONTENT.code() || msg.status().code() == NOT_MODIFIED.code();
}
@Override
protected boolean isInterim(final HttpResponseMetaData msg) {
// All known informational status codes are interim and don't need to be propagated to the business logic
return msg.status().statusClass() == INFORMATIONAL_1XX;
}
@Override
protected void onMetaDataRead(final ChannelHandlerContext ctx, final HttpResponseMetaData msg) {
final Object signal = signalsQueue.poll();
assert signal != null;
if (signal != REQUEST_WITH_EXPECT_CONTINUE_SIGNAL) {
return;
}
// Process a response for "Expect: 100-continue" request.
final HttpResponseStatus status = msg.status();
if (status.code() == CONTINUE.code()) {
ctx.fireUserEventTriggered(ContinueUserEvent.INSTANCE);
// Offer REQUEST_SIGNAL to suppress any events for the final response.
signalsQueue.offerLast(REQUEST_SIGNAL);
} else if (status.statusClass() == SUCCESSFUL_2XX) {
// Write of payload body can continue for either 100 or 2XX response code:
ctx.fireUserEventTriggered(ContinueUserEvent.INSTANCE);
} else if (!isInterim(msg)) {
// All other non-interim responses should cancel ongoing write operation when write waits for continuation.
ctx.fireUserEventTriggered(CancelWriteUserEvent.INSTANCE);
}
}
@Override
protected void onDataSeen() {
// noop
}
private static int nettyBufferToStatusCode(final ByteBuf buffer, final int start, final int length) {
ensureStatusCodeLength(length);
final int medium = buffer.getUnsignedMedium(start);
try {
return toDecimal((medium & 0xff0000) >> 16) * 100 +
toDecimal((medium & 0xff00) >> 8) * 10 +
toDecimal(medium & 0xff);
} catch (IllegalCharacterException cause) {
throw new StacklessDecoderException("Invalid start-line: HTTP status-code must contain only 3 digits",
cause);
}
}
private static void ensureStatusCodeLength(final int length) {
if (length != 3) {
throw new DecoderException("Invalid start-line: HTTP status-code must contain only 3 digits but found: " +
length + " characters");
}
}
}