| // Copyright 2023 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 "quiche/web_transport/web_transport_headers.h" | |
| #include <array> | |
| #include <cstdint> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/base/attributes.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_join.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/types/span.h" | |
| #include "quiche/common/quiche_status_utils.h" | |
| #include "quiche/common/structured_headers.h" | |
| namespace webtransport { | |
| namespace { | |
| using ::quiche::structured_headers::Dictionary; | |
| using ::quiche::structured_headers::DictionaryMember; | |
| using ::quiche::structured_headers::Item; | |
| using ::quiche::structured_headers::ItemTypeToString; | |
| using ::quiche::structured_headers::List; | |
| using ::quiche::structured_headers::ParameterizedItem; | |
| using ::quiche::structured_headers::ParameterizedMember; | |
| absl::Status CheckItemType(const ParameterizedItem& item, | |
| Item::ItemType expected_type) { | |
| if (item.item.Type() != expected_type) { | |
| return absl::InvalidArgumentError(absl::StrCat( | |
| "Expected all members to be of type ", ItemTypeToString(expected_type), | |
| ", found ", ItemTypeToString(item.item.Type()), " instead")); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::Status CheckMemberType(const ParameterizedMember& member, | |
| Item::ItemType expected_type) { | |
| if (member.member_is_inner_list || member.member.size() != 1) { | |
| return absl::InvalidArgumentError(absl::StrCat( | |
| "Expected all members to be of type", ItemTypeToString(expected_type), | |
| ", found a nested list instead")); | |
| } | |
| return CheckItemType(member.member[0], expected_type); | |
| } | |
| ABSL_CONST_INIT std::array kInitHeaderFields{ | |
| std::make_pair("u", &WebTransportInitHeader::initial_unidi_limit), | |
| std::make_pair("bl", &WebTransportInitHeader::initial_incoming_bidi_limit), | |
| std::make_pair("br", &WebTransportInitHeader::initial_outgoing_bidi_limit), | |
| }; | |
| } // namespace | |
| absl::StatusOr<std::vector<std::string>> ParseSubprotocolRequestHeader( | |
| absl::string_view value) { | |
| std::optional<List> parsed = quiche::structured_headers::ParseList(value); | |
| if (!parsed.has_value()) { | |
| return absl::InvalidArgumentError( | |
| "Failed to parse the header as an sf-list"); | |
| } | |
| std::vector<std::string> result; | |
| result.reserve(parsed->size()); | |
| for (ParameterizedMember& member : *parsed) { | |
| QUICHE_RETURN_IF_ERROR(CheckMemberType(member, Item::kTokenType)); | |
| result.push_back(std::move(member.member[0].item).TakeString()); | |
| } | |
| return result; | |
| } | |
| absl::StatusOr<std::string> SerializeSubprotocolRequestHeader( | |
| absl::Span<const std::string> subprotocols) { | |
| // Serialize tokens manually via a simple StrJoin call; this lets us provide | |
| // better error messages, and is probably more efficient too. | |
| for (const std::string& token : subprotocols) { | |
| if (!quiche::structured_headers::IsValidToken(token)) { | |
| return absl::InvalidArgumentError(absl::StrCat("Invalid token: ", token)); | |
| } | |
| } | |
| return absl::StrJoin(subprotocols, ", "); | |
| } | |
| absl::StatusOr<std::string> ParseSubprotocolResponseHeader( | |
| absl::string_view value) { | |
| std::optional<ParameterizedItem> parsed = | |
| quiche::structured_headers::ParseItem(value); | |
| if (!parsed.has_value()) { | |
| return absl::InvalidArgumentError("Failed to parse sf-item"); | |
| } | |
| QUICHE_RETURN_IF_ERROR(CheckItemType(*parsed, Item::kTokenType)); | |
| return std::move(parsed->item).TakeString(); | |
| } | |
| absl::StatusOr<std::string> SerializeSubprotocolResponseHeader( | |
| absl::string_view subprotocol) { | |
| if (!quiche::structured_headers::IsValidToken(subprotocol)) { | |
| return absl::InvalidArgumentError("Invalid token value supplied"); | |
| } | |
| return std::string(subprotocol); | |
| } | |
| absl::StatusOr<WebTransportInitHeader> ParseInitHeader( | |
| absl::string_view header) { | |
| std::optional<Dictionary> parsed = | |
| quiche::structured_headers::ParseDictionary(header); | |
| if (!parsed.has_value()) { | |
| return absl::InvalidArgumentError( | |
| "Failed to parse WebTransport-Init header as an sf-dictionary"); | |
| } | |
| WebTransportInitHeader output; | |
| for (const auto& [field_name_a, field_value] : *parsed) { | |
| for (const auto& [field_name_b, field_accessor] : kInitHeaderFields) { | |
| if (field_name_a != field_name_b) { | |
| continue; | |
| } | |
| QUICHE_RETURN_IF_ERROR(CheckMemberType(field_value, Item::kIntegerType)); | |
| int64_t value = field_value.member[0].item.GetInteger(); | |
| if (value < 0) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Received negative value for ", field_name_a)); | |
| } | |
| output.*field_accessor = value; | |
| } | |
| } | |
| return output; | |
| } | |
| absl::StatusOr<std::string> SerializeInitHeader( | |
| const WebTransportInitHeader& header) { | |
| std::vector<DictionaryMember> members; | |
| members.reserve(kInitHeaderFields.size()); | |
| for (const auto& [field_name, field_accessor] : kInitHeaderFields) { | |
| Item item(static_cast<int64_t>(header.*field_accessor)); | |
| members.push_back(std::make_pair( | |
| field_name, ParameterizedMember({ParameterizedItem(item, {})}, false, | |
| /*parameters=*/{}))); | |
| } | |
| std::optional<std::string> result = | |
| quiche::structured_headers::SerializeDictionary( | |
| Dictionary(std::move(members))); | |
| if (!result.has_value()) { | |
| return absl::InternalError("Failed to serialize the dictionary"); | |
| } | |
| return *std::move(result); | |
| } | |
| } // namespace webtransport |