Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
55 changes: 53 additions & 2 deletions docs/configuration/http_conn_man/route_config/rate_limits.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ Actions

type
*(required, string)* The type of rate limit action to perform. The currently supported action
types are *source_cluster*, *destination_cluster* , *request_headers*, *remote_address* and
*generic_key*.
types are *source_cluster*, *destination_cluster* , *request_headers*, *remote_address*,
*generic_key* and *header_value_match*.

Source Cluster
^^^^^^^^^^^^^^
Expand Down Expand Up @@ -141,6 +141,57 @@ The following descriptor entry is appended to the descriptor:

* ("generic_key", "<descriptor_value>")

Header Value Match
^^^^^^^^^^^^^^^^^^

.. code-block:: json

{
"type": "header_value_match",
"descriptor_value" : "...",
"headers" : []
}


descriptor_value
*(required, string)* The value to use in the descriptor entry.
:ref:`headers<config_http_conn_man_route_table_rate_limit_headers>`
*(required, array)* Specifies a set of headers that the rate limit action should match on.

The following descriptor entry is appended to the descriptor if the request matches the headers
specified in the action config:

* ("header_match", "<descriptor_value>")


.. _config_http_conn_man_route_table_rate_limit_headers:

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 think we now have this text in potentially 3 places in the docs. Can we de-dup this and just link to it from all 3 places?


Headers
"""""""

.. code-block:: json

[
{"name": "...", "value": "...", "regex": "..."}
]


name
*(required, string)* Specifies the name of the header in the request.

value
*(optional, string)* Specifies the value of the header. If the value is absent a request that has
the *name* header will match, regardless of the header's value.

regex
*(optional, boolean)* Specifies whether the header value is a regular
expression or not. Defaults to false. The regex grammar used in the value field
is defined `here <http://en.cppreference.com/w/cpp/regex/ecmascript>`_.

The action will check the request's headers against all the specified headers in the action config.
A match will happen if all the headers in the config are present in the request with the same
values (or based on presence if the ``value`` field is not in the config).


.. _config_http_conn_man_route_table_rate_limit_composing_actions:

Expand Down
1 change: 1 addition & 0 deletions source/common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ add_library(
redis/conn_pool_impl.cc
redis/proxy_filter.cc
router/config_impl.cc
router/config_utility.cc
router/rds_impl.cc
router/retry_state_impl.cc
router/router.cc
Expand Down
29 changes: 28 additions & 1 deletion source/common/json/config_schemas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,32 @@ const std::string Json::Schema::HTTP_RATE_LIMITS_CONFIGURATION_SCHEMA(R"EOF(
},
"required" : ["type", "descriptor_value"],
"additionalProperties" : false
},
"header_value_match" : {
"type" : "object",
"properties" : {
"type" : {
"type" : "string",
"enum" : ["header_value_match"]
},
"descriptor_value" : {"type" : "string"},
"headers" : {

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.

This is also duplicated several times now. Can we de-dup and do a sub-schema check in the ConfigUtility code?

"type" : "array",
"minItems" : 1,
"items" : {
"type" : "object",
"properties" : {
"name" : {"type" : "string"},
"value" : {"type" : "string"},
"regex" : {"type" : "boolean"}
},
"required" : ["name"],
"additionalProperties" : false
}
},
"required" : ["type", "descriptor_value", "headers"],
"additionalProperties" : false
}
}
},
"type" : "object",
Expand All @@ -626,7 +652,8 @@ const std::string Json::Schema::HTTP_RATE_LIMITS_CONFIGURATION_SCHEMA(R"EOF(
{"$ref" : "#/definitions/destination_cluster"},
{"$ref" : "#/definitions/request_headers"},
{"$ref" : "#/definitions/remote_address"},
{"$ref" : "#/definitions/generic_key"}
{"$ref" : "#/definitions/generic_key"},
{"$ref" : "#/definitions/header_value_match"}
]
}
}
Expand Down
35 changes: 0 additions & 35 deletions source/common/router/config_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,41 +38,6 @@ ShadowPolicyImpl::ShadowPolicyImpl(const Json::Object& config) {
runtime_key_ = config.getObject("shadow")->getString("runtime_key", "");
}

