diff --git a/configs/virtualhost.yaml.default b/configs/virtualhost.yaml.default new file mode 100644 index 00000000000..1731cb2b08f --- /dev/null +++ b/configs/virtualhost.yaml.default @@ -0,0 +1,27 @@ +# virtualhost.yaml +# +# This configuration file defines a virtual host that provides domain-scoped configs and +# remap rules, overriding global configs. +# +# Remap rule config flow: +# 1. Resolve to a single virtualhost +# A. Look through exact match virtualhost domains. If found, use virtualhost config. +# B. Look through wildcard virtualhost domains. If found, use virtualhost config. +# C. If no virtualhost config found, skip to 3. +# 2. Within virtualhost config, use virtualhost remap rules. +# A. Follow remap.yaml format rules. If found, use remap rule. (See remap.yaml for details) +# 3. If no virtualhost or remap rule found, use global remap rules +# +# Example: +# virtualhost: +# - id: example +# domains: +# - example.com +# - "*.com" # Only allow single left-most: "*.[domain]" format +# +# remap: +# - type: map +# from: +# url: http://example.com +# to: +# url: http://origin.example.com/ \ No newline at end of file diff --git a/doc/admin-guide/files/index.en.rst b/doc/admin-guide/files/index.en.rst index 38b1db9b41a..540f6a1c237 100644 --- a/doc/admin-guide/files/index.en.rst +++ b/doc/admin-guide/files/index.en.rst @@ -40,6 +40,7 @@ Configuration Files sni.yaml.en storage.yaml.en strategies.yaml.en + virtualhost.yaml.en jsonrpc.yaml.en :doc:`cache.config.en` @@ -93,6 +94,9 @@ Configuration Files :doc:`strategies.yaml.en` Configures NextHop strategies used with `remap.config` and replaces parent.config. +:doc:`virtualhost.yaml.en` + Defines configuration blocks that apply to a group of domains (virtualhosts). + :doc:`jsonrpc.yaml.en` Defines some of the configurable arguments of the jsonrpc endpoint. diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst index 6fdc5242750..28d061cd2b9 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -5766,3 +5766,10 @@ AIO ============ ====================================================================== Note: If you force the backend to use io_uring, you might experience failures with some (older, pre 5.4) kernel versions + +VirtualHost +=========== + +.. ts:cv:: CONFIG proxy.config.virtualhost.filename STRING virtualhost.yaml + + Sets the name of the :file:`virtualhost.yaml` file. \ No newline at end of file diff --git a/doc/admin-guide/files/virtualhost.yaml.en.rst b/doc/admin-guide/files/virtualhost.yaml.en.rst new file mode 100644 index 00000000000..b9dab213963 --- /dev/null +++ b/doc/admin-guide/files/virtualhost.yaml.en.rst @@ -0,0 +1,214 @@ + +.. 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:: ../../common.defs + +.. configfile:: virtualhost.yaml + +virtualhost.yaml +**************** + +The :file:`virtualhost.yaml` file defines configuration blocks that apply to a group of domains. +Each virtual host entry defines a set of domains and the remap rules associated with those domains. +Virtual host remap rules override global :file:`remap.yaml` rules but remain fully backward compatible +with existing configurations. If absent, ATS behaves exactly as before. + +Currently, this file only supports :file:`remap.yaml` overrides. Future versions will expand virtual +host support to additional configuration types (e.g. :file:`sni.yaml`, :file:`ssl_multicert.yaml`, +:file:`parent.config`, etc) + +By default this is named :file:`virtualhost.yaml`. The filename can be changed by setting +:ts:cv:`proxy.config.virtualhost.filename`. + + +Configuration +============= + +:file:`virtualhost.yaml` is YAML format with top level namespace **virtualhost** and a list of virtual host +entries. Each virtual host entry must provide an **id** and at least one domain defined in **domains**. + +An example configuration looks like: + +.. code-block:: yaml + + virtualhost: + - id: example + domains: + - example.com + + remap: + - type: map + from: + url: http://example.com + to: + url: http://origin.example.com/ + + +===================== ========================================================== +Field Name Description +===================== ========================================================== +``id`` Virtual host identifier to perform specific operations on +``domains`` List of domains to resolve a request to +``remap`` List of remap rules as defined in remap.yaml +===================== ========================================================== + +``domains`` + Domains can be defined as request domain name or subdomains using wildcard feature. + Wildcard support only allows single left most ``*``. This does not support regex. + When matching to a virtual host entry, domains with exact match have precedence + over wildcard. If a domain matches to multiple wildcard domains, the virtual host + config defined first has precedence. + + For example: + Supported: + - ``foo.example.com`` + - ``*.example.com`` + - ``*.com`` + + NOT Supported: + - ``foo[0-9]+.example.com`` (regex) + - ``bar.*.example.net`` (``*`` in the middle) + - ``*.bar.*.com`` (multiple ``*``) + - ``*.*.baz.com`` (multiple ``*``) + - ``baz*.example.net`` (partial wildcard) + - ``*baz.example.net`` (partial wildcard) + - ``b*z.example.net`` (partial wildcard) + - ``*`` (global) + +Evaluation Order +---------------- + +|TS| evaluates a request using deterministic precedence in the following order: + +1. Resolve to a single virtualhost + a. Check for an exact domain match. If any virtual host lists the request hostname explicitly, that virtual host is selected. + b. Check for a wildcard domain match. If any virtual host wildcard domains define a subdomain of the request hostname in the form ``*.[domain]``, that virtual host is selected. + c. If no matching virtual host exists, the request proceeds using global configuration (i.e :file:`remap.config`). Skip to step 3. +2. Within selected virtual host config, use virtual host remap rules. + a. Follow existing :file:`remap.yaml` rules and matching orders. If a matching remap rule is found, that remap rule is selected. +3. If neither virtual host nor remap rules match, ATS falls back to global :file:`remap.yaml` resolution. + +Only one virtual host entry may match a given request. If multiple entries could match, ATS uses the first matching +entry defined in :file:`virtualhost.yaml`. + + +Granular Reload +=============== + +|TS| now supports granular configuration reloads for individual virtual hosts defined in :file:`virtualhost.yaml`. +In addition to reloading the entire |TS| configuration with :option:`traffic_ctl config reload`, users can +selectively reload a single virtual host entry without affecting other virtual host entries. + +By only updating the necessary changes, this reduces configuration deployment time and improves visibility on the changes made. + +To reload for a specific virtual host, use: + +:: + + $ traffic_ctl config reload --virtualhost + +Where **** is the virtual host ID defined in :file:`virtualhost.yaml`. Only the **** virtual host +configuration will be reloaded. This does not affect other virtual hosts or global configuration files. + +Example: + +:: + + $ traffic_ctl config reload --virtualhost foo + ┌ Virtualhost: foo + └┬ Reload status: ok + ├ Message: Virtualhost successfully reloaded + + +Examples +======== + +.. code-block:: yaml + + # virtualhost.yaml + virtualhost: + - id: example + domains: + - example.com + + remap: + - type: map + from: + url: http://example.com + to: + url: http://origin.example.com/ + + # remap.yaml + remap: + - type: map + from: + url: http://example.com + to: + url: http://origin.example.com/ + +This rules translates in the following translation. + +================================================ ======================================================== +Client Request Translated Request +================================================ ======================================================== +``http://example.com/index.html`` ``http://origin.example.com/index.html`` +``http://www.x.com/index.html`` ``http://other.example.com/index.html`` +================================================ ======================================================== + +.. code-block:: yaml + + # virtualhost.yaml + virtualhost: + - id: example + domains: + - "*.example.com" + + remap: + - type: regex_map + from: + url: http://sub[0-9]+.example.com/ + to: + url: http://origin$1.example.com/ + + + - id: foo + domains: + - foo.example.com + + remap: + - type: map + from: + url: http:/foo.example.com/ + to: + url: http://foo.origin.com/ + +This rules translates in the following translation. + +================================================ ======================================================== +Client Request Translated Request +================================================ ======================================================== +``http://sub0.example.com/index.html`` ``http://origin0.example.com/index.html`` +``http://foo.example.com/index.html`` ``http://foo.origin.com/index.html`` +``http://bar.example.com/index.html`` No remap rule found in virtual host entry `example` +================================================ ======================================================== + + +See Also +======== + +:file:`remap.yaml` \ No newline at end of file diff --git a/include/proxy/VirtualHost.h b/include/proxy/VirtualHost.h new file mode 100644 index 00000000000..dd84c75a707 --- /dev/null +++ b/include/proxy/VirtualHost.h @@ -0,0 +1,102 @@ +/** @file + Virtual Host configuration + @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 "iocore/eventsystem/ConfigProcessor.h" +#include "proxy/http/remap/UrlRewrite.h" +#include "tscore/Ptr.h" + +class VirtualHostConfig : public ConfigInfo +{ +public: + VirtualHostConfig() = default; + VirtualHostConfig(const VirtualHostConfig &other) + : _entries(other._entries), + _exact_domains_to_id(other._exact_domains_to_id), + _wildcard_domains_to_id(other._wildcard_domains_to_id) + { + } + VirtualHostConfig & + operator=(const VirtualHostConfig &other) + { + if (this != &other) { + _entries = other._entries; + _exact_domains_to_id = other._exact_domains_to_id; + _wildcard_domains_to_id = other._wildcard_domains_to_id; + } + return *this; + } + ~VirtualHostConfig() = default; + + struct Entry : public RefCountObjInHeap { + std::string id; + std::vector exact_domains; + std::vector wildcard_domains; + Ptr remap_table; + + Entry *acquire() const; + void release() const; + std::string get_id() const; + }; + + bool load(); + bool set_entry(std::string_view id, Ptr &entry); + static bool load_entry(std::string_view id, Ptr &entry); + Ptr find_by_id(std::string_view id) const; + Ptr find_by_domain(std::string_view domain) const; + +private: + using entry_map = std::unordered_map>; + using name_map = std::unordered_map; + + entry_map _entries; + name_map _exact_domains_to_id; + name_map _wildcard_domains_to_id; +}; + +class VirtualHost +{ +public: + using scoped_config = ConfigProcessor::scoped_config; + + static void startup(); + static int reconfigure(); + static int reconfigure(std::string_view id); + static VirtualHostConfig *acquire(); + static void release(VirtualHostConfig *config); + +private: + static int config_callback(const char *, RecDataT, RecData, void *); + static int _configid; +}; + +struct VirtualHostConfigContinuation : public Continuation { + VirtualHostConfigContinuation() : Continuation(nullptr) { SET_HANDLER(&VirtualHostConfigContinuation::reconfigure); } + + int + reconfigure(int /* event ATS_UNUSED */, Event * /* e ATS_UNUSED */) + { + VirtualHost::reconfigure(); + delete this; + return EVENT_DONE; + } +}; diff --git a/include/proxy/http/HttpSM.h b/include/proxy/http/HttpSM.h index fc3e1252452..6220e8187e5 100644 --- a/include/proxy/http/HttpSM.h +++ b/include/proxy/http/HttpSM.h @@ -45,6 +45,7 @@ #include "api/InkAPIInternal.h" #include "proxy/ProxyTransaction.h" #include "proxy/hdrs/HdrUtils.h" +#include "proxy/VirtualHost.h" // inknet #include "proxy/http/PreWarmManager.h" @@ -306,7 +307,8 @@ class HttpSM : public Continuation, public PluginUserArgs // This unfortunately can't go into the t_state, because of circular dependencies. We could perhaps refactor // this, with a lot of work, but this is easier for now. - UrlRewrite *m_remap = nullptr; + UrlRewrite *m_remap = nullptr; + VirtualHostConfig::Entry *m_virtualhost_entry = nullptr; History history; NetVConnection * @@ -364,6 +366,7 @@ class HttpSM : public Continuation, public PluginUserArgs // Y! ebalsa: remap handlers int state_remap_request(int event, void *data); + void set_virtualhost_entry(std::string_view domain); void do_remap_request(bool); // Cache Handlers diff --git a/include/proxy/http/remap/RemapYamlConfig.h b/include/proxy/http/remap/RemapYamlConfig.h index a5ec4919c0a..05cbd1949fd 100644 --- a/include/proxy/http/remap/RemapYamlConfig.h +++ b/include/proxy/http/remap/RemapYamlConfig.h @@ -70,4 +70,9 @@ swoc::Errata parse_yaml_remap_rule(const YAML::Node &node, BUILD_TABLE_INFO *bti // Parse remap YAML node bool remap_parse_yaml_bti(const char *path, BUILD_TABLE_INFO *bti); +// Parse remap YAML node from inline YAML node (for virtualhost) +bool remap_parse_yaml_bti(YAML::Node const *remap_node, BUILD_TABLE_INFO *bti); + +bool remap_parse_yaml(YAML::Node const *remap_node, UrlRewrite *rewrite); + bool remap_parse_yaml(const char *path, UrlRewrite *rewrite); diff --git a/include/proxy/http/remap/UrlRewrite.h b/include/proxy/http/remap/UrlRewrite.h index 2f4be91b479..b869a9fcfd4 100644 --- a/include/proxy/http/remap/UrlRewrite.h +++ b/include/proxy/http/remap/UrlRewrite.h @@ -79,12 +79,14 @@ class UrlRewrite : public RefCountObjInHeap */ bool load(); + bool load_table(const std::string &config_file_path, YAML::Node const *remap_node); + /** Build the internal url write tables. * * @param path Path to configuration file. * @return 0 on success, non-zero error code on failure. */ - int BuildTable(const char *path); + int BuildTable(const char *path, YAML::Node const *remap_node = nullptr); mapping_type Remap_redirect(HTTPHdr *request_header, URL *redirect_url); bool ReverseMap(HTTPHdr *response_header); diff --git a/include/tscore/Filenames.h b/include/tscore/Filenames.h index b36e282aebe..d828eda9218 100644 --- a/include/tscore/Filenames.h +++ b/include/tscore/Filenames.h @@ -44,6 +44,7 @@ namespace filename constexpr const char *SPLITDNS = "splitdns.config"; constexpr const char *SNI = "sni.yaml"; constexpr const char *JSONRPC = "jsonrpc.yaml"; + constexpr const char *VIRTUALHOST = "virtualhost.yaml"; /////////////////////////////////////////////////////////////////// // Various other file names diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt index 34874f380f1..7bc5058a540 100644 --- a/src/proxy/CMakeLists.txt +++ b/src/proxy/CMakeLists.txt @@ -36,6 +36,7 @@ add_library( Transform.cc FetchSM.cc PluginHttpConnect.cc + VirtualHost.cc ) add_library(ts::proxy ALIAS proxy) diff --git a/src/proxy/ReverseProxy.cc b/src/proxy/ReverseProxy.cc index a7add1f7967..f38b2637739 100644 --- a/src/proxy/ReverseProxy.cc +++ b/src/proxy/ReverseProxy.cc @@ -41,6 +41,7 @@ #include "proxy/http/remap/UrlRewrite.h" #include "proxy/http/remap/UrlMapping.h" #include "proxy/http/remap/UrlMappingPathIndex.h" +#include "proxy/VirtualHost.h" namespace { @@ -119,6 +120,8 @@ init_reverse_proxy() ink_assert(0 == config_reg.attach("remap_yaml", "proxy.config.http.referer_default_redirect")); RecRegisterConfigUpdateCb("proxy.config.reverse_proxy.enabled", url_rewrite_CB, (void *)REVERSE_CHANGED); + VirtualHost::startup(); + return 0; } diff --git a/src/proxy/VirtualHost.cc b/src/proxy/VirtualHost.cc new file mode 100644 index 00000000000..1919aeafad9 --- /dev/null +++ b/src/proxy/VirtualHost.cc @@ -0,0 +1,450 @@ +/** @file + + Virtual Host configuration 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 +#include +#include +#include +#include + +#include "proxy/VirtualHost.h" +#include "mgmt/config/ConfigRegistry.h" +#include "records/RecCore.h" +#include "tscore/Filenames.h" +#include "tsutil/Convert.h" + +namespace +{ +DbgCtl dbg_ctl_virtualhost("virtualhost"); +} + +int VirtualHost::_configid = 0; + +VirtualHostConfig::Entry * +VirtualHostConfig::Entry::acquire() const +{ + auto *self = const_cast(this); + if (self) { + self->refcount_inc(); + } + return self; +} + +void +VirtualHostConfig::Entry::release() const +{ + auto *self = const_cast(this); + if (self && self->refcount_dec() == 0) { + self->free(); + } +} + +std::string +VirtualHostConfig::Entry::get_id() const +{ + return id; +} + +std::set valid_vhost_keys = {"id", "domains", "remap"}; + +template <> struct YAML::convert { + static bool + decode(const YAML::Node &node, VirtualHostConfig::Entry &item) + { + for (const auto &elem : node) { + if (std::none_of(valid_vhost_keys.begin(), valid_vhost_keys.end(), + [&elem](const std::string &s) { return s == elem.first.as(); })) { + Warning("unsupported key '%s' in VirtualHost config", elem.first.as().c_str()); + } + } + + if (!node["id"]) { + Dbg(dbg_ctl_virtualhost, "Virtual host entry must provide `id`"); + return false; + } + item.id = node["id"].as(); + + auto domains = node["domains"]; + if (!domains || !domains.IsSequence() || domains.size() == 0) { + Dbg(dbg_ctl_virtualhost, "Virtual host entry must provide at least one domain in `domains` sequence"); + return false; + } + item.exact_domains.clear(); + item.wildcard_domains.clear(); + + for (const auto &it : domains) { + auto domain_entry = it.as(); + if (domain_entry.empty()) { + Dbg(dbg_ctl_virtualhost, "Virtual host entry can't have empty domain entry"); + return false; + } + char domain[TS_MAX_HOST_NAME_LEN + 1]; + ts::transform_lower(domain_entry, domain); + + // Check if domain is wildcard, prefixed with * + if (domain[0] == '*') { + const char *subdomain = index(domain, '*'); + if (subdomain && subdomain[1] == '.') { + item.wildcard_domains.push_back(subdomain + 2); + } else { + Dbg(dbg_ctl_virtualhost, "Virtual host wildcard entry must have '*.[domain]' format"); + } + } else { + item.exact_domains.push_back(domain); + } + } + + if (item.exact_domains.empty() && item.wildcard_domains.empty()) { + Dbg(dbg_ctl_virtualhost, "Virtual host entry must have at least one domain defined"); + return false; + } + + return true; + } +}; + +bool +build_virtualhost_entry(YAML::Node const &node, Ptr &entry) +{ + entry.clear(); + Ptr vhost = make_ptr(new VirtualHostConfig::Entry); + auto &conf = *vhost; + try { + if (!YAML::convert::decode(node, conf)) { + return false; + } + } catch (YAML::Exception const &ex) { + Dbg(dbg_ctl_virtualhost, "Failed to parse virtualhost entry"); + return false; + } + + // Build UrlRewrite table for remap rules + auto remap_node = node["remap"]; + if (remap_node) { + auto table = std::make_unique(); + if (!table->load_table(conf.id, &remap_node)) { + Dbg(dbg_ctl_virtualhost, "Failed to load remap rules for virtualhost entry"); + return false; + } + conf.remap_table = make_ptr(table.release()); + } + entry = std::move(vhost); + return true; +} + +bool +VirtualHostConfig::load() +{ + _entries.clear(); + std::string config_path = RecConfigReadConfigPath("proxy.config.virtualhost.filename", ts::filename::VIRTUALHOST); + + struct stat sbuf; + if (stat(config_path.c_str(), &sbuf) == -1 && errno == ENOENT) { + Warning("Virtualhost configuration '%s' doesn't exist", config_path.c_str()); + return true; + } + + try { + YAML::Node config = YAML::LoadFile(config_path); + if (config.IsNull()) { + Dbg(dbg_ctl_virtualhost, "Empty virtualhost config: %s", config_path.c_str()); + return true; + } + + config = config["virtualhost"]; + if (config.IsNull() || !config.IsSequence()) { + Dbg(dbg_ctl_virtualhost, "Expected toplevel 'virtualhost' key to be a sequence"); + return false; + } + + for (auto const &node : config) { + Ptr entry; + if (!build_virtualhost_entry(node, entry)) { + return false; + } + + std::string vhost_id{entry->id}; + if (_entries.contains(vhost_id)) { + Dbg(dbg_ctl_virtualhost, "Duplicate virtualhost id: %s", vhost_id.c_str()); + return false; + } + + for (auto const &domain : entry->exact_domains) { + if (_exact_domains_to_id.contains(domain)) { + Dbg(dbg_ctl_virtualhost, "Exact domain (%s) already in another virtualhost config", domain.c_str()); + return false; + } + _exact_domains_to_id.emplace(domain, vhost_id); + } + + for (auto const &domain_suffix : entry->wildcard_domains) { + if (_wildcard_domains_to_id.contains(domain_suffix)) { + Dbg(dbg_ctl_virtualhost, "Wildcard domain (%s) already in another virtualhost config", domain_suffix.c_str()); + return false; + } + _wildcard_domains_to_id.emplace(domain_suffix, vhost_id); + } + + _entries.emplace(vhost_id, std::move(entry)); + } + + } catch (std::exception &ex) { + Dbg(dbg_ctl_virtualhost, "Failed to load %s: %s", config_path.c_str(), ex.what()); + return false; + } + return true; +} + +bool +VirtualHostConfig::load_entry(std::string_view id, Ptr &entry) +{ + entry.clear(); + std::string config_path = RecConfigReadConfigPath("proxy.config.virtualhost.filename", ts::filename::VIRTUALHOST); + + try { + YAML::Node config = YAML::LoadFile(config_path); + if (config.IsNull()) { + Dbg(dbg_ctl_virtualhost, "Empty virtualhost config: %s", config_path.c_str()); + return true; + } + + config = config["virtualhost"]; + if (config.IsNull() || !config.IsSequence()) { + Dbg(dbg_ctl_virtualhost, "Expected toplevel 'virtualhost' key to be a sequence"); + return false; + } + + for (auto const &node : config) { + auto config_id = node["id"]; + if (!config_id || config_id.as() != id) { + continue; + } + + Ptr vhost_entry; + if (!build_virtualhost_entry(node, vhost_entry)) { + return false; + } + entry = std::move(vhost_entry); + return true; + } + + } catch (std::exception &ex) { + Dbg(dbg_ctl_virtualhost, "Failed to load virtualhost entry (%s) in %s: %s", id.data(), config_path.c_str(), ex.what()); + return false; + } + Dbg(dbg_ctl_virtualhost, "Virtualhost with id (%s) not found", id.data()); + return true; +} + +bool +VirtualHostConfig::set_entry(std::string_view id, Ptr &entry) +{ + std::string vhost_id{id}; + // If virtualhost entry already exists, remove current entry + if (auto it = _entries.find(vhost_id); it != _entries.end()) { + Ptr curr_entry = std::move(it->second); + for (auto const &domain : curr_entry->exact_domains) { + _exact_domains_to_id.erase(domain); + } + for (auto const &domain : curr_entry->wildcard_domains) { + _wildcard_domains_to_id.erase(domain); + } + _entries.erase(vhost_id); + } + + // Add new entry into virtualhost config + if (entry) { + for (auto const &domain : entry->exact_domains) { + if (_exact_domains_to_id.contains(domain)) { + Dbg(dbg_ctl_virtualhost, "Exact domain (%s) already in another virtualhost config", domain.c_str()); + return false; + } + _exact_domains_to_id.emplace(domain, vhost_id); + } + + for (auto const &domain_suffix : entry->wildcard_domains) { + if (_wildcard_domains_to_id.contains(domain_suffix)) { + Dbg(dbg_ctl_virtualhost, "Wildcard domain (%s) already in another virtualhost config", domain_suffix.c_str()); + return false; + } + _wildcard_domains_to_id.emplace(domain_suffix, vhost_id); + } + + _entries.emplace(vhost_id, std::move(entry)); + } + return true; +} + +Ptr +VirtualHostConfig::find_by_id(std::string_view id) const +{ + if (_entries.empty()) { + return Ptr(); + } + + auto entry = _entries.find(std::string{id}); + if (entry != _entries.end()) { + return entry->second; + } + return Ptr(); +} + +Ptr +VirtualHostConfig::find_by_domain(std::string_view domain) const +{ + if (_entries.empty() || domain.empty()) { + return Ptr(); + } + + char lower_domain[TS_MAX_HOST_NAME_LEN + 1]; + ts::transform_lower(std::string{domain}, lower_domain); + + // Check for exact match domains first + auto id = _exact_domains_to_id.find(lower_domain); + if (id != _exact_domains_to_id.end()) { + auto entry = _entries.find(id->second); + if (entry != _entries.end()) { + return entry->second; + } + } + + // Check wildcard suffixes + const char *subdomain = index(lower_domain, '.'); + while (subdomain) { + subdomain++; + if (auto suffix_id = _wildcard_domains_to_id.find(subdomain); suffix_id != _wildcard_domains_to_id.end()) { + auto entry = _entries.find(suffix_id->second); + if (entry != _entries.end()) { + return entry->second; + } + } + subdomain = index(subdomain, '.'); + } + + return Ptr(); +} + +void +VirtualHost::startup() +{ + if (!reconfigure()) { + Fatal("failed to load %s", ts::filename::VIRTUALHOST); + } + RecRegisterConfigUpdateCb("proxy.config.virtualhost.filename", &VirtualHost::config_callback, nullptr); + + config::ConfigRegistry::Get_Instance().register_config("virtualhost", // registry key + ts::filename::VIRTUALHOST, // default filename + "proxy.config.virtualhost.filename", // record holding the filename + [](ConfigContext ctx) { + ctx.in_progress(); + auto yaml = ctx.supplied_yaml(); + + // RPC-supplied scalar = single entry reload by ID + if (yaml && yaml.IsScalar()) { + std::string id = yaml.as(); + if (VirtualHost::reconfigure(id)) { + ctx.complete("Reloaded virtualhost entry: " + id); + } else { + ctx.fail("Failed to reload virtualhost entry: " + id); + } + return; + } + + // Full reload (file-based or no supplied content) + if (VirtualHost::reconfigure()) { + ctx.complete("Finished loading virtualhost config"); + } else { + ctx.fail("Failed to load virtualhost config"); + } + }, + config::ConfigSource::FileAndRpc, // supports RPC content + {"proxy.config.virtualhost.filename"}); // trigger records +} + +int +VirtualHost::reconfigure() +{ + Note("%s loading ...", ts::filename::VIRTUALHOST); + auto config = std::make_unique(); + + if (!config->load()) { + Error("%s failed to load", ts::filename::VIRTUALHOST); + return 0; + } + + _configid = configProcessor.set(_configid, config.release()); + + Note("%s finished loading", ts::filename::VIRTUALHOST); + return 1; +} + +int +VirtualHost::reconfigure(std::string_view id) +{ + VirtualHost::scoped_config vhost_config; + Dbg(dbg_ctl_virtualhost, "Reconfiguring virtualhost entry: %s", id.data()); + // Reconfigure all vhosts if id not specified + if (id.empty()) { + Dbg(dbg_ctl_virtualhost, "No virtualhost specified, reconfiguring all entries"); + return reconfigure(); + } + + Ptr entry; + if (!VirtualHostConfig::load_entry(id, entry)) { + return 0; + } + + std::unique_ptr config; + if (vhost_config) { + config = std::make_unique(*vhost_config); + } else { + config = std::make_unique(); + } + + if (!config->set_entry(id, entry)) { + return 0; + } + _configid = configProcessor.set(_configid, config.release()); + return 1; +} + +VirtualHostConfig * +VirtualHost::acquire() +{ + return static_cast(configProcessor.get(_configid)); +} + +void +VirtualHost::release(VirtualHostConfig *config) +{ + if (config && _configid > 0) { + configProcessor.release(_configid, config); + } +} + +int +VirtualHost::config_callback(const char *, RecDataT, RecData, void *) +{ + eventProcessor.schedule_imm(new VirtualHostConfigContinuation, ET_TASK); + return 0; +} diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc index de97e3a2002..e3114c5c46d 100644 --- a/src/proxy/http/HttpSM.cc +++ b/src/proxy/http/HttpSM.cc @@ -279,6 +279,11 @@ HttpSM::HttpSM() : Continuation(nullptr), vc_table(this) {} HttpSM::~HttpSM() { + if (m_virtualhost_entry) { + m_virtualhost_entry->release(); + m_virtualhost_entry = nullptr; + } + http_parser_clear(&http_parser); // coverity[exn_spec_violation] - release() only does ref counting and delete on POD types @@ -4529,13 +4534,65 @@ HttpSM::check_sni_host() } } +void +HttpSM::set_virtualhost_entry(std::string_view domain) +{ + VirtualHost::scoped_config vhost_config; + // If already set, don't need to look at configs + if (m_virtualhost_entry || domain.empty() || !vhost_config) { + return; + } + + auto vhost_entry = vhost_config->find_by_domain(domain); + if (vhost_entry) { + SMDbg(dbg_ctl_url_rewrite, "Found virtualhost: %s", vhost_entry->get_id().c_str()); + // Explicitly acquire() since HttpSM holds raw pointer + m_virtualhost_entry = vhost_entry->acquire(); + } +} + void HttpSM::do_remap_request(bool run_inline) { SMDbg(dbg_ctl_http_seq, "Remapping request"); SMDbg(dbg_ctl_url_rewrite, "Starting a possible remapping for request"); + + if (!m_virtualhost_entry) { + auto host_name{t_state.hdr_info.client_request.host_get()}; + set_virtualhost_entry(host_name); + } + + // Check virtualhost remap rules before looking at remap.config + bool virtualhost_remap = false; + if (m_virtualhost_entry && m_virtualhost_entry->remap_table) { + UrlRewrite *vhost_table = m_virtualhost_entry->remap_table->acquire(); + if (vhost_table) { + // If already acquired, release ref + if (vhost_table == m_remap) { + vhost_table->release(); + } else { + if (m_remap) { + m_remap->release(); + } + m_remap = vhost_table; + } + SMDbg(dbg_ctl_url_rewrite, "Using virtualhost remap table: %s", m_virtualhost_entry->get_id().c_str()); + virtualhost_remap = true; + } + } + bool ret = remapProcessor.setup_for_remap(&t_state, m_remap); + // If no remap matches in virtualhost, revert to default remap configs + if (!ret && virtualhost_remap) { + SMDbg(dbg_ctl_url_rewrite, "No virtualhost remap rules found: using global remap table"); + if (m_remap) { + m_remap->release(); + } + m_remap = rewrite_table.load()->acquire(); + ret = remapProcessor.setup_for_remap(&t_state, m_remap); + } + check_sni_host(); // Depending on a variety of factors the HOST field may or may not have been promoted to the diff --git a/src/proxy/http/remap/RemapYamlConfig.cc b/src/proxy/http/remap/RemapYamlConfig.cc index 04070030e76..b265a800e7f 100644 --- a/src/proxy/http/remap/RemapYamlConfig.cc +++ b/src/proxy/http/remap/RemapYamlConfig.cc @@ -1024,6 +1024,47 @@ remap_parse_yaml_bti(const char *path, BUILD_TABLE_INFO *bti) return false; } +bool +remap_parse_yaml_bti(YAML::Node const *remap_node, BUILD_TABLE_INFO *bti) +{ + try { + if (!remap_node || remap_node->IsNull() || !remap_node->IsSequence()) { + Dbg(dbg_ctl_remap_yaml, "Remap node must be a sequence"); + 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 (const auto &rule : *remap_node) { + bti->reset(); + + auto errata = parse_yaml_remap_rule(rule, bti); + if (!errata.is_ok()) { + Error("Failed to parse remap rule"); + return false; + } + } + + IpAllow::enableAcceptCheck(bti->accept_check_p); + + Dbg(dbg_ctl_remap_yaml, "Successfully parsed inline remap YAML rules"); + return true; + + } catch (YAML::Exception &ex) { + Error("YAML parsing error in inline remap rules: %s", ex.what()); + } catch (std::exception &ex) { + Error("Exception parsing inline remap YAML rules: %s", ex.what()); + } + return false; +} + bool remap_parse_yaml(const char *path, UrlRewrite *rewrite) { @@ -1044,3 +1085,20 @@ remap_parse_yaml(const char *path, UrlRewrite *rewrite) return status; } + +bool +remap_parse_yaml(YAML::Node const *remap_node, UrlRewrite *rewrite) +{ + BUILD_TABLE_INFO bti; + + rewrite->pluginFactory.indicatePreReload(); + + bti.rewrite = rewrite; + bool status = remap_parse_yaml_bti(remap_node, &bti); + + rewrite->pluginFactory.indicatePostReload(status); + + bti.clear_acl_rules_list(); + + return status; +} diff --git a/src/proxy/http/remap/UrlRewrite.cc b/src/proxy/http/remap/UrlRewrite.cc index aa49bfde346..b72d4e5011e 100644 --- a/src/proxy/http/remap/UrlRewrite.cc +++ b/src/proxy/http/remap/UrlRewrite.cc @@ -95,6 +95,15 @@ UrlRewrite::load() return false; } } + return load_table(std::string(config_file_path.get()), nullptr); +} + +bool +UrlRewrite::load_table(const std::string &config_file_path, YAML::Node const *remap_node) +{ + if (remap_node) { + this->_remap_yaml = true; + } this->ts_name = nullptr; if (auto rec_str{RecGetRecordStringAlloc("proxy.config.proxy_name")}; rec_str) { @@ -143,7 +152,7 @@ UrlRewrite::load() Dbg(dbg_ctl_url_rewrite_regex, "strategyFactory file: %s", sf.c_str()); strategyFactory = new NextHopStrategyFactory(sf.c_str()); - if (TS_SUCCESS == this->BuildTable(config_file_path)) { + if (TS_SUCCESS == this->BuildTable(config_file_path.c_str(), remap_node)) { int n_rules = this->rule_count(); // Minimum # of rules to be considered a valid configuration. int required_rules; required_rules = RecGetRecordInt("proxy.config.url_remap.min_rules_required").value_or(0); @@ -816,7 +825,7 @@ UrlRewrite::InsertForwardMapping(mapping_type maptype, url_mapping *mapping, con */ int -UrlRewrite::BuildTable(const char *path) +UrlRewrite::BuildTable(const char *path, YAML::Node const *remap_node) { ink_assert(forward_mappings.empty()); ink_assert(reverse_mappings.empty()); @@ -837,7 +846,11 @@ UrlRewrite::BuildTable(const char *path) bool parse_success; if (is_remap_yaml()) { - parse_success = remap_parse_yaml(path, this); + if (remap_node) { + parse_success = remap_parse_yaml(remap_node, this); + } else { + parse_success = remap_parse_yaml(path, this); + } } else { parse_success = remap_parse_config(path, this); } diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc index 8c887a2e0f3..cd8d4c0c030 100644 --- a/src/records/RecordsConfig.cc +++ b/src/records/RecordsConfig.cc @@ -1125,6 +1125,8 @@ static constexpr RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.url_remap.acl_behavior_policy", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL} , + {RECT_CONFIG, "proxy.config.virtualhost.filename", RECD_STRING, ts::filename::VIRTUALHOST, RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} + , //############################################################################## //# diff --git a/src/traffic_ctl/CtrlCommands.cc b/src/traffic_ctl/CtrlCommands.cc index 21700b3154d..3c96aedec43 100644 --- a/src/traffic_ctl/CtrlCommands.cc +++ b/src/traffic_ctl/CtrlCommands.cc @@ -533,6 +533,17 @@ ConfigCommand::config_reload() } } + // If --virtualhost is set, inject the virtualhost id into the configs + // so the server-side handler receives it as a scalar value for + // single-entry reload. + auto vhost_arg = get_parsed_arguments()->get("virtualhost"); + if (vhost_arg) { + std::string vhost_id = vhost_arg.value(); + if (!vhost_id.empty()) { + configs["virtualhost"] = vhost_id; + } + } + using ConfigError = config::reload::errors::ConfigReloadError; auto contains_error = [](std::vector const &errors, ConfigError error) -> bool { diff --git a/src/traffic_ctl/traffic_ctl.cc b/src/traffic_ctl/traffic_ctl.cc index 6fbfc7f01d9..f80bd170aad 100644 --- a/src/traffic_ctl/traffic_ctl.cc +++ b/src/traffic_ctl/traffic_ctl.cc @@ -172,7 +172,9 @@ main([[maybe_unused]] int argc, const char **argv) "Maximum time to wait for reload completion (used with --monitor). " "Accepts duration units: 30s, 1m, 500ms, etc. 0 means no timeout", "", 1, "0") - .with_required("--monitor"); + .with_required("--monitor") + // Include virtualhost option to only reload specified entry + .add_option("--virtualhost", "", "Reload only the specific virtual host entry by id", "", 1, ""); config_command.add_command("status", "Check the configuration status", [&]() { command->execute(); }) .add_option("--token", "-t", "Configuration token to check status.", "", 1, "")