From 0934902bbad9bb2c1498c989db7da35dab198a46 Mon Sep 17 00:00:00 2001 From: rublon-bwi <134260122+rublon-bwi@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:48:18 +0200 Subject: [PATCH] Bwi/v2.3.0 (#17) * Add base PROXY support implementation * Remove some dynamic memory allocations * Rewrite configuration reading module * Make everything in core connector memory resource aware * Add logs to check is proxy is used * Add a proxy fallback, and read proxy from env * Add config entry to check application * Cleanup includes * Ddd configuration dump to check application * Update rhel8 packages * Fix http headers bug when using proxy server * Fix formatting * Fix bad optional access * Fix configuration check regresion * Fix memory management issue, remove strict allocators and make connector more polite to memory overflow errors * Fix initialization of core handler --- .clangd | 2 + CMakeLists.txt | 30 +- PAM/ssh/CMakeLists.txt | 9 +- PAM/ssh/bin/rublon_application.cpp | 56 +-- .../rublon/authentication_step_interface.hpp | 2 +- PAM/ssh/include/rublon/check_application.hpp | 107 ++++- PAM/ssh/include/rublon/configuration.hpp | 426 +++++++++++------- PAM/ssh/include/rublon/core_handler.hpp | 55 +-- PAM/ssh/include/rublon/curl.hpp | 71 ++- PAM/ssh/include/rublon/error.hpp | 13 +- PAM/ssh/include/rublon/error_handler.hpp | 9 +- PAM/ssh/include/rublon/finish.hpp | 7 +- PAM/ssh/include/rublon/init.hpp | 28 +- PAM/ssh/include/rublon/json.hpp | 4 +- PAM/ssh/include/rublon/memory.hpp | 58 +-- .../include/rublon/method/method_select.hpp | 10 +- .../rublon/method/passcode_based_auth.hpp | 44 +- PAM/ssh/include/rublon/pam_action.hpp | 12 +- PAM/ssh/include/rublon/rublon.hpp | 20 +- PAM/ssh/include/rublon/session.hpp | 124 ++++- PAM/ssh/include/rublon/sign.hpp | 32 +- PAM/ssh/include/rublon/static_string.hpp | 37 +- PAM/ssh/include/rublon/utils.hpp | 105 ++++- PAM/ssh/include/rublon/websockets.hpp | 96 ++-- PAM/ssh/lib/pam.cpp | 26 +- PAM/ssh/patches/rapidjson.patch | 13 + os/rhel/8/Dockerfile | 5 +- 27 files changed, 945 insertions(+), 456 deletions(-) create mode 100644 .clangd diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..2e4beaf --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Add: [-std:c++17] diff --git a/CMakeLists.txt b/CMakeLists.txt index a511b30..cda36dc 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ include(CTest) include(GNUInstallDirs) set(PROJECT_VERSION_MAJOR 2) -set(PROJECT_VERSION_MINOR 2) +set(PROJECT_VERSION_MINOR 3) set(PROJECT_VERSION_PATCH 0) set(RUBLON_VERSION_STRING "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") @@ -34,10 +34,10 @@ execute_process ( if ( ${outOS} MATCHES "ubuntu" OR ${outOS} MATCHES "debian" OR ${outOS} MATCHES "FREEBSD" ) install( FILES - ${CMAKE_CURRENT_LIST_DIR}/rsc/rublon.config.defaults + ${CMAKE_CURRENT_LIST_DIR}/rsc/rublon.config.defaults ${CMAKE_CURRENT_LIST_DIR}/service/01-rublon-ssh_pubkey.conf.default - ${CMAKE_CURRENT_LIST_DIR}/service/01-rublon-ssh.conf.default - ${CMAKE_CURRENT_LIST_DIR}/service/inst_pubkey.sh + ${CMAKE_CURRENT_LIST_DIR}/service/01-rublon-ssh.conf.default + ${CMAKE_CURRENT_LIST_DIR}/service/inst_pubkey.sh DESTINATION share/rublon COMPONENT @@ -50,17 +50,17 @@ install( else () install( FILES - ${CMAKE_CURRENT_LIST_DIR}/rsc/rublon.config.defaults - ${CMAKE_CURRENT_LIST_DIR}/service/01-rublon-ssh_pubkey.conf.default - ${CMAKE_CURRENT_LIST_DIR}/service/01-rublon-ssh.conf.default - ${CMAKE_CURRENT_LIST_DIR}/service/login_rublon.mod - ${CMAKE_CURRENT_LIST_DIR}/service/login_rublon.pp - ${CMAKE_CURRENT_LIST_DIR}/service/login_rublon.te - ${CMAKE_CURRENT_LIST_DIR}/service/pam_service.txt - ${CMAKE_CURRENT_LIST_DIR}/service/rublon_veritas - ${CMAKE_CURRENT_LIST_DIR}/service/inst_pubkey_rhel_9.sh - ${CMAKE_CURRENT_LIST_DIR}/service/inst_pubkey_rhel_8.sh - ${CMAKE_CURRENT_LIST_DIR}/service/inst_pubkey.sh + ${CMAKE_CURRENT_LIST_DIR}/rsc/rublon.config.defaults + ${CMAKE_CURRENT_LIST_DIR}/service/01-rublon-ssh_pubkey.conf.default + ${CMAKE_CURRENT_LIST_DIR}/service/01-rublon-ssh.conf.default + ${CMAKE_CURRENT_LIST_DIR}/service/login_rublon.mod + ${CMAKE_CURRENT_LIST_DIR}/service/login_rublon.pp + ${CMAKE_CURRENT_LIST_DIR}/service/login_rublon.te + ${CMAKE_CURRENT_LIST_DIR}/service/pam_service.txt + ${CMAKE_CURRENT_LIST_DIR}/service/rublon_veritas + ${CMAKE_CURRENT_LIST_DIR}/service/inst_pubkey_rhel_9.sh + ${CMAKE_CURRENT_LIST_DIR}/service/inst_pubkey_rhel_8.sh + ${CMAKE_CURRENT_LIST_DIR}/service/inst_pubkey.sh DESTINATION share/rublon COMPONENT diff --git a/PAM/ssh/CMakeLists.txt b/PAM/ssh/CMakeLists.txt index 26dad44..d00c296 100755 --- a/PAM/ssh/CMakeLists.txt +++ b/PAM/ssh/CMakeLists.txt @@ -8,8 +8,8 @@ set(INC ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/core_handler.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/core_handler_interface.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/curl.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/error.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/error_handler.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/error.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/finish.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/init.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/json.hpp @@ -18,8 +18,8 @@ set(INC ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/method/method_select.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/method/OTP.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/method/passcode_based_auth.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/method/PUSH.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/method/phone_call.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/method/PUSH.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/method/SMS.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/method/SmsLink.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/method/websocket_based_auth.hpp @@ -27,6 +27,7 @@ set(INC ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/non_owning_ptr.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/pam_action.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/pam.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/pam_stub.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/rublon.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/session.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/sign.hpp @@ -97,7 +98,7 @@ set(LWS_WITH_UPNG OFF) set(LWS_WITH_UDP OFF) set(LWS_WITH_HTTP_STREAM_COMPRESSION OFF) set(LWS_WITH_HTTP_BROTLI OFF) -set(LWS_WITH_ZLIB OFF) +set(LWS_WITH_ZLIB OFF) set(RAPIDJSON_BUILD_DOC OFF CACHE BOOL "" FORCE) set(RAPIDJSON_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) @@ -139,6 +140,6 @@ endif() add_subdirectory(lib) add_subdirectory(bin) -# if(${ENABLE_TESTS}) +# if(${ENABLE_TESTS}): # add_subdirectory(tests) # endif() diff --git a/PAM/ssh/bin/rublon_application.cpp b/PAM/ssh/bin/rublon_application.cpp index 709b23a..3912ccb 100644 --- a/PAM/ssh/bin/rublon_application.cpp +++ b/PAM/ssh/bin/rublon_application.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -16,77 +17,76 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] const char ** argv) { using namespace rublon; details::initLog(); PamStub pam{}; - + auto printAuthMessageAndExit = [&](const AuthenticationStatus status) { switch(status.action()) { case AuthenticationStatus::Action::Bypass: pam.print("RUBLON authentication BYPASSED"); return PAM_SUCCESS; - + case AuthenticationStatus::Action::Denied: pam.print("RUBLON authentication FAILED"); return PAM_MAXTRIES; - + case AuthenticationStatus::Action::Confirmed: pam.print("RUBLON authentication SUCCEEDED"); return PAM_SUCCESS; } - + pam.print("RUBLON connector has exited with unknown code, access DENY!\n"); return PAM_MAXTRIES; }; - - auto session = rublon::RublonFactory{}.startSession(pam); - if(not session.has_value()) { + + Session session{pam}; + auto ok = rublon::RublonFactory{}.initializeSession(session); + if(not ok.has_value()) { return printAuthMessageAndExit(AuthenticationStatus::Action::Bypass); } - - if(!session->config().logging) { + + if(!session.config().logging) { g_level = LogLevel::Warning; } - - auto & CH = session.value().coreHandler(); - + + CoreHandler_t CH{session.config()}; + auto selectMethod = [&](const MethodSelect & selector) { // return selector.create(pam); }; - + auto confirmMethod = [&](const PostMethod & postMethod) { // return postMethod.handle(CH); }; - + auto confirmCode = [&](const MethodProxy & method) mutable { // - return method.fire(session.value(), CH, pam); + return method.fire(session, CH, pam); }; - + auto finalizeTransaction = [&](const AuthenticationStatus & status) mutable -> tl::expected< AuthenticationStatus, Error > { if(status.userAuthorized()) { - auto tok = std::string{status.accessToken().data()}; - Finish finish{session.value(), std::move(tok)}; + Finish finish{session, status.accessToken()}; finish.handle(CH); } return status; }; - + auto allowLogin = [&](const AuthenticationStatus & status) -> tl::expected< int, Error > { // return printAuthMessageAndExit(status); }; - + auto mapError = [&](const Error & error) -> tl::expected< int, Error > { - return printAuthMessageAndExit(rublon::ErrorHandler{pam, session->config()}.printErrorDetails(error)); + return printAuthMessageAndExit(rublon::ErrorHandler{pam, session.config()}.printErrorDetails(error)); }; - + { - CheckApplication ca; - auto ret = - ca.call(CH, {session.value().config().systemToken.data(), session.value().config().systemToken.size()}).or_else(mapError); + CheckApplication ca{session}; + auto ret = ca.call(CH, session.config().systemToken).or_else(mapError); if(not ret.has_value()) { log(LogLevel::Error, "Check Application step failed, check configration"); return PAM_MAXTRIES; } } - - auto ret = Init{session.value()} + + auto ret = Init{session} .handle(CH, pam) // .and_then(selectMethod) .and_then(confirmMethod) @@ -94,6 +94,6 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] const char ** argv) { .and_then(finalizeTransaction) .and_then(allowLogin) .or_else(mapError); - + return ret.value_or(PAM_MAXTRIES); } diff --git a/PAM/ssh/include/rublon/authentication_step_interface.hpp b/PAM/ssh/include/rublon/authentication_step_interface.hpp index 63f7ad3..89d0fa2 100644 --- a/PAM/ssh/include/rublon/authentication_step_interface.hpp +++ b/PAM/ssh/include/rublon/authentication_step_interface.hpp @@ -35,7 +35,7 @@ class AuthenticationStep { auto event = eventListener.listen(); if(event.status == transactionConfirmed ){ log(LogLevel::Debug, " Transaction confirmed"); - return AuthenticationStatus{AuthenticationStatus::Action::Confirmed, std::string{event.accessToken.value().c_str()}}; + return AuthenticationStatus{AuthenticationStatus::Action::Confirmed, event.accessToken.value().c_str()}; } log(LogLevel::Debug, " Transaction denied"); return AuthenticationStatus{AuthenticationStatus::Action::Denied}; diff --git a/PAM/ssh/include/rublon/check_application.hpp b/PAM/ssh/include/rublon/check_application.hpp index c038c75..31f8b7f 100644 --- a/PAM/ssh/include/rublon/check_application.hpp +++ b/PAM/ssh/include/rublon/check_application.hpp @@ -1,5 +1,8 @@ #pragma once +#include "rublon/configuration.hpp" +#include "rublon/session.hpp" +#include #include #include #include @@ -10,13 +13,21 @@ #include #include +#include +#include namespace rublon { +struct closefile_deleter { + void operator()(FILE * f) const { + pclose(f); + } +}; + std::string exec(const char * cmd) { std::array< char, 128 > buffer; std::string result; - std::unique_ptr< FILE, decltype(&pclose) > pipe(popen(cmd, "r"), pclose); + std::unique_ptr< FILE, closefile_deleter > pipe(popen(cmd, "r")); if(!pipe) { return ""; } @@ -48,6 +59,7 @@ class Status { std::string_view _appTypeKey = "/type"; std::string_view _paramSystemName = "/params/os"; std::string_view _paramSSHDBase = "/params/sshd_config/"; + std::string_view _configBase = "/config/"; public: Status() : _alloc{}, _data{&_alloc}, _statusUpdated{false} { @@ -88,7 +100,7 @@ class Status { void updateSSHDConfig() { using namespace std::string_view_literals; - constexpr auto keys = make_array("authenticationmethods"sv, + constexpr auto keys = make_array< std::string_view >("authenticationmethods"sv, "challengeresponseauthentication"sv, "kbdinteractiveauthentication"sv, "logingracetime"sv, @@ -119,6 +131,85 @@ class Status { } } + template < typename T > + void updateRublonConfigParameter(std::string_view name, const T & newParam) { + memory::MonotonicStack_1k_Resource memoryResource{}; + RapidJSONPMRAlloc stackAlloc{&memoryResource}; + + std::pmr::string jsonPath{&memoryResource}; + jsonPath += _configBase; + jsonPath += name; + + JSONPointer jsonPointer{jsonPath.c_str(), &stackAlloc}; + auto * param = jsonPointer.Get(_data); + + Document::AllocatorType & alloc = _data.GetAllocator(); + Value newValue; + + if constexpr(std::is_same_v< T, std::pmr::string > || std::is_same_v< T, std::string_view >) { + newValue.SetString(newParam.data(), static_cast< rapidjson::SizeType >(newParam.size()), alloc); + if(!param || !param->IsString() || param->GetString() != newParam) { + markUpdated(); + jsonPointer.Set(_data, newValue); + } + } else if constexpr(std::is_same_v< T, bool >) { + newValue.SetBool(newParam); + if(!param || !param->IsBool() || param->GetBool() != newParam) { + markUpdated(); + jsonPointer.Set(_data, newValue); + } + } else if constexpr(std::is_integral_v< T >) { + newValue.SetInt(static_cast< int >(newParam)); + if(!param || !param->IsInt() || param->GetInt() != static_cast< int >(newParam)) { + markUpdated(); + jsonPointer.Set(_data, newValue); + } + } + } + + // For std::optional + template < typename T > + void updateRublonConfigParameter(std::string_view name, const std::optional< T > & optParam) { + memory::MonotonicStack_1k_Resource memoryResource{}; + RapidJSONPMRAlloc stackAlloc{&memoryResource}; + + std::pmr::string fullPath{&memoryResource}; + fullPath += _configBase; + fullPath += name; + + JSONPointer jsonPointer{fullPath.c_str(), &stackAlloc}; + Value* existing = jsonPointer.Get(_data); + + if (optParam.has_value()) { + // Delegate to regular (non-optional) setter + updateRublonConfigParameter(name, *optParam); + } else { + // Set the entire field to `null` if no value + if (!existing || !existing->IsNull()) { + markUpdated(); + Value nullValue(rapidjson::kNullType); + jsonPointer.Set(_data, nullValue); + } + } + } + + void updateRublonConfig(const Configuration & config) { + updateRublonConfigParameter("prompt", config.prompt); + updateRublonConfigParameter("logging", config.logging); + updateRublonConfigParameter("autopushPrompt", config.autopushPrompt); + updateRublonConfigParameter("failMode", static_cast(config.failMode)); + updateRublonConfigParameter("nonInteractiveMode", config.nonInteractiveMode); + + updateRublonConfigParameter("proxyType", config.proxyType); + updateRublonConfigParameter("proxyHost", config.proxyHost); + updateRublonConfigParameter("proxyUsername", config.proxyUsername); + updateRublonConfigParameter("proxyPassword", config.proxyPass); + updateRublonConfigParameter("proxyPort", config.proxyPort); + + updateRublonConfigParameter("proxyAuthRequired", config.proxyAuthRequired); + updateRublonConfigParameter("proxyEnabled", config.proxyEnabled); + } + void markUpdated() { _statusUpdated = true; } @@ -129,7 +220,7 @@ class Status { void save() { if(updated()) { - memory::Monotonic_8k_HeapResource tmpResource; + memory::Monotonic_8k_Resource tmpResource; RapidJSONPMRAlloc alloc{&tmpResource}; FileWriter s{_statusFilePath}; rapidjson::PrettyWriter< FileWriter, rapidjson::UTF8<>, rapidjson::UTF8<>, RapidJSONPMRAlloc > writer{s, &alloc}; @@ -140,7 +231,7 @@ class Status { std::string print() { std::string result; - memory::Monotonic_8k_HeapResource tmpResource; + memory::Monotonic_8k_Resource tmpResource; RapidJSONPMRAlloc alloc{&tmpResource}; StringWriter s{result}; rapidjson::PrettyWriter< StringWriter< std::string >, rapidjson::UTF8<>, rapidjson::UTF8<>, RapidJSONPMRAlloc > writer{s, &alloc}; @@ -156,6 +247,7 @@ class Status { }; class CheckApplication { + const Session & _sesion; tl::expected< bool, Error > persistStatus(Status & status) const { status.data().RemoveMember("systemToken"); status.save(); @@ -163,9 +255,11 @@ class CheckApplication { } public: + CheckApplication(const Session & session) : _sesion{session} {} + tl::expected< int, Error > call(const CoreHandler_t & coreHandler, std::string_view systemToken) const { - memory::Monotonic_1k_HeapResource mr; - RapidJSONPMRStackAlloc< 2048 > alloc{}; + memory::MonotonicStack_2k_Resource mr; + RapidJSONPMRAlloc alloc{&mr}; constexpr std::string_view api = "/api/app/init"; Status status; @@ -174,6 +268,7 @@ class CheckApplication { status.updateAppVersion(RUBLON_VERSION_STRING); status.updateSystemVersion(details::osName(&mr)); status.updateSSHDConfig(); + status.updateRublonConfig(_sesion.config()); if(status.updated()) { auto & alloc = status.data().GetAllocator(); diff --git a/PAM/ssh/include/rublon/configuration.hpp b/PAM/ssh/include/rublon/configuration.hpp index 6e8ae4a..0c304f3 100644 --- a/PAM/ssh/include/rublon/configuration.hpp +++ b/PAM/ssh/include/rublon/configuration.hpp @@ -1,14 +1,12 @@ #pragma once #include +#include #include #include -#include -template < typename T > -constexpr bool is_static_string_v = std::is_base_of_v< rublon::details::StaticStringBase, T >; - -static_assert(is_static_string_v< rublon::StaticString< 32 > >); +#include +#include namespace rublon { class ConfigurationFactory; @@ -16,165 +14,52 @@ 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 - StaticString< 32 > systemToken{}; - StaticString< 32 > secretKey{}; - StaticString< 4096 > apiServer{}; + std::pmr::string systemToken{memoryResource}; + std::pmr::string secretKey{memoryResource}; + std::pmr::string apiServer{memoryResource}; int prompt{}; - bool enablePasswdEmail{}; + 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 }; -namespace { - template < class C, typename T > - T member_ptr_t(T C::*v); - - template < typename T > - tl::expected< T, ConfigurationError > to(std::string_view); - - template < class T > - auto to_string(std::string_view arg) -> tl::expected< T, ConfigurationError > { - T value{}; - assert(arg.size() <= (value.size() - 1)); - std::memcpy(value.data(), arg.data(), arg.size()); - return value; - } - - template <> - auto to(std::string_view arg) -> tl::expected< bool, ConfigurationError > { - return conv::to_bool(arg); - } - template <> - auto to(std::string_view arg) -> tl::expected< int, ConfigurationError > { - return conv::to_uint32(arg).value_or(0); - } - - template <> - auto to(std::string_view arg) -> tl::expected< FailMode, ConfigurationError > { - if(arg == "bypass") - return FailMode::bypass; - if(arg == "deny") - return FailMode::deny; - return tl::unexpected{ConfigurationError{ConfigurationError::ErrorClass::BadFailMode}}; - } - - template < typename T > - auto parse(std::string_view arg) -> tl::expected< T, ConfigurationError > { - if(arg.empty()) { - return tl::unexpected{ConfigurationError::ErrorClass::Empty}; - } else { - if constexpr(is_static_string_v< T >) { - return to_string< T >(arg); - } else { - return to< T >(arg); - } - } - } - -} // namespace -struct Entry { - enum class Source { UserInput, DefaultValue }; - template < auto member > - static constexpr auto make_read_function() { - using pType = decltype(member_ptr_t(member)); - - return - [](const Entry * _this, Configuration * configuration, std::string_view userInput) -> tl::expected< Source, ConfigurationError > { - const auto setDefaultValue = [&](const ConfigurationError & error) -> tl::expected< Source, ConfigurationError > { - log(LogLevel::Warning, "applying user provided value for %s parameter, faild with %s", _this->name, error.what()); - if(_this->defaultValue != nullptr) { - configuration->*member = parse< pType >(_this->defaultValue).value(); - return Source::DefaultValue; - } else { - log(LogLevel::Error, "parameter %s has not been found and has no default value", _this->name); - if(userInput.empty()) - return tl::unexpected{ConfigurationError::ErrorClass::RequiredValueNotFound}; - else - return tl::unexpected{ConfigurationError::ErrorClass::BadInput}; - } - }; - - const auto saveValue = [&](const auto & value) -> tl::expected< Source, ConfigurationError > { - configuration->*member = value; - return Source::UserInput; - }; - - return parse< pType >(userInput).and_then(saveValue).or_else(setDefaultValue); - }; - } - - const char * name; - const char * defaultValue; - tl::expected< Source, ConfigurationError > (*_read)(const Entry * _this, Configuration * configuration, std::string_view userInput); - - bool read(Configuration * configuration, std::optional< std::string_view > userInput) const { - constexpr const auto emptyString = ""; - const auto logStored = [&](const auto & source) -> tl::expected< Source, ConfigurationError > { - rublon::log(LogLevel::Debug, - "Configuration parameter '%s' was set to '%s'%s", - this->name, - this->defaultValue, - source == Source::DefaultValue ? " (default)" : ""); - return source; - }; - - const auto logError = [&](const auto & error) -> tl::expected< Source, ConfigurationError > { - rublon::log(LogLevel::Error, - "Configuration parameter '%s' has no default value and is not provided in user configuraion, aborting", - this->name); - return tl::unexpected{error}; - }; - - return _read(this, configuration, userInput.value_or(emptyString)).and_then(logStored).or_else(logError).has_value(); - } -}; - -template < auto member > -constexpr auto make_entry(const char * name, const char * defaultValue) { - return Entry{name, defaultValue, Entry::make_read_function< member >()}; -} - -constexpr static inline std::array< Entry, 9 > configurationVariables = { // - make_entry< &Configuration::logging >("logging", "true"), - make_entry< &Configuration::systemToken >("systemToken", nullptr), - make_entry< &Configuration::secretKey >("secretKey", nullptr), - make_entry< &Configuration::apiServer >("rublonApiServer", nullptr), - make_entry< &Configuration::prompt >("prompt", "1"), - make_entry< &Configuration::enablePasswdEmail >("enablePasswdEmail", "true"), - make_entry< &Configuration::autopushPrompt >("autopushPrompt", "false"), - make_entry< &Configuration::failMode >("failMode", "deny"), - make_entry< &Configuration::nonInteractiveMode >("nonInteractiveMode", "false") -}; - -class ConfigurationFactory { +class ConfigurationReader { public: - ConfigurationFactory() = default; + ConfigurationReader(std::pmr::memory_resource * memResource, std::string_view filepath) : memoryResource(memResource) { + loadFromFile(filepath); + } - std::optional< Configuration > systemConfig() { - memory::MonotonicStackResource< 8 * 1024 > stackResource; - Configuration configuration{}; - - std::ifstream file(std::filesystem::path{"/etc/rublon.config"}); + // 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::nullopt; + return; - std::pmr::string line{&stackResource}; - line.reserve(100); - std::pmr::map< std::pmr::string, std::pmr::string > parameters{&stackResource}; - - const auto readParameterByName = [&](std::string_view name) -> std::optional< std::string_view > { - return parameters.count(name.data()) ? std::optional< std::string_view >{parameters.at(name.data())} : std::nullopt; - }; + std::pmr::string line{&memoryResource}; + line.reserve(300); while(std::getline(file, line)) { - std::pmr::string key{&stackResource}; - std::pmr::string value{&stackResource}; - + details::trimInPlace(line); if(!line.length()) continue; @@ -182,18 +67,243 @@ class ConfigurationFactory { continue; auto posEqual = line.find('='); - key = line.substr(0, posEqual); - value = line.substr(posEqual + 1); + std::pmr::string key{line.substr(0, posEqual), &memoryResource}; + std::pmr::string value{line.substr(posEqual + 1), &memoryResource}; - parameters[std::move(key)] = std::move(value); + keyValues[std::move(key)] = std::move(value); } - - for(const auto & entry : configurationVariables) { - if(not entry.read(&configuration, readParameterByName(entry.name))) - return std::nullopt; - } - - return configuration; } + + // 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(), [](unsigned char c) { return static_cast< char >(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; + 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; + }; + + /// 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}; + } + } + + auto defaultProxyPort = [&]() -> int { + memory::MonotonicStackResource< 32 > memoryResource; + + if(config.proxyType) { + std::pmr::string val{*config.proxyType, &memoryResource}; + std::transform(val.begin(), val.end(), val.begin(), [](auto c) { return std::tolower(c); }); + if(val.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 diff --git a/PAM/ssh/include/rublon/core_handler.hpp b/PAM/ssh/include/rublon/core_handler.hpp index 0168318..cf74d6f 100755 --- a/PAM/ssh/include/rublon/core_handler.hpp +++ b/PAM/ssh/include/rublon/core_handler.hpp @@ -1,9 +1,5 @@ #pragma once -#include "rublon/error.hpp" -#include "rublon/static_string.hpp" -#include -#include #include #include #include @@ -13,19 +9,22 @@ #include #include -#include #include namespace rublon { template < typename HttpHandler = CURL > class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > { - Configuration _config; + std::reference_wrapper< const Configuration > _config; mutable std::unique_ptr< WebSocket > _ws; /// TODO try to remove mutable modyfier + HttpHandler http{}; + + const Configuration & config() const noexcept { + return _config.get(); + } void signRequest(Request & request) const { - request.headers["X-Rublon-Signature"] = - std::pmr::string{signData(request.body, _config.secretKey).c_str(), request.headers.get_allocator()}; + request.headers["X-Rublon-Signature"] = signData(request.body, config().secretKey).c_str(); } bool hasSignature(const Response & response) const { @@ -50,7 +49,7 @@ class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > { bool signatureIsNatValid(const Response & response) const { const auto & xRubResp = response.headers.at("x-rublon-signature"); - const auto & sign = signData(response.body, _config.secretKey); + const auto & sign = signData(response.body, config().secretKey); const bool signatureMatch = xRubResp == sign.data(); if(not signatureMatch) log(LogLevel::Error, "Signature mismatch %s != %s ", xRubResp.c_str(), sign.data()); @@ -68,19 +67,16 @@ class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > { return coreResponse.HasParseError(); } - protected: - HttpHandler http{}; - public: CoreHandler() = delete; - CoreHandler(const Configuration & config) : _config{config}, _ws{std::make_unique(_config.apiServer)}, http{} { - log(LogLevel::Debug, "Core Handler apiServer: %s", _config.apiServer.c_str()); + CoreHandler(const Configuration & baseconfig) : _config{baseconfig}, _ws{std::make_unique< WebSocket >(_config)}, http{config()} { + log(LogLevel::Debug, "Core Handler apiServer: %s", config().apiServer.c_str()); } - CoreHandler(CoreHandler &&) noexcept = default; - CoreHandler & operator=(CoreHandler &&) = default; + CoreHandler(const CoreHandler &) = delete; + CoreHandler(CoreHandler &&) noexcept = delete; - CoreHandler(const CoreHandler &) = delete; CoreHandler & operator=(const CoreHandler &) = delete; + CoreHandler & operator=(CoreHandler &&) = delete; ~CoreHandler() noexcept = default; @@ -125,14 +121,13 @@ class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > { // additional check for mallformed responses (A invalid response, without any x-rublon-signature will stop at this check) return tl::unexpected{CoreHandlerError{CoreHandlerError::BadSigature}}; } - + log(LogLevel::Debug, "Core Response validated OK"); return resp; } tl::unexpected< Error > handleCoreException(std::string_view exceptionString) const { - log(LogLevel::Debug, "TMP got core exception: %s", exceptionString.data() ); - // can happen only dyring check application step + // can happen only during check application step if(auto error = RublonCheckApplicationException::fromString(exceptionString); error.has_value()) return tl::unexpected{Error{error.value()}}; @@ -143,9 +138,9 @@ class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > { // verification error wrong passcode etc. if(auto error = WerificationError::fromString(exceptionString); error.has_value()) return tl::unexpected{Error{error.value()}}; - - // CoreHandlerError::TransactionAccessTokenExpiredException - + + // CoreHandlerError::TransactionAccessTokenExpiredException + // other exceptions, just "throw" return tl::unexpected{Error{CoreHandlerError{CoreHandlerError::RublonCoreException}}}; } @@ -155,24 +150,24 @@ class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > { } tl::expected< Document, Error > request(RapidJSONPMRAlloc & mr, std::string_view path, const Document & body) const { - memory::StrictMonotonic_8k_HeapResource memoryResource; + memory::Monotonic_4k_Resource memoryResource; const auto validateSignature = [&](const auto & arg) { return this->validateSignature(arg); }; const auto validateResponse = [&](const auto & arg) { return this->validateResponse(mr, arg); }; const auto handleError = [&](const auto & error) { return this->handleError(error); }; - const auto pmrs = [&](const auto & txt) { return std::pmr::string{txt, &memoryResource}; }; Request request{&memoryResource}; Response response{&memoryResource}; - request.headers["Content-Type"] = pmrs("application/json"); - request.headers["Accept"] = pmrs("application/json"); + request.headers["Content-Type"] = "application/json"; + request.headers["Accept"] = "application/json"; stringifyTo(body, request.body); signRequest(request); std::pmr::string uri{&memoryResource}; - uri += _config.apiServer.c_str(); + uri.reserve(conservative_estimate(config().apiServer, path.size())); + uri += config().apiServer; uri += path.data(); return http @@ -183,8 +178,8 @@ class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > { } bool createWSConnection(std::string_view tid) const { - if(not _ws){ - _ws.reset(new WebSocket (_config.apiServer)); + if(not _ws) { + _ws.reset(new WebSocket(config())); } /// TODO connect can be separated from subscribtion on event /// TODO status of attach is not checked diff --git a/PAM/ssh/include/rublon/curl.hpp b/PAM/ssh/include/rublon/curl.hpp index 162962c..4e7b7fc 100644 --- a/PAM/ssh/include/rublon/curl.hpp +++ b/PAM/ssh/include/rublon/curl.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -51,16 +52,29 @@ struct Response { class CURL { std::unique_ptr< ::CURL, void (*)(::CURL *) > curl; + std::reference_wrapper< const Configuration > _config; + + const Configuration & conf() const noexcept { + return _config.get(); + } public: - CURL() : curl{std::unique_ptr< ::CURL, void (*)(::CURL *) >(curl_easy_init(), curl_easy_cleanup)} {} + CURL(const Configuration & config) + : curl{std::unique_ptr< ::CURL, void (*)(::CURL *) >(curl_easy_init(), curl_easy_cleanup)}, _config{config} {} + + CURL(const CURL &) = delete; + CURL(CURL &&) = delete; + + CURL & operator=(const CURL &) = delete; + CURL & operator=(CURL &&) = delete; tl::expected< std::reference_wrapper< Response >, ConnectionError > request(std::string_view uri, const Request & request, Response & response) const { - memory::MonotonicStackResource< 4 * 1024 > stackResource; + using namespace memory::literals; + memory::Monotonic_8k_Resource memoryResource; - std::pmr::string response_data{&stackResource}; - response_data.reserve(3000); + std::pmr::string response_data{&memoryResource}; + response_data.reserve(4_kB); auto curl_headers = std::unique_ptr< curl_slist, void (*)(curl_slist *) >(nullptr, curl_slist_free_all); std::for_each(request.headers.begin(), request.headers.end(), [&](auto header) { @@ -68,6 +82,55 @@ class CURL { curl_headers.reset(curl_slist_append(curl_headers.release(), (header.first + ": " + header.second).c_str())); }); + // Optional: Build full proxy URL if proxy is enabled + if(conf().proxyEnabled) { + // configuration reader check if proxy has needed fields + assert(conf().proxyType.has_value()); + assert(conf().proxyHost.has_value()); + log(LogLevel::Debug, "CURL using proxy"); + + std::pmr::string proxyUrl{&memoryResource}; + proxyUrl.reserve(conservative_estimate(conf().proxyType, conf().proxyHost, conf().proxyPort) + 10); + + if(conf().proxyType == "http" || conf().proxyType == "https" || conf().proxyType == "socks4" || conf().proxyType == "socks5") { + proxyUrl = *conf().proxyType; + proxyUrl += "://"; + proxyUrl += *conf().proxyHost; + if(conf().proxyPort.value_or(0) > 0) { + proxyUrl += ":"; + proxyUrl += std::to_string(*conf().proxyPort); + } + + log(LogLevel::Debug, "CURL using %s", proxyUrl.c_str()); + curl_easy_setopt(curl.get(), CURLOPT_PROXY, proxyUrl.c_str()); + + if(conf().proxyType == "socks4") { + curl_easy_setopt(curl.get(), CURLOPT_PROXYTYPE, CURLPROXY_SOCKS4); + } else if(conf().proxyType == "socks5") { + curl_easy_setopt(curl.get(), CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); + } else { + curl_easy_setopt(curl.get(), CURLOPT_PROXYTYPE, CURLPROXY_HTTP); + } + + if(conf().proxyAuthRequired) { + assert(conf().proxyUsername.has_value()); + assert(conf().proxyPass.has_value()); + + std::pmr::string proxyAuth{&memoryResource}; + proxyAuth.reserve(conservative_estimate(conf().proxyUsername, conf().proxyPass)); + + proxyAuth += *conf().proxyUsername; + if(conf().proxyPass->size()) { + // can proxy have name but no pass? + proxyAuth += ":"; + proxyAuth += *conf().proxyPass; + } + + curl_easy_setopt(curl.get(), CURLOPT_PROXYUSERPWD, proxyAuth.c_str()); + } + } + } + curl_easy_setopt(curl.get(), CURLOPT_VERBOSE, 0); curl_easy_setopt(curl.get(), CURLOPT_URL, uri.data()); curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, curl_headers.get()); diff --git a/PAM/ssh/include/rublon/error.hpp b/PAM/ssh/include/rublon/error.hpp index 21af816..dd85c40 100755 --- a/PAM/ssh/include/rublon/error.hpp +++ b/PAM/ssh/include/rublon/error.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -13,14 +12,18 @@ class ConfigurationError { public: enum class ErrorClass { // RequiredValueNotFound, + RequiredValueEmpty, BadFailMode, BadInput, + BadConfiguration, Empty }; constexpr static auto errorClassPrettyName = make_array< std::string_view >( // "RequiredValueNotFound", + "RequiredValueEmpty", "BadFailMode", "BadInput", + "BadConfiguration", "Empty"); constexpr static auto prettyName = "Configurtion Error"; @@ -119,15 +122,17 @@ class MethodError { class WerificationError { public: enum ErrorClass { - BadInput, // User input has incorrect characters or length - SecurityKeyException, // code: 16 - PasscodeException, // code: 18 + BadInput, // User input has incorrect characters or length + SecurityKeyException, // code: 16 + PasscodeException, // code: 18 + SendPushException, // code: 21 | this code really should not be here as rest of the codes are strictly for passcode verification TooManyRequestsException // code: 101 }; constexpr static auto errorClassPrettyName = make_array< std::string_view >( // "BadInput", "SecurityKeyException", "PasscodeException", + "SendPushException", "TooManyRequestsException"); constexpr static inline auto prettyName = "Werification Error"; diff --git a/PAM/ssh/include/rublon/error_handler.hpp b/PAM/ssh/include/rublon/error_handler.hpp index a65df2b..132e9d5 100644 --- a/PAM/ssh/include/rublon/error_handler.hpp +++ b/PAM/ssh/include/rublon/error_handler.hpp @@ -66,12 +66,15 @@ class ErrorHandler { case WerificationError::ErrorClass::BadInput: pam.print(R"(Ensure that the Secret Key is correct.)"); return AuthenticationStatus::Action::Denied; - case rublon::WerificationError::SecurityKeyException: + case WerificationError::SecurityKeyException: pam.print(R"(Ensure that the Secret Key is correct.)"); return AuthenticationStatus::Action::Denied; - case rublon::WerificationError::TooManyRequestsException: + case WerificationError::TooManyRequestsException: pam.print(R"(Too many attempts.)"); return AuthenticationStatus::Action::Denied; + case WerificationError::SendPushException: + pam.print(R"(Rublon failed to reach authenticator app)"); + break; } } @@ -89,7 +92,7 @@ class ErrorHandler { log(LogLevel::Error, R"(The provided version of the app is unsupported.)"); log(LogLevel::Error, R"(Try changing the app version.)"); return AuthenticationStatus::Action::Denied; - case rublon::RublonCheckApplicationException::MissingFieldException: + case RublonCheckApplicationException::MissingFieldException: log(LogLevel::Error, R"(The provided version of the app is unsupported.)"); log(LogLevel::Error, R"(Try changing the app version.)"); return AuthenticationStatus::Action::Denied; diff --git a/PAM/ssh/include/rublon/finish.hpp b/PAM/ssh/include/rublon/finish.hpp index c943d2f..ad598eb 100644 --- a/PAM/ssh/include/rublon/finish.hpp +++ b/PAM/ssh/include/rublon/finish.hpp @@ -4,16 +4,17 @@ #include #include #include +#include #include namespace rublon { class Finish : public AuthenticationStep { const char * apiPath = "/api/transaction/credentials"; - const std::string _accessToken; + const std::string_view _accessToken; // void addAccessToken(Document & coreRequest) const { auto & alloc = coreRequest.GetAllocator(); - coreRequest.AddMember("accessToken", Value{_accessToken.c_str(), alloc}, alloc); + coreRequest.AddMember("accessToken", Value{_accessToken.data(), alloc}, alloc); } tl::expected< bool, Error > returnOk(const Document & /*coreResponse*/) const { @@ -23,7 +24,7 @@ class Finish : public AuthenticationStep { public: const char * name = "Finalization"; - Finish(Session & session, std::string accessToken) : AuthenticationStep(session), _accessToken{std::move(accessToken)} {} + Finish(Session & session, std::string_view accessToken) : AuthenticationStep(session), _accessToken{accessToken} {} template < typename Hander_t > tl::expected< bool, Error > handle(const CoreHandlerInterface< Hander_t > & coreHandler) const { diff --git a/PAM/ssh/include/rublon/init.hpp b/PAM/ssh/include/rublon/init.hpp index df8026b..65b7343 100644 --- a/PAM/ssh/include/rublon/init.hpp +++ b/PAM/ssh/include/rublon/init.hpp @@ -1,20 +1,14 @@ #pragma once -#include "rublon/memory.hpp" -#include "rublon/utils.hpp" -#include - #include +#include #include #include #include - #include #include -[[deprecated]] extern std::string g_tid; - namespace rublon { template < class MethodSelect_t = MethodSelect > @@ -28,7 +22,7 @@ class Init : public AuthenticationStep { RapidJSONPMRAlloc alloc{&stackResource}; const auto * rublonMethods = JSONPointer{"/result/methods", &alloc}.Get(coreResponse); - const auto * rublonTid = JSONPointer{"/result/tid", &alloc}.Get(coreResponse); + const auto * rublonTid = JSONPointer{"/result/tid", &alloc}.Get(coreResponse); if(not rublonMethods) log(LogLevel::Error, "core response has no methods"); @@ -45,12 +39,12 @@ class Init : public AuthenticationStep { } void addParams(Document & coreRequest, const Pam_t & pam) const { - memory::MonotonicStackResource< 1024 > stackResource; - std::pmr::string releaseInfo{&stackResource}; + using namespace memory::literals; + memory::MonotonicStackResource< 2_kB > memoryResource; auto & alloc = coreRequest.GetAllocator(); - const auto os = details::osName(&stackResource); - const auto host = details::hostname(&stackResource); + const auto os = details::osName(&memoryResource); + const auto host = details::hostname(&memoryResource); if(os == "unknown") { log(LogLevel::Warning, "No OS information available"); @@ -62,7 +56,7 @@ class Init : public AuthenticationStep { params.AddMember("appVer", RUBLON_VERSION_STRING, alloc); if(not host.empty()) params.AddMember("hostName", Value{os.c_str(), static_cast< unsigned >(host.size()), alloc}, alloc); - if(not _session.inInteractiveMode()) + if(_session.inNonInteractiveMode()) params.AddMember("mode", "noninteractive", alloc); params.AddMember("os", Value{os.c_str(), static_cast< unsigned >(os.size()), alloc}, alloc); params.AddMember("userIP", Value{pam.ip().get(), alloc}, alloc); @@ -72,7 +66,8 @@ class Init : public AuthenticationStep { tl::expected< std::reference_wrapper< const Document >, Error > checkEnrolement(const Document & coreResponse, const Pam_t pam) const { using namespace std::string_view_literals; - memory::MonotonicStackResource< 256 > stackResource; + using namespace memory::literals; + memory::MonotonicStackResource< 1_kB > stackResource; RapidJSONPMRAlloc alloc{&stackResource}; const auto * rublonStatus = JSONPointer{"/result/status", &alloc}.Get(coreResponse); @@ -80,14 +75,15 @@ class Init : public AuthenticationStep { if(rublonStatus) { const auto & status = rublonStatus->GetString(); - log(LogLevel::Warning, "Got enrolement message with stats %s", status); if(status == "pending"sv) { if(rublonWebURI) { pam.print("Visit %s", rublonWebURI->GetString()); } } else if(status == "waiting"sv) { + log(LogLevel::Warning, "Got enrolement message with stats %s", status); return tl::unexpected{Error{RublonAuthenticationInterrupt{RublonAuthenticationInterrupt::UserWaiting}}}; } else if(status == "denied"sv) { + log(LogLevel::Warning, "Got enrolement message with stats %s", status); return tl::unexpected{Error{RublonAuthenticationInterrupt{RublonAuthenticationInterrupt::UserDenied}}}; } } @@ -99,7 +95,7 @@ class Init : public AuthenticationStep { const char * _name = "Initialization"; Init(Session & session) : base_t(session) { - log(LogLevel::Debug, "Init"); + log(LogLevel::Debug, "Starting inicialization"); } template < typename Hander_t > diff --git a/PAM/ssh/include/rublon/json.hpp b/PAM/ssh/include/rublon/json.hpp index d158600..7207bde 100644 --- a/PAM/ssh/include/rublon/json.hpp +++ b/PAM/ssh/include/rublon/json.hpp @@ -122,8 +122,8 @@ struct FileWriter { template < typename T > static void stringifyTo(const Document & body, T & to) { - memory::Monotonic_1k_HeapResource tmpResource; - RapidJSONPMRAlloc alloc{&tmpResource}; + memory::MonotonicStack_1k_Resource memoryResource; + RapidJSONPMRAlloc alloc{&memoryResource}; StringWriter< T > s{to}; rapidjson::Writer< StringWriter< T >, rapidjson::UTF8<>, rapidjson::UTF8<>, RapidJSONPMRAlloc > writer{s, &alloc}; body.Accept(writer); diff --git a/PAM/ssh/include/rublon/memory.hpp b/PAM/ssh/include/rublon/memory.hpp index 5a8c878..6d12ef7 100644 --- a/PAM/ssh/include/rublon/memory.hpp +++ b/PAM/ssh/include/rublon/memory.hpp @@ -1,9 +1,16 @@ #pragma once +#include #include namespace rublon { namespace memory { + namespace literals { + constexpr std::uint64_t operator"" _kB(unsigned long long kilobytes) { + return kilobytes * 1024ULL; + } + } // namespace literals + struct default_memory_resource { static inline std::pmr::memory_resource * _mr = std::pmr::get_default_resource(); }; @@ -16,53 +23,48 @@ namespace memory { return default_memory_resource{}._mr; } - template < std::size_t N > - class MonotonicStackResource : public std::pmr::monotonic_buffer_resource { - char _buffer[N]; - - public: - MonotonicStackResource() : std::pmr::monotonic_buffer_resource{_buffer, N, std::pmr::null_memory_resource()} {} - }; - - class MonotonicHeapResourceBase { + class MonotonicResourceBase { public: std::pmr::memory_resource * _upstream{}; std::size_t _size{}; void * _buffer{nullptr}; - MonotonicHeapResourceBase(std::size_t size) : _upstream{default_resource()}, _size{size}, _buffer{_upstream->allocate(size)} {} + MonotonicResourceBase(std::size_t size) : _upstream{default_resource()}, _size{size}, _buffer{_upstream->allocate(size)} {} - ~MonotonicHeapResourceBase() { + ~MonotonicResourceBase() { if(_buffer) _upstream->deallocate(_buffer, _size); } }; template < std::size_t N > - class MonotonicHeapResource : MonotonicHeapResourceBase, public std::pmr::monotonic_buffer_resource { + class MonotonicResource : MonotonicResourceBase, public std::pmr::monotonic_buffer_resource { public: - MonotonicHeapResource() - : MonotonicHeapResourceBase{N}, std::pmr::monotonic_buffer_resource{this->_buffer, this->_size, default_resource()} {} + MonotonicResource(std::pmr::memory_resource * mr = default_resource()) + : MonotonicResourceBase{N}, std::pmr::monotonic_buffer_resource{this->_buffer, this->_size, mr} {} }; template < std::size_t N > - class StrictMonotonicHeapResource : MonotonicHeapResourceBase, public std::pmr::monotonic_buffer_resource { + class MonotonicStackResourceBase { public: - StrictMonotonicHeapResource() - : MonotonicHeapResourceBase{N}, - std::pmr::monotonic_buffer_resource{this->_buffer, this->_size, std::pmr::null_memory_resource()} {} + char _buffer[N]; }; - using StrictMonotonic_512_HeapResource = StrictMonotonicHeapResource< 512 >; - using StrictMonotonic_1k_HeapResource = StrictMonotonicHeapResource< 1 * 1024 >; - using StrictMonotonic_2k_HeapResource = StrictMonotonicHeapResource< 2 * 1024 >; - using StrictMonotonic_4k_HeapResource = StrictMonotonicHeapResource< 4 * 1024 >; - using StrictMonotonic_8k_HeapResource = StrictMonotonicHeapResource< 8 * 1024 >; - - using Monotonic_1k_HeapResource = MonotonicHeapResource< 1 * 1024 >; - using Monotonic_2k_HeapResource = MonotonicHeapResource< 2 * 1024 >; - using Monotonic_4k_HeapResource = MonotonicHeapResource< 4 * 1024 >; - using Monotonic_8k_HeapResource = MonotonicHeapResource< 8 * 1024 >; + template < std::size_t N > + class MonotonicStackResource : MonotonicStackResourceBase< N >, public std::pmr::monotonic_buffer_resource { + public: + MonotonicStackResource() + : MonotonicStackResourceBase< N >{}, std::pmr::monotonic_buffer_resource{this->_buffer, N, default_resource()} {} + }; + + using MonotonicStack_1k_Resource = MonotonicStackResource< 1 * 1024 >; + using MonotonicStack_2k_Resource = MonotonicStackResource< 2 * 1024 >; + + + using Monotonic_1k_Resource = MonotonicResource< 1 * 1024 >; + using Monotonic_2k_Resource = MonotonicResource< 2 * 1024 >; + using Monotonic_4k_Resource = MonotonicResource< 4 * 1024 >; + using Monotonic_8k_Resource = MonotonicResource< 8 * 1024 >; } // namespace memory // class RublonMemory { diff --git a/PAM/ssh/include/rublon/method/method_select.hpp b/PAM/ssh/include/rublon/method/method_select.hpp index 616f821..0c65913 100644 --- a/PAM/ssh/include/rublon/method/method_select.hpp +++ b/PAM/ssh/include/rublon/method/method_select.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include #include @@ -129,12 +130,15 @@ class MethodSelect { int _prompts; bool _autopushPrompt; - std::vector< std::string > _methodsAvailable; // TODO pmr + std::pmr::memory_resource * _mr; + + // method name is really short, there is almost no chance that thos strings will allocate + std::pmr::vector< std::pmr::string > _methodsAvailable; public: template < typename Array_t > MethodSelect(Session & session, const Array_t & methodsEnabledInCore, int prompts, bool autopushPrompt) - : _session{session}, _prompts{prompts}, _autopushPrompt{autopushPrompt} { + : _session{session}, _prompts{prompts}, _autopushPrompt{autopushPrompt}, _mr{memory::default_resource()}, _methodsAvailable{_mr} { rublon::log(LogLevel::Debug, "Checking what methods from core are supported"); using namespace std::string_view_literals; _methodsAvailable.reserve(std::size(methodsEnabledInCore)); @@ -151,7 +155,7 @@ class MethodSelect { tl::expected< PostMethod, Error > create(Pam_t & pam) const { rublon::log(LogLevel::Debug, "prompting user to select method"); - memory::StrictMonotonic_2k_HeapResource memoryResource; + memory::Monotonic_2k_Resource memoryResource; std::pmr::map< int, MethodIds > methods_id{&memoryResource}; std::pmr::map< int, std::pmr::string > methods_names{&memoryResource}; diff --git a/PAM/ssh/include/rublon/method/passcode_based_auth.hpp b/PAM/ssh/include/rublon/method/passcode_based_auth.hpp index 1ba5dc9..ce1c7a5 100644 --- a/PAM/ssh/include/rublon/method/passcode_based_auth.hpp +++ b/PAM/ssh/include/rublon/method/passcode_based_auth.hpp @@ -1,5 +1,6 @@ #pragma once +#include "rublon/memory.hpp" #include #include @@ -12,8 +13,8 @@ namespace rublon::method { class PasscodeBasedAuth : public AuthenticationStep { protected: - const char * uri; - const char * confirmField; + const char * _uri; + const char * _confirmField; static constexpr const char * confirmCodeEndpoint = "/api/transaction/confirmCode"; static constexpr const char * confirmSecuritySSHEndpoint = "/api/transaction/confirmSecurityKeySSH"; @@ -23,10 +24,10 @@ class PasscodeBasedAuth : public AuthenticationStep { static constexpr auto _bypassCodeLength = 9; - const char * userMessage{nullptr}; + const char * _userMessage{nullptr}; - const uint_fast8_t vericodeLength; - const bool onlyDigits; + const uint_fast8_t _vericodeLength; + const bool _onlyDigits; int _prompts; constexpr static bool isdigit(char ch) { @@ -38,17 +39,17 @@ class PasscodeBasedAuth : public AuthenticationStep { } bool hasValidLength(std::string_view userInput) const { - if(userInput.size() == vericodeLength || userInput.size() == _bypassCodeLength) { + if(userInput.size() == _vericodeLength || userInput.size() == _bypassCodeLength) { log(LogLevel::Debug, "User input size %d is correct", userInput.size()); return true; } else { - log(LogLevel::Warning, "User input size %d is different than %d", userInput.size(), vericodeLength); + log(LogLevel::Warning, "User input size %d is different than %d", userInput.size(), _vericodeLength); return false; } } bool hasValidCharacters(std::string_view userInput) const { - if(onlyDigits ? digitsOnly(userInput) : true) { + if(_onlyDigits ? digitsOnly(userInput) : true) { log(LogLevel::Debug, "User input contains valid characters"); return true; } else { @@ -58,12 +59,13 @@ class PasscodeBasedAuth : public AuthenticationStep { } tl::expected< std::reference_wrapper< Document >, Error > readPasscode(Document & body, const Pam_t & pam) const { - ///TODO assert in interactive mode - auto & alloc = body.GetAllocator(); - auto vericode = pam.scan([](const char * userInput) { return std::string{userInput}; }, userMessage); + /// TODO assert in interactive mode + memory::MonotonicStackResource< 100 > memoryResource; + auto & alloc = body.GetAllocator(); + auto vericode = pam.scan([&](const char * userInput) { return std::pmr::string{userInput, &memoryResource}; }, _userMessage); if(hasValidLength(vericode) and hasValidCharacters(vericode)) { - Value confirmFieldValue(confirmField, alloc); + Value confirmFieldValue(_confirmField, alloc); body.AddMember(confirmFieldValue, Value{vericode.c_str(), alloc}, alloc); if(_session.hasAccessToken()) { @@ -80,7 +82,6 @@ class PasscodeBasedAuth : public AuthenticationStep { return AuthenticationStatus{AuthenticationStatus::Action::Confirmed}; } - tl::expected< AuthenticationStatus, Error > errorHandler(Error error, const Pam_t & pam, int promptLeft) const { if(promptLeft && error.is< WerificationError >()) { switch(error.get< WerificationError >().errorClass) { @@ -96,6 +97,9 @@ class PasscodeBasedAuth : public AuthenticationStep { case WerificationError::TooManyRequestsException: pam.print("Too Many Attempts. Try again after a minute"); break; + case WerificationError::SendPushException: + // if there is a communication problem we can't do anything here + break; } } return tl::unexpected{error}; @@ -116,11 +120,11 @@ class PasscodeBasedAuth : public AuthenticationStep { Endpoint endpoint, int prompts) : AuthenticationStep(session), - uri{(endpoint == Endpoint::ConfirmCode) ? confirmCodeEndpoint : confirmSecuritySSHEndpoint}, - confirmField{(endpoint == Endpoint::ConfirmCode) ? fieldVericode : fieldOtp}, - userMessage{userMessage}, - vericodeLength{length}, - onlyDigits{numbersOnly}, + _uri{(endpoint == Endpoint::ConfirmCode) ? confirmCodeEndpoint : confirmSecuritySSHEndpoint}, + _confirmField{(endpoint == Endpoint::ConfirmCode) ? fieldVericode : fieldOtp}, + _userMessage{userMessage}, + _vericodeLength{length}, + _onlyDigits{numbersOnly}, _prompts{prompts}, _name{_name} {} @@ -128,9 +132,9 @@ class PasscodeBasedAuth : public AuthenticationStep { tl::expected< AuthenticationStatus, Error > verify(const CoreHandlerInterface< Hander_t > & coreHandler, const Pam_t & pam) const { RapidJSONPMRStackAlloc< 2048 > alloc{}; Document body{rapidjson::kObjectType, &alloc}; - int prompts = _prompts; + int prompts = _prompts; - const auto requestAuthorization = [&](const auto & body) { return coreHandler.request(alloc, uri, body); }; + const auto requestAuthorization = [&](const auto & body) { return coreHandler.request(alloc, _uri, body); }; const auto checkCodeValidity = [&](const auto & coreResponse) { return this->checkAuthenticationStatus(coreResponse, pam); }; const auto waitForCoreToConfirm = [&](const auto &) { return waitForCoreConfirmation(coreHandler); }; const auto handleError = [&](const auto error) { return errorHandler(error, pam, prompts); }; diff --git a/PAM/ssh/include/rublon/pam_action.hpp b/PAM/ssh/include/rublon/pam_action.hpp index cb35c98..62ddcda 100644 --- a/PAM/ssh/include/rublon/pam_action.hpp +++ b/PAM/ssh/include/rublon/pam_action.hpp @@ -1,15 +1,17 @@ #pragma once #include +#include namespace rublon { class AuthenticationStatus { public: + using Token_t = std::optional< StaticString< 64 > >; enum class Action { Denied, Confirmed, Bypass }; - AuthenticationStatus(Action action, std::string authenticationToken = "") - : _action{action}, _authenticationToken{std::move(authenticationToken)} {} + AuthenticationStatus(Action action, const char * token = nullptr) + : _action{action}, _authenticationToken{token == nullptr ? Token_t{std::nullopt} : Token_t{token}} {} constexpr bool userAuthorized() const { return _action == Action::Confirmed; @@ -20,12 +22,14 @@ class AuthenticationStatus { } std::string_view accessToken() const { - return _authenticationToken; + if(not _authenticationToken) + return ""; + return {_authenticationToken->c_str(), _authenticationToken->size()}; } private: Action _action; - std::string _authenticationToken; /// TODO dynamic mem + Token_t _authenticationToken; }; } // namespace rublon diff --git a/PAM/ssh/include/rublon/rublon.hpp b/PAM/ssh/include/rublon/rublon.hpp index d30ab1d..8632cc2 100755 --- a/PAM/ssh/include/rublon/rublon.hpp +++ b/PAM/ssh/include/rublon/rublon.hpp @@ -1,5 +1,6 @@ #pragma once +#include "rublon/memory.hpp" #include #include @@ -11,17 +12,18 @@ namespace rublon { class RublonFactory { public: - tl::expected< Session, Error > startSession(const Pam_t & pam) { - details::initLog(); - - auto config = ConfigurationFactory{}.systemConfig(); - - if(not config.has_value()) { - pam.print("The configuration file does not exist or contains incorrect values"); + tl::expected< void, Error > initializeSession(Session & session) { + log(LogLevel::Debug, "Configuration read start"); + memory::MonotonicStack_2k_Resource memory_resource; + ConfigurationReader reader{&memory_resource, "/etc/rublon.config"}; + + if(auto ok = reader.applyTo(session.config()); not ok.has_value()) { + log(LogLevel::Warning, "Configuration contains errors"); + session.pam().print("The configuration file does not exist or contains incorrect values"); return tl::unexpected{ConfigurationError{}}; } - - return Session{pam, config.value()}; + log(LogLevel::Debug, "Configuration read success"); + return {}; } }; diff --git a/PAM/ssh/include/rublon/session.hpp b/PAM/ssh/include/rublon/session.hpp index bf22e19..cf7d4f0 100644 --- a/PAM/ssh/include/rublon/session.hpp +++ b/PAM/ssh/include/rublon/session.hpp @@ -1,43 +1,120 @@ #pragma once -#include "rublon/utils.hpp" -#include +#include #include #include #include -#include -#include +#include +#include + namespace rublon { -class Session { - const Pam_t & _pam; - const Configuration _config; +// class LoggingMemoryResource : public std::pmr::memory_resource { +// public: +// LoggingMemoryResource(std::pmr::memory_resource* upstream = std::pmr::get_default_resource()) +// : upstream_(upstream), allocated_bytes_(0) +// { +// pid_t pid = getpid(); +// std::ostringstream filename; +// filename << "/tmp/memory" << pid << ".log"; +// log_file_.open(filename.str(), std::ios::out | std::ios::trunc); + +// if (!log_file_) { +// throw std::runtime_error("Failed to open log file"); +// } +// log("Memory logging started."); +// } + +// ~LoggingMemoryResource() override { +// log("Memory logging ended."); +// log_file_.close(); +// } + +// protected: +// void* do_allocate(std::size_t bytes, std::size_t alignment) override { +// std::lock_guard lock(mutex_); +// void* ptr = upstream_->allocate(bytes, alignment); +// allocated_bytes_ += bytes; +// active_allocations_[ptr] = bytes; + +// log("ALLOC", ptr, bytes, alignment); +// return ptr; +// } + +// void do_deallocate(void* ptr, std::size_t bytes, std::size_t alignment) override { +// std::lock_guard lock(mutex_); +// auto it = active_allocations_.find(ptr); +// if (it != active_allocations_.end()) { +// allocated_bytes_ -= it->second; +// active_allocations_.erase(it); +// } + +// log("FREE", ptr, bytes, alignment); +// upstream_->deallocate(ptr, bytes, alignment); +// } + +// bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override { +// return this == &other; +// } + +// private: +// std::pmr::memory_resource* upstream_; +// std::ofstream log_file_; +// std::mutex mutex_; +// std::size_t allocated_bytes_; +// std::map active_allocations_; + +// void log(const std::string& action, void* ptr = nullptr, std::size_t bytes = 0, std::size_t alignment = 0) { +// auto now = std::chrono::system_clock::now(); +// auto now_time = std::chrono::system_clock::to_time_t(now); +// log_file_ << std::put_time(std::localtime(&now_time), "%F %T") +// << " | " << std::setw(6) << action; + +// if (ptr) { +// log_file_ << " | ptr=" << ptr << " bytes=" << bytes << " align=" << alignment; +// } + +// log_file_ << " | total=" << allocated_bytes_ << " bytes\n"; +// log_file_.flush(); +// } +// }; + +// std::pmr::unsynchronized_pool_resource resource{}; +// LoggingMemoryResource mr{&resource}; + +class DefaultResource { + public: + DefaultResource() { + // memory::set_default_resource(&mr); + } +}; + +class Session : public DefaultResource { + std::pmr::memory_resource * mr; + const Pam_t & _pam; + Configuration _config; std::pmr::string _tid; std::pmr::string _accessToken; - CoreHandler_t _coreHandler; - /// TODO log - /// TODO momory resource public: - Session(const Pam_t & pam, const Configuration & config) - : _pam{pam}, _config{config}, _coreHandler{_config} { - log(LogLevel::Debug, __PRETTY_FUNCTION__); + Session(const Pam_t & pam) : DefaultResource{}, mr{memory::default_resource()}, _pam{pam}, _config{mr}, _tid{mr}, _accessToken{mr} { + details::initLog(); } - Session(Session &&) noexcept = default; + Session(Session &&) noexcept = delete; Session(const Session &) = delete; Session & operator=(Session &&) noexcept = delete; Session & operator=(const Session &) = delete; - const auto & coreHandler() const { - return _coreHandler; - } const auto & pam() const { return _pam; } + auto & config() { + return _config; + } const auto & config() const { return _config; } @@ -49,7 +126,11 @@ class Session { return systemToken().data(); } - bool inInteractiveMode() const { + constexpr bool inNonInteractiveMode() const { + return not inInteractiveMode(); + } + + constexpr bool inInteractiveMode() const { return _config.nonInteractiveMode == false; } @@ -81,12 +162,7 @@ class Session { } } const char * ctransactionID() const { - if(_tid.empty()) { - log(LogLevel::Warning, "Transaction ID is not defined, but requested"); - return ""; - } else { - return _tid.data(); - } + return transactionID().data(); } }; diff --git a/PAM/ssh/include/rublon/sign.hpp b/PAM/ssh/include/rublon/sign.hpp index e1668a1..8365f12 100755 --- a/PAM/ssh/include/rublon/sign.hpp +++ b/PAM/ssh/include/rublon/sign.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -8,40 +9,37 @@ namespace rublon { +struct EVP_MD_CTX_deleter{ + void operator()(EVP_MD_CTX *ctx)const{ + EVP_MD_CTX_free(ctx); + } +}; + inline StaticString< SHA256_DIGEST_LENGTH * 2 > fileSHA256(const char * const path) { std::string fileContent; readFile(path, fileContent); StaticString< SHA256_DIGEST_LENGTH * 2 > xRublon{}; std::array< unsigned char, SHA256_DIGEST_LENGTH + 1 > hash{}; - int ret{}; - EVP_MD_CTX * ctx; - ctx = EVP_MD_CTX_new(); - - return 0; - if(ctx == NULL) + auto ctx = std::unique_ptr{EVP_MD_CTX_new()}; + + if(not ctx) goto out; // EVP_X methods return 1 on success, so does this function // Any values other than 1 denote error - ret = EVP_DigestInit(ctx, EVP_sha256()); - if(!ret) + if(not EVP_DigestInit(ctx.get(), EVP_sha256())) goto out; - - ret = EVP_DigestUpdate(ctx, fileContent.data(), fileContent.size()); - if(!ret) + + if(not EVP_DigestUpdate(ctx.get(), fileContent.data(), fileContent.size())) goto out; // Provide uint* instead of NULL to get nBytes written, 32 for SHA256 - ret = EVP_DigestFinal(ctx, hash.data(), NULL); - if(!ret) + if(not EVP_DigestFinal(ctx.get(), hash.data(), NULL)) goto out; out: - if(ctx != NULL) - EVP_MD_CTX_free(ctx); - for(unsigned int i = 0; i < SHA256_DIGEST_LENGTH; i++) sprintf(&xRublon[i * 2], "%02x", ( unsigned int ) hash[i]); @@ -51,7 +49,7 @@ out: // +1 for \0 inline StaticString< SHA256_DIGEST_LENGTH * 2 > signData(std::string_view data, std::string_view secretKey) { StaticString< SHA256_DIGEST_LENGTH * 2 > xRublon; - std::array< unsigned char, EVP_MAX_MD_SIZE > md; + std::array< unsigned char, EVP_MAX_MD_SIZE > md{}; unsigned int md_len{}; HMAC(EVP_sha256(), secretKey.data(), secretKey.size(), ( unsigned const char * ) data.data(), data.size(), md.data(), &md_len); diff --git a/PAM/ssh/include/rublon/static_string.hpp b/PAM/ssh/include/rublon/static_string.hpp index c4f5298..b78c14e 100644 --- a/PAM/ssh/include/rublon/static_string.hpp +++ b/PAM/ssh/include/rublon/static_string.hpp @@ -31,19 +31,22 @@ template < size_t N > class StaticString : public details::StaticStringBase { public: constexpr StaticString() = default; - constexpr StaticString(const char (&chars)[N]) : m_str(toStdArray(chars)) {} + constexpr StaticString(const char (&chars)[N]) : m_str(toStdArray(chars)), _size{N} {} - constexpr StaticString(std::array< const char, N > chars) : m_str(std::move(chars)) {} + constexpr StaticString(std::array< const char, N > chars) : m_str(std::move(chars)), _size{N} {} constexpr StaticString(const char * str) { - std::strncpy(m_str.data(), str, N); + _size = std::min(strlen(str), N); + std::memcpy(m_str.data(), str, _size); } void operator=(const char * str) { - std::strncpy(m_str.data(), str, N); + _size = std::min(strlen(str), N); + std::memcpy(m_str.data(), str, _size); } - + void operator=(std::string_view str) { - std::strncpy(m_str.data(), str.data(), N); + _size = std::min(str.size(), N); + std::memcpy(m_str.data(), str.data(), _size); } const char * c_str() const noexcept { @@ -67,16 +70,23 @@ class StaticString : public details::StaticStringBase { } std::size_t size() const { - return strlen(m_str.data()); + return _size; } constexpr std::size_t capacity() const noexcept { return N - 1; } - template < std::size_t M > - constexpr StaticString< N + M - 1 > operator+(const StaticString< M > & rhs) const { - return join(resize< N - 1 >(m_str), rhs.m_str); + StaticString< N > & operator+=(const char * rhs) { + auto remaining = capacity() - _size; + auto rhs_len = std::strlen(rhs); + auto copy_len = std::min(rhs_len, remaining); + + std::strncpy(data() + _size, rhs, copy_len); + _size += copy_len; + data()[_size] = '\0'; // null + + return *this; } template < std::size_t M > @@ -88,5 +98,12 @@ class StaticString : public details::StaticStringBase { private: std::array< char, N + 1 > m_str{}; + std::size_t _size; }; + +template < size_t N > +bool operator==(const StaticString< N > & lhs, const char * rhs) { + return strcmp(lhs.c_str(), rhs) == 0; +} + } // namespace rublon diff --git a/PAM/ssh/include/rublon/utils.hpp b/PAM/ssh/include/rublon/utils.hpp index 9078dd4..d846fe0 100755 --- a/PAM/ssh/include/rublon/utils.hpp +++ b/PAM/ssh/include/rublon/utils.hpp @@ -4,12 +4,16 @@ #include #include -#include +#include +#include #include +#include +#include + +#include #include #include #include -#include #include #include @@ -149,14 +153,32 @@ namespace conv { return strcmp(buf.data(), "true") == 0; } - inline tl::expected< std::uint32_t, Error > to_uint32(std::string_view userinput) noexcept { - try { - return std::stoi(userinput.data()); - } catch(const std::invalid_argument & e) { - return tl::make_unexpected(Error::NotANumber); - } catch(const std::out_of_range & e) { - return tl::make_unexpected(Error::OutOfRange); + inline std::optional< std::uint32_t > to_uint32opt(std::string_view userinput) noexcept { + constexpr auto max = std::numeric_limits< uint32_t >::digits10 + 1; + if(userinput.empty() || userinput.size() >= max) + return std::nullopt; // Avoid large or empty inputs + + char buffer[max]={0}; + std::memcpy(buffer, userinput.data(), userinput.size()); + buffer[userinput.size()] = '\0'; // Ensure null termination + + char * endptr = nullptr; + errno = 0; + + long result = std::strtol(buffer, &endptr, 10); + + if(errno == ERANGE || endptr != buffer + userinput.size() || result < 0 || result > std::numeric_limits::max()) { + return std::nullopt; } + + return static_cast< std::uint32_t >(result); + } + + inline tl::expected< std::uint32_t, Error > to_uint32(std::string_view userinput) noexcept { + auto val = to_uint32opt(userinput); + if(val) + return *val; + return tl::unexpected{Error::NotANumber}; } } // namespace conv @@ -177,15 +199,37 @@ namespace details { return ltrim(rtrim(s)); } + template < typename StrT > + void trimInPlace(StrT & s) { + // Remove leading whitespace + size_t start = 0; + while(start < s.size() && isspace(static_cast< unsigned char >(s[start]))) + ++start; + + // Remove trailing whitespace + size_t end = s.size(); + while(end > start && isspace(static_cast< unsigned char >(s[end - 1]))) + --end; + + if(start > 0 || end < s.size()) { + s = s.substr(start, end - start); + } + } + template < typename Headers > inline void headers(std::string_view data, Headers & headers) { - memory::StrictMonotonic_4k_HeapResource stackResource; + memory::Monotonic_4k_Resource stackResource; std::pmr::string tmp{&stackResource}; + tmp.reserve(300); std::istringstream resp{}; resp.rdbuf()->pubsetbuf(const_cast< char * >(data.data()), data.size()); - while(std::getline(resp, tmp) && !(trim(tmp).empty())) { + while(std::getline(resp, tmp)) { + if(tmp == "\r") + continue; + if(trim(tmp).empty()) + break; auto line = std::string_view(tmp); auto index = tmp.find(':', 0); if(index != std::string::npos) { @@ -197,7 +241,8 @@ namespace details { } std::pmr::string hostname(std::pmr::memory_resource * mr) { - std::pmr::string hostname{512, '\0', mr}; + // longest hostname on linux is 253 characters + std::pmr::string hostname{255, '\0', mr}; if(gethostname(hostname.data(), hostname.size()) != 0) { log(LogLevel::Warning, "Hostname is not available"); return ""; @@ -207,19 +252,18 @@ namespace details { } std::pmr::string osName(std::pmr::memory_resource * mr) { - memory::MonotonicStackResource< 8 * 1024 > stackResource; + memory::Monotonic_1k_Resource memoryResource; std::ifstream file(std::filesystem::path{"/etc/os-release"}); if(not file.good()) return {"unknown", mr}; - std::pmr::string line{&stackResource}; + std::pmr::string line{&memoryResource}; + std::pmr::string _key{&memoryResource}; + std::pmr::string _value{&memoryResource}; line.reserve(100); while(std::getline(file, line)) { - std::pmr::string _key{&stackResource}; - std::pmr::string _value{&stackResource}; - if(!line.length()) continue; @@ -266,4 +310,31 @@ constexpr std::array< Out, sizeof...(Types) > make_array(Types... names) { return {std::forward< Types >(names)...}; } +template < typename T > +std::size_t size_buffer(const T & item) { + using U = std::decay_t< T >; + if constexpr(std::is_same_v< U, const char * >) { + return strlen(item); + } else if constexpr(std::is_same_v< U, std::pmr::string > || std::is_same_v< U, std::string >) { + return item.size(); + } else if constexpr(std::is_integral_v< U > || std::is_floating_point_v< U >) { + return std::numeric_limits< U >::digits; + } + return 0; +} + +template < typename T > +std::size_t size_buffer(const std::optional< T > & item) { + if(item.has_value()) + return size_buffer(*item); + return 0; +} + +// min + 10% +template < typename... Args > +std::size_t conservative_estimate(const Args &... args) { + auto min = (size_buffer(args) + ...); + return min + min * 10 / 100; +} + } // namespace rublon diff --git a/PAM/ssh/include/rublon/websockets.hpp b/PAM/ssh/include/rublon/websockets.hpp index e37c877..47ffbf9 100644 --- a/PAM/ssh/include/rublon/websockets.hpp +++ b/PAM/ssh/include/rublon/websockets.hpp @@ -1,19 +1,20 @@ #pragma once -#include "rublon/json.hpp" -#include "rublon/memory.hpp" -#include "rublon/static_string.hpp" -#include #include #include #include -#include -#include -#include - +#include #include #include +#include +#include +#include +#include +#include +#include +#include + #include #include @@ -28,7 +29,7 @@ struct RublonEventData { }; class WebSocket { - std::string url; /// TODO pmr + std::reference_wrapper< const Configuration > _config; std::string_view urlv; bool event_received = false; @@ -40,9 +41,12 @@ class WebSocket { lws_client_connect_info ccinfo{}; RublonEventData * currentEvent{nullptr}; + std::pmr::string proxyUrl{}; public: - WebSocket(std::string_view uri) : url{uri.data()}, urlv{url} { + WebSocket(const Configuration & config) : _config{config}, urlv{_config.get().apiServer}, proxyUrl{_config.get().apiServer.get_allocator()} { + const auto & cfg = _config.get(); // only a alias to not use _config.get() all the time + auto lws_log_emit = [](int level, const char * line) { LogLevel rlevel{}; if(level == LLL_ERR) @@ -59,7 +63,11 @@ class WebSocket { log(rlevel, "libwesockets: %s", line); }; - lws_set_log_level(LLL_ERR | LLL_WARN, lws_log_emit); + if(_config.get().logging) { + lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_INFO | LLL_DEBUG | LLL_HEADER, lws_log_emit); + } else { + lws_set_log_level(LLL_ERR | LLL_WARN, lws_log_emit); + } memset(&info, 0, sizeof(info)); memset(&ccinfo, 0, sizeof(ccinfo)); @@ -68,10 +76,29 @@ class WebSocket { info.protocols = protocols; info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; + if(cfg.proxyEnabled && (cfg.proxyType == "http" || cfg.proxyType == "https")) { + assert(cfg.proxyType.has_value()); + assert(cfg.proxyHost.has_value()); + log(LogLevel::Debug, "WebSocket using proxy"); + + // "username:password\@server:port" + if(cfg.proxyAuthRequired) { + proxyUrl.reserve(conservative_estimate(cfg.proxyUsername, cfg.proxyPass, cfg.proxyHost) + 10); + proxyUrl += *cfg.proxyUsername; + proxyUrl += ":"; + proxyUrl += *cfg.proxyPass; + proxyUrl += "@"; + } + + proxyUrl += *cfg.proxyHost; + log(LogLevel::Debug, "WebSocket proxy %s", proxyUrl.c_str()); + info.http_proxy_address = proxyUrl.c_str(); + info.http_proxy_port = config.proxyPort.value_or(8080); + } + context = lws_create_context(&info); const std::string_view prefix = "https://"; - if(urlv.substr(0, prefix.size()) == prefix) urlv.remove_prefix(prefix.size()); @@ -85,7 +112,7 @@ class WebSocket { ccinfo.pwsi = &wsi; ccinfo.userdata = this; - log(LogLevel::Debug, "Created WS from %s", urlv.data()); + log(LogLevel::Debug, "WebSocket Created connection to %s", urlv.data()); } WebSocket(WebSocket && rhs) noexcept = default; @@ -100,28 +127,29 @@ class WebSocket { } bool attachToTransactionConfirmationChannel(std::string_view transaction_id) { - /// TODO change to StaticString and += - std::array< char, 128 > subscribe_message{0}; - std::array< char, 128 > buf{0}; + StaticString< 128 > subscribe_message{}; + unsigned char buf[128 + LWS_PRE] = {}; - sprintf(subscribe_message.data(), R"msg(42["subscribe",{"channel":"transactionConfirmation.%s"}])msg", transaction_id.data()); - memcpy(&buf[LWS_PRE], subscribe_message.data(), strlen(subscribe_message.data())); + subscribe_message += R"msg(42["subscribe",{"channel":"transactionConfirmation.)msg"; + subscribe_message += transaction_id.data(); + subscribe_message += R"("}])"; - log(LogLevel::Debug, "WS send message: %s", subscribe_message.data()); - int bytes_sent = lws_write(wsi, ( unsigned char * ) &buf[LWS_PRE], strlen(subscribe_message.data()), LWS_WRITE_TEXT); + memcpy(buf + LWS_PRE, subscribe_message.data(), subscribe_message.size()); - log(LogLevel::Debug, "WS send: %d bytes", bytes_sent); + log(LogLevel::Debug, "WebSocket send message: %s", subscribe_message.c_str()); + int bytes_sent = lws_write(wsi, buf + LWS_PRE, subscribe_message.size(), LWS_WRITE_TEXT); - if(bytes_sent < ( int ) strlen(subscribe_message.data())) { - log(LogLevel::Error, "Failed to send subscribe message"); + log(LogLevel::Debug, "WebSocket send: %d bytes", bytes_sent); + + if(bytes_sent < ( int ) subscribe_message.size()) { + log(LogLevel::Error, "WebSocket failed to send subscribe message"); return false; } return true; } bool AttachToCore(std::string_view tid) { - log(LogLevel::Debug, "connecting to %s", ccinfo.address); - /// needed here only for rublon core api URL, so 1k fine + log(LogLevel::Debug, "WebSocket attaching to rublon api at %s", ccinfo.address); lws_client_connect_via_info(&ccinfo); const int seconds = 10; @@ -156,7 +184,7 @@ class WebSocket { const int seconds = 60; auto endtime = std::chrono::steady_clock::now() + std::chrono::seconds{seconds}; - log(LogLevel::Debug, "waiting for events for %d seconds", seconds); + log(LogLevel::Debug, "WebSocket waiting for events for %d seconds", seconds); while(!event_received && std::chrono::steady_clock::now() < endtime) { lws_service(context, 1000); } @@ -179,10 +207,10 @@ class WebSocket { case LWS_CALLBACK_CLIENT_WRITEABLE: { // Perform the Socket.IO 4.x handshake (send `40` message) const std::string_view handshake = "40"; - unsigned char buf[64]; + unsigned char buf[64] = {}; memcpy(&buf[LWS_PRE], handshake.data(), handshake.size()); lws_write(wsi, &buf[LWS_PRE], handshake.size(), LWS_WRITE_TEXT); - log(LogLevel::Debug, "Sent Socket.IO handshake"); + log(LogLevel::Debug, "WebSocket Sent Socket.IO handshake"); break; } @@ -204,7 +232,7 @@ class WebSocket { /// TODO refactor to separate class if(_this->currentEvent == nullptr) return -1; - log(LogLevel::Debug, "WS got %s", input.data()); + log(LogLevel::Debug, "WebSocket got %s", input.data()); size_t startPos = input.find("[\"") + 2; size_t endPos = input.find("\",", startPos); auto & event = *_this->currentEvent; @@ -219,7 +247,7 @@ class WebSocket { startPos = endPos + 2; auto jsonString = input.substr(startPos, input.length() - startPos - 1); - memory::Monotonic_1k_HeapResource mr; + memory::Monotonic_1k_Resource mr; RapidJSONPMRAlloc alloc{&mr}; Document dataJson{&alloc}; @@ -235,19 +263,19 @@ class WebSocket { if(token != nullptr) { event.accessToken = token->GetString(); } else { - log(LogLevel::Error, "Response does not contain token"); + log(LogLevel::Error, "WebSocket response does not contain access token"); } _this->event_received = true; } else if(redirectUrl != nullptr) { - log(LogLevel::Info, "Received deny message"); + log(LogLevel::Info, "WebSocket received deny message"); _this->event_received = true; } else { - log(LogLevel::Error, "event data incorrect"); + log(LogLevel::Error, "WebSocket event data incorrect"); return -1; } } else { - log(LogLevel::Debug, "Not an confirmation event"); + log(LogLevel::Debug, "WebSocket Not an confirmation event"); } break; } diff --git a/PAM/ssh/lib/pam.cpp b/PAM/ssh/lib/pam.cpp index ee3b197..06548c2 100644 --- a/PAM/ssh/lib/pam.cpp +++ b/PAM/ssh/lib/pam.cpp @@ -57,17 +57,18 @@ pam_sm_authenticate(pam_handle_t * pamh, [[maybe_unused]] int flags, [[maybe_unu pam.print("RUBLON connector has exited with unknown code, access DENY!\n"); return PAM_MAXTRIES; }; - - auto session = rublon::RublonFactory{}.startSession(pam); - if(not session.has_value()) { + + Session session{pam}; + auto ok = rublon::RublonFactory{}.initializeSession(session); + if(not ok.has_value()) { return printAuthMessageAndExit(AuthenticationStatus::Action::Bypass); } - if(!session->config().logging) { + if(!session.config().logging) { g_level = LogLevel::Warning; } - auto & CH = session.value().coreHandler(); + CoreHandler_t CH{session.config()}; auto selectMethod = [&](const MethodSelect & selector) { // return selector.create(pam); @@ -78,14 +79,12 @@ pam_sm_authenticate(pam_handle_t * pamh, [[maybe_unused]] int flags, [[maybe_unu }; auto confirmCode = [&](const MethodProxy & method) mutable { // - return method.fire(session.value(), CH, pam); + return method.fire(session, CH, pam); }; auto finalizeTransaction = [&](const AuthenticationStatus & status) mutable -> tl::expected< AuthenticationStatus, Error > { if(status.userAuthorized()) { - auto tok = std::string{status.accessToken().data()}; - Finish finish{session.value(), std::move(tok)}; - finish.handle(CH); + Finish{session, status.accessToken()}.handle(CH); } return status; }; @@ -95,20 +94,19 @@ pam_sm_authenticate(pam_handle_t * pamh, [[maybe_unused]] int flags, [[maybe_unu }; auto mapError = [&](const Error & error) -> tl::expected< int, Error > { - return printAuthMessageAndExit(rublon::ErrorHandler{pam, session->config()}.printErrorDetails(error)); + return printAuthMessageAndExit(rublon::ErrorHandler{pam, session.config()}.printErrorDetails(error)); }; { - CheckApplication ca; - const auto & config = session.value().config(); - const auto ret = ca.call(CH, {config.systemToken.data(), config.systemToken.size()}).or_else(mapError); + CheckApplication ca{session}; + const auto ret = ca.call(CH, session.config().systemToken).or_else(mapError); if(not ret.has_value()) { log(LogLevel::Error, "Check Application step failed, check configration"); return PAM_MAXTRIES; } } - auto ret = Init{session.value()} + auto ret = Init{session} .handle(CH, pam) // .and_then(selectMethod) .and_then(confirmMethod) diff --git a/PAM/ssh/patches/rapidjson.patch b/PAM/ssh/patches/rapidjson.patch index e43d863..f9c2055 100644 --- a/PAM/ssh/patches/rapidjson.patch +++ b/PAM/ssh/patches/rapidjson.patch @@ -1,3 +1,16 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index ceda71b..0128f99 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -9,6 +9,7 @@ endif() + + SET(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules) + ++cmake_policy(SET CMP0048 NEW) + PROJECT(RapidJSON CXX) + + set(LIB_MAJOR_VERSION "1") + diff --git a/include/rapidjson/document.h b/include/rapidjson/document.h index e3e20dfb..592c5678 100644 --- a/include/rapidjson/document.h diff --git a/os/rhel/8/Dockerfile b/os/rhel/8/Dockerfile index 14377c8..ce4d66a 100644 --- a/os/rhel/8/Dockerfile +++ b/os/rhel/8/Dockerfile @@ -8,7 +8,8 @@ RUN yum update -y && yum install -y gcc \ rpm-build \ openssh-server \ gcc-c++ \ - wget -RUN wget https://yum.oracle.com/repo/OracleLinux/OL8/baseos/latest/x86_64/getPackage/pam-devel-1.3.1-34.0.1.el8_10.x86_64.rpm + wget\ + pam +RUN wget ftp://ftp.icm.edu.pl/vol/rzm5/linux-rocky/8.10/BaseOS/x86_64/os/Packages/p/pam-devel-1.3.1-36.el8_10.x86_64.rpm RUN rpm -Uvh pam*