rublon-ssh/PAM/ssh/include/rublon/configuration.hpp
2025-07-18 14:19:10 +02:00

318 lines
12 KiB
C++

#pragma once
#include <algorithm>
#include <rublon/error.hpp>
#include <rublon/memory.hpp>
#include <rublon/static_string.hpp>
#include <rublon/utils.hpp>
#include <cctype>
#include <string>
namespace rublon {
class ConfigurationFactory;
enum class FailMode { bypass, deny };
class Configuration {
private:
std::pmr::memory_resource * memoryResource;
public:
Configuration(std::pmr::memory_resource * mr) : memoryResource{mr} {}
// change to StaticString
std::pmr::string systemToken{memoryResource};
std::pmr::string secretKey{memoryResource};
std::pmr::string apiServer{memoryResource};
int prompt{};
bool enablePasswdEmail{}; // obsolete
bool logging{};
bool autopushPrompt{};
FailMode failMode{};
bool nonInteractiveMode{};
std::optional< std::pmr::string > proxyType{memoryResource};
std::optional< std::pmr::string > proxyHost{memoryResource};
std::optional< std::pmr::string > proxyUsername{memoryResource};
std::optional< std::pmr::string > proxyPass{memoryResource};
std::optional< int > proxyPort{};
bool proxyAuthRequired{}; // defaulted
bool proxyEnabled{}; // defaulted
};
class ConfigurationReader {
public:
ConfigurationReader(std::pmr::memory_resource * memResource, std::string_view filepath) : memoryResource(memResource) {
loadFromFile(filepath);
}
// Load config from file path
void loadFromFile(std::string_view filepath) {
using namespace memory::literals;
memory::MonotonicStack_1k_Resource memoryResource;
std::ifstream file(filepath.data());
if(not file.good())
return;
std::pmr::string line{&memoryResource};
line.reserve(300);
while(std::getline(file, line)) {
details::trimInPlace(line);
if(!line.length())
continue;
if(line[0] == '#' || line[0] == ';')
continue;
auto posEqual = line.find('=');
std::pmr::string key{line.substr(0, posEqual), &memoryResource};
std::pmr::string value{line.substr(posEqual + 1), &memoryResource};
details::trimInPlace(key);
details::trimInPlace(value);
keyValues[std::move(key)] = std::move(value);
}
}
// Load values into Configuration object, with defaults provided
tl::expected< bool, ConfigurationError > applyTo(Configuration & config) const {
using string = std::pmr::string;
auto logRequiredFieldNotAvailable = [](auto fieldname) { log(LogLevel::Error, "%s field is not set", fieldname); };
// Helper lambdas for conversion
auto getStringOpt = [&](const string & key) -> std::optional< std::pmr::string > {
auto it = keyValues.find(key);
if(it == keyValues.end()) {
return std::nullopt;
}
return string{it->second.data(), it->second.size(), memoryResource};
};
auto getStringReq = [&](const string & key) -> tl::expected< std::pmr::string, ConfigurationError > {
auto val = getStringOpt(key);
if(val.has_value()) {
if(val->empty()) {
return tl::unexpected{ConfigurationError::ErrorClass::RequiredValueEmpty};
}
return std::move(val.value());
}
return tl::unexpected{ConfigurationError::ErrorClass::RequiredValueNotFound};
};
auto getInt = [&](const string & key) -> std::optional< int > {
auto it = keyValues.find(key);
if(it == keyValues.end())
return std::nullopt;
return conv::to_uint32opt(it->second);
};
auto getBool = [&](const string & key) -> std::optional< bool > {
memory::MonotonicStackResource< 32 > memoryResource;
auto it = keyValues.find(key);
if(it == keyValues.end())
return std::nullopt;
if(it->second.size() > 5) {
log(LogLevel::Warning, "Configuration value %s is too long, please check", key.c_str());
return std::nullopt;
}
std::pmr::string val{&memoryResource};
val = it->second;
std::transform(val.begin(), val.end(), val.begin(), [](auto c) { return std::tolower(c); });
if(val == "1" || val == "true" || val == "yes" || val == "on")
return true;
if(val == "0" || val == "false" || val == "no" || val == "off")
return false;
return std::nullopt;
};
auto getFailMode = [&](const string & key) -> std::optional< FailMode > {
auto it = keyValues.find(key);
if(it == keyValues.end())
return std::nullopt;
auto val = it->second;
std::transform(val.begin(), val.end(), val.begin(), [](auto c) { return std::tolower(c); });
if(val == "bypass")
return FailMode::bypass;
if(val == "deny")
return FailMode::deny;
return std::nullopt;
};
auto parseProxyURL = [&](std::string_view url) -> bool {
// Very simple parser: scheme://[user[:pass]@]host[:port]
std::string_view scheme{};
std::string_view auth{};
std::string_view hostport{};
auto scheme_pos = url.find("://");
if(scheme_pos != std::string_view::npos) {
scheme = url.substr(0, scheme_pos);
url = url.substr(scheme_pos + 3);
} else {
scheme = "http"; // default
}
auto at_pos = url.find('@');
if(at_pos != std::string_view::npos) {
auth = url.substr(0, at_pos);
hostport = url.substr(at_pos + 1);
} else {
hostport = url;
}
std::string_view host = hostport;
std::string_view port_str{};
auto colon_pos = hostport.rfind(':');
if(colon_pos != std::string_view::npos && colon_pos < hostport.size() - 1) {
host = hostport.substr(0, colon_pos);
port_str = hostport.substr(colon_pos + 1);
}
config.proxyEnabled = true;
config.proxyType = scheme;
config.proxyHost = host;
if(!port_str.empty()) {
config.proxyPort = conv::to_uint32opt(port_str);
if(not config.proxyPort) {
log(LogLevel::Error, "Invalid proxy port in environment variable");
return false;
}
}
if(!auth.empty()) {
auto colon = auth.find(':');
if(colon != std::string_view::npos) {
config.proxyUsername = auth.substr(0, colon);
config.proxyPass = auth.substr(colon + 1);
} else {
config.proxyUsername = auth;
}
config.proxyAuthRequired = true;
}
return true;
};
auto toLowerCaseOpt = [](auto & str) {
if(str)
std::transform(str->cbegin(), str->cend(), str->begin(), [](auto c) { return std::tolower(c); });
};
/// NOTE:
// getStringOpt can return a valid empty string, for example configuration entry like
// option=
// will return a optional<string> val which contains empty string.
// getStringReq on the other hand, returns error in case when
// * configuration is not found -> RequiredValueNotFound
// * configuration value is empty -> RequiredValueEmpty
// Reading required fields
if(auto val = getStringReq("systemToken"); not val.has_value()) {
logRequiredFieldNotAvailable("systemToken");
return tl::unexpected{val.error()};
} else {
config.systemToken = std::move(val.value());
}
if(auto val = getStringReq("secretKey"); not val.has_value()) {
logRequiredFieldNotAvailable("secretKey");
return tl::unexpected{val.error()};
} else {
config.secretKey = std::move(val.value());
}
if(auto val = getStringReq("rublonApiServer"); not val.has_value()) {
logRequiredFieldNotAvailable("rublonApiServer");
return tl::unexpected{val.error()};
} else {
config.apiServer = std::move(val.value());
}
// optional configuration options
config.prompt = getInt("prompt").value_or(1);
config.enablePasswdEmail = getBool("enablePasswdEmail").value_or(true);
config.logging = getBool("logging").value_or(true);
config.autopushPrompt = getBool("autopushPrompt").value_or(false);
config.nonInteractiveMode = getBool("nonInteractiveMode").value_or(false);
config.failMode = getFailMode("failMode").value_or(FailMode::deny);
// reading proxy configuration
config.proxyEnabled = getBool("proxyEnabled").value_or(false);
config.proxyType = getStringOpt("proxyType");
config.proxyHost = getStringOpt("proxyHost");
// Apply fallback if no config is set
if(config.proxyEnabled && (!config.proxyType || config.proxyType->empty()) && (!config.proxyHost || config.proxyHost->empty())) {
log(LogLevel::Info, "Proxy is enabled but no configuration for it is provided, trying to read from env");
if(auto https_proxy = std::getenv("https_proxy"); https_proxy && *https_proxy) {
if(parseProxyURL(https_proxy)) {
log(LogLevel::Info, "Loaded proxy config from HTTPS_PROXY");
}
} else if(auto http_proxy = std::getenv("http_proxy"); http_proxy && *http_proxy) {
if(parseProxyURL(http_proxy)) {
log(LogLevel::Info, "Loaded proxy config from HTTP_PROXY");
}
}
}
if(config.proxyEnabled) {
if(not config.proxyType or config.proxyType->empty()) {
log(LogLevel::Error, "Proxy is enabled but proxy type is not present or empty");
return tl::unexpected{ConfigurationError::ErrorClass::BadConfiguration};
}
if(not config.proxyHost or config.proxyHost->empty()) {
log(LogLevel::Error, "Proxy is enabled but proxy server is not present or empty");
return tl::unexpected{ConfigurationError::ErrorClass::BadConfiguration};
}
}
config.proxyAuthRequired = getBool("proxyAuthRequired").value_or(false);
config.proxyUsername = getStringOpt("proxyUsername");
config.proxyPass = getStringOpt("proxyPassword");
if(config.proxyAuthRequired) {
if(not config.proxyUsername or config.proxyUsername->empty()) {
log(LogLevel::Error, "Proxy auth is required but proxy proxy username is not present or empty");
return tl::unexpected{ConfigurationError::ErrorClass::BadConfiguration};
}
if(not config.proxyPass) {
log(LogLevel::Error,
"Proxy is enabled but proxy password is not present, "
"if no password is required add an empty entry to configuration file");
return tl::unexpected{ConfigurationError::ErrorClass::BadConfiguration};
}
}
toLowerCaseOpt(config.proxyType);
toLowerCaseOpt(config.proxyHost);
auto defaultProxyPort = [&]() -> int {
if(config.proxyType.value_or("").find("socks") != std::pmr::string::npos) {
return 1080;
}
return 8080;
};
if(config.proxyEnabled and not config.proxyPort) {
config.proxyPort = getInt("proxyPort").value_or(defaultProxyPort());
}
return true;
}
private:
std::pmr::memory_resource * memoryResource;
std::pmr::map< std::pmr::string, std::pmr::string > keyValues{memoryResource};
};
} // namespace rublon