| /** @file |
| |
| Plugin init |
| |
| @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 <cstdio> |
| #include <algorithm> |
| #include <filesystem> |
| #include <optional> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| #include "tscore/ink_platform.h" |
| #include "tscore/ink_file.h" |
| #include "tscore/ParseRules.h" |
| #include "records/RecCore.h" |
| #include "tscore/Layout.h" |
| #include "proxy/Plugin.h" |
| #include "proxy/http/remap/RemapPluginInfo.h" |
| #include "tscore/ink_cap.h" |
| #include "tscore/Filenames.h" |
| #include <yaml-cpp/yaml.h> |
| |
| #define MAX_PLUGIN_ARGS 64 |
| |
| static PluginDynamicReloadMode plugin_dynamic_reload_mode = PluginDynamicReloadMode::ON; |
| |
| bool |
| isPluginDynamicReloadEnabled() |
| { |
| return PluginDynamicReloadMode::ON == plugin_dynamic_reload_mode; |
| } |
| |
| void |
| enablePluginDynamicReload() |
| { |
| plugin_dynamic_reload_mode = PluginDynamicReloadMode::ON; |
| } |
| |
| void |
| disablePluginDynamicReload() |
| { |
| plugin_dynamic_reload_mode = PluginDynamicReloadMode::OFF; |
| } |
| |
| void |
| parsePluginDynamicReloadConfig() |
| { |
| int int_plugin_dynamic_reload_mode; |
| |
| int_plugin_dynamic_reload_mode = RecGetRecordInt("proxy.config.plugin.dynamic_reload_mode").value_or(0); |
| plugin_dynamic_reload_mode = static_cast<PluginDynamicReloadMode>(int_plugin_dynamic_reload_mode); |
| |
| if (static_cast<int>(plugin_dynamic_reload_mode) < 0 || |
| static_cast<int>(plugin_dynamic_reload_mode) >= static_cast<int>(PluginDynamicReloadMode::COUNT)) { |
| Warning("proxy.config.plugin.dynamic_reload_mode out of range. using default value."); |
| plugin_dynamic_reload_mode = PluginDynamicReloadMode::ON; |
| } |
| Note("Initialized plugin_dynamic_reload_mode: %d", static_cast<int>(plugin_dynamic_reload_mode)); |
| } |
| |
| void |
| parsePluginConfig() |
| { |
| parsePluginDynamicReloadConfig(); |
| } |
| |
| static const char *plugin_dir = "."; |
| |
| static void |
| plugin_dir_init() |
| { |
| static bool once = true; |
| |
| if (once) { |
| plugin_dir = ats_stringdup(RecConfigReadPluginDir()); |
| once = false; |
| } |
| } |
| |
| using init_func_t = void (*)(int, char **); |
| |
| namespace |
| { |
| /** Plugin context for global plugins, which load via raw dlopen() rather than the |
| * PluginFactory/PluginDso path and so would otherwise have no PluginThreadContext to carry their |
| * identity. Installed as the thread-local pluginThreadContext around TSPluginInit so the plugin's |
| * continuations are stamped with it. Global plugins are never unloaded, so acquire()/release() are |
| * no-ops and instances live for the process lifetime. */ |
| class GlobalPluginContext : public PluginThreadContext |
| { |
| public: |
| explicit GlobalPluginContext(std::string_view name) { registerPluginMetrics(name); } |
| void |
| acquire() override |
| { |
| } |
| void |
| release() override |
| { |
| } |
| }; |
| |
| // Keeps global-plugin contexts reachable for the process lifetime; mutated single-threaded at startup. |
| std::vector<GlobalPluginContext *> g_global_plugin_contexts; |
| } // namespace |
| |
| static PluginLoadSummary s_plugin_load_summary; |
| |
| const PluginLoadSummary & |
| get_plugin_load_summary() |
| { |
| return s_plugin_load_summary; |
| } |
| |
| // Plugin registration vars |
| // |
| // plugin_reg_list has an entry for each plugin |
| // we've successfully been able to load |
| // plugin_reg_current is used to associate the |
| // plugin we're in the process of loading with |
| // it struct. We need this global pointer since |
| // the API doesn't have any plugin context. Init |
| // is single threaded so we can get away with the |
| // global pointer |
| // |
| DLL<PluginRegInfo> plugin_reg_list; |
| PluginRegInfo *plugin_reg_current = nullptr; |
| |
| PluginRegInfo::PluginRegInfo() = default; |
| |
| PluginRegInfo::~PluginRegInfo() |
| { |
| // We don't support unloading plugins once they are successfully loaded, so assert |
| // that we don't accidentally attempt this. |
| ink_release_assert(this->plugin_registered == false); |
| ink_release_assert(this->link.prev == nullptr); |
| |
| ats_free(this->plugin_path); |
| ats_free(this->plugin_name); |
| ats_free(this->vendor_name); |
| ats_free(this->support_email); |
| if (dlh) { |
| dlclose(dlh); |
| } |
| } |
| |
| bool |
| plugin_dso_load(const char *path, void *&handle, void *&init, std::string &error) |
| { |
| handle = dlopen(path, RTLD_NOW); |
| init = nullptr; |
| if (!handle) { |
| error.assign("unable to load '").append(path).append("': ").append(dlerror()); |
| Error("%s", error.c_str()); |
| return false; |
| } |
| |
| init = dlsym(handle, "TSPluginInit"); |
| if (!init) { |
| error.assign("unable to find TSPluginInit function in '").append(path).append("': ").append(dlerror()); |
| Error("%s", error.c_str()); |
| dlclose(handle); |
| handle = nullptr; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool |
| single_plugin_init(int argc, char *argv[], bool validateOnly) |
| { |
| char path[PATH_NAME_MAX]; |
| init_func_t init; |
| |
| if (argc < 1) { |
| return true; |
| } |
| ink_filepath_make(path, sizeof(path), plugin_dir, argv[0]); |
| |
| Note("loading plugin '%s'", path); |
| |
| for (PluginRegInfo *plugin_reg_temp = plugin_reg_list.head; plugin_reg_temp != nullptr; |
| plugin_reg_temp = (plugin_reg_temp->link).next) { |
| if (strcmp(plugin_reg_temp->plugin_path, path) == 0) { |
| Warning("multiple loading of plugin %s", path); |
| break; |
| } |
| } |
| |
| // elevate the access to read files as root if compiled with capabilities, if not |
| // change the effective user to root |
| { |
| uint32_t elevate_access = 0; |
| elevate_access = RecGetRecordInt("proxy.config.plugin.load_elevated").value_or(0); |
| ElevateAccess access(elevate_access ? ElevateAccess::FILE_PRIVILEGE : 0); |
| |
| void *handle, *initptr = nullptr; |
| std::string error; |
| bool loaded = plugin_dso_load(path, handle, initptr, error); |
| init = reinterpret_cast<init_func_t>(initptr); |
| |
| if (!loaded) { |
| if (validateOnly) { |
| return false; |
| } |
| Fatal("%s", error.c_str()); |
| return false; // this line won't get called since Fatal brings down ATS |
| } |
| |
| // Allocate a new registration structure for the |
| // plugin we're starting up |
| ink_assert(plugin_reg_current == nullptr); |
| plugin_reg_current = new PluginRegInfo; |
| plugin_reg_current->plugin_path = ats_strdup(path); |
| plugin_reg_current->dlh = handle; |
| |
| #if (!defined(kfreebsd) && defined(freebsd)) || defined(darwin) |
| optreset = 1; |
| #endif |
| #if defined(__GLIBC__) |
| optind = 0; |
| #else |
| optind = 1; |
| #endif |
| opterr = 0; |
| optarg = nullptr; |
| |
| // Install this plugin's context around TSPluginInit so the continuations it creates carry its |
| // identity (see GlobalPluginContext). |
| auto *global_context = new GlobalPluginContext(path); |
| g_global_plugin_contexts.push_back(global_context); |
| auto *prev_plugin_context = pluginThreadContext; |
| pluginThreadContext = global_context; |
| |
| init(argc, argv); |
| |
| pluginThreadContext = prev_plugin_context; |
| } // done elevating access |
| |
| if (plugin_reg_current->plugin_registered) { |
| plugin_reg_list.push(plugin_reg_current); |
| } else { |
| Fatal("plugin '%s' not registered by calling TSPluginRegister", path); |
| return false; // this line won't get called since Fatal brings down ATS |
| } |
| |
| plugin_reg_current = nullptr; |
| Note("plugin '%s' finished loading", path); |
| return true; |
| } |
| |
| static char * |
| plugin_expand(char *arg, const char *source) |
| { |
| RecDataT data_type; |
| char *str = nullptr; |
| |
| if (*arg != '$') { |
| return nullptr; |
| } |
| // skip the $ character |
| arg += 1; |
| |
| if (RecGetRecordDataType(arg, &data_type) != REC_ERR_OKAY) { |
| goto not_found; |
| } |
| |
| switch (data_type) { |
| case RECD_STRING: { |
| auto rec_str{RecGetRecordStringAlloc(arg)}; |
| if (!rec_str) { |
| goto not_found; |
| } |
| return ats_stringdup(rec_str); |
| break; |
| } |
| case RECD_FLOAT: { |
| auto float_val{RecGetRecordFloat(arg)}; |
| if (!float_val) { |
| goto not_found; |
| } |
| str = static_cast<char *>(ats_malloc(128)); |
| snprintf(str, 128, "%f", static_cast<float>(float_val.value())); |
| return str; |
| break; |
| } |
| case RECD_INT: { |
| auto int_val{RecGetRecordInt(arg)}; |
| if (!int_val) { |
| goto not_found; |
| } |
| str = static_cast<char *>(ats_malloc(128)); |
| snprintf(str, 128, "%ld", static_cast<long int>(int_val.value())); |
| return str; |
| break; |
| } |
| case RECD_COUNTER: { |
| auto count_val{RecGetRecordCounter(arg)}; |
| if (!count_val) { |
| goto not_found; |
| } |
| str = static_cast<char *>(ats_malloc(128)); |
| snprintf(str, 128, "%ld", static_cast<long int>(count_val.value())); |
| return str; |
| break; |
| } |
| default: |
| goto not_found; |
| break; |
| } |
| |
| not_found: |
| Warning("%s: unable to find parameter %s", source, arg); |
| return nullptr; |
| } |
| |
| bool |
| plugin_init(bool validateOnly) |
| { |
| ats_scoped_str path; |
| char line[1024], *p; |
| char *argv[MAX_PLUGIN_ARGS]; |
| char *vars[MAX_PLUGIN_ARGS]; |
| int argc; |
| int fd; |
| int i; |
| bool retVal = true; |
| int load_index = 0; |
| |
| plugin_dir_init(); |
| |
| s_plugin_load_summary.source = ts::filename::PLUGIN; |
| s_plugin_load_summary.entries.clear(); |
| |
| Note("%s loading ...", ts::filename::PLUGIN); |
| path = RecConfigReadConfigPath(nullptr, ts::filename::PLUGIN); |
| fd = open(path, O_RDONLY); |
| if (fd < 0) { |
| Warning("%s failed to load: %d, %s", ts::filename::PLUGIN, errno, strerror(errno)); |
| return false; |
| } |
| |
| while (ink_file_fd_readline(fd, sizeof(line) - 1, line) > 0) { |
| argc = 0; |
| p = line; |
| |
| // strip leading white space and test for comment or blank line |
| while (*p && ParseRules::is_wslfcr(*p)) { |
| ++p; |
| } |
| if ((*p == '\0') || (*p == '#')) { |
| continue; |
| } |
| |
| // not comment or blank, so rip line into tokens |
| while (true) { |
| if (argc >= MAX_PLUGIN_ARGS) { |
| Warning("Exceeded max number of args (%d) for plugin: [%s]", MAX_PLUGIN_ARGS, argc > 0 ? argv[0] : "???"); |
| break; |
| } |
| |
| while (*p && ParseRules::is_wslfcr(*p)) { |
| ++p; |
| } |
| if ((*p == '\0') || (*p == '#')) { |
| break; // EOL |
| } |
| |
| if (*p == '\"') { |
| p += 1; |
| |
| argv[argc++] = p; |
| |
| while (*p && (*p != '\"')) { |
| p += 1; |
| } |
| if (*p == '\0') { |
| break; |
| } |
| *p++ = '\0'; |
| } else { |
| argv[argc++] = p; |
| |
| while (*p && !ParseRules::is_wslfcr(*p) && (*p != '#')) { |
| p += 1; |
| } |
| if ((*p == '\0') || (*p == '#')) { |
| break; |
| } |
| *p++ = '\0'; |
| } |
| } |
| |
| for (i = 0; i < argc; i++) { |
| vars[i] = plugin_expand(argv[i], ts::filename::PLUGIN); |
| if (vars[i]) { |
| argv[i] = vars[i]; |
| } |
| } |
| |
| if (argc < MAX_PLUGIN_ARGS) { |
| argv[argc] = nullptr; |
| } else { |
| argv[MAX_PLUGIN_ARGS - 1] = nullptr; |
| } |
| |
| ++load_index; |
| std::string plugin_name = (argc > 0) ? argv[0] : "unknown"; |
| |
| retVal = single_plugin_init(argc, argv, validateOnly); |
| |
| s_plugin_load_summary.entries.push_back({std::move(plugin_name), -1, true, retVal, load_index}); |
| |
| for (i = 0; i < argc; i++) { |
| ats_free(vars[i]); |
| } |
| } |
| |
| close(fd); |
| if (retVal) { |
| Note("%s finished loading", ts::filename::PLUGIN); |
| } else { |
| Error("%s failed to load", ts::filename::PLUGIN); |
| } |
| return retVal; |
| } |
| |
| config::ConfigResult<PluginYAMLEntries> |
| parse_plugin_yaml(const char *yaml_path) |
| { |
| config::ConfigResult<PluginYAMLEntries> result; |
| YAML::Node root; |
| |
| try { |
| root = YAML::LoadFile(yaml_path); |
| } catch (const YAML::Exception &e) { |
| result.errata.note("failed to parse: {}", e.what()); |
| return result; |
| } |
| |
| if (!root["plugins"] || !root["plugins"].IsSequence()) { |
| result.errata.note("missing or invalid 'plugins' sequence"); |
| return result; |
| } |
| |
| struct IndexedEntry { |
| int seq_idx; |
| PluginYAMLEntry entry; |
| }; |
| |
| std::vector<IndexedEntry> indexed; |
| int seq_idx = 0; |
| |
| for (const auto &node : root["plugins"]) { |
| PluginYAMLEntry entry; |
| |
| if (!node["path"]) { |
| result.errata.note("plugin entry #{} missing required 'path' field", seq_idx + 1); |
| return result; |
| } |
| entry.path = node["path"].as<std::string>(); |
| |
| if (auto n = node["enabled"]; n) { |
| entry.enabled = n.as<bool>(); |
| } |
| if (auto n = node["load_order"]; n) { |
| entry.load_order = n.as<int>(); |
| } |
| if (auto n = node["params"]; n && n.IsSequence()) { |
| for (const auto &p : n) { |
| entry.params.emplace_back(p.as<std::string>()); |
| } |
| } |
| if (auto n = node["config"]; n) { |
| if (n.IsScalar()) { |
| entry.config_literal = n.as<std::string>(); |
| } else { |
| result.errata.note("plugin '{}': 'config' must be a scalar (use literal block '|' for multi-line content)", entry.path); |
| return result; |
| } |
| } |
| |
| indexed.push_back({seq_idx++, std::move(entry)}); |
| } |
| |
| std::stable_sort(indexed.begin(), indexed.end(), [](const IndexedEntry &a, const IndexedEntry &b) { |
| const bool a_has = a.entry.load_order >= 0; |
| const bool b_has = b.entry.load_order >= 0; |
| |
| if (a_has && b_has) { |
| return a.entry.load_order < b.entry.load_order; |
| } |
| return a_has && !b_has; |
| }); |
| |
| result.value.reserve(indexed.size()); |
| for (auto &[_, entry] : indexed) { |
| result.value.emplace_back(std::move(entry)); |
| } |
| |
| return result; |
| } |
| |
| /// Write inline config content to a temp file, returning the path on success. |
| static std::optional<std::string> |
| write_inline_config(const PluginYAMLEntry &entry, int index) |
| { |
| char tmp_path[PATH_NAME_MAX]; |
| |
| std::string_view stem{entry.path}; |
| if (auto pos = stem.rfind('/'); pos != std::string_view::npos) { |
| stem = stem.substr(pos + 1); |
| } |
| if (auto pos = stem.rfind('.'); pos != std::string_view::npos) { |
| stem = stem.substr(0, pos); |
| } |
| |
| snprintf(tmp_path, sizeof(tmp_path), "%s/.%.*s_inline_%d.conf", RecConfigReadConfigDir().c_str(), static_cast<int>(stem.size()), |
| stem.data(), index); |
| |
| int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); |
| if (fd < 0) { |
| Error("%s: failed to create temp config for %s: %s", ts::filename::PLUGIN_YAML, entry.path.c_str(), strerror(errno)); |
| return std::nullopt; |
| } |
| |
| auto n = write(fd, entry.config_literal.data(), entry.config_literal.size()); |
| close(fd); |
| |
| if (n < 0 || static_cast<size_t>(n) != entry.config_literal.size()) { |
| Error("%s: failed to write inline config for %s", ts::filename::PLUGIN_YAML, entry.path.c_str()); |
| return std::nullopt; |
| } |
| |
| return std::string(tmp_path); |
| } |
| |
| /// Build the argv for a single plugin: [path, inline_config_path?, params..., $record expansions]. |
| static std::optional<std::vector<std::string>> |
| build_plugin_args(const PluginYAMLEntry &entry, int index) |
| { |
| std::vector<std::string> args; |
| args.emplace_back(entry.path); |
| |
| if (!entry.config_literal.empty()) { |
| if (auto path = write_inline_config(entry, index); path) { |
| args.emplace_back(std::move(*path)); |
| } else { |
| return std::nullopt; |
| } |
| } |
| |
| for (const auto &p : entry.params) { |
| args.emplace_back(p); |
| } |
| |
| return args; |
| } |
| |
| static void |
| log_plugin_load_summary(int loaded, int disabled) |
| { |
| Note("%s: %d plugins loaded, %d disabled", ts::filename::PLUGIN_YAML, loaded, disabled); |
| |
| for (const auto &e : s_plugin_load_summary.entries) { |
| if (e.enabled) { |
| if (e.load_order >= 0) { |
| Note(" #%d %-30s load_order: %-5d loaded", e.index, e.path.c_str(), e.load_order); |
| } else { |
| Note(" #%d %-30s loaded", e.index, e.path.c_str()); |
| } |
| } else { |
| Note(" -- %-30s disabled", e.path.c_str()); |
| } |
| } |
| } |
| |
| static void |
| cleanup_inline_configs() |
| { |
| std::string config_dir = RecConfigReadConfigDir(); |
| std::error_code ec; |
| |
| try { |
| for (const auto &entry : std::filesystem::directory_iterator(config_dir, ec)) { |
| if (!entry.is_regular_file()) { |
| continue; |
| } |
| auto name = entry.path().filename().string(); |
| if (name.front() == '.' && name.find("_inline_") != std::string::npos && name.ends_with(".conf")) { |
| std::filesystem::remove(entry.path(), ec); |
| } |
| } |
| } catch (const std::exception &e) { |
| Error("%s: error cleaning up inline config files: %s", ts::filename::PLUGIN_YAML, e.what()); |
| } |
| } |
| |
| bool |
| plugin_yaml_init(bool validateOnly) |
| { |
| plugin_dir_init(); |
| |
| ats_scoped_str yaml_path; |
| |
| if (!validateOnly) { |
| cleanup_inline_configs(); |
| } |
| |
| yaml_path = RecConfigReadConfigPath(nullptr, ts::filename::PLUGIN_YAML); |
| if (access(yaml_path, R_OK) != 0) { |
| if (errno != ENOENT) { |
| Error("%s: %s", ts::filename::PLUGIN_YAML, strerror(errno)); |
| return false; |
| } |
| return plugin_init(validateOnly); |
| } |
| |
| Note("%s loading ...", ts::filename::PLUGIN_YAML); |
| |
| auto result = parse_plugin_yaml(yaml_path.get()); |
| if (!result.ok()) { |
| Error("%s: %s", ts::filename::PLUGIN_YAML, std::string(result.errata.front().text()).c_str()); |
| return false; |
| } |
| |
| s_plugin_load_summary.source = ts::filename::PLUGIN_YAML; |
| s_plugin_load_summary.entries.clear(); |
| |
| bool retVal = true; |
| int index = 0; |
| int loaded = 0; |
| int disabled = 0; |
| |
| for (const auto &entry : result.value) { |
| ++index; |
| |
| if (!entry.enabled) { |
| Note("plugin #%d skipped: %s (enabled: false)", index, entry.path.c_str()); |
| s_plugin_load_summary.entries.push_back({entry.path, entry.load_order, false, false, index}); |
| ++disabled; |
| continue; |
| } |
| |
| auto args = build_plugin_args(entry, index); |
| if (!args) { |
| return false; |
| } |
| |
| if (args->size() > MAX_PLUGIN_ARGS) { |
| Warning("%s: plugin '%s' has %zu args, exceeds typical max (%d)", ts::filename::PLUGIN_YAML, entry.path.c_str(), args->size(), |
| MAX_PLUGIN_ARGS); |
| } |
| |
| std::vector<char *> argv_ptrs; |
| std::vector<char *> expanded; |
| |
| for (auto &a : *args) { |
| char *var = plugin_expand(a.data(), ts::filename::PLUGIN_YAML); |
| expanded.emplace_back(var); |
| argv_ptrs.emplace_back(var ? var : a.data()); |
| } |
| argv_ptrs.emplace_back(nullptr); |
| |
| if (entry.load_order >= 0) { |
| Note("plugin #%d loading: %s (load_order: %d)", index, entry.path.c_str(), entry.load_order); |
| } else { |
| Note("plugin #%d loading: %s", index, entry.path.c_str()); |
| } |
| |
| retVal = single_plugin_init(static_cast<int>(args->size()), argv_ptrs.data(), validateOnly); |
| s_plugin_load_summary.entries.push_back({entry.path, entry.load_order, true, retVal, index}); |
| ++loaded; |
| |
| for (auto *v : expanded) { |
| ats_free(v); |
| } |
| |
| if (!retVal) { |
| break; |
| } |
| } |
| |
| if (retVal) { |
| log_plugin_load_summary(loaded, disabled); |
| } else { |
| Error("%s failed to load", ts::filename::PLUGIN_YAML); |
| } |
| return retVal; |
| } |