blob: c2d02f0cf677b106343ba50f5aa094a7f30fbb0b [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "quic/core/http/web_transport_http3.h"
#include <limits>
#include <memory>
#include "absl/strings/string_view.h"
#include "absl/types/optional.h"
#include "quic/core/http/capsule.h"
#include "quic/core/http/quic_spdy_session.h"
#include "quic/core/http/quic_spdy_stream.h"
#include "quic/core/quic_data_reader.h"
#include "quic/core/quic_data_writer.h"
#include "quic/core/quic_error_codes.h"
#include "quic/core/quic_stream.h"
#include "quic/core/quic_types.h"
#include "quic/core/quic_utils.h"
#include "quic/core/quic_versions.h"
#include "quic/platform/api/quic_bug_tracker.h"
#include "common/platform/api/quiche_logging.h"
#define ENDPOINT \
(session_->perspective() == Perspective::IS_SERVER ? "Server: " : "Client: ")
namespace quic {
namespace {
class QUIC_NO_EXPORT NoopWebTransportVisitor : public WebTransportVisitor {
void OnSessionReady(const spdy::SpdyHeaderBlock&) override {}
void OnSessionClosed(WebTransportSessionError /*error_code*/,
const std::string& /*error_message*/) override {}
void OnIncomingBidirectionalStreamAvailable() override {}
void OnIncomingUnidirectionalStreamAvailable() override {}
void OnDatagramReceived(absl::string_view /*datagram*/) override {}
void OnCanCreateNewOutgoingBidirectionalStream() override {}
void OnCanCreateNewOutgoingUnidirectionalStream() override {}
};
} // namespace
WebTransportHttp3::WebTransportHttp3(QuicSpdySession* session,
QuicSpdyStream* connect_stream,
WebTransportSessionId id,
bool attempt_to_use_datagram_contexts)
: session_(session),
connect_stream_(connect_stream),
id_(id),
visitor_(std::make_unique<NoopWebTransportVisitor>()) {
QUICHE_DCHECK(session_->SupportsWebTransport());
QUICHE_DCHECK(IsValidWebTransportSessionId(id, session_->version()));
QUICHE_DCHECK_EQ(connect_stream_->id(), id);
connect_stream_->RegisterHttp3DatagramRegistrationVisitor(
this, attempt_to_use_datagram_contexts);
if (session_->perspective() == Perspective::IS_CLIENT) {
context_is_known_ = true;
context_currently_registered_ = true;
if (attempt_to_use_datagram_contexts) {
context_id_ = connect_stream_->GetNextDatagramContextId();
}
}
}
void WebTransportHttp3::AssociateStream(QuicStreamId stream_id) {
streams_.insert(stream_id);
ParsedQuicVersion version = session_->version();
if (QuicUtils::IsOutgoingStreamId(version, stream_id,
session_->perspective())) {
return;
}
if (QuicUtils::IsBidirectionalStreamId(stream_id, version)) {
incoming_bidirectional_streams_.push_back(stream_id);
visitor_->OnIncomingBidirectionalStreamAvailable();
} else {
incoming_unidirectional_streams_.push_back(stream_id);
visitor_->OnIncomingUnidirectionalStreamAvailable();
}
}
void WebTransportHttp3::OnConnectStreamClosing() {
// Copy the stream list before iterating over it, as calls to ResetStream()
// can potentially mutate the |session_| list.
std::vector<QuicStreamId> streams(streams_.begin(), streams_.end());
streams_.clear();
for (QuicStreamId id : streams) {
session_->ResetStream(id, QUIC_STREAM_WEBTRANSPORT_SESSION_GONE);
}
if (context_currently_registered_) {
context_currently_registered_ = false;
connect_stream_->UnregisterHttp3DatagramContextId(context_id_);
}
connect_stream_->UnregisterHttp3DatagramRegistrationVisitor();
MaybeNotifyClose();
}
void WebTransportHttp3::CloseSession(WebTransportSessionError error_code,
absl::string_view error_message) {
if (close_sent_) {
QUIC_BUG(WebTransportHttp3 close sent twice)
<< "Calling WebTransportHttp3::CloseSession() more than once is not "
"allowed.";
return;
}
close_sent_ = true;
// There can be a race between us trying to send our close and peer sending
// one. If we received a close, however, we cannot send ours since we already
// closed the stream in response.
if (close_received_) {
QUIC_DLOG(INFO) << "Not sending CLOSE_WEBTRANSPORT_SESSION as we've "
"already sent one from peer.";
return;
}
error_code_ = error_code;
error_message_ = std::string(error_message);
QuicConnection::ScopedPacketFlusher flusher(
connect_stream_->spdy_session()->connection());
connect_stream_->WriteCapsule(
Capsule::CloseWebTransportSession(error_code, error_message),
/*fin=*/true);
}
void WebTransportHttp3::OnCloseReceived(WebTransportSessionError error_code,
absl::string_view error_message) {
if (close_received_) {
QUIC_BUG(WebTransportHttp3 notified of close received twice)
<< "WebTransportHttp3::OnCloseReceived() may be only called once.";
}
close_received_ = true;
// If the peer has sent a close after we sent our own, keep the local error.
if (close_sent_) {
QUIC_DLOG(INFO) << "Ignoring received CLOSE_WEBTRANSPORT_SESSION as we've "
"already sent our own.";
return;
}
error_code_ = error_code;
error_message_ = std::string(error_message);
connect_stream_->WriteOrBufferBody("", /*fin=*/true);
MaybeNotifyClose();
}
void WebTransportHttp3::OnConnectStreamFinReceived() {
// If we already received a CLOSE_WEBTRANSPORT_SESSION capsule, we don't need
// to do anything about receiving a FIN, since we already sent one in
// response.
if (close_received_) {
return;
}
close_received_ = true;
if (close_sent_) {
QUIC_DLOG(INFO) << "Ignoring received FIN as we've already sent our close.";
return;
}
connect_stream_->WriteOrBufferBody("", /*fin=*/true);
MaybeNotifyClose();
}
void WebTransportHttp3::CloseSessionWithFinOnlyForTests() {
QUICHE_DCHECK(!close_sent_);
close_sent_ = true;
if (close_received_) {
return;
}
connect_stream_->WriteOrBufferBody("", /*fin=*/true);
}
void WebTransportHttp3::HeadersReceived(const spdy::SpdyHeaderBlock& headers) {
if (session_->perspective() == Perspective::IS_CLIENT) {
int status_code;
if (!QuicSpdyStream::ParseHeaderStatusCode(headers, &status_code)) {
QUIC_DVLOG(1) << ENDPOINT
<< "Received WebTransport headers from server without "
"a valid status code, rejecting.";
rejection_reason_ = WebTransportHttp3RejectionReason::kNoStatusCode;
return;
}
bool valid_status = status_code >= 200 && status_code <= 299;
if (!valid_status) {
QUIC_DVLOG(1) << ENDPOINT
<< "Received WebTransport headers from server with "
"status code "
<< status_code << ", rejecting.";
rejection_reason_ = WebTransportHttp3RejectionReason::kWrongStatusCode;
return;
}
bool should_validate_version =
session_->http_datagram_support() != HttpDatagramSupport::kDraft00 &&
session_->ShouldValidateWebTransportVersion();
if (should_validate_version) {
auto draft_version_it = headers.find("sec-webtransport-http3-draft");
if (draft_version_it == headers.end()) {
QUIC_DVLOG(1) << ENDPOINT
<< "Received WebTransport headers from server without "
"a draft version, rejecting.";
rejection_reason_ =
WebTransportHttp3RejectionReason::kMissingDraftVersion;
return;
}
if (draft_version_it->second != "draft02") {
QUIC_DVLOG(1) << ENDPOINT
<< "Received WebTransport headers from server with "
"an unknown draft version ("
<< draft_version_it->second << "), rejecting.";
rejection_reason_ =
WebTransportHttp3RejectionReason::kUnsupportedDraftVersion;
return;
}
}
}
QUIC_DVLOG(1) << ENDPOINT << "WebTransport session " << id_ << " ready.";
ready_ = true;
visitor_->OnSessionReady(headers);
session_->ProcessBufferedWebTransportStreamsForSession(this);
}
WebTransportStream* WebTransportHttp3::AcceptIncomingBidirectionalStream() {
while (!incoming_bidirectional_streams_.empty()) {
QuicStreamId id = incoming_bidirectional_streams_.front();
incoming_bidirectional_streams_.pop_front();
QuicSpdyStream* stream = session_->GetOrCreateSpdyDataStream(id);
if (stream == nullptr) {
// Skip the streams that were reset in between the time they were
// receieved and the time the client has polled for them.
continue;
}
return stream->web_transport_stream();
}
return nullptr;
}
WebTransportStream* WebTransportHttp3::AcceptIncomingUnidirectionalStream() {
while (!incoming_unidirectional_streams_.empty()) {
QuicStreamId id = incoming_unidirectional_streams_.front();
incoming_unidirectional_streams_.pop_front();
QuicStream* stream = session_->GetOrCreateStream(id);
if (stream == nullptr) {
// Skip the streams that were reset in between the time they were
// receieved and the time the client has polled for them.
continue;
}
return static_cast<WebTransportHttp3UnidirectionalStream*>(stream)
->interface();
}
return nullptr;
}
bool WebTransportHttp3::CanOpenNextOutgoingBidirectionalStream() {
return session_->CanOpenOutgoingBidirectionalWebTransportStream(id_);
}
bool WebTransportHttp3::CanOpenNextOutgoingUnidirectionalStream() {
return session_->CanOpenOutgoingUnidirectionalWebTransportStream(id_);
}
WebTransportStream* WebTransportHttp3::OpenOutgoingBidirectionalStream() {
QuicSpdyStream* stream =
session_->CreateOutgoingBidirectionalWebTransportStream(this);
if (stream == nullptr) {
// If stream cannot be created due to flow control or other errors, return
// nullptr.
return nullptr;
}
return stream->web_transport_stream();
}
WebTransportStream* WebTransportHttp3::OpenOutgoingUnidirectionalStream() {
WebTransportHttp3UnidirectionalStream* stream =
session_->CreateOutgoingUnidirectionalWebTransportStream(this);
if (stream == nullptr) {
// If stream cannot be created due to flow control, return nullptr.
return nullptr;
}
return stream->interface();
}
MessageStatus WebTransportHttp3::SendOrQueueDatagram(QuicMemSlice datagram) {
return connect_stream_->SendHttp3Datagram(
context_id_, absl::string_view(datagram.data(), datagram.length()));
}
QuicByteCount WebTransportHttp3::GetMaxDatagramSize() const {
return connect_stream_->GetMaxDatagramSize(context_id_);
}
void WebTransportHttp3::SetDatagramMaxTimeInQueue(
QuicTime::Delta max_time_in_queue) {
connect_stream_->SetMaxDatagramTimeInQueue(max_time_in_queue);
}
void WebTransportHttp3::OnHttp3Datagram(
QuicStreamId stream_id, absl::optional<QuicDatagramContextId> context_id,
absl::string_view payload) {
QUICHE_DCHECK_EQ(stream_id, connect_stream_->id());
QUICHE_DCHECK(context_id == context_id_);
visitor_->OnDatagramReceived(payload);
}
void WebTransportHttp3::OnContextReceived(
QuicStreamId stream_id, absl::optional<QuicDatagramContextId> context_id,
DatagramFormatType format_type, absl::string_view format_additional_data) {
if (stream_id != connect_stream_->id()) {
QUIC_BUG(WT3 bad datagram context registration)
<< ENDPOINT << "Registered stream ID " << stream_id << ", expected "
<< connect_stream_->id();
return;
}
if (format_type != DatagramFormatType::WEBTRANSPORT) {
QUIC_DLOG(INFO) << ENDPOINT << "Ignoring unexpected datagram format type "
<< DatagramFormatTypeToString(format_type);
return;
}
if (!format_additional_data.empty()) {
QUIC_DLOG(ERROR)
<< ENDPOINT
<< "Received non-empty format additional data for context ID "
<< (context_id_.has_value() ? context_id_.value() : 0)
<< " on stream ID " << connect_stream_->id();
session_->ResetStream(connect_stream_->id(), QUIC_BAD_APPLICATION_PAYLOAD);
return;
}
if (!context_is_known_) {
context_is_known_ = true;
context_id_ = context_id;
}
if (context_id != context_id_) {
QUIC_DLOG(INFO) << ENDPOINT << "Ignoring unexpected context ID "
<< (context_id.has_value() ? context_id.value() : 0)
<< " instead of "
<< (context_id_.has_value() ? context_id_.value() : 0)
<< " on stream ID " << connect_stream_->id();
return;
}
if (session_->perspective() == Perspective::IS_SERVER) {
if (context_currently_registered_) {
QUIC_DLOG(ERROR) << ENDPOINT << "Received duplicate context ID "
<< (context_id_.has_value() ? context_id_.value() : 0)
<< " on stream ID " << connect_stream_->id();
session_->ResetStream(connect_stream_->id(), QUIC_STREAM_CANCELLED);
return;
}
context_currently_registered_ = true;
connect_stream_->RegisterHttp3DatagramContextId(
context_id_, format_type, format_additional_data, this);
}
}
void WebTransportHttp3::OnContextClosed(
QuicStreamId stream_id, absl::optional<QuicDatagramContextId> context_id,
ContextCloseCode close_code, absl::string_view close_details) {
if (stream_id != connect_stream_->id()) {
QUIC_BUG(WT3 bad datagram context registration)
<< ENDPOINT << "Closed context on stream ID " << stream_id
<< ", expected " << connect_stream_->id();
return;
}
if (context_id != context_id_) {
QUIC_DLOG(INFO) << ENDPOINT << "Ignoring unexpected close of context ID "
<< (context_id.has_value() ? context_id.value() : 0)
<< " instead of "
<< (context_id_.has_value() ? context_id_.value() : 0)
<< " on stream ID " << connect_stream_->id();
return;
}
QUIC_DLOG(INFO) << ENDPOINT
<< "Received datagram context close with close code "
<< close_code << " close details \"" << close_details
<< "\" on stream ID " << connect_stream_->id()
<< ", resetting stream";
session_->ResetStream(connect_stream_->id(), QUIC_BAD_APPLICATION_PAYLOAD);
}
void WebTransportHttp3::MaybeNotifyClose() {
if (close_notified_) {
return;
}
close_notified_ = true;
visitor_->OnSessionClosed(error_code_, error_message_);
}
WebTransportHttp3UnidirectionalStream::WebTransportHttp3UnidirectionalStream(
PendingStream* pending, QuicSpdySession* session)
: QuicStream(pending, session, /*is_static=*/false),
session_(session),
adapter_(session, this, sequencer()),
needs_to_send_preamble_(false) {}
WebTransportHttp3UnidirectionalStream::WebTransportHttp3UnidirectionalStream(
QuicStreamId id, QuicSpdySession* session, WebTransportSessionId session_id)
: QuicStream(id, session, /*is_static=*/false, WRITE_UNIDIRECTIONAL),
session_(session),
adapter_(session, this, sequencer()),
session_id_(session_id),
needs_to_send_preamble_(true) {}
void WebTransportHttp3UnidirectionalStream::WritePreamble() {
if (!needs_to_send_preamble_ || !session_id_.has_value()) {
QUIC_BUG(WebTransportHttp3UnidirectionalStream duplicate preamble)
<< ENDPOINT << "Sending preamble on stream ID " << id()
<< " at the wrong time.";
OnUnrecoverableError(QUIC_INTERNAL_ERROR,
"Attempting to send a WebTransport unidirectional "
"stream preamble at the wrong time.");
return;
}
QuicConnection::ScopedPacketFlusher flusher(session_->connection());
char buffer[sizeof(uint64_t) * 2]; // varint62, varint62
QuicDataWriter writer(sizeof(buffer), buffer);
bool success = true;
success = success && writer.WriteVarInt62(kWebTransportUnidirectionalStream);
success = success && writer.WriteVarInt62(*session_id_);
QUICHE_DCHECK(success);
WriteOrBufferData(absl::string_view(buffer, writer.length()), /*fin=*/false,
/*ack_listener=*/nullptr);
QUIC_DVLOG(1) << ENDPOINT << "Sent stream type and session ID ("
<< *session_id_ << ") on WebTransport stream " << id();
needs_to_send_preamble_ = false;
}
bool WebTransportHttp3UnidirectionalStream::ReadSessionId() {
iovec iov;
if (!sequencer()->GetReadableRegion(&iov)) {
return false;
}
QuicDataReader reader(static_cast<const char*>(iov.iov_base), iov.iov_len);
WebTransportSessionId session_id;
uint8_t session_id_length = reader.PeekVarInt62Length();
if (!reader.ReadVarInt62(&session_id)) {
// If all of the data has been received, and we still cannot associate the
// stream with a session, consume all of the data so that the stream can
// be closed.
if (sequencer()->NumBytesConsumed() + sequencer()->NumBytesBuffered() >=
sequencer()->close_offset()) {
QUIC_DLOG(WARNING)
<< ENDPOINT << "Failed to associate WebTransport stream " << id()
<< " with a session because the stream ended prematurely.";
sequencer()->MarkConsumed(sequencer()->NumBytesBuffered());
}
return false;
}
sequencer()->MarkConsumed(session_id_length);
session_id_ = session_id;
session_->AssociateIncomingWebTransportStreamWithSession(session_id, id());
return true;
}
void WebTransportHttp3UnidirectionalStream::OnDataAvailable() {
if (!session_id_.has_value()) {
if (!ReadSessionId()) {
return;
}
}
adapter_.OnDataAvailable();
}
void WebTransportHttp3UnidirectionalStream::OnCanWriteNewData() {
adapter_.OnCanWriteNewData();
}
void WebTransportHttp3UnidirectionalStream::OnClose() {
QuicStream::OnClose();
if (!session_id_.has_value()) {
return;
}
WebTransportHttp3* session = session_->GetWebTransportSession(*session_id_);
if (session == nullptr) {
QUIC_DLOG(WARNING) << ENDPOINT << "WebTransport stream " << id()
<< " attempted to notify parent session " << *session_id_
<< ", but the session could not be found.";
return;
}
session->OnStreamClosed(id());
}
void WebTransportHttp3UnidirectionalStream::OnStreamReset(
const QuicRstStreamFrame& frame) {
if (adapter_.visitor() != nullptr) {
adapter_.visitor()->OnResetStreamReceived(
Http3ErrorToWebTransportOrDefault(frame.ietf_error_code));
}
QuicStream::OnStreamReset(frame);
}
bool WebTransportHttp3UnidirectionalStream::OnStopSending(
QuicResetStreamError error) {
if (adapter_.visitor() != nullptr) {
adapter_.visitor()->OnStopSendingReceived(
Http3ErrorToWebTransportOrDefault(error.ietf_application_code()));
}
return QuicStream::OnStopSending(error);
}
void WebTransportHttp3UnidirectionalStream::OnWriteSideInDataRecvdState() {
if (adapter_.visitor() != nullptr) {
adapter_.visitor()->OnWriteSideInDataRecvdState();
}
QuicStream::OnWriteSideInDataRecvdState();
}
namespace {
constexpr uint64_t kWebTransportMappedErrorCodeFirst = 0x52e4a40fa8db;
constexpr uint64_t kWebTransportMappedErrorCodeLast = 0x52e4a40fa9e2;
constexpr WebTransportStreamError kDefaultWebTransportError = 0;
} // namespace
absl::optional<WebTransportStreamError> Http3ErrorToWebTransport(
uint64_t http3_error_code) {
// Ensure the code is within the valid range.
if (http3_error_code < kWebTransportMappedErrorCodeFirst ||
http3_error_code > kWebTransportMappedErrorCodeLast) {
return absl::nullopt;
}
// Exclude GREASE codepoints.
if ((http3_error_code - 0x21) % 0x1f == 0) {
return absl::nullopt;
}
uint64_t shifted = http3_error_code - kWebTransportMappedErrorCodeFirst;
uint64_t result = shifted - shifted / 0x1f;
QUICHE_DCHECK_LE(result, std::numeric_limits<uint8_t>::max());
return result;
}
WebTransportStreamError Http3ErrorToWebTransportOrDefault(
uint64_t http3_error_code) {
absl::optional<WebTransportStreamError> result =
Http3ErrorToWebTransport(http3_error_code);
return result.has_value() ? *result : kDefaultWebTransportError;
}
uint64_t WebTransportErrorToHttp3(
WebTransportStreamError webtransport_error_code) {
return kWebTransportMappedErrorCodeFirst + webtransport_error_code +
webtransport_error_code / 0x1e;
}
} // namespace quic