diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index b7547dc37a3..f8831e144a7 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -775,6 +775,14 @@ changing the remapped destination, ```` should be used to indicate the component that is being modified (see `URL Parts`_). Currently the only valid parts for rm-destination are QUERY, PATH, and PORT. +run-plugin +~~~~~~~~~~~~~~ +:: + + run-plugin .so " ..." + +This allows to run an existing remap plugin, conditionally, from within a +header rewrite rule. set-header ~~~~~~~~~~ @@ -1350,3 +1358,13 @@ the client where the requested data was served from.:: cond %{HEADER:ATS-SRVR-UUID} ="" [OR] cond %{CACHE} ="hit-fresh" set-header ATS-SRVR-UUID %{ID:UNIQUE} + +Apply rate limiting for some select requests +------------------------------------ + +This rule will conditiionally, based on the client request headers, apply rate +limiting to the request.:: + + cond %{REMAP_PSEUDO_HOOK} [AND] + cond %{CLIENT-HEADER:Some-Special-Header} ="yes" + run-plugin rate_limit.so "--limit=300 --error=429" diff --git a/plugins/header_rewrite/CMakeLists.txt b/plugins/header_rewrite/CMakeLists.txt index b60db872265..fbc919f58df 100644 --- a/plugins/header_rewrite/CMakeLists.txt +++ b/plugins/header_rewrite/CMakeLists.txt @@ -38,7 +38,7 @@ target_link_libraries(header_rewrite_parser PUBLIC libswoc::libswoc) target_link_libraries( header_rewrite - PRIVATE PCRE::PCRE + PRIVATE ts::tscore PCRE::PCRE PUBLIC libswoc::libswoc ) @@ -52,7 +52,7 @@ if(BUILD_TESTING) add_executable(test_header_rewrite header_rewrite_test.cc) add_test(NAME test_header_rewrite COMMAND $) - target_link_libraries(test_header_rewrite PRIVATE header_rewrite_parser) + target_link_libraries(test_header_rewrite PRIVATE header_rewrite_parser ts::inkevent ts::tscore) if(maxminddb_FOUND) target_link_libraries(test_header_rewrite PRIVATE maxminddb::maxminddb) diff --git a/plugins/header_rewrite/factory.cc b/plugins/header_rewrite/factory.cc index 0c2b55e2ec4..531f390f24c 100644 --- a/plugins/header_rewrite/factory.cc +++ b/plugins/header_rewrite/factory.cc @@ -75,6 +75,8 @@ operator_factory(const std::string &op) o = new OperatorSetBody(); } else if (op == "set-http-cntl") { o = new OperatorSetHttpCntl(); + } else if (op == "run-plugin") { + o = new OperatorRunPlugin(); } else { TSError("[%s] Unknown operator: %s", PLUGIN_NAME, op.c_str()); diff --git a/plugins/header_rewrite/header_rewrite.cc b/plugins/header_rewrite/header_rewrite.cc index b2f095e12e9..06ec0f16965 100644 --- a/plugins/header_rewrite/header_rewrite.cc +++ b/plugins/header_rewrite/header_rewrite.cc @@ -26,6 +26,8 @@ #include "ts/remap.h" #include "ts/remap_version.h" +#include "proxy/http/remap/PluginFactory.h" + #include "parser.h" #include "ruleset.h" #include "resources.h" @@ -35,19 +37,27 @@ // Debugs const char PLUGIN_NAME[] = "header_rewrite"; const char PLUGIN_NAME_DBG[] = "dbg_header_rewrite"; - namespace header_rewrite_ns { DbgCtl dbg_ctl{PLUGIN_NAME_DBG}; DbgCtl pi_dbg_ctl{PLUGIN_NAME}; + +PluginFactory plugin_factory; } // namespace header_rewrite_ns -static std::once_flag initGeoLibs; +static std::once_flag initHRWLibs; static void -initGeoLib(const std::string &dbPath) +initHRWLibraries(const std::string &dbPath) { + header_rewrite_ns::plugin_factory.setRuntimeDir(RecConfigReadRuntimeDir()).addSearchDir(RecConfigReadPluginDir()); + + if (dbPath.empty()) { + return; + } + Dbg(pi_dbg_ctl, "Loading geo db %s", dbPath.c_str()); + #if TS_USE_HRW_GEOIP GeoIPConditionGeo::initLibrary(dbPath); #elif TS_USE_HRW_MAXMINDDB @@ -99,7 +109,7 @@ class RulesConfig return _rules[hook]; } - bool parse_config(const std::string &fname, TSHttpHookID default_hook); + bool parse_config(const std::string &fname, TSHttpHookID default_hook, char *from_url = nullptr, char *to_url = nullptr); private: bool add_rule(RuleSet *rule); @@ -133,7 +143,7 @@ RulesConfig::add_rule(RuleSet *rule) // anyways (or reload for remap.config), so not really in the critical path. // bool -RulesConfig::parse_config(const std::string &fname, TSHttpHookID default_hook) +RulesConfig::parse_config(const std::string &fname, TSHttpHookID default_hook, char *from_url, char *to_url) { RuleSet *rule = nullptr; std::string filename; @@ -177,7 +187,7 @@ RulesConfig::parse_config(const std::string &fname, TSHttpHookID default_hook) continue; } - Parser p; + Parser p(from_url, to_url); // Tokenize and parse this line if (!p.parse_line(line)) { @@ -356,7 +366,7 @@ TSPluginInit(int argc, const char *argv[]) Dbg(pi_dbg_ctl, "Global geo db %s", geoDBpath.c_str()); - std::call_once(initGeoLibs, [&geoDBpath]() { initGeoLib(geoDBpath); }); + std::call_once(initHRWLibs, [&geoDBpath]() { initHRWLibraries(geoDBpath); }); // Parse the global config file(s). All rules are just appended // to the "global" Rules configuration. @@ -414,6 +424,9 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE return TS_ERROR; } + char *from_url = argv[0]; + char *to_url = argv[1]; + // argv contains the "to" and "from" URLs. Skip the first so that the // second one poses as the program name. --argc; @@ -439,15 +452,15 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE } Dbg(pi_dbg_ctl, "Remap geo db %s", geoDBpath.c_str()); - - std::call_once(initGeoLibs, [&geoDBpath]() { initGeoLib(geoDBpath); }); } + std::call_once(initHRWLibs, [&geoDBpath]() { initHRWLibraries(geoDBpath); }); + RulesConfig *conf = new RulesConfig; for (int i = optind; i < argc; ++i) { Dbg(pi_dbg_ctl, "Loading remap configuration file %s", argv[i]); - if (!conf->parse_config(argv[i], TS_REMAP_PSEUDO_HOOK)) { + if (!conf->parse_config(argv[i], TS_REMAP_PSEUDO_HOOK, from_url, to_url)) { TSError("[%s] Unable to create remap instance", PLUGIN_NAME); delete conf; return TS_ERROR; @@ -473,6 +486,7 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE void TSRemapDeleteInstance(void *ih) { + Dbg(pi_dbg_ctl, "Deleting RulesConfig"); delete static_cast(ih); } diff --git a/plugins/header_rewrite/lulu.h b/plugins/header_rewrite/lulu.h index 2d23a9ac94a..f12339346e5 100644 --- a/plugins/header_rewrite/lulu.h +++ b/plugins/header_rewrite/lulu.h @@ -27,6 +27,8 @@ #include "tscore/ink_defs.h" #include "tscore/ink_platform.h" +#include "proxy/http/remap/PluginFactory.h" + #define TS_REMAP_PSEUDO_HOOK TS_HTTP_LAST_HOOK // Ugly, but use the "last hook" for remap instances. std::string getIP(sockaddr const *s_sockaddr); @@ -38,7 +40,8 @@ extern const char PLUGIN_NAME_DBG[]; namespace header_rewrite_ns { -extern DbgCtl dbg_ctl; -extern DbgCtl pi_dbg_ctl; +extern DbgCtl dbg_ctl; +extern DbgCtl pi_dbg_ctl; +extern PluginFactory plugin_factory; } // namespace header_rewrite_ns using namespace header_rewrite_ns; diff --git a/plugins/header_rewrite/operator.h b/plugins/header_rewrite/operator.h index 84855e9a38c..27e800fa621 100644 --- a/plugins/header_rewrite/operator.h +++ b/plugins/header_rewrite/operator.h @@ -45,6 +45,8 @@ class Operator : public Statement public: Operator() { Dbg(dbg_ctl, "Calling CTOR for Operator"); } + virtual ~Operator() = default; // Very uncommon for an Operator to have a custom DTOR, but happens. + // noncopyable Operator(const Operator &) = delete; void operator=(const Operator &) = delete; diff --git a/plugins/header_rewrite/operators.cc b/plugins/header_rewrite/operators.cc index f4b652f9398..f6f96b15f13 100644 --- a/plugins/header_rewrite/operators.cc +++ b/plugins/header_rewrite/operators.cc @@ -22,8 +22,10 @@ #include #include #include +#include #include "ts/ts.h" +#include "swoc/swoc_file.h" #include "operators.h" #include "ts/apidefs.h" @@ -1092,6 +1094,7 @@ OperatorSetHttpCntl::initialize_hooks() static const char *const HttpCntls[] = { "LOGGING", "INTERCEPT_RETRY", "RESP_CACHEABLE", "REQ_CACHEABLE", "SERVER_NO_STORE", "TXN_DEBUG", "SKIP_REMAP", }; + void OperatorSetHttpCntl::exec(const Resources &res) const { @@ -1103,3 +1106,73 @@ OperatorSetHttpCntl::exec(const Resources &res) const Dbg(pi_dbg_ctl, " Turning OFF %s for transaction", HttpCntls[static_cast(_cntl_qual)]); } } + +void +OperatorRunPlugin::initialize(Parser &p) +{ + Operator::initialize(p); + + auto plugin_name = p.get_arg(); + auto plugin_args = p.get_value(); + + if (plugin_name.empty()) { + TSError("[%s] missing plugin name", PLUGIN_NAME); + return; + } + + std::vector tokens; + std::istringstream iss(plugin_args); + std::string token; + + while (iss >> std::quoted(token)) { + tokens.push_back(token); + } + + // Create argc and argv + int argc = tokens.size() + 2; + char **argv = new char *[argc]; + + argv[0] = p.from_url(); + argv[1] = p.to_url(); + + for (int i = 0; i < argc; ++i) { + argv[i + 2] = const_cast(tokens[i].c_str()); + } + + std::string error; + + // We have to escalate access while loading these plugins, just as done when loading remap.config + { + uint32_t elevate_access = 0; + + REC_ReadConfigInteger(elevate_access, "proxy.config.plugin.load_elevated"); + ElevateAccess access(elevate_access ? ElevateAccess::FILE_PRIVILEGE : 0); + + _plugin = plugin_factory.getRemapPlugin(swoc::file::path(plugin_name), argc, const_cast(argv), error, + isPluginDynamicReloadEnabled()); + } // done elevating access + + delete[] argv; + + if (!_plugin) { + TSError("[%s] Unable to load plugin '%s': %s", PLUGIN_NAME, plugin_name.c_str(), error.c_str()); + } +} + +void +OperatorRunPlugin::initialize_hooks() +{ + add_allowed_hook(TS_REMAP_PSEUDO_HOOK); + + require_resources(RSRC_CLIENT_REQUEST_HEADERS); // Need this for the txnp +} + +void +OperatorRunPlugin::exec(const Resources &res) const +{ + TSReleaseAssert(_plugin != nullptr); + + if (res._rri && res.txnp) { + _plugin->doRemap(res.txnp, res._rri); + } +} diff --git a/plugins/header_rewrite/operators.h b/plugins/header_rewrite/operators.h index 8fea20bef28..3f30d2654df 100644 --- a/plugins/header_rewrite/operators.h +++ b/plugins/header_rewrite/operators.h @@ -443,3 +443,35 @@ class OperatorSetHttpCntl : public Operator bool _flag = false; TSHttpCntlType _cntl_qual; }; + +class RemapPluginInst; // Opaque to the HRW operator, but needed in the implementation. + +class OperatorRunPlugin : public Operator +{ +public: + OperatorRunPlugin() { Dbg(dbg_ctl, "Calling CTOR for OperatorRunPlugin"); } + + // This one is special, since we have to remove the old plugin from the factory. + ~OperatorRunPlugin() override + { + Dbg(dbg_ctl, "Calling DTOR for OperatorRunPlugin"); + + if (_plugin) { + _plugin->done(); + _plugin = nullptr; + } + } + + // noncopyable + OperatorRunPlugin(const OperatorRunPlugin &) = delete; + void operator=(const OperatorRunPlugin &) = delete; + + void initialize(Parser &p) override; + +protected: + void initialize_hooks() override; + void exec(const Resources &res) const override; + +private: + RemapPluginInst *_plugin = nullptr; +}; diff --git a/plugins/header_rewrite/parser.h b/plugins/header_rewrite/parser.h index aef6cb4b4de..25bfdd0a709 100644 --- a/plugins/header_rewrite/parser.h +++ b/plugins/header_rewrite/parser.h @@ -33,12 +33,26 @@ class Parser { public: - Parser(){}; + Parser() = default; // No from/to URLs for this parser + Parser(char *from_url, char *to_url) : _from_url(from_url), _to_url(to_url) {} // noncopyable Parser(const Parser &) = delete; void operator=(const Parser &) = delete; + // These are not const char *, because, you know, everything else with argv is a char * + char * + from_url() const + { + return _from_url; + } + + char * + to_url() const + { + return _to_url; + } + bool empty() const { @@ -95,8 +109,10 @@ class Parser private: bool preprocess(std::vector tokens); - bool _cond = false; - bool _empty = false; + bool _cond = false; + bool _empty = false; + char *_from_url = nullptr; + char *_to_url = nullptr; std::vector _mods; std::string _op; std::string _arg;