Skip to content
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

http, utility: Allow to optionally decode the parsed param value when parsing query string #11795

Merged
merged 19 commits into from
Jul 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions source/common/http/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,26 @@ Utility::QueryParams Utility::parseQueryString(absl::string_view url) {
}

start++;
return parseParameters(url, start);
return parseParameters(url, start, /*decode_params=*/false);
}

Utility::QueryParams Utility::parseAndDecodeQueryString(absl::string_view url) {
size_t start = url.find('?');
if (start == std::string::npos) {
QueryParams params;
return params;
}

start++;
return parseParameters(url, start, /*decode_params=*/true);
}

Utility::QueryParams Utility::parseFromBody(absl::string_view body) {
return parseParameters(body, 0);
return parseParameters(body, 0, /*decode_params=*/true);
}

Utility::QueryParams Utility::parseParameters(absl::string_view data, size_t start) {
Utility::QueryParams Utility::parseParameters(absl::string_view data, size_t start,
bool decode_params) {
QueryParams params;

while (start < data.size()) {
Expand All @@ -252,8 +264,10 @@ Utility::QueryParams Utility::parseParameters(absl::string_view data, size_t sta

const size_t equal = param.find('=');
if (equal != std::string::npos) {
params.emplace(StringUtil::subspan(data, start, start + equal),
StringUtil::subspan(data, start + equal + 1, end));
const auto param_name = StringUtil::subspan(data, start, start + equal);
const auto param_value = StringUtil::subspan(data, start + equal + 1, end);
params.emplace(decode_params ? PercentEncoding::decode(param_name) : param_name,
decode_params ? PercentEncoding::decode(param_value) : param_value);
} else {
params.emplace(StringUtil::subspan(data, start, end), "");
}
Expand Down
11 changes: 10 additions & 1 deletion source/common/http/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ std::string createSslRedirectPath(const RequestHeaderMap& headers);
*/
QueryParams parseQueryString(absl::string_view url);

/**
* Parse a URL into query parameters.
* @param url supplies the url to parse.
* @return QueryParams the parsed and percent-decoded parameters, if any.
*/
QueryParams parseAndDecodeQueryString(absl::string_view url);

/**
* Parse a a request body into query parameters.
* @param body supplies the body to parse.
Expand All @@ -187,9 +194,11 @@ QueryParams parseFromBody(absl::string_view body);
* Parse query parameters from a URL or body.
* @param data supplies the data to parse.
* @param start supplies the offset within the data.
* @param decode_params supplies the flag whether to percent-decode the parsed parameters (both name
* and value). Set to false to keep the parameters encoded.
* @return QueryParams the parsed parameters, if any.
*/
QueryParams parseParameters(absl::string_view data, size_t start);
QueryParams parseParameters(absl::string_view data, size_t start, bool decode_params);

/**
* Finds the start of the query string in a path
Expand Down
2 changes: 1 addition & 1 deletion source/extensions/filters/http/jwt_authn/extractor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ ExtractorImpl::extract(const Http::RequestHeaderMap& headers) const {
}

// Check query parameter locations.
const auto& params = Http::Utility::parseQueryString(headers.getPathValue());
const auto& params = Http::Utility::parseAndDecodeQueryString(headers.getPathValue());
for (const auto& location_it : param_locations_) {
const auto& param_key = location_it.first;
const auto& location_spec = location_it.second;
Expand Down
4 changes: 2 additions & 2 deletions source/server/admin/admin.cc
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ void AdminImpl::writeClustersAsText(Buffer::Instance& response) {
Http::Code AdminImpl::handlerClusters(absl::string_view url,
Http::ResponseHeaderMap& response_headers,
Buffer::Instance& response, AdminStream&) {
Http::Utility::QueryParams query_params = Http::Utility::parseQueryString(url);
Http::Utility::QueryParams query_params = Http::Utility::parseAndDecodeQueryString(url);
const auto format_value = Utility::formatParam(query_params);

if (format_value.has_value() && format_value.value() == "json") {
Expand Down Expand Up @@ -622,7 +622,7 @@ ProtobufTypes::MessagePtr AdminImpl::dumpEndpointConfigs() const {
Http::Code AdminImpl::handlerConfigDump(absl::string_view url,
Http::ResponseHeaderMap& response_headers,
Buffer::Instance& response, AdminStream&) const {
Http::Utility::QueryParams query_params = Http::Utility::parseQueryString(url);
Http::Utility::QueryParams query_params = Http::Utility::parseAndDecodeQueryString(url);
const auto resource = resourceParam(query_params);
const auto mask = maskParam(query_params);
const bool include_eds = shouldIncludeEdsInDump(query_params);
Expand Down
4 changes: 2 additions & 2 deletions source/server/admin/profiling_handler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ProfilingHandler::ProfilingHandler(const std::string& profile_path) : profile_pa

Http::Code ProfilingHandler::handlerCpuProfiler(absl::string_view url, Http::ResponseHeaderMap&,
Buffer::Instance& response, AdminStream&) {
Http::Utility::QueryParams query_params = Http::Utility::parseQueryString(url);
Http::Utility::QueryParams query_params = Http::Utility::parseAndDecodeQueryString(url);
if (query_params.size() != 1 || query_params.begin()->first != "enable" ||
(query_params.begin()->second != "y" && query_params.begin()->second != "n")) {
response.add("?enable=<y|n>\n");
Expand Down Expand Up @@ -40,7 +40,7 @@ Http::Code ProfilingHandler::handlerHeapProfiler(absl::string_view url, Http::Re
return Http::Code::NotImplemented;
}

Http::Utility::QueryParams query_params = Http::Utility::parseQueryString(url);
Http::Utility::QueryParams query_params = Http::Utility::parseAndDecodeQueryString(url);
if (query_params.size() != 1 || query_params.begin()->first != "enable" ||
(query_params.begin()->second != "y" && query_params.begin()->second != "n")) {
response.add("?enable=<y|n>\n");
Expand Down
4 changes: 2 additions & 2 deletions source/server/admin/runtime_handler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ RuntimeHandler::RuntimeHandler(Server::Instance& server) : HandlerContextBase(se
Http::Code RuntimeHandler::handlerRuntime(absl::string_view url,
Http::ResponseHeaderMap& response_headers,
Buffer::Instance& response, AdminStream&) {
const Http::Utility::QueryParams params = Http::Utility::parseQueryString(url);
const Http::Utility::QueryParams params = Http::Utility::parseAndDecodeQueryString(url);
response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Json);

// TODO(jsedgwick): Use proto to structure this output instead of arbitrary JSON.
Expand Down Expand Up @@ -80,7 +80,7 @@ Http::Code RuntimeHandler::handlerRuntime(absl::string_view url,
Http::Code RuntimeHandler::handlerRuntimeModify(absl::string_view url, Http::ResponseHeaderMap&,
Buffer::Instance& response,
AdminStream& admin_stream) {
Http::Utility::QueryParams params = Http::Utility::parseQueryString(url);
Http::Utility::QueryParams params = Http::Utility::parseAndDecodeQueryString(url);
if (params.empty()) {
// Check if the params are in the request's body.
if (admin_stream.getRequestBody() != nullptr &&
Expand Down
5 changes: 3 additions & 2 deletions source/server/admin/stats_handler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Http::Code StatsHandler::handlerStats(absl::string_view url,
Http::ResponseHeaderMap& response_headers,
Buffer::Instance& response, AdminStream& admin_stream) {
Http::Code rc = Http::Code::OK;
const Http::Utility::QueryParams params = Http::Utility::parseQueryString(url);
const Http::Utility::QueryParams params = Http::Utility::parseAndDecodeQueryString(url);

const bool used_only = params.find("usedonly") != params.end();
absl::optional<std::regex> regex;
Expand Down Expand Up @@ -140,7 +140,8 @@ Http::Code StatsHandler::handlerStats(absl::string_view url,
Http::Code StatsHandler::handlerPrometheusStats(absl::string_view path_and_query,
Http::ResponseHeaderMap&,
Buffer::Instance& response, AdminStream&) {
const Http::Utility::QueryParams params = Http::Utility::parseQueryString(path_and_query);
const Http::Utility::QueryParams params =
Http::Utility::parseAndDecodeQueryString(path_and_query);
const bool used_only = params.find("usedonly") != params.end();
absl::optional<std::regex> regex;
if (!Utility::filterParam(params, response, regex)) {
Expand Down
5 changes: 4 additions & 1 deletion test/common/http/utility_fuzz_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ DEFINE_PROTO_FUZZER(const test::common::http::UtilityTestCase& input) {
}
switch (input.utility_selector_case()) {
case test::common::http::UtilityTestCase::kParseQueryString: {
// TODO(dio): Add the case when using parseAndDecodeQueryString().
Http::Utility::parseQueryString(input.parse_query_string());
break;
}
Expand Down Expand Up @@ -57,7 +58,9 @@ DEFINE_PROTO_FUZZER(const test::common::http::UtilityTestCase& input) {
}
case test::common::http::UtilityTestCase::kParseParameters: {
const auto& parse_parameters = input.parse_parameters();
Http::Utility::parseParameters(parse_parameters.data(), parse_parameters.start());
// TODO(dio): Add a case when doing parse_parameters with decode_params flag true.
Http::Utility::parseParameters(parse_parameters.data(), parse_parameters.start(),
/*decode_params*/ false);
break;
}
case test::common::http::UtilityTestCase::kFindQueryString: {
Expand Down
63 changes: 62 additions & 1 deletion test/common/http/utility_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,65 @@ namespace Http {

TEST(HttpUtility, parseQueryString) {
EXPECT_EQ(Utility::QueryParams(), Utility::parseQueryString("/hello"));
EXPECT_EQ(Utility::QueryParams(), Utility::parseAndDecodeQueryString("/hello"));

EXPECT_EQ(Utility::QueryParams(), Utility::parseQueryString("/hello?"));
EXPECT_EQ(Utility::QueryParams(), Utility::parseAndDecodeQueryString("/hello?"));

EXPECT_EQ(Utility::QueryParams({{"hello", ""}}), Utility::parseQueryString("/hello?hello"));
EXPECT_EQ(Utility::QueryParams({{"hello", ""}}),
Utility::parseAndDecodeQueryString("/hello?hello"));

EXPECT_EQ(Utility::QueryParams({{"hello", "world"}}),
Utility::parseQueryString("/hello?hello=world"));
EXPECT_EQ(Utility::QueryParams({{"hello", "world"}}),
Utility::parseAndDecodeQueryString("/hello?hello=world"));

EXPECT_EQ(Utility::QueryParams({{"hello", ""}}), Utility::parseQueryString("/hello?hello="));
EXPECT_EQ(Utility::QueryParams({{"hello", ""}}),
Utility::parseAndDecodeQueryString("/hello?hello="));

EXPECT_EQ(Utility::QueryParams({{"hello", ""}}), Utility::parseQueryString("/hello?hello=&"));
EXPECT_EQ(Utility::QueryParams({{"hello", ""}}),
Utility::parseAndDecodeQueryString("/hello?hello=&"));

EXPECT_EQ(Utility::QueryParams({{"hello", ""}, {"hello2", "world2"}}),
Utility::parseQueryString("/hello?hello=&hello2=world2"));
EXPECT_EQ(Utility::QueryParams({{"hello", ""}, {"hello2", "world2"}}),
Utility::parseAndDecodeQueryString("/hello?hello=&hello2=world2"));

EXPECT_EQ(Utility::QueryParams({{"name", "admin"}, {"level", "trace"}}),
Utility::parseQueryString("/logging?name=admin&level=trace"));
EXPECT_EQ(Utility::QueryParams({{"name", "admin"}, {"level", "trace"}}),
Utility::parseAndDecodeQueryString("/logging?name=admin&level=trace"));

EXPECT_EQ(Utility::QueryParams({{"param_value_has_encoded_ampersand", "a%26b"}}),
Utility::parseQueryString("/hello?param_value_has_encoded_ampersand=a%26b"));
EXPECT_EQ(Utility::QueryParams({{"param_value_has_encoded_ampersand", "a&b"}}),
Utility::parseAndDecodeQueryString("/hello?param_value_has_encoded_ampersand=a%26b"));

EXPECT_EQ(Utility::QueryParams({{"params_has_encoded_%26", "a%26b"}, {"ok", "1"}}),
Utility::parseQueryString("/hello?params_has_encoded_%26=a%26b&ok=1"));
EXPECT_EQ(Utility::QueryParams({{"params_has_encoded_&", "a&b"}, {"ok", "1"}}),
Utility::parseAndDecodeQueryString("/hello?params_has_encoded_%26=a%26b&ok=1"));

// A sample of request path with query strings by Prometheus:
// https://github.com/envoyproxy/envoy/issues/10926#issuecomment-651085261.
EXPECT_EQ(
Utility::QueryParams(
{{"filter",
"%28cluster.upstream_%28rq_total%7Crq_time_sum%7Crq_time_count%7Crq_time_"
"bucket%7Crq_xx%7Crq_complete%7Crq_active%7Ccx_active%29%29%7C%28server.version%29"}}),
Utility::parseQueryString(
"/stats?filter=%28cluster.upstream_%28rq_total%7Crq_time_sum%7Crq_time_count%7Crq_time_"
"bucket%7Crq_xx%7Crq_complete%7Crq_active%7Ccx_active%29%29%7C%28server.version%29"));
EXPECT_EQ(
Utility::QueryParams(
{{"filter", "(cluster.upstream_(rq_total|rq_time_sum|rq_time_count|rq_time_bucket|rq_xx|"
"rq_complete|rq_active|cx_active))|(server.version)"}}),
Utility::parseAndDecodeQueryString(
"/stats?filter=%28cluster.upstream_%28rq_total%7Crq_time_sum%7Crq_time_count%7Crq_time_"
"bucket%7Crq_xx%7Crq_complete%7Crq_active%7Ccx_active%29%29%7C%28server.version%29"));
}

TEST(HttpUtility, getResponseStatus) {
Expand Down Expand Up @@ -1212,7 +1261,19 @@ TEST(PercentEncoding, EncodeDecode) {
validatePercentEncodingEncodeDecode("_-ok-_", "_-ok-_");
}

TEST(PercentEncoding, Trailing) {
TEST(PercentEncoding, Decoding) {
EXPECT_EQ(Utility::PercentEncoding::decode("a%26b"), "a&b");
EXPECT_EQ(Utility::PercentEncoding::decode("hello%20world"), "hello world");
EXPECT_EQ(Utility::PercentEncoding::decode("upstream%7Cdownstream"), "upstream|downstream");
EXPECT_EQ(
Utility::PercentEncoding::decode(
"filter=%28cluster.upstream_%28rq_total%7Crq_time_sum%7Crq_time_count%7Crq_time_bucket%"
"7Crq_xx%7Crq_complete%7Crq_active%7Ccx_active%29%29%7C%28server.version%29"),
"filter=(cluster.upstream_(rq_total|rq_time_sum|rq_time_count|rq_time_bucket|rq_xx|rq_"
"complete|rq_active|cx_active))|(server.version)");
}

TEST(PercentEncoding, DecodingWithTrailingInput) {
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%");
Expand Down