Skip to content

Commit

Permalink
http: Change sendLocalReply to send percent-encoded GrpcMessage (#6913)
Browse files Browse the repository at this point in the history
This patch changed sendLocalReply to send GrpcMessage in percent-encoded
instead of plain string.

Signed-off-by: Dhi Aurrahman <dio@tetrate.io>
  • Loading branch information
dio authored and mattklein123 committed May 30, 2019
1 parent a363fb2 commit f326331
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Version history
* http: fixed a bug where large unbufferable responses were not tracked in stats and logs correctly.
* http: fixed a crashing bug where gRPC local replies would cause segfaults when upstream access logging was on.
* http: mitigated a race condition with the :ref:`delayed_close_timeout<envoy_api_field_config.filter.network.http_connection_manager.v2.HttpConnectionManager.delayed_close_timeout>` where it could trigger while actively flushing a pending write buffer for a downstream connection.
* http: changed `sendLocalReply` to send percent-encoded `GrpcMessage`.
* jwt_authn: make filter's parsing of JWT more flexible, allowing syntax like ``jwt=eyJhbGciOiJS...ZFnFIw,extra=7,realm=123``
* original_src filter: added the :ref:`filter<config_http_filters_original_src>`.
* rbac: migrated from v2alpha to v2.
Expand Down
67 changes: 65 additions & 2 deletions source/common/http/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,9 @@ void Utility::sendLocalReply(
enumToInt(grpc_status ? grpc_status.value()
: Grpc::Utility::httpToGrpcStatus(enumToInt(response_code))))}}};
if (!body_text.empty() && !is_head_request) {
// TODO: GrpcMessage should be percent-encoded
response_headers->insertGrpcMessage().value(body_text);
// TODO(dio): Probably it is worth to consider caching the encoded message based on gRPC
// status.
response_headers->insertGrpcMessage().value(PercentEncoding::encode(body_text));
}
encode_headers(std::move(response_headers), true); // Trailers only response
return;
Expand Down Expand Up @@ -536,5 +537,67 @@ void Utility::traversePerFilterConfigGeneric(
}
}

std::string Utility::PercentEncoding::encode(absl::string_view value) {
for (size_t i = 0; i < value.size(); ++i) {
const char& ch = value[i];
// The escaping characters are defined in
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses.
//
// We do checking for each char in the string. If the current char is included in the defined
// escaping characters, we jump to "the slow path" (append the char [encoded or not encoded] to
// the returned string one by one) started from the current index.
if (ch < ' ' || ch >= '~' || ch == '%') {
return PercentEncoding::encode(value, i);
}
}
return std::string(value);
}

std::string Utility::PercentEncoding::encode(absl::string_view value, const size_t index) {
std::string encoded;
if (index > 0) {
absl::StrAppend(&encoded, value.substr(0, index - 1));
}

for (size_t i = index; i < value.size(); ++i) {
const char& ch = value[i];
if (ch < ' ' || ch >= '~' || ch == '%') {
// For consistency, URI producers should use uppercase hexadecimal digits for all
// percent-encodings. https://tools.ietf.org/html/rfc3986#section-2.1.
absl::StrAppend(&encoded, fmt::format("%{:02X}", ch));
} else {
encoded.push_back(ch);
}
}
return encoded;
}

std::string Utility::PercentEncoding::decode(absl::string_view encoded) {
std::string decoded;
decoded.reserve(encoded.size());
for (size_t i = 0; i < encoded.size(); ++i) {
char ch = encoded[i];
if (ch == '%' && i + 2 < encoded.size()) {
const char& hi = encoded[i + 1];
const char& lo = encoded[i + 2];
if (absl::ascii_isdigit(hi)) {
ch = hi - '0';
} else {
ch = absl::ascii_toupper(hi) - 'A' + 10;
}

ch *= 16;
if (absl::ascii_isdigit(lo)) {
ch += lo - '0';
} else {
ch += absl::ascii_toupper(lo) - 'A' + 10;
}
i += 2;
}
decoded.push_back(ch);
}
return decoded;
}

} // namespace Http
} // namespace Envoy
22 changes: 22 additions & 0 deletions source/common/http/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,28 @@ class Url {
absl::string_view path_and_query_params_;
};

