Adds grpc-web support for envoy.#943
Adds grpc-web support for envoy.#943htuch merged 40 commits intoenvoyproxy:masterfrom fengli79:master
Conversation
The HotRestart holds a socket event for the domain socket used for admin purpose, it need to be deleted before the envoy server gets deleted, as the envoy server own the event_base and which already deleted the registered events with the event_base_free().
|
|
||
| namespace Grpc { | ||
|
|
||
| const std::string GrpcWebFilter::GRPC_WEB_CONTENT_TYPE{"application/grpc-web"}; |
There was a problem hiding this comment.
Global statics are disallowed (and you know this ;) )
htuch
left a comment
There was a problem hiding this comment.
As discussed offline, we should add curl to the build recipes as shown in https://github.com/lyft/envoy/blob/master/bazel/EXTERNAL_DEPS.md when it comes time to do the integration tests.
| }, this); | ||
| #endif | ||
|
|
||
| if (buffered_response_data_ && buffered_response_data_->length() > 0) { |
There was a problem hiding this comment.
Per offline convo with @fengli79 I am working on a proper solution to this part which unfortunately is pretty tricky. Hope to land this by the end of the week. It shouldn't change the rest of the code that much so we can continue to review/test in the interim.
|
Please rebase on #959 once merged. |
htuch
left a comment
There was a problem hiding this comment.
Looks generally quite good. But, I think this needs some more detailed comments in various places, see feedback below.
| namespace Envoy { | ||
| namespace Grpc { | ||
|
|
||
| const uint8_t GrpcWebFilter::GRPC_WEB_TRAILER = 0b10000000; |
There was a problem hiding this comment.
Nit: personally I find (1 << 7) much clearer than binary literals where I need to count zeroes. But, feel free to keep it this way if you'd like.
There was a problem hiding this comment.
Yes, let's keep this.
There was a problem hiding this comment.
Please add a comment that this is specifying a "trailer with no compression".
| 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()) { |
There was a problem hiding this comment.
Based on https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2, I would expect that grpc-web-text to be a prefix, not exact match. The default is "+proto", but what about the other cases?
There was a problem hiding this comment.
I plan add follow up pull requests to support other subtypes soon.
There was a problem hiding this comment.
Please add a comment here and the other site to this effect, TODO(fengli79): ...
| Http::Headers::get().ContentTypeValues.GrpcWebText == content_type->value().c_str()) { | ||
| is_text_request_ = true; | ||
| } | ||
| headers.insertContentType().value(Http::Headers::get().ContentTypeValues.Grpc); |
There was a problem hiding this comment.
From the docs, insertContentType() only inserts a header if it doesn't exist. I'm wondering how this works then, since you have test coverage for this.
There was a problem hiding this comment.
If there's no content-type header from the request, the headers.insertContentType() will create a HeaderMapEntry and return it, thus I can change its value.
If there's already a content-type header, the inserContentType() will return the existing HeaderMapEntry.
Note that content-type is an inline header, it doesn't support appending.
There was a problem hiding this comment.
That's correct. When content-type exist, it will return reference to existing entry, and following .value sets the value, so it works.
|
|
||
| const Http::HeaderEntry* accept = headers.get(Http::Headers::get().Accept); | ||
| if (accept != nullptr && | ||
| Http::Headers::get().ContentTypeValues.GrpcWebText == accept->value().c_str()) { |
There was a problem hiding this comment.
Same as above comment on grpc-web-text prefix.
| // Parse application/grpc-web-text format. | ||
| if (data.length() + decoding_buffer_.length() < 4) { | ||
| decoding_buffer_.move(data); | ||
| return Http::FilterDataStatus::Continue; |
There was a problem hiding this comment.
Do you want to use StopIterationAndBuffer here (and have the filter mechanism perform buffering for you until you have 4 bytes), or is there some reason that the filters down the chain should operate on an empty buffer? I think that would simplify the below code as well (you might not even need an explicit decoding_buffer_).
There was a problem hiding this comment.
I cannot use StopIterationAndBuffer, as I have to consume the buffer given by the filter completely and put back b64 decoded data in the same buffer. If I let the filter to buffer the data, I will lose the information about the boundary for the b64 decoded data and raw data in the filter buffer.
There was a problem hiding this comment.
Right, got it. Do you want to do StopIterationNoBufferin the < 4 branch here, since there's no data for later filters at this point?
There was a problem hiding this comment.
That makes sense. Done.
| return Http::FilterDataStatus::Continue; | ||
| } | ||
|
|
||
| // Encodes the response as base64. |
There was a problem hiding this comment.
Can you elaborate in some more depth on the transformation that is happening here? My current guess is that we're taking standard gRPC wire format, decoding and deframing, then re-encoding it using the format of gRPC Web? What is the meaning of flags in this context? It's probably not H2 flags, so decoder_ must have figured out what the delimited message flags are? I think lots of verbose comments would help.
There was a problem hiding this comment.
More comments added.
| temp->add("\r\n"); | ||
| }, &temp); | ||
| Buffer::OwnedImpl buffer; | ||
| buffer.add(&GRPC_WEB_TRAILER, 1); |
There was a problem hiding this comment.
Please describe that this is a delimited message that is being assembled as per the gRPC spec.
There was a problem hiding this comment.
It's not in gRPC spec. Instead, it's defined in gRPC-Web. Trailers are encoded in a frame denoted with 0b1000000 + 4 bytes length in network order. The frame data is CRLF separated headers.
| // Tests request data. | ||
| Buffer::OwnedImpl request_buffer(MESSAGE, MESSAGE_SIZE); | ||
| EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(request_buffer, true)); | ||
| EXPECT_EQ(0, memcmp(MESSAGE, TestUtility::bufferToString(request_buffer).c_str(), MESSAGE_SIZE)); |
There was a problem hiding this comment.
Why not just do string comparison?
There was a problem hiding this comment.
Because the buffer may contains \0.
There was a problem hiding this comment.
You can do C++ string comparisons on buffers with embedded nulls.
There was a problem hiding this comment.
Done. However, I have to wrap the MESSAGE with std::string(MESSAGE, MESSAGE_SIZE).
| Buffer::OwnedImpl request_buffer(MESSAGE, MESSAGE_SIZE); | ||
| EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(request_buffer, true)); | ||
| EXPECT_EQ(0, memcmp(MESSAGE, TestUtility::bufferToString(request_buffer).c_str(), MESSAGE_SIZE)); | ||
|
|
There was a problem hiding this comment.
What about testing request trailers?
There was a problem hiding this comment.
gRPC doesn't use request trailers. And gRPC-Web clients are in browser, also won't send request trailers.
If a request trailer presents, it will be forwarded to the backend without change, and let the backend decide whether to ignore it or reject it.
There was a problem hiding this comment.
I think you should still verify this pass-thru behavior, as it's something we intend to have happen and don't want to see a regression for.
There was a problem hiding this comment.
I think I now get grpc-web :) I'll reiterate my earlier comment that it would be helpful if the grpc-web spec was more than just a terse set of bullet points that provide a diff against the normal gRPC H2 framing, but instead provide a full spec. Thanks.
| 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()) { |
There was a problem hiding this comment.
Please add a comment here and the other site to this effect, TODO(fengli79): ...
| // Parse application/grpc-web-text format. | ||
| if (data.length() + decoding_buffer_.length() < 4) { | ||
| decoding_buffer_.move(data); | ||
| return Http::FilterDataStatus::Continue; |
There was a problem hiding this comment.
Right, got it. Do you want to do StopIterationNoBufferin the < 4 branch here, since there's no data for later filters at this point?
| // Encodes the decoded frames with base64. | ||
| for (auto& frame : frames) { | ||
| Buffer::OwnedImpl temp; | ||
| temp.add(&frame.flags_, 1); |
There was a problem hiding this comment.
Can you please add an explanation of what these flags are? Please point to the relevant specification. As a reader, I find it very confusing to have to think about both H2 flags (which clearly these aren't, but the above decode doesn't make clear), normal gRPC message delimiting flags (compression vs. no compression) and the gRPC-web special flags (with the MSB bit). If I find it confusing, I think others will too. Thanks!
There was a problem hiding this comment.
Those flags are gRPC frame flags. They are decoded as grpc frames from a grpc backend.
They are documented in the decoder.cc:
https://github.com/lyft/envoy/blob/master/source/common/grpc/codec.h#L49
There was a problem hiding this comment.
It's gRPC frame, comes from backend. It's documented in the decoder.h.
https://github.com/lyft/envoy/blob/master/source/common/grpc/codec.h#L49
| namespace Envoy { | ||
| namespace Grpc { | ||
|
|
||
| const uint8_t GrpcWebFilter::GRPC_WEB_TRAILER = 0b10000000; |
There was a problem hiding this comment.
Please add a comment that this is specifying a "trailer with no compression".
| const char B64_MESSAGE[] = "AAAAABJncnBjLXdlYi10ZXh0LWRhdGE="; | ||
| const size_t B64_MESSAGE_SIZE = 32; | ||
| const char TRAILERS[] = "\x80\x00\x00\x00\x0fgrpc-status:0\r\n"; | ||
| const size_t TRAILERS_SIZE = 20; |
There was a problem hiding this comment.
Can we use sizeof here to make this less fragile, since these are const arrays with known size?
| // Tests request data. | ||
| Buffer::OwnedImpl request_buffer(MESSAGE, MESSAGE_SIZE); | ||
| EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(request_buffer, true)); | ||
| EXPECT_EQ(0, memcmp(MESSAGE, TestUtility::bufferToString(request_buffer).c_str(), MESSAGE_SIZE)); |
There was a problem hiding this comment.
You can do C++ string comparisons on buffers with embedded nulls.
| Buffer::OwnedImpl request_buffer(MESSAGE, MESSAGE_SIZE); | ||
| EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(request_buffer, true)); | ||
| EXPECT_EQ(0, memcmp(MESSAGE, TestUtility::bufferToString(request_buffer).c_str(), MESSAGE_SIZE)); | ||
|
|
There was a problem hiding this comment.
I think you should still verify this pass-thru behavior, as it's something we intend to have happen and don't want to see a regression for.
| 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()); |
There was a problem hiding this comment.
Where do you check grpc-accept-encoding:identity,deflate,gzip., te:trailers, etc?
| const size_t TEXT_MESSAGE_SIZE = 23; | ||
| const char B64_MESSAGE[] = "AAAAABJncnBjLXdlYi10ZXh0LWRhdGE="; | ||
| const size_t B64_MESSAGE_SIZE = 32; | ||
| const char TRAILERS[] = "\x80\x00\x00\x00\x0fgrpc-status:0\r\n"; |
There was a problem hiding this comment.
Should have more than one trailer to verify multi trailer header handling.
| 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 b64 ncoded request. |
| bool is_text_response_; | ||
| Buffer::OwnedImpl decoding_buffer_; | ||
| Decoder decoder_; | ||
| }; |
There was a problem hiding this comment.
nit: new line after this and before the closing bracket for namespace Grpc
| buffer.move(temp); | ||
| encoder_callbacks_->addEncodedData(buffer); | ||
| return Http::FilterTrailersStatus::Continue; | ||
| } |
There was a problem hiding this comment.
nit: new line after this line and before bracket closing the namespace Grpc
| namespace Server { | ||
| namespace Configuration { | ||
|
|
||
| HttpFilterFactoryCb GrpcWebFilterConfig::tryCreateFilterFactory(HttpFilterType type, |
There was a problem hiding this comment.
Please add a test for the filter config loading in https://github.com/lyft/envoy/blob/master/test/server/config/http/config_test.cc
| // Implements StreamDecoderFilter. | ||
| Http::FilterHeadersStatus decodeHeaders(Http::HeaderMap&, bool) override; | ||
| Http::FilterDataStatus decodeData(Buffer::Instance&, bool) override; | ||
|
|
| namespace Envoy { | ||
| namespace Grpc { | ||
|
|
||
| // Bit mask denotes a trailers frame of gRPC-Web. |
There was a problem hiding this comment.
nit: uncompressed trailer
There was a problem hiding this comment.
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.
ok, works for me as long as it's WIP in here.
|
|
||
| private: | ||
| static const uint8_t GRPC_WEB_TRAILER; | ||
| Http::StreamDecoderFilterCallbacks* decoder_callbacks_{}; |
| static const uint8_t GRPC_WEB_TRAILER; | ||
| Http::StreamDecoderFilterCallbacks* decoder_callbacks_{}; | ||
| Http::StreamEncoderFilterCallbacks* encoder_callbacks_{}; | ||
| bool is_text_request_; |
There was a problem hiding this comment.
for consistency you can init it right here, is_text_request_{} and same with is_text_respose_
| const Json::Object&, | ||
| const std::string&, | ||
| Server::Instance&) { | ||
| if (type != HttpFilterType::Both || name != "grpc_web") { |
There was a problem hiding this comment.
just a heads up: #994
HTTP filters registration will be changed in ^
| namespace Server { | ||
| namespace Configuration { | ||
|
|
||
| HttpFilterFactoryCb GrpcWebFilterConfig::tryCreateFilterFactory(HttpFilterType type, |
There was a problem hiding this comment.
This is still the old filter creation API, can you bump to the new one? Thanks!
| } | ||
|
|
||
| // Adds te:trailers to upstream HTTP2 request. It's required for gRPC. | ||
| headers.addStatic(Http::Headers::get().TE, Http::Headers::get().TEValues.Trailers); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Automatic merge from submit-queue. [DO NOT MERGE] Auto PR to update dependencies of proxy This PR will be merged automatically once checks are successful. ```release-note none ```
Description: Adds initial support for Swift presentation of HTTP filter on-headers invocations. Risk Level: Moderate Testing: Local end-to-end Signed-off-by: Mike Schore <mike.schore@gmail.com> Signed-off-by: JP Simard <jp@jpsim.com>
Description: Adds initial support for Swift presentation of HTTP filter on-headers invocations. Risk Level: Moderate Testing: Local end-to-end Signed-off-by: Mike Schore <mike.schore@gmail.com> Signed-off-by: JP Simard <jp@jpsim.com>
Add the grpc-web support for envoy.