Adds an option for maximum header field size to OgHttp2Adapter.

This option will be used during migration for compatibility with nghttp2.

PiperOrigin-RevId: 417527069
diff --git a/http2/adapter/header_validator.cc b/http2/adapter/header_validator.cc
index 0b042b1..3a70628 100644
--- a/http2/adapter/header_validator.cc
+++ b/http2/adapter/header_validator.cc
@@ -61,6 +61,12 @@
   if (key.empty()) {
     return HEADER_FIELD_INVALID;
   }
+  if (max_field_size_.has_value() &&
+      key.size() + value.size() > max_field_size_.value()) {
+    QUICHE_VLOG(2) << "Header field size is " << key.size() + value.size()
+                   << ", exceeds max size of " << max_field_size_.value();
+    return HEADER_FIELD_INVALID;
+  }
   const absl::string_view validated_key = key[0] == ':' ? key.substr(1) : key;
   if (validated_key.find_first_not_of(kHttp2HeaderNameAllowedChars) !=
       absl::string_view::npos) {
diff --git a/http2/adapter/header_validator.h b/http2/adapter/header_validator.h
index a1d27bb..fc8c226 100644
--- a/http2/adapter/header_validator.h
+++ b/http2/adapter/header_validator.h
@@ -5,6 +5,7 @@
 #include <vector>
 
 #include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
 #include "common/platform/api/quiche_export.h"
 
 namespace http2 {
@@ -22,6 +23,8 @@
  public:
   HeaderValidator() {}
 
+  void SetMaxFieldSize(uint32_t field_size) { max_field_size_ = field_size; }
+
   // If called, this validator will allow the `:protocol` pseudo-header, as
   // described in RFC 8441.
   void AllowConnect() { allow_connect_ = true; }
@@ -46,6 +49,7 @@
   std::vector<std::string> pseudo_headers_;
   std::string status_;
   std::string method_;
+  absl::optional<size_t> max_field_size_;
   bool allow_connect_ = false;
 };
 
diff --git a/http2/adapter/header_validator_test.cc b/http2/adapter/header_validator_test.cc
index 1f202d9..09c4d43 100644
--- a/http2/adapter/header_validator_test.cc
+++ b/http2/adapter/header_validator_test.cc
@@ -19,6 +19,18 @@
   EXPECT_EQ(HeaderValidator::HEADER_OK, status);
 }
 
+TEST(HeaderValidatorTest, ExceedsMaxSize) {
+  HeaderValidator v;
+  v.SetMaxFieldSize(64u);
+  HeaderValidator::HeaderStatus status =
+      v.ValidateSingleHeader("name", "value");
+  EXPECT_EQ(HeaderValidator::HEADER_OK, status);
+  status = v.ValidateSingleHeader(
+      "name2",
+      "Antidisestablishmentariansism is supercalifragilisticexpialodocious.");
+  EXPECT_EQ(HeaderValidator::HEADER_FIELD_INVALID, status);
+}
+
 TEST(HeaderValidatorTest, NameHasInvalidChar) {
   HeaderValidator v;
   for (const bool is_pseudo_header : {true, false}) {
diff --git a/http2/adapter/nghttp2_adapter_test.cc b/http2/adapter/nghttp2_adapter_test.cc
index 4e0400a..15b3495 100644
--- a/http2/adapter/nghttp2_adapter_test.cc
+++ b/http2/adapter/nghttp2_adapter_test.cc
@@ -2668,6 +2668,57 @@
   EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS}));
 }
 
+TEST(NgHttp2AdapterTest, ServerReceivesTooLargeHeader) {
+  DataSavingVisitor visitor;
+  auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor);
+
+  // nghttp2 will accept a maximum of 64kB of huffman encoded data per header
+  // field.
+  const std::string too_large_value = std::string(80 * 1024, 'q');
+  const std::string frames = TestFrameSequence()
+                                 .ClientPreface()
+                                 .Headers(1,
+                                          {{":method", "POST"},
+                                           {":scheme", "https"},
+                                           {":authority", "example.com"},
+                                           {":path", "/this/is/request/one"},
+                                           {"x-toobig", too_large_value}},
+                                          /*fin=*/false)
+                                 .Serialize();
+  testing::InSequence s;
+
+  // Client preface (empty SETTINGS)
+  EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
+  EXPECT_CALL(visitor, OnSettingsStart());
+  EXPECT_CALL(visitor, OnSettingsEnd());
+
+  EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 0));
+  EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "POST"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one"));
+  // Further header processing is skipped, as the header field is too large.
+
+  const int64_t result = adapter->ProcessBytes(frames);
+  EXPECT_EQ(frames.size(), result);
+
+  EXPECT_TRUE(adapter->want_write());
+
+  // Since nghttp2 opted not to process the header, it generates a GOAWAY with
+  // error code COMPRESSION_ERROR.
+  EXPECT_CALL(visitor, OnBeforeFrameSent(GOAWAY, 0, 8, 0x0));
+  EXPECT_CALL(visitor,
+              OnFrameSent(GOAWAY, 0, 8, 0x0,
+                          static_cast<int>(Http2ErrorCode::COMPRESSION_ERROR)));
+
+  int send_result = adapter->Send();
+  // Some bytes should have been serialized.
+  EXPECT_EQ(0, send_result);
+  // GOAWAY.
+  EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::GOAWAY}));
+}
+
 TEST(NgHttp2AdapterTest, ServerSubmitResponse) {
   DataSavingVisitor visitor;
   auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor);
