#pragma once #include #include #include #include #include #include #include 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 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