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]: Fine grained path normalization controls implementation #15450

Closed
Closed
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d8e1b04
path transformer interface
Mar 9, 2021
effbb99
add normalization helper function
Mar 10, 2021
579c5b1
fix format
Mar 10, 2021
12cb83a
add unit test
Mar 10, 2021
33e999f
more unit tests
Mar 10, 2021
d4330f3
transformer return optional string
Mar 10, 2021
2de9ab6
build path transformer in hcm config
Mar 11, 2021
1540cdb
fix format
Mar 12, 2021
b45e089
fix clang tidy and add test cases
Mar 12, 2021
93d74e1
avoid string copy
Mar 12, 2021
7614247
refactor merge slash
Mar 12, 2021
0e08d96
reject duplicate transformation
Mar 12, 2021
0e6bca7
reject duplicate transformation
Mar 12, 2021
e9bbff2
more refactor
Mar 12, 2021
91aff85
test for path transformer
Mar 15, 2021
044d820
store paths in header map, more test
Mar 15, 2021
996b786
add path restoration
Mar 16, 2021
d39afbe
Merge branch 'main' of https://github.com/envoyproxy/envoy into path_…
Mar 16, 2021
95137da
diaable old api when new api exist
Mar 17, 2021
184359c
comments for path transformer class
Mar 17, 2021
c2810d9
fix typo
Mar 18, 2021
3d37a0f
change set and get filter path method to be private
Mar 18, 2021
e24a7e7
make set filter path and set forwarding path private
Mar 19, 2021
50a227e
add test for hcm config
Mar 21, 2021
544755a
Merge branch 'main' of https://github.com/envoyproxy/envoy into path_…
Mar 21, 2021
e9a483d
add header integration tests
Mar 23, 2021
b40bebb
fix header integration tests
Mar 23, 2021
0d7f899
fix format
Mar 23, 2021
afb4b6d
Merge branch 'main' of https://github.com/envoyproxy/envoy into path_…
Mar 24, 2021
ee8b9a3
clean up code
Mar 26, 2021
4786f15
format
Mar 26, 2021
ba7f746
Merge branch 'main' of https://github.com/envoyproxy/envoy into path_…
Mar 26, 2021
43b95f7
clean up
Apr 9, 2021
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
17 changes: 16 additions & 1 deletion include/envoy/http/header_map.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@
#include "absl/strings/string_view.h"

