mirror of
https://github.com/openappsec/openappsec.git
synced 2025-09-29 19:24:26 +03:00
Sep_24_2023-Dev
This commit is contained in:
6
components/security_apps/rate_limit/CMakeLists.txt
Normal file
6
components/security_apps/rate_limit/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
include_directories(../waap/waap_clib)
|
||||
include_directories(../waap/include)
|
||||
|
||||
add_library(rate_limit_comp rate_limit.cc)
|
||||
|
||||
add_library(rate_limit_config rate_limit_config.cc)
|
535
components/security_apps/rate_limit/rate_limit.cc
Executable file
535
components/security_apps/rate_limit/rate_limit.cc
Executable file
@@ -0,0 +1,535 @@
|
||||
#include "rate_limit.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "debug.h"
|
||||
#include "i_environment.h"
|
||||
#include "i_mainloop.h"
|
||||
#include "i_time_get.h"
|
||||
#include "rate_limit_config.h"
|
||||
#include "nginx_attachment_common.h"
|
||||
#include "http_inspection_events.h"
|
||||
#include "Waf2Util.h"
|
||||
#include "generic_rulebase/evaluators/asset_eval.h"
|
||||
#include "WaapConfigApi.h"
|
||||
#include "WaapConfigApplication.h"
|
||||
#include "PatternMatcher.h"
|
||||
#include "i_waapConfig.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <sys/types.h>
|
||||
#include <sys/ipc.h>
|
||||
#include <sys/shm.h>
|
||||
#include <sys/sem.h>
|
||||
#include <unistd.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include "hiredis/hiredis.h"
|
||||
|
||||
USE_DEBUG_FLAG(D_RATE_LIMIT);
|
||||
|
||||
using namespace std;
|
||||
|
||||
enum class RateLimitVedict { ACCEPT, DROP, DROP_AND_LOG };
|
||||
|
||||
class RateLimit::Impl
|
||||
:
|
||||
public Listener<HttpRequestHeaderEvent>
|
||||
{
|
||||
public:
|
||||
Impl() = default;
|
||||
~Impl() = default;
|
||||
|
||||
Maybe<string>
|
||||
extractUri(const string &address)
|
||||
{
|
||||
size_t protocolPos = address.find("://");
|
||||
if (protocolPos == std::string::npos) return genError("Invalid URI format: " + address);
|
||||
|
||||
size_t domainPos = address.find('/', protocolPos + 3);
|
||||
if (domainPos == std::string::npos) return string("");
|
||||
|
||||
return address.substr(domainPos);
|
||||
}
|
||||
|
||||
bool
|
||||
isRuleMatchingUri(const string &rule_uri, const string &request_uri, bool should_rule_be_exact_match)
|
||||
{
|
||||
if (rule_uri.find("*") != string::npos) {
|
||||
// first condition is for 'exact match with wildcard'
|
||||
// second is for when the rule serves as a prefix
|
||||
bool wildcard_match =
|
||||
!should_rule_be_exact_match && PatternMatcherWildcard(rule_uri + "*").match(request_uri + "/");
|
||||
wildcard_match |= PatternMatcherWildcard(rule_uri).match(request_uri);
|
||||
return wildcard_match;
|
||||
}
|
||||
|
||||
return !should_rule_be_exact_match && str_starts_with(request_uri, rule_uri);
|
||||
}
|
||||
|
||||
Maybe<RateLimitRule>
|
||||
findRateLimitRule(const string &matched_uri, string &asset_id)
|
||||
{
|
||||
WaapConfigAPI api_config;
|
||||
WaapConfigApplication application_config;
|
||||
IWaapConfig* site_config = nullptr;
|
||||
|
||||
if (WaapConfigAPI::getWaapAPIConfig(api_config)) {
|
||||
site_config = &api_config;
|
||||
} else if (WaapConfigApplication::getWaapSiteConfig(application_config)) {
|
||||
site_config = &application_config;
|
||||
}
|
||||
|
||||
if (site_config == nullptr) return genError("Failed to get asset configuration. Skipping rate limit check.");
|
||||
|
||||
asset_id = site_config->get_AssetId();
|
||||
ScopedContext rate_limit_ctx;
|
||||
rate_limit_ctx.registerValue<GenericConfigId>(AssetMatcher::ctx_key, site_config->get_AssetId());
|
||||
auto maybe_rate_limit_config = getConfiguration<RateLimitConfig>("rulebase", "rateLimit");
|
||||
if (!maybe_rate_limit_config.ok())
|
||||
return genError("Failed to get rate limit configuration. Skipping rate limit check.");
|
||||
|
||||
const auto &rate_limit_config = maybe_rate_limit_config.unpack();
|
||||
mode = rate_limit_config.getRateLimitMode();
|
||||
|
||||
if (mode == "Inactive") return genError("Rate limit mode is Inactive in policy");
|
||||
|
||||
set<string> rule_set;
|
||||
Maybe<RateLimitRule> matched_rule = genError("URI did not match any rate limit rule.");
|
||||
int rate_limit_longest_match = 0;
|
||||
for (const auto &application_url : site_config->get_applicationUrls()) {
|
||||
dbgTrace(D_RATE_LIMIT) << "Application URL: " << application_url;
|
||||
|
||||
auto maybe_uri = extractUri(application_url);
|
||||
if (!maybe_uri.ok()) {
|
||||
dbgWarning(D_RATE_LIMIT) << "Failed to extract URI from application URL: " << maybe_uri.getErr();
|
||||
continue;
|
||||
}
|
||||
|
||||
string application_uri = maybe_uri.unpack();
|
||||
if (application_uri.back() == '/') application_uri.pop_back();
|
||||
|
||||
for (const auto &rule : rate_limit_config.getRateLimitRules()) {
|
||||
string full_rule_uri = application_uri + rule.getRateLimitUri();
|
||||
int full_rule_uri_length = full_rule_uri.length();
|
||||
|
||||
// avoiding duplicates
|
||||
if (!rule_set.insert(full_rule_uri).second) continue;
|
||||
|
||||
dbgTrace(D_RATE_LIMIT)
|
||||
<< "Trying to match rule uri: "
|
||||
<< full_rule_uri
|
||||
<< " with request uri: "
|
||||
<< matched_uri;
|
||||
|
||||
if (full_rule_uri_length < rate_limit_longest_match) {
|
||||
dbgDebug(D_RATE_LIMIT)
|
||||
<< "rule is shorter then already matched rule. current rule length: "
|
||||
<< full_rule_uri_length
|
||||
<< ", previously longest matched rule length: "
|
||||
<< rate_limit_longest_match;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (full_rule_uri == matched_uri ||
|
||||
full_rule_uri == matched_uri + "/" ||
|
||||
full_rule_uri + "/" == matched_uri) {
|
||||
dbgDebug(D_RATE_LIMIT)
|
||||
<< "Found Exact match to request uri: "
|
||||
<< matched_uri
|
||||
<< ", rule uri: "
|
||||
<< full_rule_uri;
|
||||
return rule;
|
||||
}
|
||||
|
||||
if (rule.getRateLimitUri() == "/") {
|
||||
dbgDebug(D_RATE_LIMIT)
|
||||
<< "Matched new longest rule, request uri: "
|
||||
<< matched_uri
|
||||
<< ", rule uri: "
|
||||
<< full_rule_uri;
|
||||
matched_rule = rule;
|
||||
rate_limit_longest_match = full_rule_uri_length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isRuleMatchingUri(full_rule_uri, matched_uri, rule.isExactMatch())) {
|
||||
dbgDebug(D_RATE_LIMIT)
|
||||
<< "Matched new longest rule, request uri: "
|
||||
<< matched_uri
|
||||
<< ", rule uri: "
|
||||
<< full_rule_uri;
|
||||
matched_rule = rule;
|
||||
rate_limit_longest_match = full_rule_uri_length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matched_rule;
|
||||
}
|
||||
|
||||
EventVerdict
|
||||
respond(const HttpRequestHeaderEvent &event) override
|
||||
{
|
||||
if (!event.isLastHeader()) return INSPECT;
|
||||
|
||||
auto uri_ctx = Singleton::Consume<I_Environment>::by<RateLimit>()->get<string>(HttpTransactionData::uri_ctx);
|
||||
if (!uri_ctx.ok()) {
|
||||
dbgWarning(D_RATE_LIMIT) << "Unable to get URL from context, Not enforcing rate limit";
|
||||
return ACCEPT;
|
||||
}
|
||||
|
||||
string asset_id;
|
||||
auto uri = uri_ctx.unpack();
|
||||
transform(uri.begin(), uri.end(), uri.begin(), [](unsigned char c) { return tolower(c); });
|
||||
auto maybe_rule = findRateLimitRule(uri, asset_id);
|
||||
if (!maybe_rule.ok()) {
|
||||
dbgDebug(D_RATE_LIMIT) << "Not Enforcing Rate Limit: " << maybe_rule.getErr();
|
||||
return ACCEPT;
|
||||
}
|
||||
|
||||
const auto &rule = maybe_rule.unpack();
|
||||
burst = rule.getRateLimit();
|
||||
limit = static_cast<float>(rule.getRateLimit()) / (rule.getRateLimitScope() == "Minute" ? 60 : 1);
|
||||
|
||||
dbgTrace(D_RATE_LIMIT)
|
||||
<< "found rate limit rule with: "
|
||||
<< rule.getRateLimit()
|
||||
<< " per "
|
||||
<< (rule.getRateLimitScope() == "Minute" ? 60 : 1)
|
||||
<< " seconds";
|
||||
|
||||
auto maybe_source_identifier =
|
||||
Singleton::Consume<I_Environment>::by<RateLimit>()->get<string>(HttpTransactionData::source_identifier);
|
||||
if (!maybe_source_identifier.ok()) {
|
||||
dbgWarning(D_RATE_LIMIT) << "Unable to get source identifier from context, not enforcing rate limit";
|
||||
return ACCEPT;
|
||||
}
|
||||
|
||||
auto &source_identifier = maybe_source_identifier.unpack();
|
||||
dbgDebug(D_RATE_LIMIT) << "source identifier value: " << source_identifier;
|
||||
|
||||
string unique_key = asset_id + ":" + source_identifier + ":" + uri;
|
||||
if (unique_key.back() == '/') unique_key.pop_back();
|
||||
|
||||
auto verdict = decide(unique_key);
|
||||
if (verdict == RateLimitVedict::ACCEPT) {
|
||||
dbgTrace(D_RATE_LIMIT) << "Received ACCEPT verdict.";
|
||||
return ACCEPT;
|
||||
}
|
||||
|
||||
if (verdict == RateLimitVedict::DROP_AND_LOG) sendLog(uri, source_identifier, rule);
|
||||
|
||||
if (mode == "Active") {
|
||||
dbgTrace(D_RATE_LIMIT) << "Received DROP verdict, this request will be blocked by rate limit";
|
||||
return DROP;
|
||||
}
|
||||
|
||||
dbgTrace(D_RATE_LIMIT) << "Received DROP in detect mode, will not block.";
|
||||
return ACCEPT;
|
||||
}
|
||||
|
||||
string
|
||||
getListenerName() const override
|
||||
{
|
||||
return "rate limit";
|
||||
}
|
||||
|
||||
RateLimitVedict
|
||||
decide(const std::string &key) {
|
||||
if (redis == nullptr) {
|
||||
dbgDebug(D_RATE_LIMIT)
|
||||
<< "there is no connection to the redis at the moment, unable to enforce rate limit";
|
||||
reconnectRedis();
|
||||
return RateLimitVedict::ACCEPT;
|
||||
}
|
||||
|
||||
redisReply* reply = static_cast<redisReply*>(redisCommand(redis, "EVALSHA %s 1 %s %f %d",
|
||||
rate_limit_lua_script_hash.c_str(), key.c_str(), limit, burst));
|
||||
|
||||
if (reply == NULL || redis->err) {
|
||||
dbgDebug(D_RATE_LIMIT)
|
||||
<< "Error executing Redis command: No reply received, unable to enforce rate limit";
|
||||
reconnectRedis();
|
||||
return RateLimitVedict::ACCEPT;
|
||||
}
|
||||
|
||||
// redis's lua script returned true - accept
|
||||
if (reply->type == REDIS_REPLY_INTEGER) {
|
||||
freeReplyObject(reply);
|
||||
return RateLimitVedict::ACCEPT;
|
||||
}
|
||||
|
||||
// redis's lua script returned false - drop, no need to log
|
||||
if (reply->type == REDIS_REPLY_NIL) {
|
||||
freeReplyObject(reply);
|
||||
return RateLimitVedict::DROP;
|
||||
}
|
||||
|
||||
// redis's lua script returned string - drop and send log
|
||||
const char* log_str = "BLOCK AND LOG";
|
||||
if (reply->type == REDIS_REPLY_STRING && strncmp(reply->str, log_str, strlen(log_str)) == 0) {
|
||||
freeReplyObject(reply);
|
||||
return RateLimitVedict::DROP_AND_LOG;
|
||||
}
|
||||
|
||||
dbgDebug(D_RATE_LIMIT)
|
||||
<< "Got unexected reply from redis. reply type: "
|
||||
<< reply->type
|
||||
<< ". not enforcing rate limit for this request.";
|
||||
freeReplyObject(reply);
|
||||
return RateLimitVedict::ACCEPT;
|
||||
}
|
||||
|
||||
void
|
||||
sendLog(const string &uri, const string &source_identifier, const RateLimitRule& rule)
|
||||
{
|
||||
set<string> rate_limit_triggers_set;
|
||||
for (const auto &trigger : rule.getRateLimitTriggers()) {
|
||||
rate_limit_triggers_set.insert(trigger.getTriggerId());
|
||||
}
|
||||
|
||||
ScopedContext ctx;
|
||||
ctx.registerValue<std::set<GenericConfigId>>(TriggerMatcher::ctx_key, rate_limit_triggers_set);
|
||||
auto log_trigger = getConfigurationWithDefault(LogTriggerConf(), "rulebase", "log");
|
||||
|
||||
if (!log_trigger.isPreventLogActive(LogTriggerConf::SecurityType::AccessControl)) {
|
||||
dbgTrace(D_RATE_LIMIT) << "Not sending rate-limit log as it is not required";
|
||||
return;
|
||||
}
|
||||
|
||||
auto maybe_rule_by_ctx = getConfiguration<BasicRuleConfig>("rulebase", "rulesConfig");
|
||||
if (!maybe_rule_by_ctx.ok()) {
|
||||
dbgWarning(D_RATE_LIMIT)
|
||||
<< "rule was not found by the given context. Reason: "
|
||||
<< maybe_rule_by_ctx.getErr();
|
||||
return;
|
||||
}
|
||||
|
||||
string event_name = "Rate limit";
|
||||
|
||||
LogGen log = log_trigger(
|
||||
event_name,
|
||||
LogTriggerConf::SecurityType::AccessControl,
|
||||
ReportIS::Severity::HIGH,
|
||||
ReportIS::Priority::HIGH,
|
||||
true,
|
||||
LogField("practiceType", "Rate Limit"),
|
||||
ReportIS::Tags::RATE_LIMIT
|
||||
);
|
||||
|
||||
const auto &rule_by_ctx = maybe_rule_by_ctx.unpack();
|
||||
|
||||
log
|
||||
<< LogField("assetId", rule_by_ctx.getAssetId())
|
||||
<< LogField("assetName", rule_by_ctx.getAssetName())
|
||||
<< LogField("ruleId", rule_by_ctx.getRuleId())
|
||||
<< LogField("ruleName", rule_by_ctx.getRuleName())
|
||||
<< LogField("httpUriPath", uri)
|
||||
<< LogField("httpSourceId", source_identifier)
|
||||
<< LogField("securityAction", (mode == "Active" ? "Prevent" : "Detect"))
|
||||
<< LogField("waapIncidentType", "Rate Limit");
|
||||
|
||||
auto http_method =
|
||||
Singleton::Consume<I_Environment>::by<RateLimit>()->get<string>(HttpTransactionData::method_ctx);
|
||||
if (http_method.ok()) log << LogField("httpMethod", http_method.unpack());
|
||||
|
||||
auto http_host =
|
||||
Singleton::Consume<I_Environment>::by<RateLimit>()->get<string>(HttpTransactionData::host_name_ctx);
|
||||
if (http_host.ok()) log << LogField("httpHostName", http_host.unpack());
|
||||
|
||||
auto source_ip =
|
||||
Singleton::Consume<I_Environment>::by<RateLimit>()->get<IPAddr>(HttpTransactionData::client_ip_ctx);
|
||||
if (source_ip.ok()) log << LogField("sourceIP", ipAddrToStr(source_ip.unpack()));
|
||||
|
||||
auto proxy_ip =
|
||||
Singleton::Consume<I_Environment>::by<RateLimit>()->get<std::string>(HttpTransactionData::proxy_ip_ctx);
|
||||
if (proxy_ip.ok() && source_ip.ok() && ipAddrToStr(source_ip.unpack()) != proxy_ip.unpack()) {
|
||||
log << LogField("proxyIP", static_cast<std::string>(proxy_ip.unpack()));
|
||||
}
|
||||
}
|
||||
|
||||
string
|
||||
ipAddrToStr(const IPAddr& ip_address) const
|
||||
{
|
||||
char str[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &(ip_address), str, INET_ADDRSTRLEN);
|
||||
return string(str);
|
||||
}
|
||||
|
||||
Maybe<void>
|
||||
connectRedis()
|
||||
{
|
||||
disconnectRedis();
|
||||
|
||||
redisOptions options;
|
||||
memset(&options, 0, sizeof(redisOptions));
|
||||
REDIS_OPTIONS_SET_TCP(
|
||||
&options,
|
||||
"127.0.0.1",
|
||||
getConfigurationWithDefault<int>(6379, "connection", "Redis Port")
|
||||
);
|
||||
|
||||
timeval timeout;
|
||||
timeout.tv_sec = 0;
|
||||
timeout.tv_usec = getConfigurationWithDefault<int>(30000, "connection", "Redis Timeout");
|
||||
options.connect_timeout = &timeout;
|
||||
options.command_timeout = &timeout;
|
||||
|
||||
redisContext* context = redisConnectWithOptions(&options);
|
||||
if (context != nullptr && context->err) {
|
||||
dbgDebug(D_RATE_LIMIT)
|
||||
<< "Error connecting to Redis: "
|
||||
<< context->errstr;
|
||||
redisFree(context);
|
||||
return genError("");
|
||||
}
|
||||
|
||||
if (context == nullptr) return genError("");
|
||||
|
||||
redis = context;
|
||||
static string luaScript = R"(
|
||||
local key = KEYS[1]
|
||||
local rateLimit = tonumber(ARGV[1])
|
||||
local burstLimit = tonumber(ARGV[2])
|
||||
local currentTimeSeconds = tonumber(redis.call('time')[1])
|
||||
local lastRequestTimeSeconds = tonumber(redis.call('get', key .. ':lastRequestTime') or "0")
|
||||
local elapsedTimeSeconds = currentTimeSeconds - lastRequestTimeSeconds
|
||||
local tokens = tonumber(redis.call('get', key .. ':tokens') or burstLimit)
|
||||
local was_blocked = tonumber(redis.call('get', key .. ':block') or "0")
|
||||
|
||||
tokens = math.min(tokens + (elapsedTimeSeconds * rateLimit), burstLimit)
|
||||
|
||||
if tokens >= 1 then
|
||||
tokens = tokens - 1
|
||||
redis.call('set', key .. ':tokens', tokens)
|
||||
redis.call('set', key .. ':lastRequestTime', currentTimeSeconds)
|
||||
redis.call('expire', key .. ':tokens', 60)
|
||||
redis.call('expire', key .. ':lastRequestTime', 60)
|
||||
return true
|
||||
elseif was_blocked == 1 then
|
||||
redis.call('set', key .. ':block', 1)
|
||||
redis.call('expire', key .. ':block', 60)
|
||||
return false
|
||||
else
|
||||
redis.call('set', key .. ':block', 1)
|
||||
redis.call('expire', key .. ':block', 60)
|
||||
return "BLOCK AND LOG"
|
||||
end
|
||||
)";
|
||||
|
||||
// Load the Lua script in Redis and retrieve its SHA1 hash
|
||||
redisReply* loadReply =
|
||||
static_cast<redisReply*>(redisCommand(redis, "SCRIPT LOAD %s", luaScript.c_str()));
|
||||
if (loadReply != nullptr && loadReply->type == REDIS_REPLY_STRING) {
|
||||
rate_limit_lua_script_hash = loadReply->str;
|
||||
freeReplyObject(loadReply);
|
||||
}
|
||||
|
||||
return Maybe<void>();
|
||||
}
|
||||
|
||||
void
|
||||
reconnectRedis()
|
||||
{
|
||||
dbgFlow(D_RATE_LIMIT) << "Trying to reconnect to redis after failure to invoke a redis command";
|
||||
static bool is_reconnecting = false;
|
||||
if (!is_reconnecting) {
|
||||
is_reconnecting = true;
|
||||
Singleton::Consume<I_MainLoop>::by<RateLimit>()->addOneTimeRoutine(
|
||||
I_MainLoop::RoutineType::System,
|
||||
[this] ()
|
||||
{
|
||||
connectRedis();
|
||||
is_reconnecting = false;
|
||||
},
|
||||
"Reconnect redis",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void
|
||||
handleNewPolicy()
|
||||
{
|
||||
if (RateLimitConfig::isActive() && !redis) {
|
||||
connectRedis();
|
||||
registerListener();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!RateLimitConfig::isActive()) {
|
||||
disconnectRedis();
|
||||
unregisterListener();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
disconnectRedis()
|
||||
{
|
||||
if (redis) {
|
||||
redisFree(redis);
|
||||
redis = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
init()
|
||||
{
|
||||
Singleton::Consume<I_MainLoop>::by<RateLimit>()->addOneTimeRoutine(
|
||||
I_MainLoop::RoutineType::System,
|
||||
[this] ()
|
||||
{
|
||||
handleNewPolicy();
|
||||
registerConfigLoadCb([this]() { handleNewPolicy(); });
|
||||
},
|
||||
"Initialize rate limit component",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
void
|
||||
fini()
|
||||
{
|
||||
disconnectRedis();
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr auto DROP = ngx_http_cp_verdict_e::TRAFFIC_VERDICT_DROP;
|
||||
static constexpr auto ACCEPT = ngx_http_cp_verdict_e::TRAFFIC_VERDICT_ACCEPT;
|
||||
static constexpr auto INSPECT = ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INSPECT;
|
||||
|
||||
string mode;
|
||||
string rate_limit_lua_script_hash;
|
||||
int burst;
|
||||
float limit;
|
||||
redisContext* redis = nullptr;
|
||||
};
|
||||
|
||||
RateLimit::RateLimit() : Component("RateLimit"), pimpl(make_unique<Impl>()) {}
|
||||
|
||||
RateLimit::~RateLimit() = default;
|
||||
|
||||
void
|
||||
RateLimit::preload()
|
||||
{
|
||||
registerExpectedConfiguration<WaapConfigApplication>("WAAP", "WebApplicationSecurity");
|
||||
registerExpectedConfiguration<WaapConfigAPI>("WAAP", "WebAPISecurity");
|
||||
registerExpectedConfigFile("waap", Config::ConfigFileType::Policy);
|
||||
registerExpectedConfiguration<RateLimitConfig>("rulebase", "rateLimit");
|
||||
registerExpectedConfigFile("accessControlV2", Config::ConfigFileType::Policy);
|
||||
registerConfigPrepareCb([]() { RateLimitConfig::resetIsActive(); });
|
||||
}
|
||||
|
||||
void
|
||||
RateLimit::init() { pimpl->init(); }
|
||||
|
||||
void
|
||||
RateLimit::fini() { pimpl->fini(); }
|
157
components/security_apps/rate_limit/rate_limit_config.cc
Executable file
157
components/security_apps/rate_limit/rate_limit_config.cc
Executable file
@@ -0,0 +1,157 @@
|
||||
#include "rate_limit_config.h"
|
||||
|
||||
bool RateLimitConfig::is_active = false;
|
||||
|
||||
void
|
||||
RateLimitTrigger::load(cereal::JSONInputArchive &ar)
|
||||
{
|
||||
dbgTrace(D_REVERSE_PROXY) << "Serializing single Rate Limit rule's triggers";
|
||||
try {
|
||||
ar(cereal::make_nvp("id", id));
|
||||
} catch (const cereal::Exception &e) {
|
||||
dbgWarning(D_REVERSE_PROXY)
|
||||
<< "Failed to load single Rate Limit JSON rule's triggers. Error: " << e.what();
|
||||
ar.setNextName(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RateLimitRule::load(cereal::JSONInputArchive &ar)
|
||||
{
|
||||
dbgTrace(D_REVERSE_PROXY) << "Serializing single Rate Limit rule";
|
||||
try {
|
||||
ar(cereal::make_nvp("URI", uri));
|
||||
ar(cereal::make_nvp("scope", scope));
|
||||
ar(cereal::make_nvp("limit", limit));
|
||||
ar(cereal::make_nvp("triggers", rate_limit_triggers));
|
||||
} catch (const cereal::Exception &e) {
|
||||
dbgWarning(D_REVERSE_PROXY) << "Failed to load single Rate Limit JSON rule. Error: " << e.what();
|
||||
ar.setNextName(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RateLimitRule::prepare(const std::string &asset_id, int zone_id)
|
||||
{
|
||||
std::string zone_id_s = std::to_string(zone_id);
|
||||
std::string zone;
|
||||
if (isRootLocation()) {
|
||||
zone = "root_zone_" + asset_id + "_" + zone_id_s;
|
||||
} else {
|
||||
std::string zone_name_suffix = uri;
|
||||
std::replace(zone_name_suffix.begin(), zone_name_suffix.end(), '/', '_');
|
||||
zone = "zone" + zone_name_suffix + "_" + zone_id_s;
|
||||
}
|
||||
|
||||
limit_req_template_value = "zone=" + zone + " burst=" + std::to_string(limit) + " nodelay";
|
||||
|
||||
// nginx conf will look like: limit_req_zone <sourceIdentifier> zone=<location>_<id>:10m rate=<limit>r/<scope>;
|
||||
std::string rate_unit = scope == "Minute" ? "r/m" : "r/s";
|
||||
limit_req_zone_template_value =
|
||||
"zone=" + zone + ":" + cache_size + " rate=" + std::to_string(limit) + rate_unit;
|
||||
|
||||
dbgTrace(D_REVERSE_PROXY)
|
||||
<< "limit_req_zone nginx template value: "
|
||||
<< limit_req_zone_template_value
|
||||
<< ", limit_req nginx template value: "
|
||||
<< limit_req_template_value;
|
||||
}
|
||||
|
||||
bool
|
||||
RateLimitRule::isRootLocation() const
|
||||
{
|
||||
if (uri.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto non_root = uri.find_first_not_of("/");
|
||||
if (non_root != std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
RateLimitConfig::load(cereal::JSONInputArchive &ar)
|
||||
{
|
||||
dbgTrace(D_REVERSE_PROXY) << "Serializing Rate Limit config";
|
||||
try {
|
||||
ar(cereal::make_nvp("rules", rate_limit_rules));
|
||||
ar(cereal::make_nvp("mode", mode));
|
||||
prepare();
|
||||
} catch (const cereal::Exception &e) {
|
||||
dbgWarning(D_REVERSE_PROXY) << "Failed to load single Rate Limit JSON config. Error: " << e.what();
|
||||
ar.setNextName(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RateLimitConfig::addSiblingRateLimitRule(RateLimitRule &rule) {
|
||||
rule.setExactMatch();
|
||||
RateLimitRule sibling_rule(rule);
|
||||
sibling_rule.appendSlash();
|
||||
sibling_rule.setExactMatch();
|
||||
rate_limit_rules.push_back(sibling_rule);
|
||||
}
|
||||
|
||||
void
|
||||
RateLimitConfig::prepare()
|
||||
{
|
||||
// Removes invalid rules
|
||||
auto last_valid_rule =
|
||||
std::remove_if(
|
||||
rate_limit_rules.begin(),
|
||||
rate_limit_rules.end(),
|
||||
[](const RateLimitRule &rule) { return !rule; }
|
||||
);
|
||||
|
||||
rate_limit_rules.erase(last_valid_rule, rate_limit_rules.end());
|
||||
|
||||
// Removes duplicates
|
||||
sort(rate_limit_rules.begin(), rate_limit_rules.end());
|
||||
rate_limit_rules.erase(std::unique(rate_limit_rules.begin(), rate_limit_rules.end()), rate_limit_rules.end());
|
||||
|
||||
std::for_each(
|
||||
rate_limit_rules.begin(),
|
||||
rate_limit_rules.end(),
|
||||
[this](RateLimitRule &rule) { if (rule.isExactMatch()) { addSiblingRateLimitRule(rule); } }
|
||||
);
|
||||
|
||||
dbgTrace(D_REVERSE_PROXY)
|
||||
<< "Final rate-limit rules: "
|
||||
<< makeSeparatedStr(rate_limit_rules, "; ")
|
||||
<< "; Mode: "
|
||||
<< mode;
|
||||
|
||||
setIsActive(mode != "Inactive");
|
||||
}
|
||||
|
||||
const RateLimitRule
|
||||
RateLimitConfig::findLongestMatchingRule(const std::string &nginx_uri) const
|
||||
{
|
||||
dbgFlow(D_REVERSE_PROXY) << "Trying to find a matching rat-limit rule for NGINX URI: " << nginx_uri;
|
||||
|
||||
size_t longest_len = 0;
|
||||
RateLimitRule longest_matching_rule;
|
||||
for (const RateLimitRule &rule : rate_limit_rules) {
|
||||
if (rule.getRateLimitUri() == nginx_uri) {
|
||||
dbgTrace(D_REVERSE_PROXY) << "Found exact rate-limit match: " << rule;
|
||||
return rule;
|
||||
}
|
||||
|
||||
if (nginx_uri.size() < rule.getRateLimitUri().size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std::equal(rule.getRateLimitUri().rbegin(), rule.getRateLimitUri().rend(), nginx_uri.rbegin())) {
|
||||
if (rule.getRateLimitUri().size() > longest_len) {
|
||||
longest_matching_rule = rule;
|
||||
longest_len = rule.getRateLimitUri().size();
|
||||
dbgTrace(D_REVERSE_PROXY) << "Longest matching rate-limit rule so far: " << rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbgTrace(D_REVERSE_PROXY) << "Longest matching rate-limit rule: " << longest_matching_rule;
|
||||
return longest_matching_rule;
|
||||
}
|
Reference in New Issue
Block a user