diff --git a/build/source_list.bzl b/build/source_list.bzl index 3615c89c3..df02f45fe 100644 --- a/build/source_list.bzl +++ b/build/source_list.bzl @@ -394,6 +394,7 @@ quiche_core_hdrs = [ "web_transport/complete_buffer_visitor.h", "web_transport/encapsulated/encapsulated_web_transport.h", "web_transport/web_transport.h", + "web_transport/web_transport_headers.h", ] quiche_core_srcs = [ "common/capsule.cc", @@ -682,6 +683,7 @@ quiche_core_srcs = [ "spdy/core/spdy_protocol.cc", "web_transport/complete_buffer_visitor.cc", "web_transport/encapsulated/encapsulated_web_transport.cc", + "web_transport/web_transport_headers.cc", ] quiche_tool_support_hdrs = [ "common/platform/api/quiche_command_line_flags.h", @@ -1307,6 +1309,7 @@ quiche_tests_srcs = [ "spdy/core/spdy_prefixed_buffer_reader_test.cc", "spdy/core/spdy_protocol_test.cc", "web_transport/encapsulated/encapsulated_web_transport_test.cc", + "web_transport/web_transport_headers_test.cc", ] io_tests_hdrs = [ ] diff --git a/build/source_list.gni b/build/source_list.gni index 472aede03..16a7eead1 100644 --- a/build/source_list.gni +++ b/build/source_list.gni @@ -394,6 +394,7 @@ quiche_core_hdrs = [ "src/quiche/web_transport/complete_buffer_visitor.h", "src/quiche/web_transport/encapsulated/encapsulated_web_transport.h", "src/quiche/web_transport/web_transport.h", + "src/quiche/web_transport/web_transport_headers.h", ] quiche_core_srcs = [ "src/quiche/common/capsule.cc", @@ -682,6 +683,7 @@ quiche_core_srcs = [ "src/quiche/spdy/core/spdy_protocol.cc", "src/quiche/web_transport/complete_buffer_visitor.cc", "src/quiche/web_transport/encapsulated/encapsulated_web_transport.cc", + "src/quiche/web_transport/web_transport_headers.cc", ] quiche_tool_support_hdrs = [ "src/quiche/common/platform/api/quiche_command_line_flags.h", @@ -1308,6 +1310,7 @@ quiche_tests_srcs = [ "src/quiche/spdy/core/spdy_prefixed_buffer_reader_test.cc", "src/quiche/spdy/core/spdy_protocol_test.cc", "src/quiche/web_transport/encapsulated/encapsulated_web_transport_test.cc", + "src/quiche/web_transport/web_transport_headers_test.cc", ] io_tests_hdrs = [ diff --git a/build/source_list.json b/build/source_list.json index fc9ff8bf0..41946db47 100644 --- a/build/source_list.json +++ b/build/source_list.json @@ -392,7 +392,8 @@ "quiche/spdy/core/zero_copy_output_buffer.h", "quiche/web_transport/complete_buffer_visitor.h", "quiche/web_transport/encapsulated/encapsulated_web_transport.h", - "quiche/web_transport/web_transport.h" + "quiche/web_transport/web_transport.h", + "quiche/web_transport/web_transport_headers.h" ], "quiche_core_srcs": [ "quiche/common/capsule.cc", @@ -680,7 +681,8 @@ "quiche/spdy/core/spdy_prefixed_buffer_reader.cc", "quiche/spdy/core/spdy_protocol.cc", "quiche/web_transport/complete_buffer_visitor.cc", - "quiche/web_transport/encapsulated/encapsulated_web_transport.cc" + "quiche/web_transport/encapsulated/encapsulated_web_transport.cc", + "quiche/web_transport/web_transport_headers.cc" ], "quiche_tool_support_hdrs": [ "quiche/common/platform/api/quiche_command_line_flags.h", @@ -1306,7 +1308,8 @@ "quiche/spdy/core/spdy_pinnable_buffer_piece_test.cc", "quiche/spdy/core/spdy_prefixed_buffer_reader_test.cc", "quiche/spdy/core/spdy_protocol_test.cc", - "quiche/web_transport/encapsulated/encapsulated_web_transport_test.cc" + "quiche/web_transport/encapsulated/encapsulated_web_transport_test.cc", + "quiche/web_transport/web_transport_headers_test.cc" ], "io_tests_hdrs": [ diff --git a/quiche/common/structured_headers.cc b/quiche/common/structured_headers.cc index 0762d0de5..164894b81 100644 --- a/quiche/common/structured_headers.cc +++ b/quiche/common/structured_headers.cc @@ -5,9 +5,13 @@ #include "quiche/common/structured_headers.h" #include +#include +#include #include +#include #include #include +#include #include "absl/algorithm/container.h" #include "absl/container/flat_hash_set.h" @@ -574,12 +578,9 @@ class StructuredHeaderSerializer { } if (value.is_token()) { // Serializes a Token ([RFC8941] 4.1.7). - if (value.GetString().empty() || - !(absl::ascii_isalpha(value.GetString().front()) || - value.GetString().front() == '*')) - return false; - if (value.GetString().find_first_not_of(kTokenChars) != std::string::npos) + if (!IsValidToken(value.GetString())) { return false; + } output_ << value.GetString(); return true; } @@ -720,6 +721,38 @@ class StructuredHeaderSerializer { } // namespace +absl::string_view ItemTypeToString(Item::ItemType type) { + switch (type) { + case Item::kNullType: + return "null"; + case Item::kIntegerType: + return "integer"; + case Item::kDecimalType: + return "decimal"; + case Item::kStringType: + return "string"; + case Item::kTokenType: + return "token"; + case Item::kByteSequenceType: + return "byte sequence"; + case Item::kBooleanType: + return "boolean"; + } + return "[invalid type]"; +} + +bool IsValidToken(absl::string_view str) { + // Validate Token value per [RFC8941] 4.1.7. + if (str.empty() || + !(absl::ascii_isalpha(str.front()) || str.front() == '*')) { + return false; + } + if (str.find_first_not_of(kTokenChars) != std::string::npos) { + return false; + } + return true; +} + Item::Item() {} Item::Item(std::string value, Item::ItemType type) { switch (type) { diff --git a/quiche/common/structured_headers.h b/quiche/common/structured_headers.h index af1b19968..a9a79cdd1 100644 --- a/quiche/common/structured_headers.h +++ b/quiche/common/structured_headers.h @@ -5,7 +5,8 @@ #ifndef QUICHE_COMMON_STRUCTURED_HEADERS_H_ #define QUICHE_COMMON_STRUCTURED_HEADERS_H_ -#include +#include +#include #include #include #include @@ -141,6 +142,12 @@ class QUICHE_EXPORT Item { value_; }; +// Returns a human-readable representation of an ItemType. +QUICHE_EXPORT absl::string_view ItemTypeToString(Item::ItemType type); + +// Returns `true` if the string is a valid Token value. +QUICHE_EXPORT bool IsValidToken(absl::string_view str); + // Holds a ParameterizedIdentifier (draft 9 only). The contained Item must be a // Token, and there may be any number of parameters. Parameter ordering is not // significant. diff --git a/quiche/web_transport/web_transport_headers.cc b/quiche/web_transport/web_transport_headers.cc new file mode 100644 index 000000000..7112f2519 --- /dev/null +++ b/quiche/web_transport/web_transport_headers.cc @@ -0,0 +1,65 @@ +// 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 +#include +#include +#include + +#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/structured_headers.h" + +namespace webtransport { + +using ::quiche::structured_headers::ItemTypeToString; +using ::quiche::structured_headers::List; +using ::quiche::structured_headers::ParameterizedItem; +using ::quiche::structured_headers::ParameterizedMember; + +absl::StatusOr> ParseSubprotocolRequestHeader( + absl::string_view value) { + std::optional parsed = quiche::structured_headers::ParseList(value); + if (!parsed.has_value()) { + return absl::InvalidArgumentError( + "Failed to parse the header as an sf-list"); + } + + std::vector result; + result.reserve(parsed->size()); + for (ParameterizedMember& member : *parsed) { + if (member.member_is_inner_list || member.member.size() != 1) { + return absl::InvalidArgumentError( + "Expected all members to be tokens, found a nested list instead"); + } + ParameterizedItem& item = member.member[0]; + if (!item.item.is_token()) { + return absl::InvalidArgumentError( + absl::StrCat("Expected all members to be tokens, found ", + ItemTypeToString(item.item.Type()), " instead")); + } + result.push_back(std::move(item).item.TakeString()); + } + return result; +} + +absl::StatusOr SerializeSubprotocolRequestHeader( + absl::Span 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, ", "); +} + +} // namespace webtransport diff --git a/quiche/web_transport/web_transport_headers.h b/quiche/web_transport/web_transport_headers.h new file mode 100644 index 000000000..b3b16b2bd --- /dev/null +++ b/quiche/web_transport/web_transport_headers.h @@ -0,0 +1,30 @@ +// 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. + +#ifndef QUICHE_WEB_TRANSPORT_WEB_TRANSPORT_HEADERS_H_ +#define QUICHE_WEB_TRANSPORT_WEB_TRANSPORT_HEADERS_H_ + +#include +#include + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/common/platform/api/quiche_export.h" + +namespace webtransport { + +inline constexpr absl::string_view kSubprotocolRequestHeader = + "WebTransport-Subprotocols-Available"; +inline constexpr absl::string_view kSubprotocolResponseHeader = + "WebTransport-Subprotocol"; + +QUICHE_EXPORT absl::StatusOr> +ParseSubprotocolRequestHeader(absl::string_view value); +QUICHE_EXPORT absl::StatusOr SerializeSubprotocolRequestHeader( + absl::Span subprotocols); + +} // namespace webtransport + +#endif // QUICHE_WEB_TRANSPORT_WEB_TRANSPORT_HEADERS_H_ diff --git a/quiche/web_transport/web_transport_headers_test.cc b/quiche/web_transport/web_transport_headers_test.cc new file mode 100644 index 000000000..38cc2c42e --- /dev/null +++ b/quiche/web_transport/web_transport_headers_test.cc @@ -0,0 +1,61 @@ +// 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 "absl/status/status.h" +#include "quiche/common/platform/api/quiche_test.h" +#include "quiche/common/test_tools/quiche_test_utils.h" + +namespace webtransport { +namespace { + +using ::quiche::test::IsOkAndHolds; +using ::quiche::test::StatusIs; +using ::testing::ElementsAre; +using ::testing::HasSubstr; + +TEST(WebTransportHeaders, ParseSubprotocolRequestHeader) { + EXPECT_THAT(ParseSubprotocolRequestHeader("test"), + IsOkAndHolds(ElementsAre("test"))); + EXPECT_THAT(ParseSubprotocolRequestHeader("moqt-draft01, moqt-draft02"), + IsOkAndHolds(ElementsAre("moqt-draft01", "moqt-draft02"))); + EXPECT_THAT(ParseSubprotocolRequestHeader("moqt-draft01; a=b, moqt-draft02"), + IsOkAndHolds(ElementsAre("moqt-draft01", "moqt-draft02"))); + EXPECT_THAT(ParseSubprotocolRequestHeader("moqt-draft01, moqt-draft02; a=b"), + IsOkAndHolds(ElementsAre("moqt-draft01", "moqt-draft02"))); + EXPECT_THAT(ParseSubprotocolRequestHeader("\"test\""), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("found string instead"))); + EXPECT_THAT(ParseSubprotocolRequestHeader("42"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("found integer instead"))); + EXPECT_THAT(ParseSubprotocolRequestHeader("a, (b)"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("found a nested list instead"))); + EXPECT_THAT(ParseSubprotocolRequestHeader("a, (b c)"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("found a nested list instead"))); + EXPECT_THAT(ParseSubprotocolRequestHeader("foo, ?1, bar"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("found boolean instead"))); + EXPECT_THAT(ParseSubprotocolRequestHeader("(a"), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("parse the header as an sf-list"))); +} + +TEST(WebTransportHeaders, SerializeSubprotocolRequestHeader) { + EXPECT_THAT(SerializeSubprotocolRequestHeader({"test"}), + IsOkAndHolds("test")); + EXPECT_THAT(SerializeSubprotocolRequestHeader({"foo", "bar"}), + IsOkAndHolds("foo, bar")); + EXPECT_THAT(SerializeSubprotocolRequestHeader({"moqt-draft01", "a/b/c"}), + IsOkAndHolds("moqt-draft01, a/b/c")); + EXPECT_THAT( + SerializeSubprotocolRequestHeader({"abcd", "0123", "efgh"}), + StatusIs(absl::StatusCode::kInvalidArgument, "Invalid token: 0123")); +} + +} // namespace +} // namespace webtransport