namespace Envoy {
namespace Http {
namespace Extensions {
namespace NetworkFilters {
namespace HttpConnectionManager {

class HttpConnectionManagerConfig;
}
} // namespace NetworkFilters
} // namespace Extensions
namespace Http {
// Used by ASSERTs to validate internal consistency. E.g. valid HTTP header keys/values should
// never contain embedded NULLs.
static inline bool validHeaderString(absl::string_view s) {
Expand Down Expand Up @@ -807,6 +814,14 @@ class RequestHeaderMap
public:
INLINE_REQ_STRING_HEADERS(DEFINE_INLINE_STRING_HEADER)
INLINE_REQ_NUMERIC_HEADERS(DEFINE_INLINE_NUMERIC_HEADER)
virtual absl::string_view getForwardingPath() PURE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comments explaining what these methods are for.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, will fix.

virtual absl::string_view getFilterPath() PURE;

private:
friend class Envoy::Extensions::NetworkFilters::HttpConnectionManager::
HttpConnectionManagerConfig;
virtual void setForwardingPath(absl::string_view path) PURE;
virtual void setFilterPath(absl::string_view path) PURE;
};
using RequestHeaderMapPtr = std::unique_ptr<RequestHeaderMap>;
using RequestHeaderMapOptRef = OptRef<RequestHeaderMap>;
Expand Down
2 changes: 2 additions & 0 deletions source/common/http/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,10 @@ envoy_cc_library(
":legacy_path_canonicalizer",
"//include/envoy/http:header_map_interface",
"//source/common/common:logger_lib",
"//source/common/protobuf:utility_lib",
"//source/common/runtime:runtime_features_lib",
"@com_googlesource_googleurl//url:envoy_url",
"@envoy_api//envoy/type/http/v3:pkg_cc_proto",
],
)

Expand Down
2 changes: 2 additions & 0 deletions source/common/http/conn_manager_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,8 @@ class ConnectionManagerConfig {
* @return LocalReply configuration which supplies mapping for local reply generated by Envoy.
*/
virtual const LocalReply::LocalReply& localReply() const PURE;

virtual void normalizePath(Http::RequestHeaderMap&) const PURE;
};
} // namespace Http
} // namespace Envoy
4 changes: 4 additions & 0 deletions source/common/http/conn_manager_utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,10 @@ bool ConnectionManagerUtility::maybeNormalizePath(RequestHeaderMap& request_head
return true; // It's as valid as it is going to get.
}
bool is_valid_path = true;
config.normalizePath(request_headers);
chaoqin-li1123 marked this conversation as resolved.
Show resolved Hide resolved
chaoqin-li1123 marked this conversation as resolved.
Show resolved Hide resolved
if (!config.shouldNormalizePath() && !config.shouldMergeSlashes()) {
chaoqin-li1123 marked this conversation as resolved.
Show resolved Hide resolved
return true;
}
if (config.shouldNormalizePath()) {
is_valid_path = PathUtil::canonicalPath(request_headers);
}
Expand Down
8 changes: 6 additions & 2 deletions source/common/http/header_map_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@ class RequestHeaderMapImpl final : public TypedHeaderMapImpl<RequestHeaderMap>,
INLINE_REQ_NUMERIC_HEADERS(DEFINE_INLINE_HEADER_NUMERIC_FUNCS)
INLINE_REQ_RESP_STRING_HEADERS(DEFINE_INLINE_HEADER_STRING_FUNCS)
INLINE_REQ_RESP_NUMERIC_HEADERS(DEFINE_INLINE_HEADER_NUMERIC_FUNCS)
absl::string_view getForwardingPath() override { return forwarding_path_; }
absl::string_view getFilterPath() override { return filter_path_; }

protected:
// NOTE: Because inline_headers_ is a variable size member, it must be the last member in the
Expand All @@ -478,9 +480,11 @@ class RequestHeaderMapImpl final : public TypedHeaderMapImpl<RequestHeaderMap>,
INLINE_REQ_RESP_STRING_HEADERS(DEFINE_HEADER_HANDLE)
INLINE_REQ_RESP_NUMERIC_HEADERS(DEFINE_HEADER_HANDLE)
};

void setForwardingPath(absl::string_view path) override { forwarding_path_ = path; }
void setFilterPath(absl::string_view path) override { filter_path_ = path; }
std::string forwarding_path_;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the intended use for these two be documented?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

std::string filter_path_;
using HeaderHandles = ConstSingleton<HeaderHandleValues>;

RequestHeaderMapImpl() { clearInline(); }

HeaderEntryImpl* inline_headers_[];
Expand Down
100 changes: 74 additions & 26 deletions source/common/http/path_utility.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "common/http/path_utility.h"

#include "envoy/common/exception.h"

