| // 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::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.item.GetIfInteger(); |
| } else if constexpr (kExpectedType == Item::kStringType) { |
| return item.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.item.Type()), " instead")); |
| } |
| return std::move(*ptr); |
| } |
| |
| template <Item::ItemType kExpectedType> |
| auto GetMember(auto&& member) |
| -> decltype(GetItem<kExpectedType>(member.member[0])) { |
| if (member.member_is_inner_list || member.member.size() != 1) { |
| return absl::InvalidArgumentError(absl::StrCat( |
| "Expected all members to be of type", ItemTypeToString(kExpectedType), |
| ", found a nested list instead")); |
| } |
| return GetItem<kExpectedType>(member.member[0]); |
| } |
| |
| 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.push_back(ParameterizedMember(Item(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); |
| } |
| |
| absl::StatusOr<std::string> SerializeSubprotocolResponseHeader( |
| absl::string_view subprotocol) { |
| Item item(std::string(subprotocol), Item::kStringType); |
| std::optional<std::string> serialized = |
| quiche::structured_headers::SerializeItem(item); |
| 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) { |
| 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 |