diff --git a/http2/adapter/oghttp2_adapter_test.cc b/http2/adapter/oghttp2_adapter_test.cc
index 4970f52..9635bdf 100644
--- a/http2/adapter/oghttp2_adapter_test.cc
+++ b/http2/adapter/oghttp2_adapter_test.cc
@@ -3318,6 +3318,66 @@
                                             spdy::SpdyFrameType::GOAWAY}));
 }
 
+TEST(OgHttp2AdapterServerTest, ServerReceivesTooLargeHeader) {
+  DataSavingVisitor visitor;
+  OgHttp2Adapter::Options options{.perspective = Perspective::kServer,
+                                  .max_header_field_size = 64 * 1024};
+  auto adapter = OgHttp2Adapter::Create(visitor, options);
+
+  // Due to configuration, the library will accept a maximum of 64kB of huffman
+  // encoded data per header field.
+  const std::string too_large_value = std::string(80 * 1024, 'q');
+  const std::string frames = TestFrameSequence()
+                                 .ClientPreface()
+                                 .Headers(1,
+                                          {{":method", "POST"},
+                                           {":scheme", "https"},
+                                           {":authority", "example.com"},
+                                           {":path", "/this/is/request/one"},
+                                           {"x-toobig", too_large_value}},
+                                          /*fin=*/false)
+                                 .Serialize();
+  testing::InSequence s;
+
+  // Client preface (empty SETTINGS)
+  EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
+  EXPECT_CALL(visitor, OnSettingsStart());
+  EXPECT_CALL(visitor, OnSettingsEnd());
+
+  EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 0));
+  EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "POST"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com"));
+  EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one"));
+  EXPECT_CALL(visitor, OnConnectionError(ConnectionError::kParseError));
+  // Further header processing is skipped, as the header field is too large.
+
+  const int64_t result = adapter->ProcessBytes(frames);
+  EXPECT_EQ(static_cast<int64_t>(frames.size()), result);
+
+  EXPECT_TRUE(adapter->want_write());
+
+  EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0));
+  EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0));
+  EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1));
+  EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0));
+  // Since the library opted not to process the header, it generates a GOAWAY
+  // with error code COMPRESSION_ERROR.
+  EXPECT_CALL(visitor, OnBeforeFrameSent(GOAWAY, 0, _, 0x0));
+  EXPECT_CALL(visitor,
+              OnFrameSent(GOAWAY, 0, _, 0x0,
+                          static_cast<int>(Http2ErrorCode::COMPRESSION_ERROR)));
+
+  int send_result = adapter->Send();
+  // Some bytes should have been serialized.
+  EXPECT_EQ(0, send_result);
+  // SETTINGS, SETTINGS ack, and GOAWAY.
+  EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS,
+                                            spdy::SpdyFrameType::SETTINGS,
+                                            spdy::SpdyFrameType::GOAWAY}));
+}
+
 TEST(OgHttp2AdapterServerTest, ServerRejectsStreamData) {
   DataSavingVisitor visitor;
   OgHttp2Adapter::Options options{.perspective = Perspective::kServer};
diff --git a/http2/adapter/oghttp2_session.cc b/http2/adapter/oghttp2_session.cc
index 0357f7b..ec746d4 100644
--- a/http2/adapter/oghttp2_session.cc
+++ b/http2/adapter/oghttp2_session.cc
@@ -313,6 +313,9 @@
     remaining_preface_ = {spdy::kHttp2ConnectionHeaderPrefix,
                           spdy::kHttp2ConnectionHeaderPrefixSize};
   }
+  if (options_.max_header_field_size.has_value()) {
+    headers_handler_.SetMaxFieldSize(options_.max_header_field_size.value());
+  }
 }
 
 OgHttp2Session::~OgHttp2Session() {}
diff --git a/http2/adapter/oghttp2_session.h b/http2/adapter/oghttp2_session.h
index d9f00df..72e36e9 100644
--- a/http2/adapter/oghttp2_session.h
+++ b/http2/adapter/oghttp2_session.h
@@ -43,6 +43,8 @@
     absl::optional<size_t> max_hpack_encoding_table_capacity = absl::nullopt;
     // The maximum number of decoded header bytes that a stream can receive.
     absl::optional<uint32_t> max_header_list_bytes = absl::nullopt;
+    // The maximum size of an individual header field, including name and value.
+    absl::optional<uint32_t> max_header_field_size = absl::nullopt;
     // Whether to automatically send PING acks when receiving a PING.
     bool auto_ping_ack = true;
     // Whether (as server) to send a RST_STREAM NO_ERROR when sending a fin on
@@ -257,6 +259,9 @@
       return validator_.status_header();
     }
     void AllowConnect() { validator_.AllowConnect(); }
+    void SetMaxFieldSize(uint32_t field_size) {
+      validator_.SetMaxFieldSize(field_size);
+    }
 
    private:
     OgHttp2Session& session_;