diff --git a/api/server/response_options.proto b/api/server/response_options.proto index 17ed8e1e9..69b3bd7d6 100644 --- a/api/server/response_options.proto +++ b/api/server/response_options.proto @@ -6,7 +6,12 @@ import "google/protobuf/wrappers.proto"; import "validate/validate.proto"; import "envoy/api/v2/core/base.proto"; +// Options that control the test server response. message ResponseOptions { + // List of additional response headers. repeated envoy.api.v2.core.HeaderValueOption response_headers = 1; + // Number of 'a' characters in the the response body. uint32 response_body_size = 2 [(validate.rules).uint32 = {lte: 4194304}]; -} \ No newline at end of file + // If true, then echo request headers in the response body. + bool echo_request_headers = 3; +} diff --git a/source/server/README.md b/source/server/README.md index bdc72cb26..28e847771 100644 --- a/source/server/README.md +++ b/source/server/README.md @@ -66,6 +66,62 @@ admin: port_value: 8081 ``` +## Response Options config + +The ResponseOptions proto can be used in the test-server filter config or passed in `x-nighthawk-test-server-config`` +request header. + +The following parameters are available: + +* `response_body_size` - number of 'a' characters repeated in the response body. +* `response_headers` - list of headers to add to response. If `append` is set to + `true`, then the header is appended. +* `echo_request_headers` - if set to `true`, then append the dump of request headers to the response + body. + +The response options could be used to test and debug proxy or server configuration, for +example, to verify request headers that are added by intermediate proxy: + +``` +$ curl -6 -v [::1]:8080/nighthawk + +* Trying ::1:8080... +* TCP_NODELAY set +* Connected to ::1 (::1) port 8080 (#0) +> GET /nighthawk +> Host: [::1]:8080 +> User-Agent: curl/7.68.0 +> Accept: */* +> +* Mark bundle as not supporting multiuse +< HTTP/1.1 200 OK +< content-length: 254 +< content-type: text/plain +< foo: bar +< foo: bar2 +< x-nh: 1 +< date: Wed, 03 Jun 2020 13:34:41 GMT +< server: envoy +< x-service: nighthawk_cluster +< via: 1.1 envoy +< +aaaaaaaaaa +Request Headers: +':authority', '[::1]:8080' +':path', '/nighthawk' +':method', 'GET' +':scheme', 'https' +'user-agent', 'curl/7.68.0' +'accept', '*/*' +'x-forwarded-proto', 'http' +'via', '1.1 google' +'x-forwarded-for', '::1,::1' +* Connection #0 to host ::1 left intact +``` + +This example shows that intermediate proxy has added `x-forwarded-proto` and +`x-forwarded-for` request headers. + ## Running the test server diff --git a/source/server/http_test_server_filter.cc b/source/server/http_test_server_filter.cc index 2e6bd183c..b43c413ed 100644 --- a/source/server/http_test_server_filter.cc +++ b/source/server/http_test_server_filter.cc @@ -55,8 +55,12 @@ void HttpTestServerDecoderFilter::applyConfigToResponseHeaders( void HttpTestServerDecoderFilter::sendReply() { if (error_message_ == absl::nullopt) { + std::string response_body(base_config_.response_body_size(), 'a'); + if (request_headers_dump_.has_value()) { + response_body += *request_headers_dump_; + } decoder_callbacks_->sendLocalReply( - static_cast(200), std::string(base_config_.response_body_size(), 'a'), + static_cast(200), response_body, [this](Envoy::Http::ResponseHeaderMap& direct_response_headers) { applyConfigToResponseHeaders(direct_response_headers, base_config_); }, @@ -78,6 +82,11 @@ HttpTestServerDecoderFilter::decodeHeaders(Envoy::Http::RequestHeaderMap& header if (request_config_header) { mergeJsonConfig(request_config_header->value().getStringView(), base_config_, error_message_); } + if (base_config_.echo_request_headers()) { + std::stringstream headers_dump; + headers_dump << "\nRequest Headers:\n" << headers; + request_headers_dump_ = headers_dump.str(); + } if (end_stream) { sendReply(); } diff --git a/source/server/http_test_server_filter.h b/source/server/http_test_server_filter.h index 113e6bbca..f7ba094a2 100644 --- a/source/server/http_test_server_filter.h +++ b/source/server/http_test_server_filter.h @@ -73,6 +73,7 @@ class HttpTestServerDecoderFilter : public Envoy::Http::StreamDecoderFilter { Envoy::Http::StreamDecoderFilterCallbacks* decoder_callbacks_; nighthawk::server::ResponseOptions base_config_; absl::optional error_message_; + absl::optional request_headers_dump_; }; } // namespace Server diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index 8d73636f5..d627db78c 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -189,6 +189,29 @@ TEST_P(HttpTestServerIntegrationTest, TestHeaderConfig) { EXPECT_EQ(std::string(10, 'a'), response->body()); } +TEST_P(HttpTestServerIntegrationTest, TestEchoHeaders) { + for (auto unique_header : {"one", "two", "three"}) { + Envoy::BufferingStreamDecoderPtr response = makeSingleRequest( + lookupPort("http"), "GET", "/somepath", "", downstream_protocol_, version_, "foo.com", "", + [unique_header](Envoy::Http::RequestHeaderMapImpl& request_headers) { + request_headers.addCopy(Envoy::Http::LowerCaseString("gray"), "pidgeon"); + request_headers.addCopy(Envoy::Http::LowerCaseString("red"), "fox"); + request_headers.addCopy(Envoy::Http::LowerCaseString("unique_header"), unique_header); + request_headers.addCopy( + Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, + "{echo_request_headers: true}"); + }); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + EXPECT_THAT(response->body(), HasSubstr(R"(':authority', 'foo.com')")); + EXPECT_THAT(response->body(), HasSubstr(R"(':path', '/somepath')")); + EXPECT_THAT(response->body(), HasSubstr(R"(':method', 'GET')")); + EXPECT_THAT(response->body(), HasSubstr(R"('gray', 'pidgeon')")); + EXPECT_THAT(response->body(), HasSubstr(R"('red', 'fox')")); + EXPECT_THAT(response->body(), HasSubstr(unique_header)); + } +} + class HttpTestServerIntegrationNoConfigTest : public HttpTestServerIntegrationTestBase { public: void SetUp() override { initialize(); }