class PercentEncoding {
public:
/**
* Encodes string view to its percent encoded representation.
* @param value supplies string to be encoded.
* @return std::string percent-encoded string based on
* https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses.
*/
static std::string encode(absl::string_view value);

/**
* Decodes string view from its percent encoded representation.
* @param encoded supplies string to be decoded.
* @return std::string decoded string https://tools.ietf.org/html/rfc3986#section-2.1.
*/
static std::string decode(absl::string_view value);

private:
// Encodes string view to its percent encoded representation, with start index.
static std::string encode(absl::string_view value, const size_t index);
};

/**
* Append to x-forwarded-for header.
* @param headers supplies the headers to append to.
Expand Down
54 changes: 54 additions & 0 deletions test/common/http/utility_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,33 @@ TEST(HttpUtility, SendLocalGrpcReply) {
absl::nullopt, false);
}

TEST(HttpUtility, SendLocalGrpcReplyWithUpstreamJsonPayload) {
MockStreamDecoderFilterCallbacks callbacks;
bool is_reset = false;

std::string json = R"EOF(
{
"error": {
"code": 401,
"message": "Unauthorized"
}
}
)EOF";

EXPECT_CALL(callbacks, encodeHeaders_(_, true))
.WillOnce(Invoke([&](const HeaderMap& headers, bool) -> void {
EXPECT_EQ(headers.Status()->value().getStringView(), "200");
EXPECT_NE(headers.GrpcStatus(), nullptr);
EXPECT_EQ(headers.GrpcStatus()->value().getStringView(),
std::to_string(enumToInt(Grpc::Status::GrpcStatus::Unauthenticated)));
EXPECT_NE(headers.GrpcMessage(), nullptr);
const auto& encoded = Utility::PercentEncoding::encode(json);
EXPECT_EQ(headers.GrpcMessage()->value().getStringView(), encoded);
}));
Utility::sendLocalReply(true, callbacks, is_reset, Http::Code::Unauthorized, json, absl::nullopt,
false);
}

TEST(HttpUtility, RateLimitedGrpcStatus) {
MockStreamDecoderFilterCallbacks callbacks;

Expand Down Expand Up @@ -789,5 +816,32 @@ TEST(Url, ParsingTest) {
"/path?query=param&query2=param2#fragment");
}

void validatePercentEncodingEncodeDecode(absl::string_view source,
absl::string_view expected_encoded) {
EXPECT_EQ(Utility::PercentEncoding::encode(source), expected_encoded);
EXPECT_EQ(Utility::PercentEncoding::decode(expected_encoded), source);
}

TEST(PercentEncoding, EncodeDecode) {
const std::string json = R"EOF(
{
"error": {
"code": 401,
"message": "Unauthorized"
}
}
)EOF";
validatePercentEncodingEncodeDecode(json, "%0A{%0A \"error\": {%0A \"code\": 401,%0A "
" \"message\": \"Unauthorized\"%0A }%0A}%0A ");
validatePercentEncodingEncodeDecode("too large", "too large");
validatePercentEncodingEncodeDecode("_-ok-_", "_-ok-_");
}

TEST(PercentEncoding, Trailing) {
EXPECT_EQ(Utility::PercentEncoding::decode("too%20lar%20"), "too lar ");
EXPECT_EQ(Utility::PercentEncoding::decode("too%20larg%e"), "too larg%e");
EXPECT_EQ(Utility::PercentEncoding::decode("too%20large%"), "too large%");
}

} // namespace Http
} // namespace Envoy
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "common/buffer/buffer_impl.h"
#include "common/grpc/codec.h"
#include "common/http/header_map_impl.h"
#include "common/http/utility.h"

#include "extensions/filters/http/grpc_http1_reverse_bridge/filter.h"
#include "extensions/filters/http/well_known_names.h"
Expand Down Expand Up @@ -67,7 +68,9 @@ TEST_F(ReverseBridgeTest, InvalidGrpcRequest) {
EXPECT_CALL(decoder_callbacks_, encodeHeaders_(_, _)).WillOnce(Invoke([](auto& headers, auto) {
EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().Status, "200"));
EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().GrpcStatus, "2"));
EXPECT_THAT(headers, HeaderValueOf(Http::Headers::get().GrpcMessage, "invalid request body"));
EXPECT_THAT(headers,
HeaderValueOf(Http::Headers::get().GrpcMessage,
Http::Utility::PercentEncoding::encode("invalid request body")));
}));
EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, filter_->decodeData(buffer, false));
EXPECT_EQ(decoder_callbacks_.details_, "grpc_bridge_data_too_small");
Expand Down

0 comments on commit f326331

Please sign in to comment.