Add functions to handle WebTransport subprotocol negotiation headers.
Based on https://github.com/ietf-wg-webtrans/draft-ietf-webtrans-http3/pull/144
PiperOrigin-RevId: 583029408
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 3615c89..df02f45 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -394,6 +394,7 @@
"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 @@
"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 @@
"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 472aede..16a7eea 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -394,6 +394,7 @@
"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 @@
"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 @@
"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 fc9ff8b..41946db 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 0762d0d..164894b 100644
--- a/quiche/common/structured_headers.cc
+++ b/quiche/common/structured_headers.cc
@@ -5,9 +5,13 @@
#include "quiche/common/structured_headers.h"
#include <cmath>
+#include <cstddef>
+#include <cstdint>
#include <optional>
+#include <sstream>
#include <string>
#include <utility>
+#include <vector>
#include "absl/algorithm/container.h"
#include "absl/container/flat_hash_set.h"
@@ -574,12 +578,9 @@
}
if (value.is_token()) {
// Serializes a Token ([RFC8941] 4.1.7).
- if (value.GetString().empty() ||
- !(absl::ascii_isalpha(value.GetString().front()) ||
- value.GetString().front() == '*'))
+ if (!IsValidToken(value.GetString())) {
return false;
- if (value.GetString().find_first_not_of(kTokenChars) != std::string::npos)
- return false;
+ }
output_ << value.GetString();
return true;
}
@@ -720,6 +721,38 @@
} // 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 af1b199..a9a79cd 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 <algorithm>
+#include <cstddef>
+#include <cstdint>
#include <map>
#include <optional>
#include <string>
@@ -141,6 +142,12 @@
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 0000000..7112f25
--- /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 <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#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<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) {
+ 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<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, ", ");
+}
+
+} // 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 0000000..b3b16b2
--- /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 <string>
+#include <vector>
+
+#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<std::vector<std::string>>
+ParseSubprotocolRequestHeader(absl::string_view value);
+QUICHE_EXPORT absl::StatusOr<std::string> SerializeSubprotocolRequestHeader(
+ absl::Span<const std::string> 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 0000000..38cc2c4
--- /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