Upstream::ResourcePriority ConfigUtility::parsePriority(const Json::Object& config) {
std::string priority_string = config.getString("priority", "default");
if (priority_string == "default") {
return Upstream::ResourcePriority::Default;
} else if (priority_string == "high") {
return Upstream::ResourcePriority::High;
} else {
throw EnvoyException(fmt::format("invalid resource priority '{}'", priority_string));
}
}

bool ConfigUtility::matchHeaders(const Http::HeaderMap& request_headers,
const std::vector<HeaderData>& config_headers) {
bool matches = true;

if (!config_headers.empty()) {
for (const HeaderData& cfg_header_data : config_headers) {
const Http::HeaderEntry* header = request_headers.get(cfg_header_data.name_);
if (cfg_header_data.value_.empty()) {
matches &= (header != nullptr);
} else if (!cfg_header_data.is_regex_) {
matches &= (header != nullptr) && (header->value() == cfg_header_data.value_.c_str());
} else {
matches &= (header != nullptr) &&
std::regex_match(header->value().c_str(), cfg_header_data.regex_pattern_);
}
if (!matches) {
break;
}
}
}

return matches;
}

HashPolicyImpl::HashPolicyImpl(const Json::Object& config)
: header_name_(config.getString("header_name")) {}

Expand Down
34 changes: 2 additions & 32 deletions source/common/router/config_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include "envoy/runtime/runtime.h"
#include "envoy/upstream/cluster_manager.h"

#include "common/router/config_utility.h"

