| // 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/algorithm/container.h" | |
| #include "absl/base/attributes.h" | |
| #include "absl/container/flat_hash_set.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/ascii.h" | |
| #include "absl/strings/str_cat.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::ItemView; | |
| using ::quiche::structured_headers::List; | |
| using ::quiche::structured_headers::ParameterizedItem; | |
| using ::quiche::structured_headers::ParameterizedMember; | |
| template <Item::ItemType kExpectedType> | |
| auto* GetItemAsPtr(auto&& item) { | |
| if constexpr (kExpectedType == Item::kIntegerType) { | |
| return item.GetIfInteger(); | |
| } else if constexpr (kExpectedType == Item::kStringType) { | |
| return item.GetIfString(); | |
| } else { | |
| static_assert(false); | |
| } | |
| } | |
| template <Item::ItemType kExpectedType> | |
| auto GetItem(auto&& item) -> absl::StatusOr< | |
| std::remove_pointer_t<decltype(GetItemAsPtr<kExpectedType>(item))>> { | |
| auto* ptr = GetItemAsPtr<kExpectedType>(item); | |
| if (!ptr) { | |
| return absl::InvalidArgumentError(absl::StrCat( | |
| "Expected all members to be of type ", ItemTypeToString(kExpectedType), | |
| ", found ", ItemTypeToString(item.Type()), " instead")); | |
| } | |
| return std::move(*ptr); | |
| } | |
| template <Item::ItemType kExpectedType> | |
| auto GetMember(auto&& member) { | |
| auto item = member.GetIfItem(); | |
| using ReturnType = decltype(GetItem<kExpectedType>(item->item)); | |
| if (!item) { | |
| return ReturnType(absl::InvalidArgumentError(absl::StrCat( | |
| "Expected all members to be of type", ItemTypeToString(kExpectedType), | |
| ", found a nested list instead"))); | |
| } | |
| return GetItem<kExpectedType>(item->item); | |
| } | |
| 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_ASSIGN_OR_RETURN(std::string v, | |
| GetMember<Item::kStringType>(member)); | |
| result.push_back(std::move(v)); | |
| } | |
| return result; | |
| } | |
| absl::StatusOr<std::string> SerializeSubprotocolRequestHeader( | |
| absl::Span<const std::string> subprotocols) { | |
| quiche::structured_headers::List list; | |
| list.reserve(subprotocols.size()); | |
| for (const std::string& subprotocol : subprotocols) { | |
| list.emplace_back(Item(Item::string, subprotocol)); | |
| } | |
| std::optional<std::string> serialized = | |
| quiche::structured_headers::SerializeList(list); | |
| if (!serialized.has_value()) { | |
| return absl::InvalidArgumentError("Invalid subprotocol list supplied"); | |
| } | |
| return *std::move(serialized); | |
| } | |
| 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"); | |
| } | |
| return GetItem<Item::kStringType>(parsed->item); | |
| } | |
| absl::StatusOr<std::string> SerializeSubprotocolResponseHeader( | |
| absl::string_view subprotocol) { | |
| std::optional<std::string> serialized = | |
| quiche::structured_headers::SerializeItem( | |
| ItemView(ItemView::string, subprotocol)); | |
| if (!serialized.has_value()) { | |
| return absl::InvalidArgumentError("Invalid subprotocol name supplied"); | |
| } | |
| return *std::move(serialized); | |
| } | |
| bool ValidateSubprotocolName(absl::string_view name) { | |
| return !name.empty() && | |
| absl::c_all_of(name, [](char c) { return absl::ascii_isprint(c); }); | |
| } | |
| template <typename S> | |
| bool ValidateSubprotocolListBase(absl::Span<const S> list) { | |
| absl::flat_hash_set<absl::string_view> examined; | |
| for (const absl::string_view name : list) { | |
| if (!ValidateSubprotocolName(name)) { | |
| return false; | |
| } | |
| auto [it, added] = examined.insert(name); | |
| if (!added) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool ValidateSubprotocolList(absl::Span<const absl::string_view> list) { | |
| return ValidateSubprotocolListBase(list); | |
| } | |
| bool ValidateSubprotocolList(absl::Span<const std::string> list) { | |
| return ValidateSubprotocolListBase(list); | |
| } | |
| 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_ASSIGN_OR_RETURN(int64_t value, | |
| GetMember<Item::kIntegerType>(field_value)); | |
| 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) { | |
| members.emplace_back(field_name, | |
| Item(static_cast<int64_t>(header.*field_accessor))); | |
| } | |
| 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 |