#include "common/common/logger.h"
#include "common/http/legacy_path_canonicalizer.h"
#include "common/runtime/runtime_features.h"
Expand Down Expand Up @@ -35,16 +37,56 @@ absl::optional<std::string> canonicalizePath(absl::string_view original_path) {
bool PathUtil::canonicalPath(RequestHeaderMap& headers) {
ASSERT(headers.Path());
const auto original_path = headers.getPathValue();
// canonicalPath is supposed to apply on path component in URL instead of :path header
const absl::optional<std::string> normalized_path = PathTransformer::rfcNormalize(original_path);
if (!normalized_path.has_value()) {
return false;
}
headers.setPath(normalized_path.value());
return true;
}

void PathUtil::mergeSlashes(RequestHeaderMap& headers) {
ASSERT(headers.Path());
const auto original_path = headers.getPathValue();
const absl::optional<std::string> normalized =
PathTransformer::mergeSlashes(original_path).value();
if (normalized.has_value()) {
headers.setPath(normalized.value());
}
}

absl::string_view PathUtil::removeQueryAndFragment(const absl::string_view path) {
absl::string_view ret = path;
// Trim query parameters and/or fragment if present.
size_t offset = ret.find_first_of("?#");
if (offset != absl::string_view::npos) {
ret.remove_suffix(ret.length() - offset);
}
return ret;
}

absl::optional<std::string> PathTransformer::mergeSlashes(absl::string_view original_path) {
const absl::string_view::size_type query_start = original_path.find('?');
const absl::string_view path = original_path.substr(0, query_start);
const absl::string_view query = absl::ClippedSubstr(original_path, query_start);
if (path.find("//") == absl::string_view::npos) {
return std::string(original_path);
}
const absl::string_view path_prefix = absl::StartsWith(path, "/") ? "/" : absl::string_view();
const absl::string_view path_suffix = absl::EndsWith(path, "/") ? "/" : absl::string_view();
return absl::StrCat(path_prefix, absl::StrJoin(absl::StrSplit(path, '/', absl::SkipEmpty()), "/"),
path_suffix, query);
}

absl::optional<std::string> PathTransformer::rfcNormalize(absl::string_view original_path) {
const auto query_pos = original_path.find('?');
auto normalized_path_opt = canonicalizePath(
chaoqin-li1123 marked this conversation as resolved.
Show resolved Hide resolved
query_pos == original_path.npos
? original_path
: absl::string_view(original_path.data(), query_pos) // '?' is not included
);

if (!normalized_path_opt.has_value()) {
return false;
return {};
}
auto& normalized_path = normalized_path_opt.value();
const absl::string_view query_suffix =
Expand All @@ -54,35 +96,41 @@ bool PathUtil::canonicalPath(RequestHeaderMap& headers) {
if (!query_suffix.empty()) {
normalized_path.insert(normalized_path.end(), query_suffix.begin(), query_suffix.end());
}
headers.setPath(normalized_path);
return true;
return normalized_path;
}

void PathUtil::mergeSlashes(RequestHeaderMap& headers) {
ASSERT(headers.Path());
const auto original_path = headers.getPathValue();
// Only operate on path component in URL.
const absl::string_view::size_type query_start = original_path.find('?');
const absl::string_view path = original_path.substr(0, query_start);
const absl::string_view query = absl::ClippedSubstr(original_path, query_start);
if (path.find("//") == absl::string_view::npos) {
return;
PathTransformer::PathTransformer(
envoy::type::http::v3::PathTransformation const& path_transformation) {
const auto& operations = path_transformation.operations();
chaoqin-li1123 marked this conversation as resolved.
Show resolved Hide resolved
std::vector<uint64_t> operation_hashes;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the length, it might be better to use absl::flat_hash_set.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be overkill to use a hash set here since we expect a very small number of transformation.

for (auto const& operation : operations) {
uint64_t operation_hash = MessageUtil::hash(operation);
if (find(operation_hashes.begin(), operation_hashes.end(), operation_hash) !=
operation_hashes.end()) {
// Currently we only have RFC normalization and merge slashes, don't expect duplicates for
// these transformations.
throw EnvoyException("Duplicate path transformation");
chaoqin-li1123 marked this conversation as resolved.
Show resolved Hide resolved
chaoqin-li1123 marked this conversation as resolved.
Show resolved Hide resolved
}
if (operation.has_normalize_path_rfc_3986()) {
chaoqin-li1123 marked this conversation as resolved.
Show resolved Hide resolved
transformations_.emplace_back(PathTransformer::rfcNormalize);
} else if (operation.has_merge_slashes()) {
transformations_.emplace_back(PathTransformer::mergeSlashes);
}
operation_hashes.push_back(operation_hash);
}
const absl::string_view path_prefix = absl::StartsWith(path, "/") ? "/" : absl::string_view();
const absl::string_view path_suffix = absl::EndsWith(path, "/") ? "/" : absl::string_view();
headers.setPath(absl::StrCat(path_prefix,
absl::StrJoin(absl::StrSplit(path, '/', absl::SkipEmpty()), "/"),
path_suffix, query));
}

absl::string_view PathUtil::removeQueryAndFragment(const absl::string_view path) {
absl::string_view ret = path;
// Trim query parameters and/or fragment if present.
size_t offset = ret.find_first_of("?#");
if (offset != absl::string_view::npos) {
ret.remove_suffix(ret.length() - offset);
absl::optional<std::string> PathTransformer::transform(absl::string_view original) const {
absl::optional<std::string> path_string = std::string(original);
absl::string_view path_string_view = original;
for (Transformation const& transformation : transformations_) {
path_string = transformation(path_string_view);
if (!path_string.has_value()) {
return {};
}
path_string_view = path_string.value();
}
return ret;
return path_string;
}

} // namespace Http
Expand Down
25 changes: 25 additions & 0 deletions source/common/http/path_utility.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#pragma once

#include <list>

#include "envoy/http/header_map.h"
#include "envoy/type/http/v3/path_transformation.pb.h"

#include "common/protobuf/protobuf.h"
#include "common/protobuf/utility.h"

#include "absl/strings/string_view.h"

Expand All @@ -24,5 +30,24 @@ class PathUtil {
static absl::string_view removeQueryAndFragment(const absl::string_view path);
};

class PathTransformer {
chaoqin-li1123 marked this conversation as resolved.
Show resolved Hide resolved
public:
PathTransformer(envoy::type::http::v3::PathTransformation const& path_transformation);

// Take a string_view as argument and return an optional string.
// The optional will be null if the transformation fail.
absl::optional<std::string> transform(const absl::string_view original_path) const;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could you mention that this will execute all configured transformations, or something along those lines?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, will add comments.


static absl::optional<std::string> mergeSlashes(absl::string_view original_path);

static absl::optional<std::string> rfcNormalize(absl::string_view original_path);

private:
using Transformation = std::function<absl::optional<std::string>(absl::string_view)>;
// A sequence of transformations specified by path_transformation.operations()
// Transformations will be applied to a path string in order in transform().
std::list<Transformation> transformations_;
};

} // namespace Http
} // namespace Envoy
5 changes: 5 additions & 0 deletions source/common/router/config_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,11 @@ void RouteEntryImplBase::finalizeRequestHeaders(Http::RequestHeaderMap& headers,
request_headers_parser_->evaluateHeaders(headers, stream_info);
}

// Restore the forwarding path if none of the filter mutate the path.
if (headers.Path() && headers.getFilterPath() == headers.getPathValue()) {
headers.setPath(std::string(headers.getForwardingPath()));
}

if (!host_rewrite_.empty()) {
headers.setHost(host_rewrite_);
} else if (auto_host_rewrite_header_) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ envoy_cc_extension(
"//source/common/filter/http:filter_config_discovery_lib",
"//source/common/http:conn_manager_lib",
"//source/common/http:default_server_string_lib",
"//source/common/http:path_utility_lib",
"//source/common/http:request_id_extension_lib",
"//source/common/http:utility_lib",
"//source/common/http/http1:codec_lib",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "common/http/http2/codec_impl.h"
#include "common/http/http3/quic_codec_factory.h"
#include "common/http/http3/well_known_names.h"
#include "common/http/path_utility.h"
#include "common/http/request_id_extension_impl.h"
#include "common/http/utility.h"
#include "common/local_reply/local_reply.h"
Expand Down Expand Up @@ -266,6 +267,16 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig(
idle_timeout_ = absl::nullopt;
}

// If the path normalization options has been set.
if (config.has_path_normalization_options()) {
normalize_path_ = false;
merge_slashes_ = false;
forwarding_path_transformer_ = std::make_unique<Http::PathTransformer>(
chaoqin-li1123 marked this conversation as resolved.
Show resolved Hide resolved
config.path_normalization_options().forwarding_transformation());
filter_path_transformer_ = std::make_unique<Http::PathTransformer>(
config.path_normalization_options().http_filter_transformation());
}

if (config.strip_any_host_port() && config.strip_matching_host_port()) {
throw EnvoyException(fmt::format(
"Error: Only one of `strip_matching_host_port` or `strip_any_host_port` can be set."));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "common/http/date_provider_impl.h"
#include "common/http/http1/codec_stats.h"
#include "common/http/http2/codec_stats.h"
#include "common/http/path_utility.h"
#include "common/json/json_loader.h"
#include "common/local_reply/local_reply.h"
#include "common/router/rds_impl.h"
Expand Down Expand Up @@ -179,6 +180,23 @@ class HttpConnectionManagerConfig : Logger::Loggable<Logger::Id::config>,
}
std::chrono::milliseconds delayedCloseTimeout() const override { return delayed_close_timeout_; }
const LocalReply::LocalReply& localReply() const override { return *local_reply_; }
void normalizePath(Http::RequestHeaderMap& request_headers) const override {
chaoqin-li1123 marked this conversation as resolved.
Show resolved Hide resolved
if (forwarding_path_transformer_ == nullptr) {
return;
}
const auto original_path = request_headers.getPathValue();
absl::optional<std::string> forwarding_path =
forwarding_path_transformer_->transform(original_path);
absl::optional<std::string> filter_path;
if (forwarding_path.has_value()) {
request_headers.setForwardingPath(forwarding_path.value());
filter_path = filter_path_transformer_->transform(forwarding_path.value());
}
if (filter_path.has_value()) {
request_headers.setPath(filter_path.value());
request_headers.setFilterPath(filter_path.value());
}
}

private:
enum class CodecType { HTTP1, HTTP2, HTTP3, AUTO };
Expand Down Expand Up @@ -251,8 +269,8 @@ class HttpConnectionManagerConfig : Logger::Loggable<Logger::Id::config>,
const bool proxy_100_continue_;
const bool stream_error_on_invalid_http_messaging_;
std::chrono::milliseconds delayed_close_timeout_;
const bool normalize_path_;
const bool merge_slashes_;
bool normalize_path_;
bool merge_slashes_;
Http::StripPortType strip_port_type_;
const envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction
headers_with_underscores_action_;
Expand All @@ -264,6 +282,8 @@ class HttpConnectionManagerConfig : Logger::Loggable<Logger::Id::config>,
static const uint64_t RequestTimeoutMs = 0;
// request header timeout is disabled by default
static const uint64_t RequestHeaderTimeoutMs = 0;
std::unique_ptr<Http::PathTransformer> forwarding_path_transformer_;
std::unique_ptr<Http::PathTransformer> filter_path_transformer_;
};

/**
Expand Down
1 change: 1 addition & 0 deletions source/server/admin/admin.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ class AdminImpl : public Admin,
return runCallback(path_and_query, response_headers, response, filter);
};
}
void normalizePath(Http::RequestHeaderMap&) const override {}

private:
/**
Expand Down
1 change: 1 addition & 0 deletions test/common/http/conn_manager_impl_fuzz_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ class FuzzConfig : public ConnectionManagerConfig {
const Http::Http1Settings& http1Settings() const override { return http1_settings_; }
bool shouldNormalizePath() const override { return false; }
bool shouldMergeSlashes() const override { return false; }
void normalizePath(Http::RequestHeaderMap&) const override {}
Http::StripPortType stripPortType() const override { return Http::StripPortType::None; }
envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction
headersWithUnderscoresAction() const override {
Expand Down
1 change: 1 addition & 0 deletions test/common/http/conn_manager_impl_test_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ class HttpConnectionManagerImplTest : public testing::Test, public ConnectionMan
const Http::Http1Settings& http1Settings() const override { return http1_settings_; }
bool shouldNormalizePath() const override { return normalize_path_; }
bool shouldMergeSlashes() const override { return merge_slashes_; }
void normalizePath(Http::RequestHeaderMap&) const override {}
Http::StripPortType stripPortType() const override { return strip_port_type_; }
const RequestIDExtensionSharedPtr& requestIDExtension() override { return request_id_extension_; }
envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction
Expand Down
Loading