namespace Router {

/**
Expand Down Expand Up @@ -48,38 +50,6 @@ class SslRedirectRoute : public Route {
static const SslRedirector SSL_REDIRECTOR;
};

/**
* Utility routines for loading route configuration and matching runtime request headers.
*/
class ConfigUtility {
public:
struct HeaderData {
HeaderData(const Http::LowerCaseString& name, const std::string& value, const bool is_regex)
: name_(name), value_(value), regex_pattern_(value_, std::regex::optimize),
is_regex_(is_regex) {}

const Http::LowerCaseString name_;
const std::string value_;
const std::regex regex_pattern_;
const bool is_regex_;
};

/**
* @return the resource priority parsed from JSON.
*/
static Upstream::ResourcePriority parsePriority(const Json::Object& config);

/**
* See if the specified headers are present in the request headers.
* @param headers supplies the list of headers to match
* @param request_headers supplies the list of request headers to compare against search_list
* @return true all the headers (and values) in the search_list set are found in the
* request_headers
*/
static bool matchHeaders(const Http::HeaderMap& headers,
const std::vector<HeaderData>& request_headers);
};

/**
* Holds all routing configuration for an entire virtual host.
*/
Expand Down
40 changes: 40 additions & 0 deletions source/common/router/config_utility.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include "config_utility.h"

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.

Did you change any code in config_utility or is it just copy/paste to a new file without any changes?

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.

Copy Paste

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.

OK not a big deal but in the future I would do a separate code move PR, just easier to review.


namespace Router {

Upstream::ResourcePriority ConfigUtility::parsePriority(const Json::Object& config) {
std::string priority_string = config.getString("priority", "default");
if (priority_string == "default") {
return Upstream::ResourcePriority::Default;
} else if (priority_string == "high") {
return Upstream::ResourcePriority::High;
} else {
throw EnvoyException(fmt::format("invalid resource priority '{}'", priority_string));
}
}

bool ConfigUtility::matchHeaders(const Http::HeaderMap& request_headers,
const std::vector<HeaderData>& config_headers) {
bool matches = true;

if (!config_headers.empty()) {
for (const HeaderData& cfg_header_data : config_headers) {
const Http::HeaderEntry* header = request_headers.get(cfg_header_data.name_);
if (cfg_header_data.value_.empty()) {
matches &= (header != nullptr);
} else if (!cfg_header_data.is_regex_) {
matches &= (header != nullptr) && (header->value() == cfg_header_data.value_.c_str());
} else {
matches &= (header != nullptr) &&
std::regex_match(header->value().c_str(), cfg_header_data.regex_pattern_);
}
if (!matches) {
break;
}
}
}

return matches;
}

} // Router
42 changes: 42 additions & 0 deletions source/common/router/config_utility.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#pragma once

#include "envoy/json/json_object.h"
#include "envoy/upstream/resource_manager.h"

#include "common/http/headers.h"

namespace Router {

/**
* Utility routines for loading route configuration and matching runtime request headers.
*/
class ConfigUtility {
public:
struct HeaderData {
HeaderData(const Http::LowerCaseString& name, const std::string& value, const bool is_regex)
: name_(name), value_(value), regex_pattern_(value_, std::regex::optimize),
is_regex_(is_regex) {}

const Http::LowerCaseString name_;
const std::string value_;
const std::regex regex_pattern_;
const bool is_regex_;
};

/**
* @return the resource priority parsed from JSON.
*/
static Upstream::ResourcePriority parsePriority(const Json::Object& config);

/**
* See if the specified headers are present in the request headers.
* @param headers supplies the list of headers to match
* @param request_headers supplies the list of request headers to compare against search_list
* @return true all the headers (and values) in the search_list set are found in the
* request_headers
*/
static bool matchHeaders(const Http::HeaderMap& headers,
const std::vector<HeaderData>& request_headers);
};

} // Router
25 changes: 25 additions & 0 deletions source/common/router/router_ratelimit.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "router_ratelimit.h"

#include "common/common/empty_string.h"
#include "common/json/config_schemas.h"

namespace Router {
Expand Down Expand Up @@ -50,6 +51,28 @@ void GenericKeyAction::populateDescriptor(const Router::RouteEntry&,
descriptor.entries_.push_back({"generic_key", descriptor_value_});
}

HeaderValueMatchAction::HeaderValueMatchAction(const Json::Object& action)
: descriptor_value_(action.getString("descriptor_value")) {
std::vector<Json::ObjectPtr> config_headers = action.getObjectArray("headers");
for (const Json::ObjectPtr& header_map : config_headers) {
// An empty header value allows for matching to be only based on header presence.
// Regex is an opt-in. Unless explicitly mentioned, the header values will be used for
// exact string matching.
action_headers_.emplace_back(Http::LowerCaseString(header_map->getString("name")),
header_map->getString("value", EMPTY_STRING),
header_map->getBoolean("regex", false));
}
}

void HeaderValueMatchAction::populateDescriptor(const Router::RouteEntry&,
::RateLimit::Descriptor& descriptor,
const std::string&, const Http::HeaderMap& headers,
const std::string&) const {
if (ConfigUtility::matchHeaders(headers, action_headers_)) {
descriptor.entries_.push_back({"header_match", descriptor_value_});
}
}

RateLimitPolicyEntryImpl::RateLimitPolicyEntryImpl(const Json::Object& config)
: disable_key_(config.getString("disable_key", "")), stage_(config.getInteger("stage", 0)) {

Expand All @@ -67,6 +90,8 @@ RateLimitPolicyEntryImpl::RateLimitPolicyEntryImpl(const Json::Object& config)
actions_.emplace_back(new RemoteAddressAction());
} else if (type == "generic_key") {
actions_.emplace_back(new GenericKeyAction(*action));
} else if (type == "header_value_match") {
actions_.emplace_back(new HeaderValueMatchAction(*action));
} else {
throw EnvoyException(fmt::format("unknown http rate limit filter action '{}'", type));
}
Expand Down
18 changes: 18 additions & 0 deletions source/common/router/router_ratelimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "envoy/router/router_ratelimit.h"

#include "common/http/filter/ratelimit.h"
#include "common/router/config_utility.h"

namespace Router {

Expand Down Expand Up @@ -76,6 +77,23 @@ class GenericKeyAction : public RateLimitAction {
const std::string descriptor_value_;
};

/**
* Action for header value match rate limiting.
*/
class HeaderValueMatchAction : public RateLimitAction {
public:
HeaderValueMatchAction(const Json::Object& action);

// Router::RateLimitAction
void populateDescriptor(const Router::RouteEntry& route, ::RateLimit::Descriptor& descriptor,
const std::string& local_service_cluster, const Http::HeaderMap& headers,
const std::string& remote_address) const override;

private:
const std::string descriptor_value_;
std::vector<Router::ConfigUtility::HeaderData> action_headers_;
};

/*
* Implementation of RateLimitPolicyEntry that holds the action for the configuration.
*/
Expand Down
Loading