blob: 04070030e7685b2340994793e4dd70215f72ab19 [file] [log] [blame]
/** @file
*
* YAML remap configuration file parsing implementation.
*
* @section license License
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "proxy/http/remap/RemapYamlConfig.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <filesystem>
#include <algorithm>
#include <string>
#include <string_view>
#include <vector>
#include "tscore/Diags.h"
#include "tscore/ink_string.h"
#include "tsutil/ts_errata.h"
#include "tsutil/PostScript.h"
#include "swoc/bwf_base.h"
#include "swoc/bwf_ex.h"
#include "swoc/swoc_file.h"
#include "proxy/http/remap/UrlRewrite.h"
#include "proxy/http/remap/UrlMapping.h"
#include "proxy/http/remap/RemapConfig.h"
#include "proxy/http/remap/AclFiltering.h"
#include "records/RecCore.h"
namespace
{
DbgCtl dbg_ctl_remap_yaml{"remap_yaml"};
DbgCtl dbg_ctl_url_rewrite{"url_rewrite"};
} // end anonymous namespace
swoc::Errata
parse_yaml_url(const YAML::Node &node, URL &url, bool host_check, std::string_view &url_str)
{
if (!node || !node.IsMap()) {
return swoc::Errata("URL must be a map");
}
url.create(nullptr);
// Use url first if defined
ParseResult rparse;
if (node["url"]) {
url_str = node["url"].as<std::string_view>();
if (host_check) {
rparse = url.parse_regex(url_str);
} else {
rparse = url.parse_no_host_check(url_str);
}
if (rparse != ParseResult::DONE) {
return swoc::Errata("malformed URL: {}", url_str);
}
return {};
}
// Build URL string from components
if (node["scheme"]) {
url.scheme_set(node["scheme"].as<std::string>());
}
if (node["host"]) {
url.host_set(node["host"].as<std::string>());
}
if (node["port"]) {
url.port_set(node["port"].as<int>());
}
if (node["path"]) {
url.path_set(node["path"].as<std::string>());
}
return {};
}
swoc::Errata
remap_validate_yaml_filter_args(acl_filter_rule **rule_pp, const YAML::Node &node, ACLBehaviorPolicy behavior_policy)
{
acl_filter_rule *rule;
int j;
bool new_rule_flg = false;
if (!rule_pp) {
Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Invalid argument(s)");
return swoc::Errata("Invalid argument(s)");
}
Dbg(dbg_ctl_url_rewrite, "validate_filter_args: ");
for (const auto &rule : node) {
Dbg(dbg_ctl_url_rewrite, "\"%s\" \n", rule.first.as<std::string>().c_str());
}
ts::PostScript free_rule([&]() -> void {
if (new_rule_flg) {
delete rule;
*rule_pp = nullptr;
}
});
if ((rule = *rule_pp) == nullptr) {
rule = new acl_filter_rule();
if (unlikely((*rule_pp = rule) == nullptr)) {
Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Memory allocation error");
return swoc::Errata("Memory allocation Error");
}
new_rule_flg = true;
Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] new acl_filter_rule class was created during remap rule processing");
}
if (!node || !node.IsMap()) {
return swoc::Errata("filters must be a map");
}
// Parse method
auto parse_method = [&](const std::string &method_str) {
int m = hdrtoken_tokenize(method_str.c_str(), method_str.length(), nullptr) - HTTP_WKSIDX_CONNECT;
if (m >= 0 && m < HTTP_WKSIDX_METHODS_CNT) {
rule->standard_method_lookup[m] = true;
} else {
Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Using nonstandard method [%s]", method_str.c_str());
rule->nonstandard_methods.insert(method_str);
}
rule->method_restriction_enabled = true;
};
if (node["method"]) {
if (node["method"].IsSequence()) {
for (const auto &method : node["method"]) {
parse_method(method.as<std::string>());
}
} else {
parse_method(node["method"].as<std::string>());
}
}
// Parse src_ip (and src_ip_invert)
auto parse_src_ip = [&](const std::string &ip_str, bool invert) -> swoc::Errata {
if (rule->src_ip_cnt >= ACL_FILTER_MAX_SRC_IP) {
Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Too many \"src_ip=\" filters");
return swoc::Errata("Defined more than {} src_ip filters", ACL_FILTER_MAX_SRC_IP);
}
src_ip_info_t *ipi = &rule->src_ip_array[rule->src_ip_cnt];
if (invert) {
ipi->invert = true;
}
std::string_view arg{ip_str};
if (arg == "all") {
ipi->match_all_addresses = true;
} else if (ats_ip_range_parse(arg, ipi->start, ipi->end) != 0) {
Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Unable to parse IP value in %s", ip_str.c_str());
return swoc::Errata("Unable to parse IP value: {}", ip_str);
}
for (j = 0; j < rule->src_ip_cnt; j++) {
if (rule->src_ip_array[j].start == ipi->start && rule->src_ip_array[j].end == ipi->end) {
ipi->reset();
return {};
}
}
if (ipi) {
rule->src_ip_cnt++;
rule->src_ip_valid = 1;
}
return {};
};
if (node["src_ip"]) {
if (node["src_ip"].IsSequence()) {
for (const auto &src_ip : node["src_ip"]) {
auto errata = parse_src_ip(src_ip.as<std::string>(), false);
if (!errata.is_ok()) {
return errata;
}
}
} else {
auto errata = parse_src_ip(node["src_ip"].as<std::string>(), false);
if (!errata.is_ok()) {
return errata;
}
}
}
if (node["src_ip_invert"]) {
if (node["src_ip_invert"].IsSequence()) {
for (const auto &src_ip_invert : node["src_ip_invert"]) {
auto errata = parse_src_ip(src_ip_invert.as<std::string>(), true);
if (!errata.is_ok()) {
return errata;
}
}
} else {
auto errata = parse_src_ip(node["src_ip_invert"].as<std::string>(), true);
if (!errata.is_ok()) {
return errata;
}
}
}
// Parse src_ip_category (and src_ip_category_invert)
auto parse_src_ip_category = [&](const std::string &ip_category, bool invert) -> swoc::Errata {
if (rule->src_ip_category_cnt >= ACL_FILTER_MAX_SRC_IP) {
Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Too many \"src_ip_category=\" filters");
return swoc::Errata("Defined more than {} src_ip_category filters", ACL_FILTER_MAX_SRC_IP);
}
src_ip_category_info_t *ipi = &rule->src_ip_category_array[rule->src_ip_category_cnt];
ipi->category.assign(ip_category);
if (invert) {
ipi->invert = true;
}
for (j = 0; j < rule->src_ip_category_cnt; j++) {
if (rule->src_ip_category_array[j].category == ipi->category) {
ipi->reset();
return {};
}
}
if (ipi) {
rule->src_ip_category_cnt++;
rule->src_ip_category_valid = 1;
}
return {};
};
if (node["src_ip_category"]) {
auto errata = parse_src_ip_category(node["src_ip_category"].as<std::string>(), false);
if (!errata.is_ok()) {
return errata;
}
}
if (node["src_ip_category_invert"]) {
auto errata = parse_src_ip_category(node["src_ip_category_invert"].as<std::string>(), true);
if (!errata.is_ok()) {
return errata;
}
}
// Parse in_ip (and in_ip_invert)
auto parse_in_ip = [&](const std::string &in_ip, bool invert) -> swoc::Errata {
if (rule->in_ip_cnt >= ACL_FILTER_MAX_IN_IP) {
Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Too many \"in_ip=\" filters");
return swoc::Errata("Defined more than {} in_ip filters", ACL_FILTER_MAX_IN_IP);
}
src_ip_info_t *ipi = &rule->in_ip_array[rule->in_ip_cnt];
if (invert) {
ipi->invert = true;
}
// important! use copy of argument
std::string_view arg{in_ip};
if (arg == "all") {
ipi->match_all_addresses = true;
} else if (ats_ip_range_parse(arg, ipi->start, ipi->end) != 0) {
Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Unable to parse IP value in %s", in_ip.c_str());
return swoc::Errata("Unable to parse IP value: {}", in_ip);
}
for (j = 0; j < rule->in_ip_cnt; j++) {
if (rule->in_ip_array[j].start == ipi->start && rule->in_ip_array[j].end == ipi->end) {
ipi->reset();
return {};
}
}
if (ipi) {
rule->in_ip_cnt++;
rule->in_ip_valid = 1;
}
return {};
};
if (node["in_ip"]) {
if (node["in_ip"].IsSequence()) {
for (const auto &in_ip : node["in_ip"]) {
auto errata = parse_in_ip(in_ip.as<std::string>(), false);
if (!errata.is_ok()) {
return errata;
}
}
} else {
auto errata = parse_in_ip(node["in_ip"].as<std::string>(), false);
if (!errata.is_ok()) {
return errata;
}
}
}
if (node["in_ip_invert"]) {
if (node["in_ip_invert"].IsSequence()) {
for (const auto &in_ip_invert : node["in_ip_invert"]) {
auto errata = parse_in_ip(in_ip_invert.as<std::string>(), true);
if (!errata.is_ok()) {
return errata;
}
}
} else {
auto errata = parse_in_ip(node["in_ip_invert"].as<std::string>(), true);
if (!errata.is_ok()) {
return errata;
}
}
}
// Parse action
if (node["action"]) {
if (node["action"].IsSequence()) {
Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Only one action is allowed per remap ACL");
return swoc::Errata("Only one action is allowed per remap ACL");
}
std::string action_str = node["action"].as<std::string>();
if (behavior_policy == ACLBehaviorPolicy::ACL_BEHAVIOR_MODERN) {
// With the new matching policy, we don't allow the legacy "allow" and
// "deny" actions. Users must transition to either add_allow/add_deny or
// set_allow/set_deny.
if (is_inkeylist(action_str.c_str(), "allow", "deny", nullptr)) {
Dbg(dbg_ctl_url_rewrite,
R"([validate_filter_args] "allow" and "deny" are no longer valid. Use add_allow/add_deny or set_allow/set_deny: "%s"")",
action_str.c_str());
return swoc::Errata("\"allow\" and \"deny\" are no longer valid. Use add_allow/add_deny or set_allow/set_deny: {}",
action_str.c_str());
}
}
if (is_inkeylist(action_str.c_str(), "add_allow", "add_deny", nullptr)) {
rule->add_flag = 1;
} else {
rule->add_flag = 0;
}
// Remove "deny" from this list when MATCH_ON_IP_AND_METHOD is removed in 11.x.
if (is_inkeylist(action_str.c_str(), "0", "off", "deny", "set_deny", "add_deny", "disable", nullptr)) {
rule->allow_flag = 0;
// Remove "allow" from this list when MATCH_ON_IP_AND_METHOD is removed in 11.x.
} else if (is_inkeylist(action_str.c_str(), "1", "on", "allow", "set_allow", "add_allow", "enable", nullptr)) {
rule->allow_flag = 1;
} else {
Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Unknown argument \"%s\"", action_str.c_str());
return swoc::Errata("Unknown action: {}", action_str);
}
}
// Parse internal
if (node["internal"] && node["internal"].as<bool>()) {
rule->internal = 1;
}
if (dbg_ctl_url_rewrite.on()) {
rule->print();
}
free_rule.release();
return {};
}
swoc::Errata
parse_map_referer(const YAML::Node &node, url_mapping *url_mapping)
{
if (!node || !node.IsMap()) {
return swoc::Errata("redirect must be a map");
}
if (!node["url"]) {
return swoc::Errata("Missing 'url' field in redirect map-with-referer");
}
std::string url = node["url"].as<std::string>();
url_mapping->filter_redirect_url = ats_strdup(url.c_str());
if (!strcasecmp(url.c_str(), "<default>") || !strcasecmp(url.c_str(), "default") ||
!strcasecmp(url.c_str(), "<default_redirect_url>") || !strcasecmp(url.c_str(), "default_redirect_url")) {
url_mapping->default_redirect_url = true;
}
url_mapping->redir_chunk_list = redirect_tag_str::parse_format_redirect_url(ats_strdup(url.c_str()));
if (!node["regex"] || !node["regex"].IsSequence()) {
return swoc::Errata("'regex' field must be sequence");
}
referer_info *ri;
for (const auto &rule : node["regex"]) {
char refinfo_error_buf[1024];
bool refinfo_error = false;
std::string regex = rule.as<std::string>();
ri = new referer_info(regex.c_str(), &refinfo_error, refinfo_error_buf, sizeof(refinfo_error_buf));
if (refinfo_error) {
delete ri;
ri = nullptr;
return swoc::Errata("Incorrect Referer regular expression \"{}\" - {}", regex.c_str(), refinfo_error_buf);
}
if (ri && ri->negative) {
if (ri->any) {
url_mapping->optional_referer = true; /* referer header is optional */
delete ri;
ri = nullptr;
} else {
url_mapping->negative_referer = true; /* we have negative referer in list */
}
}
if (ri) {
ri->next = url_mapping->referer_list;
url_mapping->referer_list = ri;
}
}
return {};
}
swoc::Errata
parse_yaml_plugins(const YAML::Node &node, url_mapping *url_mapping, BUILD_TABLE_INFO *bti)
{
char *err;
if (!node["name"]) {
return swoc::Errata("plugin missing 'name' field");
}
std::string plugin_name = node["name"].as<std::string>();
Dbg(dbg_ctl_remap_yaml, "Loading plugin: %s", plugin_name.c_str());
std::vector<std::string> plugin_params;
/* Prepare remap plugin parameters from the config */
if ((err = url_mapping->fromURL.string_get(nullptr)) == nullptr) {
return swoc::Errata("Can't load fromURL from URL class");
}
plugin_params.emplace_back(err);
ats_free(err);
if ((err = url_mapping->toURL.string_get(nullptr)) == nullptr) {
return swoc::Errata("Can't load toURL from URL class");
}
plugin_params.emplace_back(err);
ats_free(err);
// Add plugin parameters
if (node["params"] && node["params"].IsSequence()) {
for (const auto &param : node["params"]) {
plugin_params.push_back(param.as<std::string>());
Dbg(dbg_ctl_remap_yaml, " Plugin param: %s", plugin_params.back().c_str());
}
}
std::vector<char *> pargv;
for (auto &param : plugin_params) {
pargv.push_back(param.data());
}
RemapPluginInst *pi = nullptr;
std::string error;
{
uint32_t elevate_access = 0;
elevate_access = RecGetRecordInt("proxy.config.plugin.load_elevated").value_or(0);
ElevateAccess access(elevate_access ? ElevateAccess::FILE_PRIVILEGE : 0);
pi = bti->rewrite->pluginFactory.getRemapPlugin(swoc::file::path(plugin_name), pargv.size(), pargv.data(), error,
isPluginDynamicReloadEnabled());
} // done elevating access
if (nullptr == pi) {
return swoc::Errata("failed to instantiate plugin ({}) to remap rule: {}", plugin_name.c_str(), error.c_str());
}
url_mapping->add_plugin_instance(pi);
return {};
}
swoc::Errata
parse_yaml_filter_directive(const YAML::Node &node, BUILD_TABLE_INFO *bti)
{
acl_filter_rule *rp;
// Check for activate_filters directive
if (node["activate_filter"]) {
std::string filter_name = node["activate_filter"].as<std::string>();
// Check if for ip_allow filter
if (filter_name == "ip_allow") {
bti->ip_allow_check_enabled_p = true;
return {};
}
if ((rp = acl_filter_rule::find_byname(bti->rules_list, filter_name.c_str())) == nullptr) {
Dbg(dbg_ctl_url_rewrite, "(Undefined filter '%s' in activate_filter directive)", filter_name.c_str());
return swoc::Errata("(Undefined filter '{}' in activate_filter directive)", filter_name.c_str());
}
acl_filter_rule::requeue_in_active_list(&bti->rules_list, rp);
return {};
}
// Check for deactivate_filters directive
if (node["deactivate_filter"]) {
std::string filter_name = node["deactivate_filter"].as<std::string>();
// Check if for ip_allow filter
if (strcmp(filter_name.c_str(), "ip_allow") == 0) {
bti->ip_allow_check_enabled_p = false;
return {};
}
if ((rp = acl_filter_rule::find_byname(bti->rules_list, filter_name.c_str())) == nullptr) {
Dbg(dbg_ctl_url_rewrite, "(Undefined filter '%s' in deactivate_filter directive)", filter_name.c_str());
return swoc::Errata("(Undefined filter '{}' in deactivate_filter directive)", filter_name.c_str());
}
acl_filter_rule::requeue_in_passive_list(&bti->rules_list, rp);
return {};
}
// Check for delete_filters directive
if (node["delete_filter"]) {
std::string filter_name = node["delete_filter"].as<std::string>();
acl_filter_rule::delete_byname(&bti->rules_list, filter_name.c_str());
return {};
}
// Check for define_filter directive
if (node["define_filter"]) {
return parse_yaml_define_directive(node["define_filter"], bti);
}
return swoc::Errata("Not a filter directive");
}
swoc::Errata
parse_yaml_define_directive(const YAML::Node &node, BUILD_TABLE_INFO *bti)
{
bool flg;
acl_filter_rule *rp;
swoc::Errata errata;
if (!node || !node.IsMap()) {
return swoc::Errata("named filters must be a map");
}
// When iterating over a YAML map, each element is a key-value pair
// We expect a single-entry map here
auto it = node.begin();
std::string filter_name = it->first.as<std::string>();
const YAML::Node filter_spec = it->second;
flg = ((rp = acl_filter_rule::find_byname(bti->rules_list, filter_name.c_str())) == nullptr) ? true : false;
// coverity[alloc_arg]
if ((errata = remap_validate_yaml_filter_args(&rp, filter_spec, bti->behavior_policy)).is_ok() && rp) {
if (flg) { // new filter - add to list
acl_filter_rule **rpp = nullptr;
Dbg(dbg_ctl_url_rewrite, "[parse_directive] new rule \"%s\" was created", filter_name.c_str());
for (rpp = &bti->rules_list; *rpp; rpp = &((*rpp)->next)) {
;
}
(*rpp = rp)->name(filter_name.c_str());
}
Dbg(dbg_ctl_url_rewrite, "[parse_directive] %zu argument(s) were added to rule \"%s\"", filter_spec.size(),
filter_name.c_str());
rp->add_node(filter_spec); // store string arguments for future processing
}
return errata;
}
swoc::Errata
process_yaml_filter_opt(url_mapping *mp, const YAML::Node &node, const BUILD_TABLE_INFO *bti)
{
acl_filter_rule *rp, **rpp;
swoc::Errata errata;
if (unlikely(!mp || !bti)) {
Dbg(dbg_ctl_url_rewrite, "[process_yaml_filter_opt] Invalid argument(s)");
return swoc::Errata("[process_yaml_filter_opt] Invalid argument(s)");
}
// ACLs are processed in this order:
// 1. A remap.config ACL line for an individual remap rule.
// 2. All named ACLs in remap.config.
// 3. Rules as specified in ip_allow.yaml.
if (node["acl_filter"]) {
Dbg(dbg_ctl_url_rewrite, "[process_yaml_filter_opt] Add per remap filter");
for (rpp = &mp->filter; *rpp; rpp = &((*rpp)->next)) {
;
}
errata = remap_validate_yaml_filter_args(rpp, node["acl_filter"], bti->behavior_policy);
}
for (rp = bti->rules_list; rp; rp = rp->next) {
for (rpp = &mp->filter; *rpp; rpp = &((*rpp)->next)) {
;
}
if (rp->active_queue_flag) {
Dbg(dbg_ctl_url_rewrite, "[process_yaml_filter_opt] Add active main filter \"%s\"",
rp->filter_name ? rp->filter_name : "<nullptr>");
for (rpp = &mp->filter; *rpp; rpp = &((*rpp)->next)) {
;
}
errata = remap_validate_yaml_filter_args(rpp, rp->node, bti->behavior_policy);
if (!errata.is_ok()) {
break;
}
if (auto rule = *rpp; rule) {
// If no IP addresses are listed, treat that like `@src_ip=all`.
if (rule->src_ip_valid == 0 && rule->src_ip_cnt == 0) {
src_ip_info_t *ipi = &rule->src_ip_array[rule->src_ip_cnt];
ipi->match_all_addresses = true;
rule->src_ip_cnt++;
rule->src_ip_valid = 1;
}
}
}
}
// Set the ip allow flag for this rule to the current ip allow flag state
mp->ip_allow_check_enabled_p = bti->ip_allow_check_enabled_p;
return errata;
}
swoc::Errata
parse_yaml_remap_fragment(const char *path, BUILD_TABLE_INFO *bti)
{
// We need to create a new bti so that we don't clobber any state in the parent parse, but we want
// to keep the ACL rules from the parent because ACLs must be global across the full set of config
// files.
BUILD_TABLE_INFO nbti;
bool success;
if (access(path, R_OK) == -1) {
return swoc::Errata("{}: {}", path, strerror(errno));
}
nbti.rules_list = bti->rules_list;
nbti.rewrite = bti->rewrite;
Dbg(dbg_ctl_url_rewrite, "[%s] including remap configuration from %s", __func__, path);
success = remap_parse_yaml_bti(path, &nbti);
// The sub-parse might have updated the rules list, so push it up to the parent parse.
bti->rules_list = nbti.rules_list;
if (success) {
// register the included file with the management subsystem so that we can correctly
// reload them when they change
load_remap_file_cb(ts::filename::REMAP_YAML, path);
} else {
return swoc::Errata("failed to parse included file {}", path);
}
return {};
}
swoc::Errata
parse_yaml_include_directive(const std::string &include_path, BUILD_TABLE_INFO *bti)
{
ats_scoped_str path;
swoc::Errata errata;
// The included path is relative to SYSCONFDIR
path = RecConfigReadConfigPath(nullptr, include_path.c_str());
if (ink_file_is_directory(path)) {
struct dirent **entrylist;
int n_entries;
n_entries = scandir(path, &entrylist, nullptr, alphasort);
if (n_entries == -1) {
return swoc::Errata("failed to open {}: {}", path.get(), strerror(errno));
}
for (int j = 0; j < n_entries; ++j) {
ats_scoped_str subpath;
if (isdot(entrylist[j]->d_name) || isdotdot(entrylist[j]->d_name)) {
continue;
}
subpath = Layout::relative_to(path.get(), entrylist[j]->d_name);
if (ink_file_is_directory(subpath)) {
continue;
}
errata = parse_yaml_remap_fragment(subpath, bti);
if (!errata.is_ok()) {
break;
}
}
free_directory_list(n_entries, entrylist);
} else {
errata = parse_yaml_remap_fragment(path, bti);
}
return errata;
}
swoc::Errata
parse_yaml_remap_rule(const YAML::Node &node, BUILD_TABLE_INFO *bti)
{
std::string errStr;
std::string_view fromScheme{}, toScheme{};
std::string_view fromHost{}, toHost{};
std::string_view fromUrl{}, toUrl{};
std::string_view fromPath;
char *fromHost_lower = nullptr;
char *fromHost_lower_ptr = nullptr;
char fromHost_lower_buf[1024];
mapping_type maptype;
bool is_cur_mapping_regex;
const char *type_id_str;
swoc::Errata errata;
const char *valid_scheme = nullptr;
std::unique_ptr<url_mapping> new_mapping;
std::unique_ptr<UrlRewrite::RegexMapping> reg_map;
if (!node || !node.IsMap()) {
return swoc::Errata("remap rule must be a map");
}
// Parse for include directive first
if (node["include"]) {
return parse_yaml_include_directive(node["include"].as<std::string>(), bti);
}
// Parse for filter directives (activate/deactivate/delete/define)
if (node["activate_filter"] || node["deactivate_filter"] || node["delete_filter"] || node["define_filter"]) {
return parse_yaml_filter_directive(node, bti);
}
// Parse rule type
if (!node["type"]) {
return swoc::Errata("remap rule missing 'type' field");
}
std::string type_str = node["type"].as<std::string>();
is_cur_mapping_regex = (strncasecmp("regex_", type_str.c_str(), 6) == 0);
type_id_str = is_cur_mapping_regex ? (type_str.c_str() + 6) : type_str.c_str();
// Check to see whether is a reverse or forward mapping
maptype = get_mapping_type(type_id_str, bti);
if (maptype == mapping_type::NONE) {
return swoc::Errata("unknown mapping type: {}", type_str);
}
new_mapping = std::make_unique<url_mapping>();
// apply filter rules if we have to
errata = process_yaml_filter_opt(new_mapping.get(), node, bti);
if (!errata.is_ok()) {
swoc::bwprint(errStr, "Failed to process filter: {}", errata);
goto MAP_ERROR;
}
// update sticky flag
bti->accept_check_p = bti->accept_check_p && bti->ip_allow_check_enabled_p;
new_mapping->map_id = 0;
if (node["mapid"]) {
new_mapping->map_id = node["mapid"].as<unsigned int>();
}
// Parse from URL
if (!node["from"]) {
errStr = "remap rule missing 'from' field";
goto MAP_ERROR;
}
errata = parse_yaml_url(node["from"], new_mapping->fromURL, true, fromUrl);
if (!errata.is_ok()) {
swoc::bwprint(errStr, "malformed From URL: {}", errata);
goto MAP_ERROR;
}
// Parse to URL
if (!node["to"]) {
errStr = "remap rule missing 'to' field";
goto MAP_ERROR;
}
errata = parse_yaml_url(node["to"], new_mapping->toURL, false, toUrl);
if (!errata.is_ok()) {
swoc::bwprint(errStr, "malformed To URL: {}", errata);
goto MAP_ERROR;
}
// Check if valid schemes
fromScheme = new_mapping->fromURL.scheme_get();
toScheme = new_mapping->toURL.scheme_get();
if (fromScheme.empty()) {
new_mapping->fromURL.scheme_set(std::string_view{URL_SCHEME_HTTP});
new_mapping->wildcard_from_scheme = true;
fromScheme = new_mapping->fromURL.scheme_get();
}
valid_scheme = is_valid_scheme(fromScheme, toScheme);
if (valid_scheme != nullptr) {
errStr = valid_scheme;
goto MAP_ERROR;
}
// Check if map_with_referer is used
if (node["redirect"] && maptype == mapping_type::FORWARD_MAP_REFERER) {
errata = parse_map_referer(node["redirect"], new_mapping.get());
if (!errata.is_ok()) {
swoc::bwprint(errStr, "invalid map_with_referer: {}", errata);
goto MAP_ERROR;
}
}
// Check to see the fromHost remapping is a relative one
fromHost = new_mapping->fromURL.host_get();
if (fromHost.empty()) {
if (maptype == mapping_type::FORWARD_MAP || maptype == mapping_type::FORWARD_MAP_REFERER ||
maptype == mapping_type::FORWARD_MAP_WITH_RECV_PORT) {
fromPath = new_mapping->fromURL.path_get();
if ((fromPath.empty() || fromPath[0] != '/') && (fromUrl.empty() || fromUrl[0] != '/')) {
errStr = "relative remappings must begin with a /";
goto MAP_ERROR;
} else {
fromHost = ""sv;
}
} else {
errStr = "remap source in reverse mappings requires a hostname";
goto MAP_ERROR;
}
}
toHost = new_mapping->toURL.host_get();
if (toHost.empty()) {
errStr = "The remap destinations require a hostname";
goto MAP_ERROR;
}
// Get rid of trailing slashes since they interfere
// with our ability to send redirects
// You might be tempted to remove these lines but the new
// optimized header system will introduce problems. You
// might get two slashes occasionally instead of one because
// the rest of the system assumes that trailing slashes have
// been removed.
if (unlikely(fromHost.length() >= sizeof(fromHost_lower_buf))) {
fromHost_lower = (fromHost_lower_ptr = static_cast<char *>(ats_malloc(fromHost.length() + 1)));
} else {
fromHost_lower = &fromHost_lower_buf[0];
}
// Canonicalize the hostname by making it lower case
memcpy(fromHost_lower, fromHost.data(), fromHost.length());
fromHost_lower[fromHost.length()] = 0;
LowerCaseStr(fromHost_lower);
// set the normalized string so nobody else has to normalize this
new_mapping->fromURL.host_set({fromHost_lower, fromHost.length()});
if (is_cur_mapping_regex) {
reg_map = std::make_unique<UrlRewrite::RegexMapping>();
if (!process_regex_mapping_config(fromHost_lower, new_mapping.get(), reg_map.get())) {
errStr = "could not process regex mapping config line";
goto MAP_ERROR;
}
Dbg(dbg_ctl_url_rewrite, "Configured regex rule for host [%s]", fromHost_lower);
}
// If a TS receives a request on a port which is set to tunnel mode
// (ie, blind forwarding) and a client connects directly to the TS,
// then the TS will use its IPv4 address and remap rules given
// to send the request to its proper destination.
// See HttpTransact::HandleBlindTunnel().
// Therefore, for a remap with "type: map" and "scheme: tunnel",
// we also needs to convert hostname to its IPv4 addr
// and gives a new remap rule with the IPv4 addr.
if ((maptype == mapping_type::FORWARD_MAP || maptype == mapping_type::FORWARD_MAP_REFERER ||
maptype == mapping_type::FORWARD_MAP_WITH_RECV_PORT) &&
fromScheme == std::string_view{URL_SCHEME_TUNNEL} && (fromHost_lower[0] < '0' || fromHost_lower[0] > '9')) {
addrinfo *ai_records; // returned records.
ip_text_buffer ipb; // buffer for address string conversion.
if (0 == getaddrinfo(fromHost_lower, nullptr, nullptr, &ai_records)) {
for (addrinfo *ai_spot = ai_records; ai_spot; ai_spot = ai_spot->ai_next) {
if (ats_is_ip(ai_spot->ai_addr) && !ats_is_ip_any(ai_spot->ai_addr) && ai_spot->ai_protocol == IPPROTO_TCP) {
url_mapping *u_mapping;
ats_ip_ntop(ai_spot->ai_addr, ipb, sizeof ipb);
u_mapping = new url_mapping;
u_mapping->fromURL.create(nullptr);
u_mapping->fromURL.copy(&new_mapping->fromURL);
u_mapping->fromURL.host_set({ipb});
u_mapping->toURL.create(nullptr);
u_mapping->toURL.copy(&new_mapping->toURL);
if (!bti->rewrite->InsertForwardMapping(maptype, u_mapping, ipb)) {
errStr = "unable to add mapping rule to lookup table";
freeaddrinfo(ai_records);
goto MAP_ERROR;
}
}
}
freeaddrinfo(ai_records);
}
}
// check for a 'strategy' and if wire it up if one exists.
if (node["strategy"] && (maptype == mapping_type::FORWARD_MAP || maptype == mapping_type::FORWARD_MAP_REFERER ||
maptype == mapping_type::FORWARD_MAP_WITH_RECV_PORT)) {
std::string strategy_name = node["strategy"].as<std::string>();
new_mapping->strategy = bti->rewrite->strategyFactory->strategyInstance(strategy_name.c_str());
if (new_mapping->strategy == nullptr) {
errStr = "missing 'strategy' name argument, unable to add mapping rule";
goto MAP_ERROR;
}
Dbg(dbg_ctl_url_rewrite, "mapped the 'strategy' named %s", strategy_name.c_str());
}
// Check "remap" plugin options and load .so object
if (node["plugins"] && (maptype == mapping_type::FORWARD_MAP || maptype == mapping_type::FORWARD_MAP_REFERER ||
maptype == mapping_type::FORWARD_MAP_WITH_RECV_PORT)) {
if (!node["plugins"] || !node["plugins"].IsSequence()) {
errStr = "plugins must be a sequence";
goto MAP_ERROR;
}
for (const auto &plugin : node["plugins"]) {
errata = parse_yaml_plugins(plugin, new_mapping.get(), bti);
if (!errata.is_ok()) {
swoc::bwprint(errStr, "{}", errata);
goto MAP_ERROR;
}
}
}
// Now add the mapping to appropriate container
if (!bti->rewrite->InsertMapping(maptype, new_mapping.release(), reg_map.release(), fromHost_lower, is_cur_mapping_regex)) {
errStr = "unable to add mapping rule to lookup table";
goto MAP_ERROR;
}
ats_free_null(fromHost_lower_ptr);
Dbg(dbg_ctl_remap_yaml, "Successfully added mapping rule");
return {};
// Deal with error / warning scenarios
MAP_ERROR:
Error("%s", errStr.c_str());
return swoc::Errata(errStr);
}
bool
remap_parse_yaml_bti(const char *path, BUILD_TABLE_INFO *bti)
{
try {
Dbg(dbg_ctl_remap_yaml, "Parsing YAML config file: %s", path);
YAML::Node config = YAML::LoadFile(path);
if (config.IsNull()) {
Dbg(dbg_ctl_remap_yaml, "Empty YAML config file");
return true; // a missing file is ok - treat as empty, no rules.
}
Dbg(dbg_ctl_url_rewrite, "[BuildTable] UrlRewrite::BuildTable()");
ACLBehaviorPolicy behavior_policy = ACLBehaviorPolicy::ACL_BEHAVIOR_LEGACY;
if (!UrlRewrite::get_acl_behavior_policy(behavior_policy)) {
Warning("Failed to get ACL matching policy.");
return false;
}
bti->behavior_policy = behavior_policy;
// Parse global filters section (optional)
if (config["acl_filters"] && config["acl_filters"].IsMap()) {
for (const auto &filter_def : config["acl_filters"]) {
auto errata = parse_yaml_define_directive(filter_def, bti);
if (!errata.is_ok()) {
Error("Failed to parse acl_filters section");
return false;
}
}
}
if (config["remap"].IsNull() || !config["remap"].IsSequence()) {
Error("Expected toplevel 'remap' key to be a sequence");
return false;
}
// Parse each remap rule
for (const auto &rule : config["remap"]) {
// Reset bti state for each rule (but keep rules_list for named filters)
bti->reset();
auto errata = parse_yaml_remap_rule(rule, bti);
if (!errata.is_ok()) {
Error("Failed to parse remap rule");
return false;
}
}
IpAllow::enableAcceptCheck(bti->accept_check_p);
Dbg(dbg_ctl_remap_yaml, "Successfully parsed remap.yaml config");
return true;
} catch (YAML::Exception &ex) {
Error("YAML parsing error in %s: %s", path, ex.what());
} catch (std::exception &ex) {
Error("Exception parsing YAML config %s: %s", path, ex.what());
}
return false;
}
bool
remap_parse_yaml(const char *path, UrlRewrite *rewrite)
{
BUILD_TABLE_INFO bti;
/* If this happens to be a config reload, the list of loaded remap plugins is non-empty, and we
* can signal all these plugins that a reload has begun. */
rewrite->pluginFactory.indicatePreReload();
bti.rewrite = rewrite;
bool status = remap_parse_yaml_bti(path, &bti);
/* Now after we parsed the configuration and (re)loaded plugins and plugin instances
* accordingly notify all plugins that we are done */
rewrite->pluginFactory.indicatePostReload(status);
bti.clear_acl_rules_list();
return status;
}