| // 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 |