Skip to content
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
13 changes: 12 additions & 1 deletion api/envoy/config/route/v3/route_components.proto
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ message WeightedCluster {
}
}

// [#next-free-field: 14]
// [#next-free-field: 15]
message RouteMatch {
option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RouteMatch";

Expand Down Expand Up @@ -518,6 +518,17 @@ message RouteMatch {
// Note that CONNECT support is currently considered alpha in Envoy.
// [#comment: TODO(htuch): Replace the above comment with an alpha tag.]
ConnectMatcher connect_matcher = 12;

// If specified, the route is a path-separated prefix rule meaning that the
// ``:path`` header (without the query string) must either exactly match the
// ``path_separated_prefix`` or have it as a prefix, followed by ``/``
//
// For example, ``/api/dev`` would match
// ``/api/dev``, ``/api/dev/``, ``/api/dev/v1``, and ``/api/dev?param=true``
// but would not match ``/api/developer``
//
// Expect the value to not contain ``?`` or ``#`` and not to end in ``/``
string path_separated_prefix = 14 [(validate.rules).string = {pattern: "^[^?#]+[^?#/]$"}];
}

// Indicates that prefix/path matching should be case sensitive. The default
Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ New Features
* matching: the matching API can now express a match tree that will always match by omitting a matcher at the top level.
* outlier_detection: :ref:`max_ejection_time_jitter<envoy_v3_api_field_config.cluster.v3.OutlierDetection.base_ejection_time>` configuration added to allow adding a random value to the ejection time to prevent 'thundering herd' scenarios. Defaults to 0 so as to not break or change the behavior of existing deployments.
* redis: support for hostnames returned in ``cluster_slots`` response is now available.
* router: added a path-separated prefix matcher, to make route creation more efficient. :ref:`path_separated_prefix <envoy_v3_api_field_config.route.v3.RouteMatch.path_separated_prefix>`.
* schema_validator_tool: added ``bootstrap`` checking to the
:ref:`schema validator check tool <install_tools_schema_validator_check_tool>`.
* schema_validator_tool: added ``--fail-on-deprecated`` and ``--fail-on-wip`` to the
Expand Down
1 change: 1 addition & 0 deletions envoy/router/router.h
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,7 @@ enum class PathMatchType {
Prefix,
Exact,
Regex,
PathSeparatedPrefix,
};

/**
Expand Down
39 changes: 39 additions & 0 deletions source/common/router/config_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ RouteEntryImplBaseConstSharedPtr createAndValidateRoute(
route = std::make_shared<ConnectRouteEntryImpl>(vhost, route_config, optional_http_filters,
factory_context, validator);
break;
case envoy::config::route::v3::RouteMatch::PathSpecifierCase::kPathSeparatedPrefix: {
route = std::make_shared<PathSeparatedPrefixRouteEntryImpl>(
vhost, route_config, optional_http_filters, factory_context, validator);
break;
}
case envoy::config::route::v3::RouteMatch::PathSpecifierCase::PATH_SPECIFIER_NOT_SET:
break; // throw the error below.
}
Expand Down Expand Up @@ -1402,6 +1407,40 @@ RouteConstSharedPtr ConnectRouteEntryImpl::matches(const Http::RequestHeaderMap&
return nullptr;
}

PathSeparatedPrefixRouteEntryImpl::PathSeparatedPrefixRouteEntryImpl(
const VirtualHostImpl& vhost, const envoy::config::route::v3::Route& route,
const OptionalHttpFilters& optional_http_filters,
Server::Configuration::ServerFactoryContext& factory_context,
ProtobufMessage::ValidationVisitor& validator)
: RouteEntryImplBase(vhost, route, optional_http_filters, factory_context, validator),
prefix_(route.match().path_separated_prefix()),
path_matcher_(Matchers::PathMatcher::createPrefix(prefix_, !case_sensitive_)) {}

void PathSeparatedPrefixRouteEntryImpl::rewritePathHeader(Http::RequestHeaderMap& headers,
bool insert_envoy_original_path) const {
finalizePathHeader(headers, prefix_, insert_envoy_original_path);
}

absl::optional<std::string> PathSeparatedPrefixRouteEntryImpl::currentUrlPathAfterRewrite(
const Http::RequestHeaderMap& headers) const {
return currentUrlPathAfterRewriteWithMatchedPath(headers, prefix_);
}

RouteConstSharedPtr
PathSeparatedPrefixRouteEntryImpl::matches(const Http::RequestHeaderMap& headers,
const StreamInfo::StreamInfo& stream_info,
uint64_t random_value) const {
if (!RouteEntryImplBase::matchRoute(headers, stream_info, random_value)) {
return nullptr;
}
absl::string_view path = Http::PathUtil::removeQueryAndFragment(headers.getPathValue());
if (path.size() >= prefix_.size() && path_matcher_->match(path) &&
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The path_patcher_ will did the invoke Http::PathUtil::removeQueryAndFragment on the path again.

return matcher_.match(Http::PathUtil::removeQueryAndFragment(path));

Not sure whether worth to use envoy::type::matcher::v3::StringMatcher here directly. Leave this to other reviewers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, you are correct.
I think the alternative is to not invoke path_matcher_->match method, which could result in inconsistencies in the future if this method is updated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It seemed like the most concise way to do it, without expanding the StringMatcher definition.

(path.size() == prefix_.size() || path[prefix_.size()] == '/')) {
return clusterEntry(headers, random_value);
}
return nullptr;
}

VirtualHostImpl::VirtualHostImpl(
const envoy::config::route::v3::VirtualHost& virtual_host,
const OptionalHttpFilters& optional_http_filters, const ConfigImpl& global_route_config,
Expand Down
33 changes: 33 additions & 0 deletions source/common/router/config_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,39 @@ class ConnectRouteEntryImpl : public RouteEntryImplBase {
bool supportsPathlessHeaders() const override { return true; }
};

/**
* Route entry implementation for path separated prefix match routing.
*/
class PathSeparatedPrefixRouteEntryImpl : public RouteEntryImplBase {
public:
PathSeparatedPrefixRouteEntryImpl(const VirtualHostImpl& vhost,
const envoy::config::route::v3::Route& route,
const OptionalHttpFilters& optional_http_filters,
Server::Configuration::ServerFactoryContext& factory_context,
ProtobufMessage::ValidationVisitor& validator);

// Router::PathMatchCriterion
const std::string& matcher() const override { return prefix_; }
PathMatchType matchType() const override { return PathMatchType::PathSeparatedPrefix; }

// Router::Matchable
RouteConstSharedPtr matches(const Http::RequestHeaderMap& headers,
const StreamInfo::StreamInfo& stream_info,
uint64_t random_value) const override;

// Router::DirectResponseEntry
void rewritePathHeader(Http::RequestHeaderMap& headers,
bool insert_envoy_original_path) const override;

// Router::RouteEntry
absl::optional<std::string>
currentUrlPathAfterRewrite(const Http::RequestHeaderMap& headers) const override;

private:
const std::string prefix_;
const Matchers::PathMatcherConstSharedPtr path_matcher_;
};

// Contextual information used to construct the route actions for a match tree.
struct RouteActionContext {
const VirtualHostImpl& vhost;
Expand Down
28 changes: 28 additions & 0 deletions source/extensions/filters/http/jwt_authn/matcher.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "source/common/common/logger.h"
#include "source/common/common/matchers.h"
#include "source/common/common/regex.h"
#include "source/common/http/path_utility.h"
#include "source/common/router/config_impl.h"

#include "absl/strings/match.h"
Expand Down Expand Up @@ -154,6 +155,31 @@ class ConnectMatcherImpl : public BaseMatcherImpl {
return false;
}
};

class PathSeparatedPrefixMatcherImpl : public BaseMatcherImpl {
public:
PathSeparatedPrefixMatcherImpl(const RequirementRule& rule)
: BaseMatcherImpl(rule), prefix_(rule.match().path_separated_prefix()),
path_matcher_(Matchers::PathMatcher::createPrefix(prefix_, !case_sensitive_)) {}

bool matches(const Http::RequestHeaderMap& headers) const override {
if (!BaseMatcherImpl::matchRoute(headers)) {
return false;
}
absl::string_view path = Http::PathUtil::removeQueryAndFragment(headers.getPathValue());
if (path.size() >= prefix_.size() && path_matcher_->match(path) &&
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm a bit unhappy we have duplicative logic here @qiwzhang. I've probably complained before and this is unrelated to this PR, so just an observation, but by having to copy+paste all our matching logic in two places, we are leaving open a significant opportunity for drift and check vs. use semantic differences between this and routing.

(path.size() == prefix_.size() || path[prefix_.size()] == '/')) {
ENVOY_LOG(debug, "Path-separated prefix requirement '{}' matched.", prefix_);
return true;
}
return false;
}

private:
// prefix string
const std::string prefix_;
const Matchers::PathMatcherConstSharedPtr path_matcher_;
};
} // namespace

MatcherConstPtr Matcher::create(const RequirementRule& rule) {
Expand All @@ -166,6 +192,8 @@ MatcherConstPtr Matcher::create(const RequirementRule& rule) {
return std::make_unique<RegexMatcherImpl>(rule);
case RouteMatch::PathSpecifierCase::kConnectMatcher:
return std::make_unique<ConnectMatcherImpl>(rule);
case RouteMatch::PathSpecifierCase::kPathSeparatedPrefix:
return std::make_unique<PathSeparatedPrefixMatcherImpl>(rule);
case RouteMatch::PathSpecifierCase::PATH_SPECIFIER_NOT_SET:
break; // Fall through to PANIC.
}
Expand Down
Loading