blob: 5dfdf486e15cb5be6eee1e97c3638e6db5a621bb [file]
/** @file
SSL Multi-Certificate 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/ssl_multicert.h"
#include <cerrno>
#include <cctype>
#include <exception>
#include <set>
#include <yaml-cpp/yaml.h>
#include "swoc/swoc_file.h"
#include "swoc/TextView.h"
#include "tsutil/ts_diag_levels.h"
namespace
{
constexpr swoc::Errata::Severity ERRATA_NOTE_SEV{static_cast<swoc::Errata::severity_type>(DL_Note)};
constexpr swoc::Errata::Severity ERRATA_WARN_SEV{static_cast<swoc::Errata::severity_type>(DL_Warning)};
constexpr swoc::Errata::Severity ERRATA_ERROR_SEV{static_cast<swoc::Errata::severity_type>(DL_Error)};
// YAML key names.
constexpr char KEY_SSL_CERT_NAME[] = "ssl_cert_name";
constexpr char KEY_DEST_IP[] = "dest_ip";
constexpr char KEY_SSL_KEY_NAME[] = "ssl_key_name";
constexpr char KEY_SSL_CA_NAME[] = "ssl_ca_name";
constexpr char KEY_SSL_OCSP_NAME[] = "ssl_ocsp_name";
constexpr char KEY_SSL_KEY_DIALOG[] = "ssl_key_dialog";
constexpr char KEY_DEST_FQDN[] = "dest_fqdn";
constexpr char KEY_SSL_TICKET_ENABLED[] = "ssl_ticket_enabled";
constexpr char KEY_SSL_TICKET_NUMBER[] = "ssl_ticket_number";
constexpr char KEY_ACTION[] = "action";
constexpr char KEY_SSL_MULTICERT[] = "ssl_multicert";
std::set<std::string> const valid_keys = {
KEY_SSL_CERT_NAME, KEY_DEST_IP, KEY_SSL_KEY_NAME, KEY_SSL_CA_NAME, KEY_SSL_OCSP_NAME,
KEY_SSL_KEY_DIALOG, KEY_DEST_FQDN, KEY_SSL_TICKET_ENABLED, KEY_SSL_TICKET_NUMBER, KEY_ACTION,
};
/**
* Parse a line in legacy key=value format, handling quoted values.
*
* Tokenizes on whitespace but respects quoted strings. Each token should be
* key=value format.
*/
std::vector<std::pair<std::string, std::string>>
parse_legacy_line(swoc::TextView line)
{
std::vector<std::pair<std::string, std::string>> result;
while (!line.ltrim_if(isspace).empty()) {
swoc::TextView key = line.split_prefix_at('=');
if (key.empty()) {
// No '=' found, skip this malformed token by consuming to next whitespace.
line.take_prefix_if(isspace);
continue;
}
key.trim_if(isspace);
swoc::TextView value;
if (!line.empty() && (line.front() == '"' || line.front() == '\'')) {
// Quoted value: extract until closing quote.
char const quote = line.front();
line.remove_prefix(1);
value = line.take_prefix_at(quote);
} else {
// Unquoted value: extract until whitespace.
value = line.take_prefix_if(isspace);
}
value.trim_if(isspace);
if (!key.empty()) {
result.emplace_back(std::string{key}, std::string{value});
}
}
return result;
}
/// Emit a single SSLMultiCertEntry to a YAML::Emitter.
void
emit_entry(YAML::Emitter &emitter, config::SSLMultiCertEntry const &entry)
{
emitter << YAML::BeginMap;
auto write_field = [&](char const *key, std::string const &value) {
if (!value.empty()) {
emitter << YAML::Key << key << YAML::Value << value;
}
};
auto write_int_field = [&](char const *key, std::optional<int> const &value) {
if (value.has_value()) {
emitter << YAML::Key << key << YAML::Value << value.value();
}
};
write_field(KEY_SSL_CERT_NAME, entry.ssl_cert_name);
write_field(KEY_DEST_IP, entry.dest_ip);
write_field(KEY_SSL_KEY_NAME, entry.ssl_key_name);
write_field(KEY_SSL_CA_NAME, entry.ssl_ca_name);
write_field(KEY_SSL_OCSP_NAME, entry.ssl_ocsp_name);
write_field(KEY_SSL_KEY_DIALOG, entry.ssl_key_dialog);
write_field(KEY_DEST_FQDN, entry.dest_fqdn);
write_field(KEY_ACTION, entry.action);
write_int_field(KEY_SSL_TICKET_ENABLED, entry.ssl_ticket_enabled);
write_int_field(KEY_SSL_TICKET_NUMBER, entry.ssl_ticket_number);
emitter << YAML::EndMap;
}
} // namespace
namespace YAML
{
template <> struct convert<config::SSLMultiCertEntry> {
static bool
decode(Node const &node, config::SSLMultiCertEntry &entry)
{
if (node[KEY_SSL_CERT_NAME]) {
entry.ssl_cert_name = node[KEY_SSL_CERT_NAME].as<std::string>();
}
if (node[KEY_DEST_IP]) {
entry.dest_ip = node[KEY_DEST_IP].as<std::string>();
}
if (node[KEY_SSL_KEY_NAME]) {
entry.ssl_key_name = node[KEY_SSL_KEY_NAME].as<std::string>();
}
if (node[KEY_SSL_CA_NAME]) {
entry.ssl_ca_name = node[KEY_SSL_CA_NAME].as<std::string>();
}
if (node[KEY_SSL_OCSP_NAME]) {
entry.ssl_ocsp_name = node[KEY_SSL_OCSP_NAME].as<std::string>();
}
if (node[KEY_SSL_KEY_DIALOG]) {
entry.ssl_key_dialog = node[KEY_SSL_KEY_DIALOG].as<std::string>();
}
if (node[KEY_DEST_FQDN]) {
entry.dest_fqdn = node[KEY_DEST_FQDN].as<std::string>();
}
if (node[KEY_SSL_TICKET_ENABLED]) {
entry.ssl_ticket_enabled = node[KEY_SSL_TICKET_ENABLED].as<int>();
}
if (node[KEY_SSL_TICKET_NUMBER]) {
entry.ssl_ticket_number = node[KEY_SSL_TICKET_NUMBER].as<int>();
}
if (node[KEY_ACTION]) {
entry.action = node[KEY_ACTION].as<std::string>();
}
return true;
}
};
} // namespace YAML
namespace config
{
ConfigResult<SSLMultiCertConfig>
SSLMultiCertParser::parse(std::string const &filename)
{
std::error_code ec;
std::string content = swoc::file::load(filename, ec);
if (ec) {
// Missing ssl_multicert.* is an acceptable runtime state.
if (ec.value() == ENOENT) {
return {{}, swoc::Errata(ERRATA_WARN_SEV, "Cannot open SSL certificate configuration \"{}\" - {}", filename, ec)};
}
return {{}, swoc::Errata(ERRATA_ERROR_SEV, "Failed to read SSL certificate configuration from \"{}\" - {}", filename, ec)};
}
if (content.empty()) {
return {{}, {}};
}
Format const format = detect_format(content, filename);
if (format == Format::YAML) {
return parse_yaml(content);
}
return parse_legacy(content);
}
SSLMultiCertParser::Format
SSLMultiCertParser::detect_format(std::string_view content, std::string const &filename)
{
swoc::TextView const fn{filename};
// Check file extension first.
if (fn.ends_with(".yaml") || fn.ends_with(".yml")) {
return Format::YAML;
}
if (fn.ends_with(".config")) {
return Format::Legacy;
}
// Fall back to content inspection.
if (content.find("ssl_multicert:") != std::string_view::npos) {
return Format::YAML;
}
// Legacy format uses key=value.
if (content.find('=') != std::string_view::npos) {
return Format::Legacy;
}
// Default to YAML as that's the preferred format.
return Format::YAML;
}
ConfigResult<SSLMultiCertConfig>
SSLMultiCertParser::parse_yaml(std::string_view content)
{
SSLMultiCertConfig result;
swoc::Errata errata;
std::set<std::string> unknown_keys;
try {
YAML::Node config = YAML::Load(std::string(content));
if (config.IsNull()) {
return {result, std::move(errata)};
}
if (!config[KEY_SSL_MULTICERT]) {
return {result, swoc::Errata("expected a toplevel 'ssl_multicert' node")};
}
YAML::Node entries = config[KEY_SSL_MULTICERT];
if (!entries.IsSequence()) {
return {result, swoc::Errata("expected 'ssl_multicert' to be a sequence")};
}
for (auto const &entry_node : entries) {
auto const mark = entry_node.Mark();
if (!entry_node.IsMap()) {
return {result, swoc::Errata(ERRATA_ERROR_SEV, "Expected ssl_multicert entries to be maps at line {}, column {}", mark.line,
mark.column)};
}
for (auto const &field : entry_node) {
std::string key = field.first.as<std::string>();
if (valid_keys.find(key) == valid_keys.end() && unknown_keys.insert(key).second) {
errata.note(ERRATA_NOTE_SEV, "Ignoring unknown ssl_multicert key '{}' at line {}, column {}", key, mark.line,
mark.column);
}
}
result.push_back(entry_node.as<SSLMultiCertEntry>());
}
} catch (std::exception const &ex) {
return {result, swoc::Errata("YAML parse error: {}", ex.what())};
}
return {result, std::move(errata)};
}
ConfigResult<SSLMultiCertConfig>
SSLMultiCertParser::parse_legacy(std::string_view content)
{
SSLMultiCertConfig result;
swoc::Errata errata;
std::set<std::string> unknown_keys;
swoc::TextView src{content};
while (!src.empty()) {
swoc::TextView line = src.take_prefix_at('\n');
line.trim_if(isspace);
// Skip empty lines and comments.
if (line.empty() || line.front() == '#') {
continue;
}
auto const kv_pairs = parse_legacy_line(line);
if (kv_pairs.empty()) {
continue;
}
SSLMultiCertEntry entry;
for (auto const &[key, value] : kv_pairs) {
if (key == KEY_SSL_CERT_NAME) {
entry.ssl_cert_name = value;
} else if (key == KEY_DEST_IP) {
entry.dest_ip = value;
} else if (key == KEY_SSL_KEY_NAME) {
entry.ssl_key_name = value;
} else if (key == KEY_SSL_CA_NAME) {
entry.ssl_ca_name = value;
} else if (key == KEY_SSL_OCSP_NAME) {
entry.ssl_ocsp_name = value;
} else if (key == KEY_SSL_KEY_DIALOG) {
entry.ssl_key_dialog = value;
} else if (key == KEY_DEST_FQDN) {
entry.dest_fqdn = value;
} else if (key == KEY_ACTION) {
entry.action = value;
} else if (key == KEY_SSL_TICKET_ENABLED) {
entry.ssl_ticket_enabled = swoc::svtoi(value);
} else if (key == KEY_SSL_TICKET_NUMBER) {
entry.ssl_ticket_number = swoc::svtoi(value);
} else if (unknown_keys.insert(key).second) {
errata.note(ERRATA_NOTE_SEV, "Ignoring unknown ssl_multicert key '{}' in legacy format", key);
}
}
result.push_back(std::move(entry));
}
return {result, std::move(errata)};
}
std::string
SSLMultiCertMarshaller::to_yaml(SSLMultiCertConfig const &config)
{
YAML::Emitter yaml;
yaml << YAML::BeginMap;
yaml << YAML::Key << KEY_SSL_MULTICERT << YAML::Value << YAML::BeginSeq;
for (auto const &entry : config) {
emit_entry(yaml, entry);
}
yaml << YAML::EndSeq << YAML::EndMap;
return yaml.c_str();
}
std::string
SSLMultiCertMarshaller::to_json(SSLMultiCertConfig const &config)
{
YAML::Emitter json;
json << YAML::DoubleQuoted << YAML::Flow;
json << YAML::BeginMap;
json << YAML::Key << KEY_SSL_MULTICERT << YAML::Value << YAML::BeginSeq;
for (auto const &entry : config) {
emit_entry(json, entry);
}
json << YAML::EndSeq << YAML::EndMap;
return json.c_str();
}
} // namespace config