Skip to content

Commit

Permalink
udpa: UDPA URI encoding/decoding utils.
Browse files Browse the repository at this point in the history
These map between the structured udpa::core::v1::ResourceName message and flat udpa:// URI
representations of resource names.

Risk level: Low
Testing: Unit tests added.

Part of envoyproxy#11264.

Signed-off-by: Harvey Tuch <[email protected]>
  • Loading branch information
htuch committed Jun 21, 2020
1 parent 4936ce6 commit 3f60b86
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 0 deletions.
10 changes: 10 additions & 0 deletions source/common/config/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,16 @@ envoy_cc_library(
],
)

envoy_cc_library(
name = "udpa_resource_lib",
srcs = ["udpa_resource.cc"],
hdrs = ["udpa_resource.h"],
deps = [
"//source/common/http:utility_lib",
"@com_github_cncf_udpa//udpa/core/v1:pkg_cc_proto",
],
)

envoy_cc_library(
name = "update_ack_lib",
hdrs = ["update_ack.h"],
Expand Down
79 changes: 79 additions & 0 deletions source/common/config/udpa_resource.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#include "common/config/udpa_resource.h"

#include <algorithm>

#include "common/common/fmt.h"
#include "common/http/utility.h"

#include "absl/strings/str_cat.h"
#include "absl/strings/str_split.h"

// TODO(htuch): This file has a bunch of ad hoc URI encoding/decoding based on Envoy's HTTP util
// functions. Once https://github.com/envoyproxy/envoy/issues/6588 lands, we can replace with GURL.

namespace Envoy {
namespace Config {

using PercentEncoding = Http::Utility::PercentEncoding;

std::string UdpaResourceName::encodeUri(const udpa::core::v1::ResourceName& resource_name,
const EncodeOptions& options) {
// We need to percent-encode authority, id, path and query params. Qualified types should not have
// reserved characters.
const std::string authority = PercentEncoding::encode(resource_name.authority(), "%/#?&=");
std::vector<std::string> path_components;
for (const auto& id_component : resource_name.id()) {
path_components.emplace_back(PercentEncoding::encode(id_component, "%/#?&="));
}
const std::string path = absl::StrJoin(path_components, "/");
std::vector<std::string> query_param_components;
for (const auto& context_param : resource_name.context().params()) {
query_param_components.emplace_back(
absl::StrCat(PercentEncoding::encode(context_param.first, "%#&="), "=",
PercentEncoding::encode(context_param.second, "%#&=")));
}
if (options.sort_context_params_) {
std::sort(query_param_components.begin(), query_param_components.end());
}
const std::string query_params =
query_param_components.empty() ? "" : "?" + absl::StrJoin(query_param_components, "&");
return absl::StrCat("udpa://", authority, "/", resource_name.qualified_type(),
path.empty() && query_params.empty() ? "" : "/", path, query_params);
}

udpa::core::v1::ResourceName UdpaResourceName::decodeUri(absl::string_view resource_uri) {
if (!absl::StartsWith(resource_uri, "udpa://")) {
throw UdpaResourceName::DecodeException(
fmt::format("{} does not have udpa:// scheme", resource_uri));
}
absl::string_view host, path;
Http::Utility::extractHostPathFromUri(resource_uri, host, path);
udpa::core::v1::ResourceName decoded_resource_name;
decoded_resource_name.set_authority(PercentEncoding::decode(host));
const size_t query_params_start = path.find('?');
Http::Utility::QueryParams query_params;
if (query_params_start != absl::string_view::npos) {
query_params = Http::Utility::parseQueryString(path.substr(query_params_start));
for (const auto& it : query_params) {
(*decoded_resource_name.mutable_context()
->mutable_params())[PercentEncoding::decode(it.first)] =
PercentEncoding::decode(it.second);
}
path = path.substr(0, query_params_start);
}
// This is guaranteed by Http::Utility::extractHostPathFromUri.
ASSERT(absl::StartsWith(path, "/"));
const std::vector<absl::string_view> path_components = absl::StrSplit(path.substr(1), '/');
decoded_resource_name.set_qualified_type(std::string(path_components[0]));
if (decoded_resource_name.qualified_type().empty()) {
throw UdpaResourceName::DecodeException(
fmt::format("Qualified type missing from {}", resource_uri));
}
for (auto it = std::next(path_components.cbegin()); it != path_components.cend(); it++) {
decoded_resource_name.add_id(PercentEncoding::decode(*it));
}
return decoded_resource_name;
}

} // namespace Config
} // namespace Envoy
48 changes: 48 additions & 0 deletions source/common/config/udpa_resource.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include "envoy/common/exception.h"

#include "absl/strings/string_view.h"
#include "udpa/core/v1/resource_name.pb.h"

namespace Envoy {
namespace Config {

// Utilities for URI encoding/decoding of udpa::core::v1::ResourceName.
class UdpaResourceName {
public:
// Options for encoded URIs.
struct EncodeOptions {
// Should the context params be sorted by key? This provides deterministic encoding.
bool sort_context_params_{};
};

/**
* Encode a udpa::core::v1::ResourceName message as a udpa:// URI string.
*
* @param resource_name resource name message.
* @param options encoding options.
* @return std::string udpa:// URI for resource_name.
*/
static std::string encodeUri(const udpa::core::v1::ResourceName& resource_name,
const EncodeOptions& options);
static std::string encodeUri(const udpa::core::v1::ResourceName& resource_name) {
return encodeUri(resource_name, {});
}

// Thrown when an exception occurs during URI decoding.
class DecodeException : public EnvoyException {
public:
DecodeException(const std::string& what) : EnvoyException(what) {}
};

/**
* Decode a udpa:// URI string to a udpa::core::v1::ResourceName.
*
* @param resource_uri udpa:// resource URI.
* @return udpa::core::v1::ResourceName resource name message for resource_uri.
* @throws DecodeException when parsing fails.
*/
static udpa::core::v1::ResourceName decodeUri(absl::string_view resource_uri);
};

} // namespace Config
} // namespace Envoy
9 changes: 9 additions & 0 deletions test/common/config/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,15 @@ envoy_cc_test(
],
)

