-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Adds grpc-web support for envoy. #943
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b137f32
312dd58
d0cec57
ab3855f
489470a
8ecec66
8ef4b1c
2362192
4c9a291
43297c3
2f20d0d
b4f942e
7a205c9
9d6b6d8
8c958b4
6acc12f
1394d32
ffee1d4
7db18bb
2d195c3
539dcf5
7471364
6bf1e12
e2a5d4e
25b9c37
f6bca59
475a35c
8e848f6
4950429
92b511d
69b34b0
02fb75a
7251c10
a1b6643
15244b4
88e5c63
0f93298
275cb7e
6dc3008
ddb90fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| .. _config_http_filters_grpc_web: | ||
|
|
||
| gRPC-Web filter | ||
| ==================== | ||
|
|
||
| gRPC :ref:`architecture overview <arch_overview_grpc>`. | ||
|
|
||
| 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": {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| #include "common/grpc/grpc_web_filter.h" | ||
|
|
||
| #include <arpa/inet.h> | ||
|
|
||
| #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()) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above comment on grpc-web-text prefix. |
||
| // 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both of these addStatic() calls can use the O(1) insert varieties since you added the headers to the O(1) map. This would yield better performance. @fengli79 not urgent but I would do a follow up or add to a future grpc-web PR.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will address in a follow up PR. Also, there are other stuffs need to be enhanced, like add some wrapper methods in Buffer::OwnedImpl to avoid the additional buffer allocation. Today I have to use a temp buffer when processing trailers, as the Buffer::OwnedImpl doesn't allow me to insert data to a particular position. |
||
| // 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<const char*>(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<Frame> 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<Buffer::Instance*>(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; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: new line after this line and before bracket closing the namespace Grpc
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
|
||
| } // namespace Grpc | ||
| } // namespace Envoy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<GrpcWebFilterConfig> register_; | ||
|
|
||
| } // namespace Configuration | ||
| } // namespace Server | ||
| } // namespace Envoy |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: uncompressed trailer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to add another bit mask 0b00000001 for compression. So it's not necessary to mention the compression here once it gets in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, works for me as long as it's WIP in here.