diff --git a/include/config/remap.h b/include/config/remap.h new file mode 100644 index 00000000000..9c6cb1055d3 --- /dev/null +++ b/include/config/remap.h @@ -0,0 +1,92 @@ +/** @file + + Shared remap configuration parsing and marshalling. + + @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. +*/ + +#pragma once + +#include +#include + +#include + +#include "config/config_result.h" + +namespace config +{ + +using RemapConfig = YAML::Node; + +/** + * Parser for remap.yaml / remap.config configuration files. + * + * The parser normalizes the legacy remap.config format into the YAML tree shape + * consumed by the remap.yaml loader so both traffic_server and traffic_ctl can + * share one conversion path. + */ +class RemapParser +{ +public: + /** + * Parse a remap configuration file. + * + * The format is auto-detected based on the filename extension and content. + * + * @param[in] filename Path to the configuration file. + * @return ConfigResult containing the parsed configuration tree or errors. + */ + ConfigResult parse(std::string const &filename); + + /** + * Parse remap configuration content from a string. + * + * The format is auto-detected from @a filename and @a content. + * + * @param[in] content The configuration content to parse. + * @param[in] filename Synthetic filename used for format detection. + * @return ConfigResult containing the parsed configuration tree or errors. + */ + ConfigResult parse_content(std::string_view content, std::string const &filename = "remap.yaml"); + +private: + enum class Format { YAML, Legacy }; + + Format detect_format(std::string_view content, std::string const &filename) const; + ConfigResult parse_yaml(std::string_view content) const; + ConfigResult parse_legacy(std::string_view content) const; +}; + +/** + * Marshaller for remap configuration. + */ +class RemapMarshaller +{ +public: + /** + * Serialize configuration to YAML format. + * + * @param[in] config The configuration tree to serialize. + * @return YAML string representation. + */ + std::string to_yaml(RemapConfig const &config); +}; + +} // namespace config diff --git a/include/proxy/http/remap/RemapConfig.h b/include/proxy/http/remap/RemapConfig.h index 129d619f26f..2babcc71e5e 100644 --- a/include/proxy/http/remap/RemapConfig.h +++ b/include/proxy/http/remap/RemapConfig.h @@ -27,25 +27,6 @@ class UrlRewrite; -#define BUILD_TABLE_MAX_ARGS 2048 - -// Remap inline options -#define REMAP_OPTFLG_MAP_WITH_REFERER 0x0001u /* "map_with_referer" option */ -#define REMAP_OPTFLG_PLUGIN 0x0002u /* "plugin=" option (per remap plugin) */ -#define REMAP_OPTFLG_PPARAM 0x0004u /* "pparam=" option (per remap plugin option) */ -#define REMAP_OPTFLG_METHOD 0x0008u /* "method=" option (used for ACL filtering) */ -#define REMAP_OPTFLG_SRC_IP 0x0010u /* "src_ip=" option (used for ACL filtering) */ -#define REMAP_OPTFLG_SRC_IP_CATEGORY 0x0020u /* "src_ip_category=" option (used for ACL filtering) */ -#define REMAP_OPTFLG_ACTION 0x0040u /* "action=" option (used for ACL filtering) */ -#define REMAP_OPTFLG_INTERNAL 0x0080u /* only allow internal requests to hit this remap */ -#define REMAP_OPTFLG_IN_IP 0x0100u /* "in_ip=" option (used for ACL filtering)*/ -#define REMAP_OPTFLG_STRATEGY 0x0200u /* "strategy=" the name of the nexthop selection strategy */ -#define REMAP_OPTFLG_VOLUME 0x0400u /* "volume=" cache volume override */ -#define REMAP_OPTFLG_MAP_ID 0x0800u /* associate a map ID with this rule */ -#define REMAP_OPTFLG_INVERT 0x80000000u /* "invert" the rule (for src_ip and src_ip_category at least) */ -#define REMAP_OPTFLG_ALL_FILTERS \ - (REMAP_OPTFLG_METHOD | REMAP_OPTFLG_SRC_IP | REMAP_OPTFLG_SRC_IP_CATEGORY | REMAP_OPTFLG_ACTION | REMAP_OPTFLG_INTERNAL) - enum class ACLBehaviorPolicy { ACL_BEHAVIOR_LEGACY = 0, ACL_BEHAVIOR_MODERN, @@ -55,12 +36,6 @@ struct BUILD_TABLE_INFO { BUILD_TABLE_INFO(); ~BUILD_TABLE_INFO(); - unsigned long remap_optflg = 0; - int paramc = 0; - int argc = 0; - char *paramv[BUILD_TABLE_MAX_ARGS]; - char *argv[BUILD_TABLE_MAX_ARGS]; - ACLBehaviorPolicy behavior_policy{ACLBehaviorPolicy::ACL_BEHAVIOR_LEGACY}; // Default 0. bool ip_allow_check_enabled_p = true; bool accept_check_p = true; @@ -79,17 +54,6 @@ struct BUILD_TABLE_INFO { BUILD_TABLE_INFO &operator=(const BUILD_TABLE_INFO &) = delete; // disabled }; -const char *remap_parse_directive(BUILD_TABLE_INFO *bti, char *errbuf, size_t errbufsize); -bool remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti); - -const char *remap_validate_filter_args(acl_filter_rule **rule_pp, const char *const *argv, int argc, char *errStrBuf, - size_t errStrBufSize, ACLBehaviorPolicy behavior_policy); - -unsigned long remap_check_option(const char *const *argv, int argc, unsigned long findmode = 0, int *_ret_idx = nullptr, - const char **argptr = nullptr); - -bool remap_parse_config(const char *path, UrlRewrite *rewrite); - using load_remap_file_func = void (*)(const char *, const char *); extern load_remap_file_func load_remap_file_cb; diff --git a/include/proxy/http/remap/UrlRewrite.h b/include/proxy/http/remap/UrlRewrite.h index 2f4be91b479..498897c278a 100644 --- a/include/proxy/http/remap/UrlRewrite.h +++ b/include/proxy/http/remap/UrlRewrite.h @@ -265,6 +265,6 @@ class UrlRewrite : public RefCountObjInHeap void url_rewrite_remap_request(const UrlMappingContainer &mapping_container, URL *request_url, int scheme = -1); -mapping_type get_mapping_type(const char *type_str, BUILD_TABLE_INFO *bti); +mapping_type get_mapping_type(const char *type_str); bool process_regex_mapping_config(const char *from_host_lower, url_mapping *new_mapping, UrlRewrite::RegexMapping *reg_map); diff --git a/src/config/CMakeLists.txt b/src/config/CMakeLists.txt index dd761eeb808..d275a4dc659 100644 --- a/src/config/CMakeLists.txt +++ b/src/config/CMakeLists.txt @@ -17,10 +17,10 @@ set(CONFIG_PUBLIC_HEADERS ${PROJECT_SOURCE_DIR}/include/config/config_result.h ${PROJECT_SOURCE_DIR}/include/config/ssl_multicert.h - ${PROJECT_SOURCE_DIR}/include/config/storage.h + ${PROJECT_SOURCE_DIR}/include/config/storage.h ${PROJECT_SOURCE_DIR}/include/config/remap.h ) -add_library(tsconfig ssl_multicert.cc storage.cc) +add_library(tsconfig ssl_multicert.cc storage.cc remap.cc) add_library(ts::config ALIAS tsconfig) @@ -48,7 +48,7 @@ endif() clang_tidy_check(tsconfig) if(BUILD_TESTING) - add_executable(test_tsconfig unit_tests/test_ssl_multicert.cc unit_tests/test_storage.cc) + add_executable(test_tsconfig unit_tests/test_ssl_multicert.cc unit_tests/test_storage.cc unit_tests/test_remap.cc) target_link_libraries(test_tsconfig PRIVATE tsconfig ts::tscore Catch2::Catch2WithMain) diff --git a/src/config/remap.cc b/src/config/remap.cc new file mode 100644 index 00000000000..6e8c65035f7 --- /dev/null +++ b/src/config/remap.cc @@ -0,0 +1,631 @@ +/** @file + + Shared remap configuration parsing and marshalling 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 "config/remap.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "swoc/swoc_file.h" +#include "tscore/MatcherUtils.h" +#include "tscore/Tokenizer.h" +#include "tsutil/ts_diag_levels.h" + +namespace +{ + +constexpr swoc::Errata::Severity ERRATA_NOTE_SEV{static_cast(DL_Note)}; +constexpr swoc::Errata::Severity ERRATA_ERROR_SEV{static_cast(DL_Error)}; + +struct ParsedOption { + std::string key; + std::string value; + bool has_value = true; + bool invert = false; +}; + +YAML::Node +make_empty_remap_config() +{ + YAML::Node root{YAML::NodeType::Map}; + root["remap"] = YAML::Node{YAML::NodeType::Sequence}; + return root; +} + +YAML::Node +ensure_sequence(YAML::Node node, char const *key) +{ + if (!node[key]) { + node[key] = YAML::Node{YAML::NodeType::Sequence}; + } + return node[key]; +} + +void +append_scalar_or_promote(YAML::Node &node, char const *key, std::string const &value) +{ + if (!node[key]) { + node[key] = value; + return; + } + + if (node[key].IsSequence()) { + node[key].push_back(value); + return; + } + + YAML::Node seq{YAML::NodeType::Sequence}; + seq.push_back(node[key].as()); + seq.push_back(value); + node[key] = seq; +} + +std::string +trim_ascii(std::string_view text) +{ + size_t start = 0; + size_t end = text.size(); + + while (start < end && std::isspace(static_cast(text[start]))) { + ++start; + } + while (end > start && std::isspace(static_cast(text[end - 1]))) { + --end; + } + + return std::string{text.substr(start, end - start)}; +} + +bool +is_define_filter_directive(std::string_view directive) +{ + return directive == ".definefilter" || directive == ".deffilter" || directive == ".defflt"; +} + +bool +is_delete_filter_directive(std::string_view directive) +{ + return directive == ".deletefilter" || directive == ".delfilter" || directive == ".delflt"; +} + +bool +is_activate_filter_directive(std::string_view directive) +{ + return directive == ".usefilter" || directive == ".activefilter" || directive == ".activatefilter" || directive == ".useflt"; +} + +bool +is_deactivate_filter_directive(std::string_view directive) +{ + return directive == ".unusefilter" || directive == ".deactivatefilter" || directive == ".unactivefilter" || + directive == ".deuseflt" || directive == ".unuseflt"; +} + +std::string_view +remap_type_base(std::string_view type) +{ + return type.starts_with("regex_") ? type.substr(6) : type; +} + +std::optional +parse_option(std::string_view raw) +{ + if (raw == "internal") { + return ParsedOption{"internal", "", false, false}; + } + + size_t pos = raw.find('='); + if (pos == std::string_view::npos) { + return std::nullopt; + } + + ParsedOption option; + option.key = std::string{raw.substr(0, pos)}; + option.value = std::string{raw.substr(pos + 1)}; + option.has_value = true; + + if ((option.key == "src_ip" || option.key == "src_ip_category" || option.key == "in_ip") && !option.value.empty() && + option.value.front() == '~') { + option.invert = true; + option.value.erase(option.value.begin()); + } + + return option; +} + +swoc::Errata +apply_acl_option(YAML::Node &filter, ParsedOption const &option) +{ + swoc::Errata errata; + + auto require_value = [&](char const *label) -> bool { + if (option.value.empty()) { + errata.note(ERRATA_ERROR_SEV, "empty argument in @{}=", label); + return false; + } + return true; + }; + + if (option.key == "method") { + if (require_value("method")) { + append_scalar_or_promote(filter, "method", option.value); + } + return errata; + } + + if (option.key == "src_ip") { + if (require_value("src_ip")) { + append_scalar_or_promote(filter, option.invert ? "src_ip_invert" : "src_ip", option.value); + } + return errata; + } + + if (option.key == "src_ip_category") { + if (require_value("src_ip_category")) { + append_scalar_or_promote(filter, option.invert ? "src_ip_category_invert" : "src_ip_category", option.value); + } + return errata; + } + + if (option.key == "in_ip") { + if (require_value("in_ip")) { + append_scalar_or_promote(filter, option.invert ? "in_ip_invert" : "in_ip", option.value); + } + return errata; + } + + if (option.key == "action") { + if (!require_value("action")) { + return errata; + } + if (filter["action"]) { + errata.note(ERRATA_ERROR_SEV, "only one @action= is allowed per remap ACL"); + } else { + filter["action"] = option.value; + } + return errata; + } + + if (option.key == "internal") { + filter["internal"] = true; + return errata; + } + + errata.note(ERRATA_ERROR_SEV, "unsupported ACL option @{}", option.key); + return errata; +} + +swoc::Errata +parse_volume_value(std::string_view raw, YAML::Node target) +{ + swoc::Errata errata; + + if (raw.empty()) { + errata.note(ERRATA_ERROR_SEV, "empty @volume= directive"); + return errata; + } + + YAML::Node seq{YAML::NodeType::Sequence}; + size_t start = 0; + + while (start <= raw.size()) { + size_t comma = raw.find(',', start); + auto token = raw.substr(start, comma == std::string_view::npos ? raw.size() - start : comma - start); + + std::string trimmed = trim_ascii(token); + if (trimmed.empty()) { + errata.note(ERRATA_ERROR_SEV, "invalid @volume={} (expected comma-separated numbers 1-255)", std::string{raw}); + return errata; + } + + try { + size_t consumed = 0; + unsigned long const value = std::stoul(trimmed, &consumed, 10); + if (consumed != trimmed.size() || value < 1 || value > 255) { + errata.note(ERRATA_ERROR_SEV, "invalid @volume={} (expected comma-separated numbers 1-255)", std::string{raw}); + return errata; + } + seq.push_back(static_cast(value)); + } catch (std::exception const &) { + errata.note(ERRATA_ERROR_SEV, "invalid @volume={} (expected comma-separated numbers 1-255)", std::string{raw}); + return errata; + } + + if (comma == std::string_view::npos) { + break; + } + start = comma + 1; + if (start == raw.size()) { + errata.note(ERRATA_ERROR_SEV, "invalid @volume={} (trailing comma)", std::string{raw}); + return errata; + } + } + + if (seq.size() == 1) { + target = seq[0].as(); + } else { + target = seq; + } + + return errata; +} + +swoc::Errata +parse_rule_options(YAML::Node &entry, std::vector const &options) +{ + swoc::Errata errata; + YAML::Node acl_filter{YAML::NodeType::Map}; + YAML::Node plugins{YAML::NodeType::Sequence}; + int current_plugin_idx = -1; + + for (auto const &raw_option : options) { + auto parsed = parse_option(raw_option); + if (!parsed.has_value()) { + errata.note(ERRATA_NOTE_SEV, "ignoring invalid remap option '@{}'", raw_option); + continue; + } + + auto const &option = *parsed; + + if (option.key == "plugin") { + if (option.value.empty()) { + errata.note(ERRATA_ERROR_SEV, "empty argument in @plugin="); + continue; + } + YAML::Node plugin{YAML::NodeType::Map}; + plugin["name"] = option.value; + plugins.push_back(plugin); + current_plugin_idx = static_cast(plugins.size()) - 1; + continue; + } + + if (option.key == "pparam") { + if (option.value.empty()) { + errata.note(ERRATA_ERROR_SEV, "empty argument in @pparam="); + continue; + } + if (current_plugin_idx < 0) { + errata.note(ERRATA_NOTE_SEV, "ignoring orphan @pparam={} with no preceding @plugin=", option.value); + continue; + } + ensure_sequence(plugins[current_plugin_idx], "params").push_back(option.value); + continue; + } + + if (option.key == "strategy") { + if (option.value.empty()) { + errata.note(ERRATA_ERROR_SEV, "empty argument in @strategy="); + } else { + entry["strategy"] = option.value; + } + continue; + } + + if (option.key == "mapid") { + if (option.value.empty()) { + errata.note(ERRATA_ERROR_SEV, "empty argument in @mapid="); + continue; + } + try { + size_t consumed = 0; + unsigned long const value = std::stoul(option.value, &consumed, 10); + if (consumed != option.value.size() || value > std::numeric_limits::max()) { + throw std::out_of_range("invalid mapid"); + } + entry["mapid"] = static_cast(value); + } catch (std::exception const &) { + errata.note(ERRATA_ERROR_SEV, "invalid @mapid={}", option.value); + } + continue; + } + + if (option.key == "volume") { + auto volume_errata = parse_volume_value(option.value, entry["volume"]); + errata.note(std::move(volume_errata)); + continue; + } + + if (option.key == "method" || option.key == "src_ip" || option.key == "src_ip_category" || option.key == "in_ip" || + option.key == "action" || option.key == "internal") { + auto acl_errata = apply_acl_option(acl_filter, option); + errata.note(std::move(acl_errata)); + continue; + } + + errata.note(ERRATA_NOTE_SEV, "ignoring invalid remap option '@{}'", raw_option); + } + + if (acl_filter.size() > 0) { + entry["acl_filter"] = acl_filter; + } + if (plugins.size() > 0) { + entry["plugins"] = plugins; + } + + return errata; +} + +swoc::Errata +parse_filter_directive(YAML::Node remap_entries, std::string const &directive, std::vector const ¶ms, + std::vector const &options) +{ + swoc::Errata errata; + YAML::Node entry{YAML::NodeType::Map}; + + if (is_define_filter_directive(directive)) { + if (params.size() < 2) { + errata.note(ERRATA_ERROR_SEV, "directive \"{}\" must have name argument", directive); + return errata; + } + if (options.empty()) { + errata.note(ERRATA_ERROR_SEV, "directive \"{}\" must have filter parameter(s)", directive); + return errata; + } + + YAML::Node filter{YAML::NodeType::Map}; + for (auto const &raw_option : options) { + auto parsed = parse_option(raw_option); + if (!parsed.has_value()) { + errata.note(ERRATA_ERROR_SEV, "unsupported ACL option @{}", raw_option); + continue; + } + auto acl_errata = apply_acl_option(filter, *parsed); + errata.note(std::move(acl_errata)); + } + + if (!errata.is_ok()) { + return errata; + } + + entry["define_filter"][params[1]] = filter; + remap_entries.push_back(entry); + return errata; + } + + if (params.size() < 2) { + errata.note(ERRATA_ERROR_SEV, "directive \"{}\" must have name argument", directive); + return errata; + } + + if (is_delete_filter_directive(directive)) { + entry["delete_filter"] = params[1]; + } else if (is_activate_filter_directive(directive)) { + entry["activate_filter"] = params[1]; + } else if (is_deactivate_filter_directive(directive)) { + entry["deactivate_filter"] = params[1]; + } else if (directive == ".include") { + entry["include"] = params[1]; + } else { + errata.note(ERRATA_ERROR_SEV, "unknown directive \"{}\"", directive); + return errata; + } + + remap_entries.push_back(entry); + return errata; +} + +swoc::Errata +parse_legacy_line(YAML::Node remap_entries, std::vector const ¶ms, std::vector const &options) +{ + swoc::Errata errata; + + if (params.empty()) { + return errata; + } + + if (params[0].starts_with('.')) { + return parse_filter_directive(remap_entries, params[0], params, options); + } + + if (params.size() < 3) { + errata.note(ERRATA_ERROR_SEV, "malformed remap rule"); + return errata; + } + + YAML::Node entry{YAML::NodeType::Map}; + entry["type"] = params[0]; + + std::string from_url = params[1]; + if (from_url.size() >= 2 && from_url.ends_with("//")) { + from_url.erase(from_url.size() - 2); + entry["unique"] = true; + } + + entry["from"]["url"] = from_url; + entry["to"]["url"] = params[2]; + + if (remap_type_base(params[0]) == "map_with_referer") { + if (params.size() < 4) { + errata.note(ERRATA_ERROR_SEV, "map_with_referer requires a redirect URL"); + return errata; + } + entry["redirect"]["url"] = params[3]; + if (params.size() > 4) { + auto regex_seq = ensure_sequence(entry["redirect"], "regex"); + for (size_t i = 4; i < params.size(); ++i) { + regex_seq.push_back(params[i]); + } + } + } else if (params.size() > 3) { + entry["tag"] = params[3]; + } + + auto option_errata = parse_rule_options(entry, options); + errata.note(std::move(option_errata)); + if (!errata.is_ok()) { + return errata; + } + + remap_entries.push_back(entry); + return errata; +} + +} // namespace + +namespace config +{ + +ConfigResult +RemapParser::parse(std::string const &filename) +{ + ConfigResult result; + + std::error_code ec; + std::string content = swoc::file::load(filename, ec); + if (ec) { + result.value = make_empty_remap_config(); + if (ec.value() == ENOENT) { + result.file_not_found = true; + return result; + } + + result.errata.note(ERRATA_ERROR_SEV, "failed to read remap configuration from \"{}\" - {}", filename, ec.message()); + return result; + } + + return parse_content(content, filename); +} + +ConfigResult +RemapParser::parse_content(std::string_view content, std::string const &filename) +{ + if (content.empty()) { + return {make_empty_remap_config(), {}}; + } + + Format const format = detect_format(content, filename); + if (format == Format::YAML) { + return parse_yaml(content); + } + return parse_legacy(content); +} + +RemapParser::Format +RemapParser::detect_format(std::string_view content, std::string const &filename) const +{ + if (filename.ends_with(".yaml") || filename.ends_with(".yml")) { + return Format::YAML; + } + if (filename.ends_with(".config")) { + return Format::Legacy; + } + if (content.find("remap:") != std::string_view::npos || content.find("acl_filters:") != std::string_view::npos) { + return Format::YAML; + } + return Format::Legacy; +} + +ConfigResult +RemapParser::parse_yaml(std::string_view content) const +{ + ConfigResult result; + + try { + result.value = YAML::Load(std::string{content}); + if (!result.value || result.value.IsNull()) { + result.value = make_empty_remap_config(); + } else if (!result.value.IsMap()) { + result.errata.note(ERRATA_ERROR_SEV, "expected remap YAML configuration to be a map"); + } + } catch (YAML::Exception const &ex) { + result.value = make_empty_remap_config(); + result.errata.note(ERRATA_ERROR_SEV, "YAML parsing error: {}", ex.what()); + } catch (std::exception const &ex) { + result.value = make_empty_remap_config(); + result.errata.note(ERRATA_ERROR_SEV, "exception parsing remap YAML: {}", ex.what()); + } + + return result; +} + +ConfigResult +RemapParser::parse_legacy(std::string_view content) const +{ + ConfigResult result; + result.value = make_empty_remap_config(); + + Tokenizer white_tok{" \t"}; + std::string buffer{content}; + char *tok_state = nullptr; + int line_no = 0; + + for (char *line = tokLine(buffer.data(), &tok_state, '\\'); line != nullptr; line = tokLine(nullptr, &tok_state, '\\')) { + ++line_no; + + while (*line && std::isspace(static_cast(*line))) { + ++line; + } + + if (*line == '\0' || *line == '#') { + continue; + } + + size_t size = std::strlen(line); + while (size > 0 && std::isspace(static_cast(line[size - 1]))) { + line[--size] = '\0'; + } + + if (*line == '\0' || *line == '#') { + continue; + } + + std::vector params; + std::vector options; + + int tok_count = white_tok.Initialize(line, SHARE_TOKS | ALLOW_SPACES); + for (int idx = 0; idx < tok_count; ++idx) { + auto *token = const_cast(white_tok[idx]); + if (token[0] == '@' && token[1] != '\0') { + options.emplace_back(token + 1); + } else { + params.emplace_back(token); + } + } + + auto line_errata = parse_legacy_line(result.value["remap"], params, options); + if (!line_errata.is_ok()) { + result.errata.note(ERRATA_ERROR_SEV, "error on line {} - {}", line_no, line_errata.front().text()); + return result; + } + + result.errata.note(std::move(line_errata)); + } + + return result; +} + +std::string +RemapMarshaller::to_yaml(RemapConfig const &config) +{ + YAML::Emitter emitter; + emitter << config; + return emitter.c_str(); +} + +} // namespace config diff --git a/src/config/unit_tests/test_remap.cc b/src/config/unit_tests/test_remap.cc new file mode 100644 index 00000000000..6ea8e6b1733 --- /dev/null +++ b/src/config/unit_tests/test_remap.cc @@ -0,0 +1,181 @@ +/** @file + + Unit tests for shared remap configuration parsing and marshalling. + + @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 "config/remap.h" + +#include +#include +#include + +#include + +using namespace config; + +namespace +{ + +class TempFile +{ +public: + TempFile(std::string const &filename, std::string const &content) + { + _path = std::filesystem::temp_directory_path() / filename; + std::ofstream ofs(_path); + ofs << content; + } + + ~TempFile() { std::filesystem::remove(_path); } + + std::string + path() const + { + return _path.string(); + } + +private: + std::filesystem::path _path; +}; + +ConfigResult +parse_file(std::string const &content, std::string const &filename) +{ + TempFile file(filename, content); + RemapParser parser; + return parser.parse(file.path()); +} + +} // namespace + +TEST_CASE("RemapParser converts legacy remap.config directives and rule options", "[remap][legacy][parser]") +{ + static constexpr char LEGACY_CONFIG[] = R"REMAP( +.definefilter deny_write @method=PUT @method=DELETE @action=deny @src_ip=10.0.0.0/8 +.activatefilter deny_write +map http://example.com// http://origin.example.com edge-tag @plugin=conf_remap.so @pparam=proxy.config.foo=1 @plugin=hdr.so @pparam=a @pparam=b @strategy=my_strategy @mapid=42 @volume=3,4 @method=GET @src_ip=192.0.2.10 @src_ip=~198.51.100.0/24 @internal +map_with_referer http://refer.example.com http://origin.example.com http://deny.example.com (.*[.])?allowed[.]com ~evil[.]example +.include remap-extra.yaml +)REMAP"; + + auto result = parse_file(LEGACY_CONFIG, "remap.config"); + + REQUIRE(result.ok()); + REQUIRE(result.value["remap"]); + REQUIRE(result.value["remap"].IsSequence()); + REQUIRE(result.value["remap"].size() == 5); + + SECTION("Named filter directive") + { + auto filter = result.value["remap"][0]["define_filter"]["deny_write"]; + REQUIRE(filter); + CHECK(filter["method"].IsSequence()); + CHECK(filter["method"][0].as() == "PUT"); + CHECK(filter["method"][1].as() == "DELETE"); + CHECK(filter["action"].as() == "deny"); + CHECK(filter["src_ip"].as() == "10.0.0.0/8"); + } + + SECTION("Main mapping rule") + { + auto rule = result.value["remap"][2]; + REQUIRE(rule); + CHECK(rule["type"].as() == "map"); + CHECK(rule["unique"].as() == true); + CHECK(rule["from"]["url"].as() == "http://example.com"); + CHECK(rule["to"]["url"].as() == "http://origin.example.com"); + CHECK(rule["tag"].as() == "edge-tag"); + CHECK(rule["strategy"].as() == "my_strategy"); + CHECK(rule["mapid"].as() == 42); + CHECK(rule["volume"].IsSequence()); + CHECK(rule["volume"][0].as() == 3); + CHECK(rule["volume"][1].as() == 4); + + auto acl = rule["acl_filter"]; + REQUIRE(acl); + CHECK(acl["method"].as() == "GET"); + CHECK(acl["src_ip"].as() == "192.0.2.10"); + CHECK(acl["src_ip_invert"].as() == "198.51.100.0/24"); + CHECK(acl["internal"].as() == true); + + auto plugins = rule["plugins"]; + REQUIRE(plugins); + REQUIRE(plugins.IsSequence()); + REQUIRE(plugins.size() == 2); + CHECK(plugins[0]["name"].as() == "conf_remap.so"); + CHECK(plugins[0]["params"][0].as() == "proxy.config.foo=1"); + CHECK(plugins[1]["name"].as() == "hdr.so"); + CHECK(plugins[1]["params"][0].as() == "a"); + CHECK(plugins[1]["params"][1].as() == "b"); + } + + SECTION("map_with_referer conversion") + { + auto rule = result.value["remap"][3]; + REQUIRE(rule["type"].as() == "map_with_referer"); + CHECK(rule["redirect"]["url"].as() == "http://deny.example.com"); + REQUIRE(rule["redirect"]["regex"].IsSequence()); + CHECK(rule["redirect"]["regex"][0].as() == "(.*[.])?allowed[.]com"); + CHECK(rule["redirect"]["regex"][1].as() == "~evil[.]example"); + } +} + +TEST_CASE("RemapParser round-trips legacy remap config through YAML", "[remap][legacy][roundtrip]") +{ + static constexpr char LEGACY_CONFIG[] = R"REMAP( +map http://www.example.com http://127.0.0.1:8080 @volume=4 @mapid=7 @plugin=conf_remap.so @pparam=foo=bar +)REMAP"; + + RemapParser parser; + RemapMarshaller marshaller; + + auto legacy = parser.parse_content(LEGACY_CONFIG, "remap.config"); + REQUIRE(legacy.ok()); + + std::string yaml = marshaller.to_yaml(legacy.value); + CHECK(yaml.find("volume: 4") != std::string::npos); + CHECK(yaml.find("mapid: 7") != std::string::npos); + CHECK(yaml.find("name: conf_remap.so") != std::string::npos); + + auto round_trip = parser.parse_content(yaml, "remap.yaml"); + REQUIRE(round_trip.ok()); + REQUIRE(round_trip.value["remap"].IsSequence()); + REQUIRE(round_trip.value["remap"].size() == 1); + + auto rule = round_trip.value["remap"][0]; + CHECK(rule["volume"].as() == 4); + CHECK(rule["mapid"].as() == 7); + CHECK(rule["plugins"][0]["params"][0].as() == "foo=bar"); +} + +TEST_CASE("RemapParser reports errors for malformed legacy directives", "[remap][legacy][error]") +{ + static constexpr char LEGACY_CONFIG[] = R"REMAP( +.definefilter broken +)REMAP"; + + auto result = parse_file(LEGACY_CONFIG, "remap.config"); + + REQUIRE_FALSE(result.ok()); + REQUIRE_FALSE(result.errata.empty()); + CHECK(std::string(result.errata.front().text()).find("directive \".definefilter\" must have filter parameter(s)") != + std::string::npos); +} diff --git a/src/proxy/http/remap/CMakeLists.txt b/src/proxy/http/remap/CMakeLists.txt index e6d4771049b..08dc4352905 100644 --- a/src/proxy/http/remap/CMakeLists.txt +++ b/src/proxy/http/remap/CMakeLists.txt @@ -39,7 +39,7 @@ add_library(ts::http_remap ALIAS http_remap) target_link_libraries( http_remap PUBLIC ts::proxy ts::tscore - PRIVATE ts::inkevent ts::inkutils yaml-cpp::yaml-cpp + PRIVATE ts::config ts::inkevent ts::inkutils yaml-cpp::yaml-cpp ) if(BUILD_TESTING) diff --git a/src/proxy/http/remap/RemapConfig.cc b/src/proxy/http/remap/RemapConfig.cc index bbefa27c68f..b8dfe3aaf81 100644 --- a/src/proxy/http/remap/RemapConfig.cc +++ b/src/proxy/http/remap/RemapConfig.cc @@ -1,6 +1,6 @@ /** @file * - * Remap configuration file parsing. + * Remap configuration runtime helpers. * * @section license License * @@ -21,187 +21,44 @@ * limitations under the License. */ -#include "proxy/http/remap/AclFiltering.h" -#include "swoc/swoc_file.h" - #include "proxy/http/remap/RemapConfig.h" -#include "proxy/http/remap/UrlRewrite.h" -#include "proxy/ReverseProxy.h" -#include "tscore/Layout.h" -#include "proxy/hdrs/HTTP.h" -#include "tscore/ink_platform.h" -#include "tscore/List.h" -#include "tscore/ink_cap.h" -#include "tscore/Tokenizer.h" -#include "tscore/Filenames.h" -#include "proxy/IPAllow.h" -#include "proxy/http/remap/PluginFactory.h" -#include "iocore/cache/Cache.h" -extern CacheHostRecord *createCacheHostRecord(const char *volume_str, char *errbuf, size_t errbufsize); +#include +#include +#include +#include +#include -using namespace std::literals; - -#define modulePrefix "[ReverseProxy]" +#include "proxy/hdrs/HTTP.h" +#include "proxy/http/remap/UrlRewrite.h" +#include "tscore/Diags.h" load_remap_file_func load_remap_file_cb = nullptr; -namespace -{ -DbgCtl dbg_ctl_url_rewrite{"url_rewrite"}; -DbgCtl dbg_ctl_remap_plugin{"remap_plugin"}; -DbgCtl dbg_ctl_url_rewrite_regex{"url_rewrite_regex"}; - -} // end anonymous namespace - -/** - Returns the length of the URL. - - Will replace the terminator with a '/' if this is a full URL and - there are no '/' in it after the host. This ensures that class - URL parses the URL correctly. - -*/ -static int -UrlWhack(char *toWhack, int *origLength) -{ - int length = strlen(toWhack); - char *tmp; - *origLength = length; - - // Check to see if this a full URL - tmp = strstr(toWhack, "://"); - if (tmp != nullptr) { - if (strchr(tmp + 3, '/') == nullptr) { - toWhack[length] = '/'; - length++; - } - } - return length; -} - -const char * -is_valid_scheme(std::string_view fromScheme, std::string_view toScheme) -{ - const char *errStr = nullptr; - // Include support for HTTPS scheme - // includes support for FILE scheme - if ((fromScheme != std::string_view{URL_SCHEME_HTTP} && fromScheme != std::string_view{URL_SCHEME_HTTPS} && - fromScheme != std::string_view{URL_SCHEME_FILE} && fromScheme != std::string_view{URL_SCHEME_TUNNEL} && - fromScheme != std::string_view{URL_SCHEME_WS} && fromScheme != std::string_view{URL_SCHEME_WSS} && - fromScheme != std::string_view{URL_SCHEME_HTTP_UDS} && fromScheme != std::string_view{URL_SCHEME_HTTPS_UDS}) || - (toScheme != std::string_view{URL_SCHEME_HTTP} && toScheme != std::string_view{URL_SCHEME_HTTPS} && - toScheme != std::string_view{URL_SCHEME_TUNNEL} && toScheme != std::string_view{URL_SCHEME_WS} && - toScheme != std::string_view{URL_SCHEME_WSS})) { - errStr = "only http, https, http+unix, https+unix, ws, wss, and tunnel remappings are supported"; - return errStr; - } - - // If mapping from WS or WSS we must map out to WS or WSS - if ((fromScheme == std::string_view{URL_SCHEME_WSS} || fromScheme == std::string_view{URL_SCHEME_WS}) && - (toScheme != std::string_view{URL_SCHEME_WSS} && toScheme != std::string_view{URL_SCHEME_WS})) { - errStr = "WS or WSS can only be mapped out to WS or WSS."; - } - return errStr; -} - -/** - Cleanup *char[] array - each item in array must be allocated via - ats_malloc or similar "x..." function. - -*/ -static void -clear_xstr_array(char *v[], size_t vsize) -{ - for (unsigned i = 0; i < vsize; i++) { - v[i] = static_cast(ats_free_null(v[i])); - } -} - -BUILD_TABLE_INFO::BUILD_TABLE_INFO() - -{ - memset(this->paramv, 0, sizeof(this->paramv)); - memset(this->argv, 0, sizeof(this->argv)); -} +BUILD_TABLE_INFO::BUILD_TABLE_INFO() = default; BUILD_TABLE_INFO::~BUILD_TABLE_INFO() { - this->reset(); + clear_acl_rules_list(); } void BUILD_TABLE_INFO::reset() { - this->paramc = this->argc = 0; - clear_xstr_array(this->paramv, sizeof(this->paramv) / sizeof(char *)); - clear_xstr_array(this->argv, sizeof(this->argv) / sizeof(char *)); } void BUILD_TABLE_INFO::clear_acl_rules_list() { - // clean up any leftover named filter rules auto *rp = rules_list; + while (rp != nullptr) { auto *tmp = rp->next; delete rp; rp = tmp; } -} - -static const char * -process_filter_opt(url_mapping *mp, const BUILD_TABLE_INFO *bti, char *errStrBuf, int errStrBufSize) -{ - acl_filter_rule *rp, **rpp; - const char *errStr = nullptr; - - if (unlikely(!mp || !bti || !errStrBuf || errStrBufSize <= 0)) { - Dbg(dbg_ctl_url_rewrite, "[process_filter_opt] Invalid argument(s)"); - return "[process_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 (!errStr && (bti->remap_optflg & REMAP_OPTFLG_ALL_FILTERS) != 0) { - Dbg(dbg_ctl_url_rewrite, "[process_filter_opt] Add per remap filter"); - for (rpp = &mp->filter; *rpp; rpp = &((*rpp)->next)) { - ; - } - errStr = remap_validate_filter_args(rpp, bti->argv, bti->argc, errStrBuf, errStrBufSize, 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_filter_opt] Add active main filter \"%s\" (argc=%d)", - rp->filter_name ? rp->filter_name : "", rp->argc); - for (rpp = &mp->filter; *rpp; rpp = &((*rpp)->next)) { - ; - } - if ((errStr = remap_validate_filter_args(rpp, rp->argv, rp->argc, errStrBuf, errStrBufSize, bti->behavior_policy)) != - nullptr) { - 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 errStr; + rules_list = nullptr; } bool @@ -209,7 +66,7 @@ is_inkeylist(const char *key, ...) { va_list ap; - if (unlikely(key == nullptr || key[0] == '\0')) { + if (key == nullptr || key[0] == '\0') { return false; } @@ -229,109 +86,6 @@ is_inkeylist(const char *key, ...) return false; } -static const char * -parse_define_directive(const char *directive, BUILD_TABLE_INFO *bti, char *errbuf, size_t errbufsize) -{ - bool flg; - acl_filter_rule *rp; - const char *cstr = nullptr; - - if (bti->paramc < 2) { - snprintf(errbuf, errbufsize, "Directive \"%s\" must have name argument", directive); - Dbg(dbg_ctl_url_rewrite, "[parse_directive] %s", errbuf); - return errbuf; - } - if (bti->argc < 1) { - snprintf(errbuf, errbufsize, "Directive \"%s\" must have filter parameter(s)", directive); - Dbg(dbg_ctl_url_rewrite, "[parse_directive] %s", errbuf); - return errbuf; - } - - flg = ((rp = acl_filter_rule::find_byname(bti->rules_list, bti->paramv[1])) == nullptr) ? true : false; - // coverity[alloc_arg] - if ((cstr = remap_validate_filter_args(&rp, bti->argv, bti->argc, errbuf, errbufsize, bti->behavior_policy)) == nullptr && 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", bti->paramv[1]); - for (rpp = &bti->rules_list; *rpp; rpp = &((*rpp)->next)) { - ; - } - (*rpp = rp)->name(bti->paramv[1]); - } - Dbg(dbg_ctl_url_rewrite, "[parse_directive] %d argument(s) were added to rule \"%s\"", bti->argc, bti->paramv[1]); - rp->add_argv(bti->argc, bti->argv); // store string arguments for future processing - } - - return cstr; -} - -static const char * -parse_delete_directive(const char *directive, BUILD_TABLE_INFO *bti, char *errbuf, size_t errbufsize) -{ - if (bti->paramc < 2) { - snprintf(errbuf, errbufsize, "Directive \"%s\" must have name argument", directive); - Dbg(dbg_ctl_url_rewrite, "[parse_directive] %s", errbuf); - return errbuf; - } - - acl_filter_rule::delete_byname(&bti->rules_list, bti->paramv[1]); - return nullptr; -} - -static const char * -parse_activate_directive(const char *directive, BUILD_TABLE_INFO *bti, char *errbuf, size_t errbufsize) -{ - acl_filter_rule *rp; - - if (bti->paramc < 2) { - snprintf(errbuf, errbufsize, "Directive \"%s\" must have name argument", directive); - Dbg(dbg_ctl_url_rewrite, "[parse_directive] %s", errbuf); - return errbuf; - } - - // Check if for ip_allow filter - if (strcmp(bti->paramv[1], "ip_allow") == 0) { - bti->ip_allow_check_enabled_p = true; - return nullptr; - } - - if ((rp = acl_filter_rule::find_byname(bti->rules_list, bti->paramv[1])) == nullptr) { - snprintf(errbuf, errbufsize, R"(Undefined filter "%s" in directive "%s")", bti->paramv[1], directive); - Dbg(dbg_ctl_url_rewrite, "[parse_directive] %s", errbuf); - return errbuf; - } - - acl_filter_rule::requeue_in_active_list(&bti->rules_list, rp); - return nullptr; -} - -static const char * -parse_deactivate_directive(const char *directive, BUILD_TABLE_INFO *bti, char *errbuf, size_t errbufsize) -{ - acl_filter_rule *rp; - - if (bti->paramc < 2) { - snprintf(errbuf, errbufsize, "Directive \"%s\" must have name argument", directive); - Dbg(dbg_ctl_url_rewrite, "[parse_directive] %s", errbuf); - return errbuf; - } - - // Check if for ip_allow filter - if (strcmp(bti->paramv[1], "ip_allow") == 0) { - bti->ip_allow_check_enabled_p = false; - return nullptr; - } - - if ((rp = acl_filter_rule::find_byname(bti->rules_list, bti->paramv[1])) == nullptr) { - snprintf(errbuf, errbufsize, R"(Undefined filter "%s" in directive "%s")", bti->paramv[1], directive); - Dbg(dbg_ctl_url_rewrite, "[parse_directive] %s", errbuf); - return errbuf; - } - - acl_filter_rule::requeue_in_passive_list(&bti->rules_list, rp); - return nullptr; -} - void free_directory_list(int n_entries, struct dirent **entrylist) { @@ -342,669 +96,30 @@ free_directory_list(int n_entries, struct dirent **entrylist) free(entrylist); } -static const char * -parse_remap_fragment(const char *path, BUILD_TABLE_INFO *bti, char *errbuf, size_t errbufsize) -{ - // 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) { - snprintf(errbuf, errbufsize, "%s: %s", path, strerror(errno)); - return errbuf; - } - - 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_config_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, path); - } else { - snprintf(errbuf, errbufsize, "failed to parse included file %s", path); - return errbuf; - } - - return nullptr; -} - -static const char * -parse_include_directive(const char *directive, BUILD_TABLE_INFO *bti, char *errbuf, size_t errbufsize) -{ - if (bti->paramc < 2) { - snprintf(errbuf, errbufsize, "Directive \"%s\" must have a path argument", directive); - Dbg(dbg_ctl_url_rewrite, "[%s] %s", __func__, errbuf); - return errbuf; - } - - for (unsigned i = 1; i < static_cast(bti->paramc); ++i) { - ats_scoped_str path; - const char *errmsg = nullptr; - - // The included path is relative to SYSCONFDIR, just like remap.config is. - path = RecConfigReadConfigPath(nullptr, bti->paramv[i]); - - if (ink_file_is_directory(path)) { - struct dirent **entrylist = nullptr; - int n_entries; - - n_entries = scandir(path, &entrylist, nullptr, alphasort); - if (n_entries == -1) { - snprintf(errbuf, errbufsize, "failed to open %s: %s", path.get(), strerror(errno)); - return errbuf; - } - - 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; - } - - errmsg = parse_remap_fragment(subpath, bti, errbuf, errbufsize); - if (errmsg != nullptr) { - break; - } - } - - free_directory_list(n_entries, entrylist); - - } else { - errmsg = parse_remap_fragment(path, bti, errbuf, errbufsize); - } - - if (errmsg) { - return errmsg; - } - } - - return nullptr; -} - -struct remap_directive { - const char *name; - const char *(*parser)(const char *, BUILD_TABLE_INFO *, char *, size_t); -}; - -static const remap_directive directives[] = { - - {".definefilter", parse_define_directive }, - {".deffilter", parse_define_directive }, - {".defflt", parse_define_directive }, - - {".deletefilter", parse_delete_directive }, - {".delfilter", parse_delete_directive }, - {".delflt", parse_delete_directive }, - - {".usefilter", parse_activate_directive }, - {".activefilter", parse_activate_directive }, - {".activatefilter", parse_activate_directive }, - {".useflt", parse_activate_directive }, - - {".unusefilter", parse_deactivate_directive}, - {".deactivatefilter", parse_deactivate_directive}, - {".unactivefilter", parse_deactivate_directive}, - {".deuseflt", parse_deactivate_directive}, - {".unuseflt", parse_deactivate_directive}, - - {".include", parse_include_directive }, -}; - -const char * -remap_parse_directive(BUILD_TABLE_INFO *bti, char *errbuf, size_t errbufsize) -{ - const char *directive = nullptr; - - // Check arguments - if (unlikely(!bti || !errbuf || errbufsize == 0 || !bti->paramc || (directive = bti->paramv[0]) == nullptr)) { - Dbg(dbg_ctl_url_rewrite, "[parse_directive] Invalid argument(s)"); - return "Invalid argument(s)"; - } - - for (unsigned i = 0; i < countof(directives); ++i) { - if (strcmp(directive, directives[i].name) == 0) { - return directives[i].parser(directive, bti, errbuf, errbufsize); - } - } - - snprintf(errbuf, errbufsize, "Unknown directive \"%s\"", directive); - Dbg(dbg_ctl_url_rewrite, "[parse_directive] %s", errbuf); - return errbuf; -} - const char * -remap_validate_filter_args(acl_filter_rule **rule_pp, const char *const *argv, int argc, char *errStrBuf, size_t errStrBufSize, - ACLBehaviorPolicy behavior_policy) +is_valid_scheme(std::string_view fromScheme, std::string_view toScheme) { - acl_filter_rule *rule; - int i, j; - bool new_rule_flg = false; - - if (!rule_pp) { - Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Invalid argument(s)"); - return "Invalid argument(s)"; - } - - if (dbg_ctl_url_rewrite.on()) { - printf("validate_filter_args: "); - for (i = 0; i < argc; i++) { - printf("\"%s\" ", argv[i]); - } - printf("\n"); - } - - 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 "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"); - } - - bool action_flag = false; - for (i = 0; i < argc; i++) { - unsigned long ul; - bool hasarg; - - const char *argptr; - if ((ul = remap_check_option(&argv[i], 1, 0, nullptr, &argptr)) == 0) { - Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Unknown remap option - %s", argv[i]); - snprintf(errStrBuf, errStrBufSize, "Unknown option - \"%s\"", argv[i]); - errStrBuf[errStrBufSize - 1] = 0; - if (new_rule_flg) { - delete rule; - *rule_pp = nullptr; - } - return errStrBuf; - } - - // Every filter operator requires an argument except @internal. - hasarg = !(ul & REMAP_OPTFLG_INTERNAL); - - if (hasarg && (!argptr || !argptr[0])) { - Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Empty argument in %s", argv[i]); - snprintf(errStrBuf, errStrBufSize, "Empty argument in \"%s\"", argv[i]); - errStrBuf[errStrBufSize - 1] = 0; - if (new_rule_flg) { - delete rule; - *rule_pp = nullptr; - } - return errStrBuf; - } - - if (ul & REMAP_OPTFLG_METHOD) { /* "method=" option */ - // Please remember that the order of hash idx creation is very important and it is defined - // in HTTP.cc file. 0 in our array is the first method, CONNECT - int m = hdrtoken_tokenize(argptr, strlen(argptr), 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]", argptr); - rule->nonstandard_methods.insert(argptr); - } - rule->method_restriction_enabled = true; - } - - if (ul & REMAP_OPTFLG_SRC_IP) { /* "src_ip=" option */ - if (rule->src_ip_cnt >= ACL_FILTER_MAX_SRC_IP) { - Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Too many \"src_ip=\" filters"); - snprintf(errStrBuf, errStrBufSize, "Defined more than %d \"src_ip=\" filters!", ACL_FILTER_MAX_SRC_IP); - errStrBuf[errStrBufSize - 1] = 0; - if (new_rule_flg) { - delete rule; - *rule_pp = nullptr; - } - return errStrBuf; - } - src_ip_info_t *ipi = &rule->src_ip_array[rule->src_ip_cnt]; - if (ul & REMAP_OPTFLG_INVERT) { - ipi->invert = true; - } - std::string_view arg{argptr}; - 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", argv[i]); - snprintf(errStrBuf, errStrBufSize, "Unable to parse IP value in %s", argv[i]); - errStrBuf[errStrBufSize - 1] = 0; - if (new_rule_flg) { - delete rule; - *rule_pp = nullptr; - } - return errStrBuf; - } - 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(); - ipi = nullptr; - break; /* we have the same src_ip in the list */ - } - } - if (ipi) { - rule->src_ip_cnt++; - rule->src_ip_valid = 1; - } - } - - if (ul & REMAP_OPTFLG_SRC_IP_CATEGORY) { /* "src_ip_category=" option */ - 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"); - snprintf(errStrBuf, errStrBufSize, "Defined more than %d \"src_ip_category=\" filters!", ACL_FILTER_MAX_SRC_IP); - errStrBuf[errStrBufSize - 1] = 0; - if (new_rule_flg) { - delete rule; - *rule_pp = nullptr; - } - return errStrBuf; - } - src_ip_category_info_t *ipi = &rule->src_ip_category_array[rule->src_ip_category_cnt]; - ipi->category.assign(argptr); - if (ul & REMAP_OPTFLG_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(); - ipi = nullptr; - break; /* we have the same src_ip_category in the list */ - } - } - if (ipi) { - rule->src_ip_category_cnt++; - rule->src_ip_category_valid = 1; - } - } - - if (ul & REMAP_OPTFLG_IN_IP) { /* "in_ip=" option */ - if (rule->in_ip_cnt >= ACL_FILTER_MAX_IN_IP) { - Dbg(dbg_ctl_url_rewrite, "[validate_filter_args] Too many \"in_ip=\" filters"); - snprintf(errStrBuf, errStrBufSize, "Defined more than %d \"in_ip=\" filters!", ACL_FILTER_MAX_IN_IP); - errStrBuf[errStrBufSize - 1] = 0; - if (new_rule_flg) { - delete rule; - *rule_pp = nullptr; - } - return errStrBuf; - } - src_ip_info_t *ipi = &rule->in_ip_array[rule->in_ip_cnt]; - if (ul & REMAP_OPTFLG_INVERT) { - ipi->invert = true; - } - // important! use copy of argument - std::string_view arg{argptr}; - 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", argv[i]); - snprintf(errStrBuf, errStrBufSize, "Unable to parse IP value in %s", argv[i]); - errStrBuf[errStrBufSize - 1] = 0; - if (new_rule_flg) { - delete rule; - *rule_pp = nullptr; - } - return errStrBuf; - } - 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(); - ipi = nullptr; - break; /* we have the same src_ip in the list */ - } - } - if (ipi) { - rule->in_ip_cnt++; - rule->in_ip_valid = 1; - } - } - - if (ul & REMAP_OPTFLG_ACTION) { /* "action=" option */ - if (action_flag) { - std::string_view err = "Only one @action= is allowed per remap ACL"; - Dbg(dbg_ctl_url_rewrite, "%s", err.data()); - snprintf(errStrBuf, errStrBufSize, "%s", err.data()); - if (new_rule_flg) { - delete rule; - *rule_pp = nullptr; - } - return errStrBuf; - } - action_flag = true; - 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(argptr, "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"")", - argv[i]); - snprintf(errStrBuf, errStrBufSize, - R"("allow" and "deny" are no longer valid. Use add_allow/add_deny or set_allow/set_deny: "%s"")", argv[i]); - errStrBuf[errStrBufSize - 1] = 0; - if (new_rule_flg) { - delete rule; - *rule_pp = nullptr; - } - return errStrBuf; - } - } - if (is_inkeylist(argptr, "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(argptr, "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(argptr, "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\"", argv[i]); - snprintf(errStrBuf, errStrBufSize, "Unknown argument \"%s\"", argv[i]); - errStrBuf[errStrBufSize - 1] = 0; - if (new_rule_flg) { - delete rule; - *rule_pp = nullptr; - } - return errStrBuf; - } - } - - if (ul & REMAP_OPTFLG_INTERNAL) { - rule->internal = 1; - } - } + const char *errStr = nullptr; - if (dbg_ctl_url_rewrite.on()) { - rule->print(); + if ((fromScheme != std::string_view{URL_SCHEME_HTTP} && fromScheme != std::string_view{URL_SCHEME_HTTPS} && + fromScheme != std::string_view{URL_SCHEME_FILE} && fromScheme != std::string_view{URL_SCHEME_TUNNEL} && + fromScheme != std::string_view{URL_SCHEME_WS} && fromScheme != std::string_view{URL_SCHEME_WSS} && + fromScheme != std::string_view{URL_SCHEME_HTTP_UDS} && fromScheme != std::string_view{URL_SCHEME_HTTPS_UDS}) || + (toScheme != std::string_view{URL_SCHEME_HTTP} && toScheme != std::string_view{URL_SCHEME_HTTPS} && + toScheme != std::string_view{URL_SCHEME_TUNNEL} && toScheme != std::string_view{URL_SCHEME_WS} && + toScheme != std::string_view{URL_SCHEME_WSS})) { + errStr = "only http, https, http+unix, https+unix, ws, wss, and tunnel remappings are supported"; + return errStr; } - return nullptr; /* success */ -} - -unsigned long -remap_check_option(const char *const *argv, int argc, unsigned long findmode, int *_ret_idx, const char **argptr) -{ - unsigned long ret_flags = 0; - int idx = 0; - - if (argptr) { - *argptr = nullptr; + if ((fromScheme == std::string_view{URL_SCHEME_WSS} || fromScheme == std::string_view{URL_SCHEME_WS}) && + (toScheme != std::string_view{URL_SCHEME_WSS} && toScheme != std::string_view{URL_SCHEME_WS})) { + errStr = "WS or WSS can only be mapped out to WS or WSS."; } - if (argv && argc > 0) { - for (int i = 0; i < argc; i++) { - if (!strcasecmp(argv[i], "map_with_referer")) { - if ((findmode & REMAP_OPTFLG_MAP_WITH_REFERER) != 0) { - idx = i; - } - ret_flags |= REMAP_OPTFLG_MAP_WITH_REFERER; - } else if (!strncasecmp(argv[i], "plugin=", 7)) { - if ((findmode & REMAP_OPTFLG_PLUGIN) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][7]; - } - ret_flags |= REMAP_OPTFLG_PLUGIN; - } else if (!strncasecmp(argv[i], "pparam=", 7)) { - if ((findmode & REMAP_OPTFLG_PPARAM) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][7]; - } - ret_flags |= REMAP_OPTFLG_PPARAM; - } else if (!strncasecmp(argv[i], "method=", 7)) { - if ((findmode & REMAP_OPTFLG_METHOD) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][7]; - } - ret_flags |= REMAP_OPTFLG_METHOD; - } else if (!strncasecmp(argv[i], "src_ip=~", 8)) { - if ((findmode & REMAP_OPTFLG_SRC_IP) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][8]; - } - ret_flags |= (REMAP_OPTFLG_SRC_IP | REMAP_OPTFLG_INVERT); - } else if (!strncasecmp(argv[i], "src_ip_category=~", 17)) { - if ((findmode & REMAP_OPTFLG_SRC_IP_CATEGORY) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][17]; - } - ret_flags |= (REMAP_OPTFLG_SRC_IP_CATEGORY | REMAP_OPTFLG_INVERT); - } else if (!strncasecmp(argv[i], "src_ip=", 7)) { - if ((findmode & REMAP_OPTFLG_SRC_IP) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][7]; - } - ret_flags |= REMAP_OPTFLG_SRC_IP; - } else if (!strncasecmp(argv[i], "src_ip_category=", 16)) { - if ((findmode & REMAP_OPTFLG_SRC_IP_CATEGORY) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][16]; - } - ret_flags |= REMAP_OPTFLG_SRC_IP_CATEGORY; - } else if (!strncasecmp(argv[i], "in_ip=~", 7)) { - if ((findmode & REMAP_OPTFLG_IN_IP) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][7]; - } - ret_flags |= (REMAP_OPTFLG_IN_IP | REMAP_OPTFLG_INVERT); - } else if (!strncasecmp(argv[i], "in_ip=", 6)) { - if ((findmode & REMAP_OPTFLG_IN_IP) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][6]; - } - ret_flags |= REMAP_OPTFLG_IN_IP; - } else if (!strncasecmp(argv[i], "action=", 7)) { - if ((findmode & REMAP_OPTFLG_ACTION) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][7]; - } - ret_flags |= REMAP_OPTFLG_ACTION; - } else if (!strncasecmp(argv[i], "mapid=", 6)) { - if ((findmode & REMAP_OPTFLG_MAP_ID) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][6]; - } - ret_flags |= REMAP_OPTFLG_MAP_ID; - } else if (!strncasecmp(argv[i], "internal", 8)) { - if ((findmode & REMAP_OPTFLG_INTERNAL) != 0) { - idx = i; - } - ret_flags |= REMAP_OPTFLG_INTERNAL; - } else if (!strncasecmp(argv[i], "strategy=", 9)) { - if ((findmode & REMAP_OPTFLG_STRATEGY) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][9]; - } - ret_flags |= REMAP_OPTFLG_STRATEGY; - } else if (!strncasecmp(argv[i], "volume=", 7)) { - if ((findmode & REMAP_OPTFLG_VOLUME) != 0) { - idx = i; - } - if (argptr) { - *argptr = &argv[i][7]; - } - ret_flags |= REMAP_OPTFLG_VOLUME; - } else { - Warning("ignoring invalid remap option '%s'", argv[i]); - } - if ((findmode & ret_flags) && !argptr) { - if (_ret_idx) { - *_ret_idx = idx; - } - return ret_flags; - } - } - } - if (_ret_idx) { - *_ret_idx = idx; - } - return ret_flags; + return errStr; } -/** - * @brief loads a remap plugin - * - * @pparam mp url mapping - * @pparam errbuf error buffer - * @pparam errbufsize size of the error buffer - * @pparam jump_to_argc - * @pparam plugin_found_at - * @return success - true, failure - false - */ -bool -remap_load_plugin(const char *const *argv, int argc, url_mapping *mp, char *errbuf, int errbufsize, int jump_to_argc, - int *plugin_found_at, UrlRewrite *rewrite) -{ - char *c, *err; - const char *new_argv[1024]; - char *pargv[1024]; - int idx = 0; - int parc = 0; - *plugin_found_at = 0; - - memset(pargv, 0, sizeof(pargv)); - memset(new_argv, 0, sizeof(new_argv)); - - ink_assert(static_cast(argc) < countof(new_argv)); - - if (jump_to_argc != 0) { - argc -= jump_to_argc; - int i = 0; - while (argv[i + jump_to_argc]) { - new_argv[i] = argv[i + jump_to_argc]; - i++; - } - argv = &new_argv[0]; - if (!remap_check_option(argv, argc, REMAP_OPTFLG_PLUGIN, &idx)) { - return false; - } - } else { - if (unlikely(!mp || (remap_check_option(argv, argc, REMAP_OPTFLG_PLUGIN, &idx) & REMAP_OPTFLG_PLUGIN) == 0)) { - snprintf(errbuf, errbufsize, "Can't find remap plugin keyword or \"url_mapping\" is nullptr"); - return false; /* incorrect input data - almost impossible case */ - } - } - - if (unlikely((c = (char *)strchr(argv[idx], (int)'=')) == nullptr || !(*(++c)))) { - snprintf(errbuf, errbufsize, "Can't find remap plugin file name in \"@%s\"", argv[idx]); - return false; /* incorrect input data */ - } - - Dbg(dbg_ctl_remap_plugin, "using path %s for plugin", c); - - /* Prepare remap plugin parameters from the config */ - if ((err = mp->fromURL.string_get(nullptr)) == nullptr) { - snprintf(errbuf, errbufsize, "Can't load fromURL from URL class"); - return false; - } - pargv[parc++] = ats_strdup(err); - ats_free(err); - - if ((err = mp->toURL.string_get(nullptr)) == nullptr) { - snprintf(errbuf, errbufsize, "Can't load toURL from URL class"); - return false; - } - pargv[parc++] = ats_strdup(err); - ats_free(err); - - bool plugin_encountered = false; - // how many plugin parameters we have for this remapping - for (idx = 0; idx < argc && parc < static_cast(countof(pargv) - 1); idx++) { - if (plugin_encountered && !strncasecmp("plugin=", argv[idx], 7) && argv[idx][7]) { - *plugin_found_at = idx; - break; // if there is another plugin, lets deal with that later - } - - if (!strncasecmp("plugin=", argv[idx], 7)) { - plugin_encountered = true; - } - - if (!strncasecmp("pparam=", argv[idx], 7) && argv[idx][7]) { - pargv[parc++] = const_cast(&(argv[idx][7])); - } - } - - Dbg(dbg_ctl_url_rewrite, "Viewing all parameters for config line"); - for (int k = 0; k < argc; k++) { - Dbg(dbg_ctl_url_rewrite, "Argument %d: %s", k, argv[k]); - } - - Dbg(dbg_ctl_url_rewrite, "Viewing parsed plugin parameters for %s: [%d]", c, *plugin_found_at); - for (int k = 0; k < parc; k++) { - Dbg(dbg_ctl_url_rewrite, "Argument %d: %s", k, pargv[k]); - } - - 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 = rewrite->pluginFactory.getRemapPlugin(swoc::file::path(const_cast(c)), parc, pargv, error, - isPluginDynamicReloadEnabled()); - } // done elevating access - - bool result = true; - if (nullptr == pi) { - snprintf(errbuf, errbufsize, "%s", error.c_str()); - result = false; - } else { - mp->add_plugin_instance(pi); - } - - ats_free(pargv[0]); // fromURL - ats_free(pargv[1]); // toURL - - return result; -} -/** will process the regex mapping configuration and create objects in - output argument reg_map. It assumes existing data in reg_map is - inconsequential and will be perfunctorily null-ed; -*/ bool process_regex_mapping_config(const char *from_host_lower, url_mapping *new_mapping, UrlRewrite::RegexMapping *reg_map) { @@ -1019,22 +134,22 @@ process_regex_mapping_config(const char *from_host_lower, url_mapping *new_mappi reg_map->url_map = new_mapping; - // using from_host_lower (and not new_mapping->fromURL.host_get()) - // as this one will be nullptr-terminated (required by pcre_compile) - if (reg_map->regular_expression.compile(from_host_lower) == false) { + // Use the normalized host buffer because it is NUL-terminated for the regex compiler. + if (!reg_map->regular_expression.compile(from_host_lower)) { Warning("pcre_compile failed! Regex has error starting at %s", from_host_lower); - goto lFail; + goto fail; } captures = reg_map->regular_expression.get_capture_count(); if (captures == -1) { Warning("pcre_fullinfo failed!"); - goto lFail; + goto fail; } - if (captures >= UrlRewrite::MAX_REGEX_SUBS) { // off by one for $0 (implicit capture) + + if (captures >= UrlRewrite::MAX_REGEX_SUBS) { Warning("regex has %d capturing subpatterns (including entire regex); Max allowed: %d", captures + 1, UrlRewrite::MAX_REGEX_SUBS); - goto lFail; + goto fail; } to_host = new_mapping->toURL.host_get(); @@ -1044,7 +159,7 @@ process_regex_mapping_config(const char *from_host_lower, url_mapping *new_mappi substitution_id = to_host[i + 1] - '0'; if ((substitution_id < 0) || (substitution_id > captures)) { Warning("Substitution id [%c] has no corresponding capture pattern in regex [%s]", to_host[i + 1], from_host_lower); - goto lFail; + goto fail; } reg_map->substitution_markers[reg_map->n_substitutions] = i; reg_map->substitution_ids[reg_map->n_substitutions] = substitution_id; @@ -1052,520 +167,16 @@ process_regex_mapping_config(const char *from_host_lower, url_mapping *new_mappi } } - // so the regex itself is stored in fromURL.host; string to match - // will be in the request; string to use for substitutions will be - // in this buffer reg_map->to_url_host_template_len = to_host_len; reg_map->to_url_host_template = static_cast(ats_malloc(to_host_len)); memcpy(reg_map->to_url_host_template, to_host.data(), to_host_len); return true; -lFail: +fail: ats_free(reg_map->to_url_host_template); reg_map->to_url_host_template = nullptr; reg_map->to_url_host_template_len = 0; return false; } - -bool -remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti) -{ - char errBuf[1024]; - char errStrBuf[1024]; - const char *errStr; - - Tokenizer whiteTok(" \t"); - - // Vars to parse line in file - char *tok_state, *cur_line, *cur_line_tmp; - int cur_line_size, cln = 0; // Our current line number - ParseResult rparse; - - // Vars to build the mapping - std::string_view fromScheme{}, toScheme{}; - std::string_view fromHost{}, toHost{}; - char *map_from, *map_from_start; - char *map_to, *map_to_start; - const char *tmp; // Appease the DEC compiler - char *fromHost_lower = nullptr; - char *fromHost_lower_ptr = nullptr; - char fromHost_lower_buf[1024]; - url_mapping *new_mapping = nullptr; - mapping_type maptype; - referer_info *ri; - int origLength; - int length; - int tok_count; - - UrlRewrite::RegexMapping *reg_map; - bool is_cur_mapping_regex; - const char *type_id_str; - - std::error_code ec; - std::string content{swoc::file::load(swoc::file::path{path}, ec)}; - if (ec.value() == ENOENT) { // a missing file is ok - treat as empty, no rules. - return true; - } - if (ec.value()) { - Warning("Failed to open remapping configuration file %s - %s", path, strerror(ec.value())); - return false; - } - - 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; - - for (cur_line = tokLine(content.data(), &tok_state, '\\'); cur_line != nullptr;) { - reg_map = nullptr; - new_mapping = nullptr; - errStrBuf[0] = 0; - bti->reset(); - - // Strip leading whitespace - while (*cur_line && isascii(*cur_line) && isspace(*cur_line)) { - ++cur_line; - } - - if ((cur_line_size = strlen(cur_line)) <= 0) { - cur_line = tokLine(nullptr, &tok_state, '\\'); - ++cln; - continue; - } - - // Strip trailing whitespace - cur_line_tmp = cur_line + cur_line_size - 1; - while (cur_line_tmp != cur_line && isascii(*cur_line_tmp) && isspace(*cur_line_tmp)) { - *cur_line_tmp = '\0'; - --cur_line_tmp; - } - - if (strlen(cur_line) <= 0 || *cur_line == '#' || *cur_line == '\0') { - cur_line = tokLine(nullptr, &tok_state, '\\'); - ++cln; - continue; - } - - Dbg(dbg_ctl_url_rewrite, "[BuildTable] Parsing: \"%s\"", cur_line); - - tok_count = whiteTok.Initialize(cur_line, (SHARE_TOKS | ALLOW_SPACES)); - - for (int j = 0; j < tok_count; j++) { - if ((const_cast(whiteTok[j]))[0] == '@') { - if ((const_cast(whiteTok[j]))[1]) { - bti->argv[bti->argc++] = ats_strdup(&(((char *)whiteTok[j])[1])); - } - } else { - bti->paramv[bti->paramc++] = ats_strdup((char *)whiteTok[j]); - } - } - - // Initial verification for number of arguments - if (bti->paramc < 1 || (bti->paramc < 3 && bti->paramv[0][0] != '.') || bti->paramc > BUILD_TABLE_MAX_ARGS) { - snprintf(errStrBuf, sizeof(errStrBuf), "malformed line %d in file %s", cln + 1, path); - errStr = errStrBuf; - goto MAP_ERROR; - } - // just check all major flags/optional arguments - bti->remap_optflg = remap_check_option(bti->argv, bti->argc); - - // Check directive keywords (starting from '.') - if (bti->paramv[0][0] == '.') { - if ((errStr = remap_parse_directive(bti, errBuf, sizeof(errBuf))) != nullptr) { - snprintf(errStrBuf, sizeof(errStrBuf), "error on line %d - %s", cln + 1, errStr); - errStr = errStrBuf; - goto MAP_ERROR; - } - // We skip the rest of the parsing here. - cur_line = tokLine(nullptr, &tok_state, '\\'); - ++cln; - continue; - } - - is_cur_mapping_regex = (strncasecmp("regex_", bti->paramv[0], 6) == 0); - type_id_str = is_cur_mapping_regex ? (bti->paramv[0] + 6) : bti->paramv[0]; - - // Check to see whether is a reverse or forward mapping - maptype = get_mapping_type(type_id_str, bti); - if (maptype == mapping_type::NONE) { - snprintf(errStrBuf, sizeof(errStrBuf), "unknown mapping type at line %d", cln + 1); - errStr = errStrBuf; - goto MAP_ERROR; - } - - new_mapping = new url_mapping(); - - // apply filter rules if we have to - if (process_filter_opt(new_mapping, bti, errStrBuf, sizeof(errStrBuf)) != nullptr) { - errStr = errStrBuf; - 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 ((bti->remap_optflg & REMAP_OPTFLG_MAP_ID) != 0) { - int idx = 0; - int ret = remap_check_option(bti->argv, bti->argc, REMAP_OPTFLG_MAP_ID, &idx); - - if (ret & REMAP_OPTFLG_MAP_ID) { - char *c = strchr(bti->argv[idx], static_cast('=')); - - new_mapping->map_id = static_cast(atoi(++c)); - } - } - - // Parse @volume= option with comma-separated syntax (@volume=3,4) - for (int i = 0; i < bti->argc; i++) { - if (!strncasecmp(bti->argv[i], "volume=", 7)) { - const char *volume_str = &bti->argv[i][7]; - - if (!volume_str || !*volume_str) { - snprintf(errStrBuf, sizeof(errStrBuf), "Empty @volume= directive at line %d", cln + 1); - errStr = errStrBuf; - goto MAP_ERROR; - } - - { - swoc::TextView vol_list{volume_str}; - - if (vol_list.back() == ',') { - snprintf(errStrBuf, sizeof(errStrBuf), "Invalid @volume=%s at line %d (trailing comma)", volume_str, cln + 1); - errStr = errStrBuf; - goto MAP_ERROR; - } - while (!vol_list.empty()) { - swoc::TextView span; - swoc::TextView token{vol_list.take_prefix_at(',')}; - auto n = swoc::svtoi(token, &span); - - if (span.size() != token.size() || token.empty()) { - snprintf(errStrBuf, sizeof(errStrBuf), "Invalid @volume=%s at line %d (expected comma-separated numbers 1-255)", - volume_str, cln + 1); - errStr = errStrBuf; - goto MAP_ERROR; - } else if (n < 1 || n > 255) { - snprintf(errStrBuf, sizeof(errStrBuf), "Volume number %jd out of range (1-255) in @volume=%s at line %d", n, - volume_str, cln + 1); - errStr = errStrBuf; - goto MAP_ERROR; - } - } - } - - // Check if cache is ready (will be true during config reload, possibly false during initial startup) - if (CacheProcessor::IsCacheEnabled() == CacheInitState::INITIALIZED) { - char volume_errbuf[256]; - CacheHostRecord *rec = createCacheHostRecord(volume_str, volume_errbuf, sizeof(volume_errbuf)); - - if (!rec) { - snprintf(errStrBuf, sizeof(errStrBuf), "Failed to build volume record for @volume=%s at line %d: %s", volume_str, - cln + 1, volume_errbuf); - errStr = errStrBuf; - goto MAP_ERROR; - } - new_mapping->volume_host_rec.store(rec, std::memory_order_release); - Dbg(dbg_ctl_url_rewrite, "[BuildTable] Cache volume directive built: @volume=%s", volume_str); - } else { - // Store the volume string for lazy initialization after cache is ready - new_mapping->setVolume(volume_str); - Dbg(dbg_ctl_url_rewrite, "[BuildTable] Cache volume directive stored (deferred): @volume=%s", volume_str); - } - break; - } - } - - map_from = bti->paramv[1]; - length = UrlWhack(map_from, &origLength); - - // FIX --- what does this comment mean? - // - // URL::create modified map_from so keep a point to - // the beginning of the string - if ((tmp = (map_from_start = map_from)) != nullptr && length > 2 && tmp[length - 1] == '/' && tmp[length - 2] == '/') { - new_mapping->unique = true; - length -= 2; - } - - new_mapping->fromURL.create(nullptr); - rparse = new_mapping->fromURL.parse_regex(tmp, length); - - map_from_start[origLength] = '\0'; // Unwhack - - if (rparse != ParseResult::DONE) { - snprintf(errStrBuf, sizeof(errStrBuf), "malformed From URL: %.*s", length, tmp); - errStr = errStrBuf; - goto MAP_ERROR; - } - - map_to = bti->paramv[2]; - length = UrlWhack(map_to, &origLength); - map_to_start = map_to; - tmp = map_to; - - new_mapping->toURL.create(nullptr); - rparse = new_mapping->toURL.parse_no_host_check(std::string_view(tmp, length)); - map_to_start[origLength] = '\0'; // Unwhack - - if (rparse != ParseResult::DONE) { - snprintf(errStrBuf, sizeof(errStrBuf), "malformed To URL: %.*s", length, tmp); - errStr = errStrBuf; - goto MAP_ERROR; - } - - fromScheme = new_mapping->fromURL.scheme_get(); - // If the rule is "/" or just some other relative path - // we need to default the scheme to http - if (fromScheme.empty()) { - new_mapping->fromURL.scheme_set(std::string_view{URL_SCHEME_HTTP}); - fromScheme = new_mapping->fromURL.scheme_get(); - new_mapping->wildcard_from_scheme = true; - } - toScheme = new_mapping->toURL.scheme_get(); - - errStr = is_valid_scheme(fromScheme, toScheme); - if (errStr != nullptr) { - goto MAP_ERROR; - } - - // Check if a tag is specified. - if (bti->paramv[3] != nullptr) { - if (maptype == mapping_type::FORWARD_MAP_REFERER) { - new_mapping->filter_redirect_url = ats_strdup(bti->paramv[3]); - if (!strcasecmp(bti->paramv[3], "") || !strcasecmp(bti->paramv[3], "default") || - !strcasecmp(bti->paramv[3], "") || !strcasecmp(bti->paramv[3], "default_redirect_url")) { - new_mapping->default_redirect_url = true; - } - new_mapping->redir_chunk_list = redirect_tag_str::parse_format_redirect_url(bti->paramv[3]); - for (int j = bti->paramc; j > 4; j--) { - if (bti->paramv[j - 1] != nullptr) { - char refinfo_error_buf[1024]; - bool refinfo_error = false; - - ri = new referer_info(bti->paramv[j - 1], &refinfo_error, refinfo_error_buf, sizeof(refinfo_error_buf)); - if (refinfo_error) { - snprintf(errStrBuf, sizeof(errStrBuf), "%s Incorrect Referer regular expression \"%s\" at line %d - %s", modulePrefix, - bti->paramv[j - 1], cln + 1, refinfo_error_buf); - Error("%s", errStrBuf); - delete ri; - ri = nullptr; - } - - if (ri && ri->negative) { - if (ri->any) { - new_mapping->optional_referer = true; /* referer header is optional */ - delete ri; - ri = nullptr; - } else { - new_mapping->negative_referer = true; /* we have negative referer in list */ - } - } - if (ri) { - ri->next = new_mapping->referer_list; - new_mapping->referer_list = ri; - } - } - } - } else { - new_mapping->tag = ats_strdup(&(bti->paramv[3][0])); - } - } - - // 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) { - if (*map_from_start != '/') { - 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(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()}); - - reg_map = nullptr; - if (is_cur_mapping_regex) { - reg_map = new UrlRewrite::RegexMapping(); - if (!process_regex_mapping_config(fromHost_lower, new_mapping, reg_map)) { - errStr = "could not process regex mapping config line"; - goto MAP_ERROR; - } - Dbg(dbg_ctl_url_rewrite_regex, "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 rule like "map tunnel://hostname..." - // in remap.config, 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->paramv[3] != nullptr) { - u_mapping->tag = ats_strdup(&(bti->paramv[3][0])); - } - - 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 ((bti->remap_optflg & REMAP_OPTFLG_STRATEGY) != 0 && - (maptype == mapping_type::FORWARD_MAP || maptype == mapping_type::FORWARD_MAP_REFERER || - maptype == mapping_type::FORWARD_MAP_WITH_RECV_PORT)) { - const char *strategy = strchr(bti->argv[0], static_cast('=')); - if (strategy == nullptr) { - errStr = "missing 'strategy' name argument, unable to add mapping rule"; - goto MAP_ERROR; - } else { - strategy++; - new_mapping->strategy = bti->rewrite->strategyFactory->strategyInstance(strategy); - if (!new_mapping->strategy) { - snprintf(errStrBuf, sizeof(errStrBuf), "no strategy named '%s' is defined in the config", strategy); - errStr = errStrBuf; - goto MAP_ERROR; - } - Dbg(dbg_ctl_url_rewrite_regex, "mapped the 'strategy' named %s", strategy); - } - } - - // Check "remap" plugin options and load .so object - if ((bti->remap_optflg & REMAP_OPTFLG_PLUGIN) != 0 && - (maptype == mapping_type::FORWARD_MAP || maptype == mapping_type::FORWARD_MAP_REFERER || - maptype == mapping_type::FORWARD_MAP_WITH_RECV_PORT)) { - if ((remap_check_option(bti->argv, bti->argc, REMAP_OPTFLG_PLUGIN, &tok_count) & REMAP_OPTFLG_PLUGIN) != 0) { - int plugin_found_at = 0; - int jump_to_argc = 0; - - // this loads the first plugin - if (!remap_load_plugin(bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), 0, &plugin_found_at, - bti->rewrite)) { - Dbg(dbg_ctl_remap_plugin, "Remap plugin load error - %s", errStrBuf[0] ? errStrBuf : "Unknown error"); - errStr = errStrBuf; - goto MAP_ERROR; - } - // this loads any subsequent plugins (if present) - while (plugin_found_at) { - jump_to_argc += plugin_found_at; - if (!remap_load_plugin(bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), jump_to_argc, &plugin_found_at, - bti->rewrite)) { - Dbg(dbg_ctl_remap_plugin, "Remap plugin load error - %s", errStrBuf[0] ? errStrBuf : "Unknown error"); - errStr = errStrBuf; - goto MAP_ERROR; - } - } - } - } - - // Now add the mapping to appropriate container - if (!bti->rewrite->InsertMapping(maptype, new_mapping, reg_map, fromHost_lower, is_cur_mapping_regex)) { - errStr = "unable to add mapping rule to lookup table"; - goto MAP_ERROR; - } - - fromHost_lower_ptr = static_cast(ats_free_null(fromHost_lower_ptr)); - - cur_line = tokLine(nullptr, &tok_state, '\\'); - ++cln; - continue; - - // Deal with error / warning scenarios - MAP_ERROR: - - snprintf(errBuf, sizeof(errBuf), "%s failed to add remap rule at %s line %d: %s", modulePrefix, path, cln + 1, errStr); - Error("%s", errBuf); - - delete reg_map; - delete new_mapping; - return false; - } /* end of while(cur_line != nullptr) */ - - IpAllow::enableAcceptCheck(bti->accept_check_p); - return true; -} - -bool -remap_parse_config(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_config_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; -} diff --git a/src/proxy/http/remap/RemapYamlConfig.cc b/src/proxy/http/remap/RemapYamlConfig.cc index 04070030e76..c77aae83ee8 100644 --- a/src/proxy/http/remap/RemapYamlConfig.cc +++ b/src/proxy/http/remap/RemapYamlConfig.cc @@ -40,16 +40,80 @@ #include "swoc/bwf_ex.h" #include "swoc/swoc_file.h" +#include "config/remap.h" +#include "iocore/cache/Cache.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" +extern CacheHostRecord *createCacheHostRecord(const char *volume_str, char *errbuf, size_t errbufsize); + namespace { DbgCtl dbg_ctl_remap_yaml{"remap_yaml"}; DbgCtl dbg_ctl_url_rewrite{"url_rewrite"}; + +swoc::Errata +parse_yaml_volume(const YAML::Node &node, url_mapping *url_mapping) +{ + if (!url_mapping) { + return swoc::Errata("invalid url_mapping for volume"); + } + + std::string volume_str; + + auto append_volume = [&](int volume) -> swoc::Errata { + if (volume < 1 || volume > 255) { + return swoc::Errata("volume number {} out of range (1-255)", volume); + } + if (!volume_str.empty()) { + volume_str.append(","); + } + volume_str.append(std::to_string(volume)); + return {}; + }; + + if (node.IsScalar()) { + try { + return append_volume(node.as()); + } catch (std::exception const &) { + return swoc::Errata("invalid volume value '{}'", node.Scalar()); + } + } + + if (node.IsSequence()) { + for (auto const &item : node) { + if (!item.IsScalar()) { + return swoc::Errata("volume sequence entries must be scalars"); + } + try { + auto errata = append_volume(item.as()); + if (!errata.is_ok()) { + return errata; + } + } catch (std::exception const &) { + return swoc::Errata("invalid volume value '{}'", item.Scalar()); + } + } + } else { + return swoc::Errata("volume must be a scalar or sequence"); + } + + if (CacheProcessor::IsCacheEnabled() == CacheInitState::INITIALIZED) { + char volume_errbuf[256]; + CacheHostRecord *rec = createCacheHostRecord(volume_str.c_str(), volume_errbuf, sizeof(volume_errbuf)); + if (!rec) { + return swoc::Errata("failed to build volume record for volume={}: {}", volume_str, volume_errbuf); + } + url_mapping->volume_host_rec.store(rec, std::memory_order_release); + } else { + url_mapping->setVolume(volume_str.c_str()); + } + + return {}; +} } // end anonymous namespace swoc::Errata @@ -753,7 +817,7 @@ parse_yaml_remap_rule(const YAML::Node &node, BUILD_TABLE_INFO *bti) 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); + maptype = get_mapping_type(type_id_str); if (maptype == mapping_type::NONE) { return swoc::Errata("unknown mapping type: {}", type_str); } @@ -770,11 +834,27 @@ parse_yaml_remap_rule(const YAML::Node &node, BUILD_TABLE_INFO *bti) // update sticky flag bti->accept_check_p = bti->accept_check_p && bti->ip_allow_check_enabled_p; + if (node["unique"]) { + new_mapping->unique = node["unique"].as(); + } + new_mapping->map_id = 0; if (node["mapid"]) { new_mapping->map_id = node["mapid"].as(); } + if (node["tag"]) { + new_mapping->tag = ats_strdup(node["tag"].as().c_str()); + } + + if (node["volume"]) { + errata = parse_yaml_volume(node["volume"], new_mapping.get()); + if (!errata.is_ok()) { + swoc::bwprint(errStr, "invalid volume: {}", errata); + goto MAP_ERROR; + } + } + // Parse from URL if (!node["from"]) { errStr = "remap rule missing 'from' field"; @@ -967,7 +1047,21 @@ 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); + config::RemapParser parser; + auto parse_result = parser.parse(path); + YAML::Node config = parse_result.value; + + if (parse_result.file_not_found) { + Dbg(dbg_ctl_remap_yaml, "Missing YAML config file"); + return true; + } + + if (!parse_result.ok()) { + std::string const error_text = + parse_result.errata.empty() ? "unknown error" : std::string(parse_result.errata.front().text()); + Error("Failed to parse YAML config file %s: %s", path, error_text.c_str()); + return false; + } if (config.IsNull()) { Dbg(dbg_ctl_remap_yaml, "Empty YAML config file"); diff --git a/src/proxy/http/remap/UrlRewrite.cc b/src/proxy/http/remap/UrlRewrite.cc index 5885c8cf212..8190eebb743 100644 --- a/src/proxy/http/remap/UrlRewrite.cc +++ b/src/proxy/http/remap/UrlRewrite.cc @@ -847,12 +847,7 @@ UrlRewrite::BuildTable(const char *path) temporary_redirects.hash_lookup.reset(new URLTable); forward_mappings_with_recv_port.hash_lookup.reset(new URLTable); - bool parse_success; - if (is_remap_yaml()) { - parse_success = remap_parse_yaml(path, this); - } else { - parse_success = remap_parse_config(path, this); - } + bool parse_success = remap_parse_yaml(path, this); if (!parse_success) { return TS_ERROR; @@ -1116,18 +1111,15 @@ UrlRewrite::_destroyList(RegexMappingList &mappings) * Convert a YAML rule type string to a mapping_type enum. */ mapping_type -get_mapping_type(const char *type_str, BUILD_TABLE_INFO *bti) +get_mapping_type(const char *type_str) { // Check to see whether is a reverse or forward mapping if (!strcasecmp("reverse_map", type_str)) { Dbg(dbg_ctl_url_rewrite, "[BuildTable] - mapping_type::REVERSE_MAP"); return mapping_type::REVERSE_MAP; } else if (!strcasecmp("map", type_str)) { - Dbg(dbg_ctl_url_rewrite, "[BuildTable] - %s", - ((bti->remap_optflg & REMAP_OPTFLG_MAP_WITH_REFERER) == 0) ? "mapping_type::FORWARD_MAP" : - "mapping_type::FORWARD_MAP_REFERER"); - return ((bti->remap_optflg & REMAP_OPTFLG_MAP_WITH_REFERER) == 0) ? mapping_type::FORWARD_MAP : - mapping_type::FORWARD_MAP_REFERER; + Dbg(dbg_ctl_url_rewrite, "[BuildTable] - mapping_type::FORWARD_MAP"); + return mapping_type::FORWARD_MAP; } else if (!strcasecmp("redirect", type_str)) { Dbg(dbg_ctl_url_rewrite, "[BuildTable] - mapping_type::PERMANENT_REDIRECT"); return mapping_type::PERMANENT_REDIRECT; diff --git a/src/proxy/http/remap/unit-tests/test_RemapRules.cc b/src/proxy/http/remap/unit-tests/test_RemapRules.cc index 012e8b55bd7..311a272487a 100644 --- a/src/proxy/http/remap/unit-tests/test_RemapRules.cc +++ b/src/proxy/http/remap/unit-tests/test_RemapRules.cc @@ -29,6 +29,7 @@ #include "proxy/hdrs/HdrHeap.h" #include "proxy/http/remap/RemapConfig.h" +#include "proxy/http/remap/RemapYamlConfig.h" #include "proxy/http/remap/UrlMapping.h" #include "proxy/http/remap/UrlRewrite.h" #include "records/RecordsConfig.h" @@ -101,7 +102,7 @@ SCENARIO("Parsing ACL named filters", "[proxy][remap]") auto cpath = write_test_remap(config, "test2"); THEN("The remap parse fails with an error") { - REQUIRE(remap_parse_config_bti(cpath.c_str(), &bti) == false); + REQUIRE(remap_parse_yaml_bti(cpath.c_str(), &bti) == false); } } @@ -114,7 +115,7 @@ SCENARIO("Parsing ACL named filters", "[proxy][remap]") auto cpath = write_test_remap(config, "test2"); THEN("The rule uses the last action specified") { - REQUIRE(remap_parse_config_bti(cpath.c_str(), &bti) == true); + REQUIRE(remap_parse_yaml_bti(cpath.c_str(), &bti) == true); REQUIRE((bti.rules_list != nullptr && bti.rules_list->next == nullptr)); REQUIRE((bti.rules_list != nullptr && bti.rules_list->allow_flag == true)); } diff --git a/src/proxy/http/remap/unit-tests/test_RemapRulesYaml.cc b/src/proxy/http/remap/unit-tests/test_RemapRulesYaml.cc index 513982370c4..afca314f819 100644 --- a/src/proxy/http/remap/unit-tests/test_RemapRulesYaml.cc +++ b/src/proxy/http/remap/unit-tests/test_RemapRulesYaml.cc @@ -259,4 +259,43 @@ SCENARIO("Parsing UrlRewrite", "[proxy][remap]") REQUIRE(urlrw->forwardMappingWithRecvPortLookup(&url.url, 0, host, strlen(host), urlmap) == false); } } + GIVEN("YAML remap rules with unique, tag, and volume metadata") + { + std::unique_ptr urlrw = std::make_unique(); + urlrw->set_remap_yaml(true); + + std::string config = R"RMCFG( + remap: + - type: map + unique: true + tag: edge-tag + volume: + - 3 + - 4 + from: + url: http://meta.example.com + to: + url: http://127.0.0.1:8080 + )RMCFG"; + + auto cpath = write_test_remap(config, "yaml-metadata"); + ts::PostScript file_cleanup([&]() -> void { std::filesystem::remove(cpath.c_str()); }); + int rc = urlrw->BuildTable(cpath.c_str()); + EasyURL url("http://meta.example.com"); + const char *host = "meta.example.com"; + + THEN("the metadata is applied to the loaded rule") + { + REQUIRE(rc == TS_SUCCESS); + REQUIRE(urlrw->rule_count() == 1); + + UrlMappingContainer urlmap; + REQUIRE(urlrw->forwardMappingLookup(&url.url, 80, host, strlen(host), urlmap)); + REQUIRE(urlmap.getMapping() != nullptr); + CHECK(urlmap.getMapping()->unique); + REQUIRE(urlmap.getMapping()->tag != nullptr); + CHECK(std::string{urlmap.getMapping()->tag} == "edge-tag"); + CHECK(urlmap.getMapping()->getVolume() == "3,4"); + } + } } diff --git a/src/traffic_ctl/ConvertConfigCommand.cc b/src/traffic_ctl/ConvertConfigCommand.cc index 6bb0415caee..792c6215135 100644 --- a/src/traffic_ctl/ConvertConfigCommand.cc +++ b/src/traffic_ctl/ConvertConfigCommand.cc @@ -22,6 +22,7 @@ */ #include "ConvertConfigCommand.h" +#include "config/remap.h" #include "config/ssl_multicert.h" #include "config/storage.h" @@ -50,11 +51,57 @@ ConvertConfigCommand::ConvertConfigCommand(ts::Arguments *args) : CtrlCommand(ar _volume_config_file = convert_args[1]; _output_file = convert_args[2]; _invoked_func = [this]() { convert_storage(); }; + } else if (args->get("remap")) { + auto const &convert_args = args->get("remap"); + if (convert_args.size() < 2) { + throw std::invalid_argument("remap requires "); + } + _input_file = convert_args[0]; + _output_file = convert_args[1]; + _invoked_func = [this]() { convert_remap(); }; } else { throw std::invalid_argument("Unsupported config type for conversion"); } } +void +ConvertConfigCommand::convert_remap() +{ + config::RemapParser parser; + config::ConfigResult result = parser.parse(_input_file); + + if (result.file_not_found) { + _printer->write_output("Failed to parse input file '" + _input_file + "': file not found"); + return; + } + + if (!result.ok()) { + std::string error_msg = "Failed to parse input file '" + _input_file + "'"; + if (!result.errata.empty()) { + error_msg += ": "; + error_msg += std::string(result.errata.front().text()); + } + _printer->write_output(error_msg); + return; + } + + config::RemapMarshaller marshaller; + std::string const yaml_output = marshaller.to_yaml(result.value); + + if (_output_file == "-") { + std::cout << yaml_output << '\n'; + } else { + std::ofstream out(_output_file); + if (!out) { + _printer->write_output("Failed to open output file '" + _output_file + "' for writing"); + return; + } + out << yaml_output << '\n'; + out.close(); + _printer->write_output("Converted " + _input_file + " -> " + _output_file); + } +} + void ConvertConfigCommand::convert_ssl_multicert() { diff --git a/src/traffic_ctl/ConvertConfigCommand.h b/src/traffic_ctl/ConvertConfigCommand.h index fd6032f826a..ec4a96264cb 100644 --- a/src/traffic_ctl/ConvertConfigCommand.h +++ b/src/traffic_ctl/ConvertConfigCommand.h @@ -29,7 +29,7 @@ * Command handler for configuration format conversion. * * Converts configuration files from legacy formats to YAML. - * Supports: ssl_multicert, storage + * Supports: ssl_multicert, storage, remap */ class ConvertConfigCommand : public CtrlCommand { @@ -42,6 +42,7 @@ class ConvertConfigCommand : public CtrlCommand ConvertConfigCommand(ts::Arguments *args); private: + void convert_remap(); void convert_ssl_multicert(); void convert_storage(); diff --git a/src/traffic_ctl/traffic_ctl.cc b/src/traffic_ctl/traffic_ctl.cc index 9ca08ef8ad8..fc26f147419 100644 --- a/src/traffic_ctl/traffic_ctl.cc +++ b/src/traffic_ctl/traffic_ctl.cc @@ -219,6 +219,10 @@ main([[maybe_unused]] int argc, const char **argv) .add_example_usage("traffic_ctl config convert storage ") .add_example_usage("traffic_ctl config convert storage storage.config volume.config storage.yaml") .add_example_usage("traffic_ctl config convert storage storage.config volume.config - # output to stdout"); + convert_command.add_command("remap", "Convert remap.config to remap.yaml", "", 2, Command_Execute) + .add_example_usage("traffic_ctl config convert remap ") + .add_example_usage("traffic_ctl config convert remap remap.config remap.yaml") + .add_example_usage("traffic_ctl config convert remap remap.config - # output to stdout"); // host commands host_command.add_command("status", "Get one or more host statuses", "", MORE_THAN_ZERO_ARG_N, Command_Execute) diff --git a/tests/gold_tests/traffic_ctl/convert_remap/convert_remap.test.py b/tests/gold_tests/traffic_ctl/convert_remap/convert_remap.test.py new file mode 100644 index 00000000000..3a0e18338bd --- /dev/null +++ b/tests/gold_tests/traffic_ctl/convert_remap/convert_remap.test.py @@ -0,0 +1,39 @@ +''' +Test the traffic_ctl config convert remap command. +''' +# 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. + +Test.Summary = 'Test traffic_ctl config convert remap command.' + +ts = Test.MakeATSProcess("ts", enable_cache=False) + +tr = Test.AddTestRun("Test legacy remap.config conversion to stdout") +tr.Setup.Copy('legacy_config/basic.config') +tr.Processes.Default.Command = 'traffic_ctl config convert remap basic.config -' +tr.Processes.Default.Streams.stdout = "gold/basic.yaml" +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Env = ts.Env +tr.Processes.Default.StartBefore(ts) +tr.StillRunningAfter = ts + +tr = Test.AddTestRun("Test legacy remap.config conversion to file") +tr.Setup.Copy('legacy_config/basic.config') +tr.Processes.Default.Command = 'traffic_ctl config convert remap basic.config generated.yaml > /dev/null && cat generated.yaml' +tr.Processes.Default.Streams.stdout = "gold/basic.yaml" +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Env = ts.Env +tr.StillRunningAfter = ts diff --git a/tests/gold_tests/traffic_ctl/convert_remap/gold/basic.yaml b/tests/gold_tests/traffic_ctl/convert_remap/gold/basic.yaml new file mode 100644 index 00000000000..d8a7db99dee --- /dev/null +++ b/tests/gold_tests/traffic_ctl/convert_remap/gold/basic.yaml @@ -0,0 +1,41 @@ +remap: + - define_filter: + deny_write: + method: + - PUT + - DELETE + action: deny + src_ip: 10.0.0.0/8 + - activate_filter: deny_write + - type: map + unique: true + from: + url: http://example.com + to: + url: http://origin.example.com + tag: edge-tag + strategy: my_strategy + mapid: 42 + volume: + - 3 + - 4 + acl_filter: + method: GET + src_ip: 192.0.2.10 + src_ip_invert: 198.51.100.0/24 + internal: true + plugins: + - name: conf_remap.so + params: + - proxy.config.foo=1 + - type: map_with_referer + from: + url: http://refer.example.com + to: + url: http://origin.example.com + redirect: + url: http://deny.example.com + regex: + - (.*[.])?allowed[.]com + - ~evil[.]example + - include: remap-extra.yaml diff --git a/tests/gold_tests/traffic_ctl/convert_remap/legacy_config/basic.config b/tests/gold_tests/traffic_ctl/convert_remap/legacy_config/basic.config new file mode 100644 index 00000000000..a8410454ef8 --- /dev/null +++ b/tests/gold_tests/traffic_ctl/convert_remap/legacy_config/basic.config @@ -0,0 +1,5 @@ +.definefilter deny_write @method=PUT @method=DELETE @action=deny @src_ip=10.0.0.0/8 +.activatefilter deny_write +map http://example.com// http://origin.example.com edge-tag @plugin=conf_remap.so @pparam=proxy.config.foo=1 @strategy=my_strategy @mapid=42 @volume=3,4 @method=GET @src_ip=192.0.2.10 @src_ip=~198.51.100.0/24 @internal +map_with_referer http://refer.example.com http://origin.example.com http://deny.example.com (.*[.])?allowed[.]com ~evil[.]example +.include remap-extra.yaml