envoy_cc_test(
name = "udpa_resource_test",
srcs = ["udpa_resource_test.cc"],
deps = [
"//source/common/config:udpa_resource_lib",
"//test/test_common:utility_lib",
],
)

envoy_proto_library(
name = "version_converter_proto",
srcs = ["version_converter.proto"],
Expand Down
72 changes: 72 additions & 0 deletions test/common/config/udpa_resource_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#include "common/config/udpa_resource.h"

#include "test/test_common/utility.h"

#include "gtest/gtest.h"

namespace Envoy {
namespace Config {
namespace {

const std::string EscapedUri =
"udpa://f123%25%2F%23%3F%26%3Do/envoy.config.listener.v3.Listener/b%25%2F%23%3F%26%3Dar//"
"baz?%25%23%26%3Dab=cde%25%23%26%3Df";
const std::string EscapedUriWithManyQueryParams =
"udpa://f123%25%2F%23%3F%26%3Do/envoy.config.listener.v3.Listener/b%25%2F%23%3F%26%3Dar//"
"baz?%25%23%26%3D=bar&%25%23%26%3Dab=cde%25%23%26%3Df&foo=%25%23%26%3D";

// for all x. encodeUri(decodeUri(x)) = x where x comes from sample of valid udpa:// URIs.
// TODO(htuch): write a fuzzer that validates this property as well.
TEST(UdpaResourceNameTest, DecodeEncode) {
const std::vector<std::string> uris = {
"udpa://foo/envoy.config.listener.v3.Listener",
"udpa://foo/envoy.config.listener.v3.Listener/bar",
"udpa://foo/envoy.config.listener.v3.Listener/bar/baz",
"udpa://foo/envoy.config.listener.v3.Listener/?ab=cde",
"udpa://foo/envoy.config.listener.v3.Listener/bar?ab=cd",
"udpa://foo/envoy.config.listener.v3.Listener/bar/baz?ab=cde",
"udpa://foo/envoy.config.listener.v3.Listener/bar/baz?ab=",
"udpa://foo/envoy.config.listener.v3.Listener/bar/baz?=cd",
"udpa://foo/envoy.config.listener.v3.Listener/bar/baz?ab=cde&ba=edc&z=f",
EscapedUri,
EscapedUriWithManyQueryParams,
};
UdpaResourceName::EncodeOptions encode_options;
encode_options.sort_context_params_ = true;
for (const std::string& uri : uris) {
EXPECT_EQ(uri, UdpaResourceName::encodeUri(UdpaResourceName::decodeUri(uri), encode_options));
}
}

// Validate that URI decoding behaves as expected component-wise.
TEST(UdpaResourceNameTest, DecodeSuccess) {
const auto resource_name = UdpaResourceName::decodeUri(EscapedUriWithManyQueryParams);
EXPECT_EQ("f123%/#?&=o", resource_name.authority());
EXPECT_EQ("envoy.config.listener.v3.Listener", resource_name.qualified_type());
EXPECT_EQ(3, resource_name.id().size());
EXPECT_EQ("b%/#?&=ar", resource_name.id()[0]);
EXPECT_EQ("", resource_name.id()[1]);
EXPECT_EQ("baz", resource_name.id()[2]);
EXPECT_EQ(3, resource_name.context().params().size());
EXPECT_EQ("bar", resource_name.context().params().at("%#&="));
EXPECT_EQ("cde%#&=f", resource_name.context().params().at("%#&=ab"));
EXPECT_EQ("%#&=", resource_name.context().params().at("foo"));
}

// Negative tests for URI decoding.
TEST(UdpaResourceNameTest, DecodeFail) {
{
EXPECT_THROW_WITH_MESSAGE(UdpaResourceName::decodeUri("foo://"),
UdpaResourceName::DecodeException,
"foo:// does not have udpa:// scheme");
}
{
EXPECT_THROW_WITH_MESSAGE(UdpaResourceName::decodeUri("udpa://foo"),
UdpaResourceName::DecodeException,
"Qualified type missing from udpa://foo");
}
}

} // namespace
} // namespace Config
} // namespace Envoy
1 change: 1 addition & 0 deletions tools/spelling/spelling_dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,7 @@ typedef
typeid
typesafe
ucontext
udpa
uint
un-
unacked
Expand Down

0 comments on commit 3f60b86

Please sign in to comment.