diff --git a/docs/configuration/http_filters/grpc_web_filter.rst b/docs/configuration/http_filters/grpc_web_filter.rst new file mode 100644 index 0000000000000..aa871344b191f --- /dev/null +++ b/docs/configuration/http_filters/grpc_web_filter.rst @@ -0,0 +1,17 @@ +.. _config_http_filters_grpc_web: + +gRPC-Web filter +==================== + +gRPC :ref:`architecture overview `. + +This is a filter which enables the bridging of a gRPC-Web client to a compliant gRPC server by +following https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md. + +.. code-block:: json + + { + "type": "both", + "name": "grpc_web", + "config": {} + } diff --git a/docs/configuration/http_filters/http_filters.rst b/docs/configuration/http_filters/http_filters.rst index 2e579eb503fc1..7efb0bfc73aa8 100644 --- a/docs/configuration/http_filters/http_filters.rst +++ b/docs/configuration/http_filters/http_filters.rst @@ -10,6 +10,7 @@ HTTP filters fault_filter dynamodb_filter grpc_http1_bridge_filter + grpc_web_filter health_check_filter rate_limit_filter router_filter diff --git a/docs/intro/arch_overview/grpc.rst b/docs/intro/arch_overview/grpc.rst index 386ab8d13ecbe..4ef1dc600a474 100644 --- a/docs/intro/arch_overview/grpc.rst +++ b/docs/intro/arch_overview/grpc.rst @@ -17,3 +17,7 @@ application layer: The response is translated back to HTTP/1.1. * When installed, the bridge filter gathers per RPC statistics in addition to the standard array of global HTTP statistics. +* gRPC-Web is supported by a :ref:`filter ` that allows a gRPC-Web + client to send requests to Envoy over HTTP/1.1 and get proxied to a gRPC server. It's under + active development and is expected to be the successor to the gRPC :ref:`bridge filter + `. diff --git a/include/envoy/http/header_map.h b/include/envoy/http/header_map.h index a016f5533788f..9fe948d479e45 100644 --- a/include/envoy/http/header_map.h +++ b/include/envoy/http/header_map.h @@ -214,6 +214,7 @@ class HeaderEntry { HEADER_FUNC(Expect) \ HEADER_FUNC(ForwardedFor) \ HEADER_FUNC(ForwardedProto) \ + HEADER_FUNC(GrpcAcceptEncoding) \ HEADER_FUNC(GrpcMessage) \ HEADER_FUNC(GrpcStatus) \ HEADER_FUNC(Host) \ @@ -226,6 +227,7 @@ class HeaderEntry { HEADER_FUNC(Scheme) \ HEADER_FUNC(Server) \ HEADER_FUNC(Status) \ + HEADER_FUNC(TE) \ HEADER_FUNC(TransferEncoding) \ HEADER_FUNC(Upgrade) \ HEADER_FUNC(UserAgent) \ diff --git a/source/common/grpc/BUILD b/source/common/grpc/BUILD index f4f1b6c13e119..0f235ef49def4 100644 --- a/source/common/grpc/BUILD +++ b/source/common/grpc/BUILD @@ -56,6 +56,19 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "grpc_web_filter_lib", + srcs = ["grpc_web_filter.cc"], + hdrs = ["grpc_web_filter.h"], + deps = [ + ":codec_lib", + ":common_lib", + "//include/envoy/http:filter_interface", + "//source/common/common:base64_lib", + "//source/common/http:headers_lib", + ], +) + envoy_cc_library( name = "rpc_channel_lib", srcs = ["rpc_channel_impl.cc"], diff --git a/source/common/grpc/grpc_web_filter.cc b/source/common/grpc/grpc_web_filter.cc new file mode 100644 index 0000000000000..db1271820eb5a --- /dev/null +++ b/source/common/grpc/grpc_web_filter.cc @@ -0,0 +1,127 @@ +#include "common/grpc/grpc_web_filter.h" + +#include + +#include "common/common/base64.h" +#include "common/http/headers.h" + +namespace Envoy { +namespace Grpc { + +// Bit mask denotes a trailers frame of gRPC-Web. +const uint8_t GrpcWebFilter::GRPC_WEB_TRAILER = 0b10000000; + +// Implements StreamDecoderFilter. +// TODO(fengli): Implements the subtypes of gRPC-Web content-type, like +proto, etc. +Http::FilterHeadersStatus GrpcWebFilter::decodeHeaders(Http::HeaderMap& headers, bool) { + const Http::HeaderEntry* content_type = headers.ContentType(); + if (content_type != nullptr && + Http::Headers::get().ContentTypeValues.GrpcWebText == content_type->value().c_str()) { + // Checks whether gRPC-Web client is sending base64 encoded request. + is_text_request_ = true; + } + headers.insertContentType().value(Http::Headers::get().ContentTypeValues.Grpc); + + const Http::HeaderEntry* accept = headers.get(Http::Headers::get().Accept); + if (accept != nullptr && + Http::Headers::get().ContentTypeValues.GrpcWebText == accept->value().c_str()) { + // Checks whether gRPC-Web client is asking for base64 encoded response. + is_text_response_ = true; + } + + // Adds te:trailers to upstream HTTP2 request. It's required for gRPC. + headers.addStatic(Http::Headers::get().TE, Http::Headers::get().TEValues.Trailers); + // Adds grpc-accept-encoding:identity,deflate,gzip. It's required for gRPC. + headers.addStatic(Http::Headers::get().GrpcAcceptEncoding, + Http::Headers::get().GrpcAcceptEncodingValues.Default); + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterDataStatus GrpcWebFilter::decodeData(Buffer::Instance& data, bool) { + if (!is_text_request_) { + // No additional transcoding required if gRPC client is sending binary request. + return Http::FilterDataStatus::Continue; + } + + // Parse application/grpc-web-text format. + if (data.length() + decoding_buffer_.length() < 4) { + decoding_buffer_.move(data); + return Http::FilterDataStatus::StopIterationNoBuffer; + } + + const uint64_t needed = + (data.length() + decoding_buffer_.length()) / 4 * 4 - decoding_buffer_.length(); + decoding_buffer_.move(data, needed); + const std::string decoded = Base64::decode( + std::string(static_cast(decoding_buffer_.linearize(decoding_buffer_.length())), + decoding_buffer_.length())); + decoding_buffer_.drain(decoding_buffer_.length()); + decoding_buffer_.move(data); + data.add(decoded); + return Http::FilterDataStatus::Continue; +} + +// Implements StreamEncoderFilter. +Http::FilterHeadersStatus GrpcWebFilter::encodeHeaders(Http::HeaderMap& headers, bool) { + if (is_text_response_) { + headers.insertContentType().value(Http::Headers::get().ContentTypeValues.GrpcWebText); + } else { + headers.insertContentType().value(Http::Headers::get().ContentTypeValues.GrpcWeb); + } + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterDataStatus GrpcWebFilter::encodeData(Buffer::Instance& data, bool) { + if (!is_text_response_) { + // No additional transcoding required if gRPC-Web client asked for binary response. + return Http::FilterDataStatus::Continue; + } + + // The decoder always consumes and drains the given buffer. Incomplete data frame is buffered + // inside the decoder. + std::vector frames; + decoder_.decode(data, frames); + if (frames.empty()) { + // We don't have enough data to decode for one single frame, stop iteration until more data + // comes in. + return Http::FilterDataStatus::StopIterationNoBuffer; + } + + // Encodes the decoded gRPC frames with base64. + for (auto& frame : frames) { + Buffer::OwnedImpl temp; + temp.add(&frame.flags_, 1); + const uint32_t length = htonl(frame.length_); + temp.add(&length, 4); + if (frame.length_ > 0) { + temp.add(*frame.data_); + } + data.add(Base64::encode(temp, temp.length())); + } + return Http::FilterDataStatus::Continue; +} + +Http::FilterTrailersStatus GrpcWebFilter::encodeTrailers(Http::HeaderMap& trailers) { + // Trailers are expected to come all in once, and will be encoded into one single trailers frame. + // Trailers in the trailers frame are separated by CRLFs. + Buffer::OwnedImpl temp; + trailers.iterate([](const Http::HeaderEntry& header, void* context) -> void { + Buffer::Instance* temp = static_cast(context); + temp->add(header.key().c_str(), header.key().size()); + temp->add(":"); + temp->add(header.value().c_str(), header.value().size()); + temp->add("\r\n"); + }, &temp); + Buffer::OwnedImpl buffer; + // Adds the trailers frame head. + buffer.add(&GRPC_WEB_TRAILER, 1); + // Adds the trailers frame length. + const uint32_t length = htonl(temp.length()); + buffer.add(&length, 4); + buffer.move(temp); + encoder_callbacks_->addEncodedData(buffer); + return Http::FilterTrailersStatus::Continue; +} + +} // namespace Grpc +} // namespace Envoy diff --git a/source/common/grpc/grpc_web_filter.h b/source/common/grpc/grpc_web_filter.h new file mode 100644 index 0000000000000..e68d47cb86956 --- /dev/null +++ b/source/common/grpc/grpc_web_filter.h @@ -0,0 +1,48 @@ +#pragma once + +#include "envoy/http/filter.h" + +#include "common/buffer/buffer_impl.h" +#include "common/common/non_copyable.h" +#include "common/grpc/codec.h" + +namespace Envoy { +namespace Grpc { + +/** + * See docs/configuration/http_filters/grpc_web_filter.rst + */ +class GrpcWebFilter : public Http::StreamFilter, NonCopyable { +public: + GrpcWebFilter(){}; + virtual ~GrpcWebFilter(){}; + + void onDestroy() override{}; + + // Implements StreamDecoderFilter. + Http::FilterHeadersStatus decodeHeaders(Http::HeaderMap&, bool) override; + Http::FilterDataStatus decodeData(Buffer::Instance&, bool) override; + Http::FilterTrailersStatus decodeTrailers(Http::HeaderMap&) override { + return Http::FilterTrailersStatus::Continue; + } + void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks&) override {} + + // Implements StreamEncoderFilter. + Http::FilterHeadersStatus encodeHeaders(Http::HeaderMap&, bool) override; + Http::FilterDataStatus encodeData(Buffer::Instance&, bool) override; + Http::FilterTrailersStatus encodeTrailers(Http::HeaderMap& trailers) override; + void setEncoderFilterCallbacks(Http::StreamEncoderFilterCallbacks& callbacks) override { + encoder_callbacks_ = &callbacks; + } + +private: + static const uint8_t GRPC_WEB_TRAILER; + Http::StreamEncoderFilterCallbacks* encoder_callbacks_{}; + bool is_text_request_{}; + bool is_text_response_{}; + Buffer::OwnedImpl decoding_buffer_; + Decoder decoder_; +}; + +} // namespace Grpc +} // namespace Envoy diff --git a/source/common/http/headers.h b/source/common/http/headers.h index b6116763ad07e..4e2e6d4c3cab6 100644 --- a/source/common/http/headers.h +++ b/source/common/http/headers.h @@ -44,6 +44,7 @@ class HeaderValues { const LowerCaseString ForwardedProto{"x-forwarded-proto"}; const LowerCaseString GrpcMessage{"grpc-message"}; const LowerCaseString GrpcStatus{"grpc-status"}; + const LowerCaseString GrpcAcceptEncoding{"grpc-accept-encoding"}; const LowerCaseString Host{":authority"}; const LowerCaseString HostLegacy{"host"}; const LowerCaseString KeepAlive{"keep-alive"}; @@ -57,6 +58,7 @@ class HeaderValues { const LowerCaseString Server{"server"}; const LowerCaseString Status{":status"}; const LowerCaseString TransferEncoding{"transfer-encoding"}; + const LowerCaseString TE{"te"}; const LowerCaseString Upgrade{"upgrade"}; const LowerCaseString UserAgent{"user-agent"}; const LowerCaseString XB3TraceId{"x-b3-traceid"}; @@ -71,6 +73,9 @@ class HeaderValues { struct { const std::string Text{"text/plain"}; + const std::string Grpc{"application/grpc"}; + const std::string GrpcWeb{"application/grpc-web"}; + const std::string GrpcWebText{"application/grpc-web-text"}; } ContentTypeValues; struct { @@ -106,6 +111,14 @@ class HeaderValues { struct { const std::string EnvoyHealthChecker{"Envoy/HC"}; } UserAgentValues; + + struct { + const std::string Default{"identity,deflate,gzip"}; + } GrpcAcceptEncodingValues; + + struct { + const std::string Trailers{"trailers"}; + } TEValues; }; typedef ConstSingleton Headers; diff --git a/source/exe/BUILD b/source/exe/BUILD index ed61583b45f29..c7184cd019fb0 100644 --- a/source/exe/BUILD +++ b/source/exe/BUILD @@ -35,6 +35,7 @@ envoy_cc_library( "//source/server/config/http:dynamo_lib", "//source/server/config/http:fault_lib", "//source/server/config/http:grpc_http1_bridge_lib", + "//source/server/config/http:grpc_web_lib", "//source/server/config/http:lightstep_lib", "//source/server/config/http:ratelimit_lib", "//source/server/config/http:router_lib", diff --git a/source/server/config/http/BUILD b/source/server/config/http/BUILD index 2aad7bb20b21b..e545a1c99cf37 100644 --- a/source/server/config/http/BUILD +++ b/source/server/config/http/BUILD @@ -53,6 +53,17 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "grpc_web_lib", + srcs = ["grpc_web.cc"], + hdrs = ["grpc_web.h"], + deps = [ + "//include/envoy/server:instance_interface", + "//source/common/grpc:grpc_web_filter_lib", + "//source/server/config/network:http_connection_manager_lib", + ], +) + envoy_cc_library( name = "lightstep_lib", srcs = ["lightstep_http_tracer.cc"], diff --git a/source/server/config/http/grpc_web.cc b/source/server/config/http/grpc_web.cc new file mode 100644 index 0000000000000..b35ff2fd7ee5b --- /dev/null +++ b/source/server/config/http/grpc_web.cc @@ -0,0 +1,32 @@ +#include "server/config/http/grpc_web.h" + +#include "common/grpc/grpc_web_filter.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +HttpFilterFactoryCb GrpcWebFilterConfig::createFilterFactory(HttpFilterType type, + const Json::Object&, + const std::string&, + Server::Instance&) { + if (type != HttpFilterType::Both) { + throw EnvoyException(fmt::format( + "{} gRPC-Web filter must be configured as both a decoder and encoder filter.", name())); + } + + return [](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamFilter(Http::StreamFilterSharedPtr{new Grpc::GrpcWebFilter()}); + }; +} + +std::string GrpcWebFilterConfig::name() { return "grpc_web"; } + +/** + * Static registration for the gRPC-Web filter. @see RegisterNamedHttpFilterConfigFactory. + */ +static RegisterNamedHttpFilterConfigFactory register_; + +} // namespace Configuration +} // namespace Server +} // namespace Envoy diff --git a/source/server/config/http/grpc_web.h b/source/server/config/http/grpc_web.h new file mode 100644 index 0000000000000..9bf131c4fba8c --- /dev/null +++ b/source/server/config/http/grpc_web.h @@ -0,0 +1,19 @@ +#pragma once + +#include "server/config/network/http_connection_manager.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +class GrpcWebFilterConfig : public NamedHttpFilterConfigFactory { +public: + HttpFilterFactoryCb createFilterFactory(HttpFilterType type, const Json::Object&, + const std::string&, Server::Instance&) override; + + std::string name() override; +}; + +} // namespace Configuration +} // namespace Server +} // namespace Envoy diff --git a/test/common/grpc/BUILD b/test/common/grpc/BUILD index 37b39fb54d7a7..fcd6116a58a82 100644 --- a/test/common/grpc/BUILD +++ b/test/common/grpc/BUILD @@ -42,6 +42,17 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "grpc_web_filter_test", + srcs = ["grpc_web_filter_test.cc"], + deps = [ + "//source/common/grpc:grpc_web_filter_lib", + "//test/mocks/http:http_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:utility_lib", + ], +) + envoy_cc_test( name = "rpc_channel_impl_test", srcs = ["rpc_channel_impl_test.cc"], diff --git a/test/common/grpc/grpc_web_filter_test.cc b/test/common/grpc/grpc_web_filter_test.cc new file mode 100644 index 0000000000000..9e49b766f3c18 --- /dev/null +++ b/test/common/grpc/grpc_web_filter_test.cc @@ -0,0 +1,148 @@ +#include "common/buffer/buffer_impl.h" +#include "common/common/base64.h" +#include "common/grpc/grpc_web_filter.h" +#include "common/http/header_map_impl.h" +#include "common/http/headers.h" + +#include "test/mocks/http/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/printers.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::NiceMock; +using testing::Invoke; + +namespace Envoy { +namespace Grpc { +namespace { +const char MESSAGE[] = "\x00\x00\x00\x00\x11grpc-web-bin-data"; +const size_t MESSAGE_SIZE = sizeof(MESSAGE) - 1; +const char TEXT_MESSAGE[] = "\x00\x00\x00\x00\x12grpc-web-text-data"; +const size_t TEXT_MESSAGE_SIZE = sizeof(TEXT_MESSAGE) - 1; +const char B64_MESSAGE[] = "AAAAABJncnBjLXdlYi10ZXh0LWRhdGE="; +const size_t B64_MESSAGE_SIZE = sizeof(B64_MESSAGE) - 1; +const char TRAILERS[] = "\x80\x00\x00\x00\x20grpc-status:0\r\ngrpc-message:ok\r\n"; +const size_t TRAILERS_SIZE = sizeof(TRAILERS) - 1; +} // namespace + +class GrpcWebFilterTest : public testing::Test { +public: + GrpcWebFilterTest() : filter_() { filter_.setEncoderFilterCallbacks(encoder_callbacks_); } + + ~GrpcWebFilterTest() override {} + + GrpcWebFilter filter_; + NiceMock encoder_callbacks_; +}; + +TEST_F(GrpcWebFilterTest, BinaryUnary) { + // Tests request headers. + Http::TestHeaderMapImpl request_headers; + request_headers.addViaCopy(Http::Headers::get().ContentType, + Http::Headers::get().ContentTypeValues.GrpcWeb); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::Headers::get().ContentTypeValues.Grpc, + request_headers.ContentType()->value().c_str()); + EXPECT_EQ(Http::Headers::get().TEValues.Trailers, request_headers.TE()->value().c_str()); + EXPECT_EQ(Http::Headers::get().GrpcAcceptEncodingValues.Default, + request_headers.GrpcAcceptEncoding()->value().c_str()); + + // Tests request data. + Buffer::OwnedImpl request_buffer(MESSAGE, MESSAGE_SIZE); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(request_buffer, true)); + EXPECT_EQ(std::string(MESSAGE, MESSAGE_SIZE), TestUtility::bufferToString(request_buffer)); + + // Tests request trailers, they are passed through. + Http::TestHeaderMapImpl request_trailers; + request_trailers.addViaCopy(Http::Headers::get().GrpcStatus, "0"); + request_trailers.addViaCopy(Http::Headers::get().GrpcMessage, "ok"); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_trailers)); + EXPECT_STREQ("0", request_trailers.GrpcStatus()->value().c_str()); + EXPECT_STREQ("ok", request_trailers.GrpcMessage()->value().c_str()); + + // Tests response headers. + Http::TestHeaderMapImpl response_headers; + response_headers.addViaCopy(Http::Headers::get().Status, "200"); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.encodeHeaders(response_headers, false)); + EXPECT_EQ("200", response_headers.get_(Http::Headers::get().Status.get())); + EXPECT_EQ(Http::Headers::get().ContentTypeValues.GrpcWeb, + response_headers.ContentType()->value().c_str()); + + // Tests response data. + Buffer::OwnedImpl response_buffer(MESSAGE, MESSAGE_SIZE); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.encodeData(response_buffer, false)); + EXPECT_EQ(std::string(MESSAGE, MESSAGE_SIZE), TestUtility::bufferToString(response_buffer)); + response_buffer.drain(response_buffer.length()); + + // Tests response trailers. + Buffer::OwnedImpl trailers_buffer; + EXPECT_CALL(encoder_callbacks_, addEncodedData(_)) + .WillOnce(Invoke([&](Buffer::Instance& data) { trailers_buffer.move(data); })); + Http::TestHeaderMapImpl response_trailers; + response_trailers.addViaCopy(Http::Headers::get().GrpcStatus, "0"); + response_trailers.addViaCopy(Http::Headers::get().GrpcMessage, "ok"); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.encodeTrailers(response_trailers)); + EXPECT_EQ(std::string(TRAILERS, TRAILERS_SIZE), TestUtility::bufferToString(trailers_buffer)); +} + +TEST_F(GrpcWebFilterTest, TextUnary) { + // Tests request headers. + Http::TestHeaderMapImpl request_headers; + request_headers.addViaCopy(Http::Headers::get().ContentType, + Http::Headers::get().ContentTypeValues.GrpcWebText); + request_headers.addViaCopy(Http::Headers::get().Accept, + Http::Headers::get().ContentTypeValues.GrpcWebText); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ(Http::Headers::get().ContentTypeValues.Grpc, + request_headers.ContentType()->value().c_str()); + EXPECT_EQ(Http::Headers::get().TEValues.Trailers, request_headers.TE()->value().c_str()); + EXPECT_EQ(Http::Headers::get().GrpcAcceptEncodingValues.Default, + request_headers.GrpcAcceptEncoding()->value().c_str()); + + // Tests request data. + Buffer::OwnedImpl request_buffer(B64_MESSAGE, B64_MESSAGE_SIZE); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(request_buffer, true)); + EXPECT_EQ(std::string(TEXT_MESSAGE, TEXT_MESSAGE_SIZE), + TestUtility::bufferToString(request_buffer)); + + // Tests request trailers, they are passed through. + Http::TestHeaderMapImpl request_trailers; + request_trailers.addViaCopy(Http::Headers::get().GrpcStatus, "0"); + request_trailers.addViaCopy(Http::Headers::get().GrpcMessage, "ok"); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_trailers)); + EXPECT_STREQ("0", request_trailers.GrpcStatus()->value().c_str()); + EXPECT_STREQ("ok", request_trailers.GrpcMessage()->value().c_str()); + + // Tests response headers. + Http::TestHeaderMapImpl response_headers; + response_headers.addViaCopy(Http::Headers::get().Status, "200"); + response_headers.addViaCopy(Http::Headers::get().ContentType, + Http::Headers::get().ContentTypeValues.Grpc); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.encodeHeaders(response_headers, false)); + EXPECT_EQ("200", response_headers.get_(Http::Headers::get().Status.get())); + EXPECT_EQ(Http::Headers::get().ContentTypeValues.GrpcWebText, + response_headers.ContentType()->value().c_str()); + + // Tests response data. + Buffer::OwnedImpl response_buffer(TEXT_MESSAGE, TEXT_MESSAGE_SIZE); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.encodeData(response_buffer, false)); + EXPECT_EQ(std::string(B64_MESSAGE, B64_MESSAGE_SIZE), + TestUtility::bufferToString(response_buffer)); + response_buffer.drain(response_buffer.length()); + + // Tests response trailers. + Buffer::OwnedImpl trailers_buffer; + EXPECT_CALL(encoder_callbacks_, addEncodedData(_)) + .WillOnce(Invoke([&](Buffer::Instance& data) { trailers_buffer.move(data); })); + Http::TestHeaderMapImpl response_trailers; + response_trailers.addViaCopy(Http::Headers::get().GrpcStatus, "0"); + response_trailers.addViaCopy(Http::Headers::get().GrpcMessage, "ok"); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.encodeTrailers(response_trailers)); + EXPECT_EQ(std::string(TRAILERS, TRAILERS_SIZE), TestUtility::bufferToString(trailers_buffer)); +} +} // namespace Grpc +} // namespace Envoy diff --git a/test/server/config/http/BUILD b/test/server/config/http/BUILD index 1de16b1b075f2..845f205d4f29f 100644 --- a/test/server/config/http/BUILD +++ b/test/server/config/http/BUILD @@ -16,6 +16,7 @@ envoy_cc_test( "//source/server/config/http:dynamo_lib", "//source/server/config/http:fault_lib", "//source/server/config/http:grpc_http1_bridge_lib", + "//source/server/config/http:grpc_web_lib", "//source/server/config/http:lightstep_lib", "//source/server/config/http:ratelimit_lib", "//source/server/config/http:router_lib", diff --git a/test/server/config/http/config_test.cc b/test/server/config/http/config_test.cc index 2469caafe0496..66ad037da01a3 100644 --- a/test/server/config/http/config_test.cc +++ b/test/server/config/http/config_test.cc @@ -4,6 +4,7 @@ #include "server/config/http/dynamo.h" #include "server/config/http/fault.h" #include "server/config/http/grpc_http1_bridge.h" +#include "server/config/http/grpc_web.h" #include "server/config/http/lightstep_http_tracer.h" #include "server/config/http/ratelimit.h" #include "server/config/http/router.h" @@ -110,6 +111,22 @@ TEST(HttpFilterConfigTest, GrpcHttp1BridgeFilter) { cb(filter_callback); } +TEST(HttpFilterConfigTest, GrpcWebFilter) { + std::string json_string = R"EOF( + { + } + )EOF"; + + Json::ObjectSharedPtr json_config = Json::Factory::loadFromString(json_string); + NiceMock server; + GrpcWebFilterConfig factory; + HttpFilterFactoryCb cb = + factory.createFilterFactory(HttpFilterType::Both, *json_config, "stats", server); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + cb(filter_callback); +} + TEST(HttpFilterConfigTest, HealthCheckFilter) { std::string json_string = R"EOF( {