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
This commit is contained in:
rublon-bwi 2025-07-18 11:48:18 +02:00 committed by GitHub
parent af64f8e9e3
commit 0934902bba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 945 additions and 456 deletions

2
.clangd Normal file
View File

@ -0,0 +1,2 @@
CompileFlags:
Add: [-std:c++17]

View File

@ -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

View File

@ -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()

View File

@ -5,6 +5,7 @@
#include <security/pam_modules.h>
#include <syslog.h>
#include <rublon/bits.hpp>
#include <rublon/check_application.hpp>
#include <rublon/error.hpp>
#include <rublon/error_handler.hpp>
@ -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);
}

View File

@ -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};

View File

@ -1,5 +1,8 @@
#pragma once
#include "rublon/configuration.hpp"
#include "rublon/session.hpp"
#include <cstdio>
#include <rublon/bits.hpp>
#include <rublon/core_handler.hpp>
#include <rublon/curl.hpp>
@ -10,13 +13,21 @@
#include <rapidjson/prettywriter.h>
#include <rapidjson/rapidjson.h>
#include <string>
#include <string_view>
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<std::string_view>("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<T>
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<int>(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();

View File

@ -1,14 +1,12 @@
#pragma once
#include <rublon/error.hpp>
#include <rublon/memory.hpp>
#include <rublon/static_string.hpp>
#include <rublon/utils.hpp>
#include <type_traits>
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 <cctype>
#include <string>
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<string> val which contains empty string.
// getStringReq on the other hand, returns error in case when
// * configuration is not found -> RequiredValueNotFound
// * configuration value is empty -> RequiredValueEmpty
// Reading required fields
if(auto val = getStringReq("systemToken"); not val.has_value()) {
logRequiredFieldNotAvailable("systemToken");
return tl::unexpected{val.error()};
} else {
config.systemToken = std::move(val.value());
}
if(auto val = getStringReq("secretKey"); not val.has_value()) {
logRequiredFieldNotAvailable("secretKey");
return tl::unexpected{val.error()};
} else {
config.secretKey = std::move(val.value());
}
if(auto val = getStringReq("rublonApiServer"); not val.has_value()) {
logRequiredFieldNotAvailable("rublonApiServer");
return tl::unexpected{val.error()};
} else {
config.apiServer = std::move(val.value());
}
// optional configuration options
config.prompt = getInt("prompt").value_or(1);
config.enablePasswdEmail = getBool("enablePasswdEmail").value_or(true);
config.logging = getBool("logging").value_or(true);
config.autopushPrompt = getBool("autopushPrompt").value_or(false);
config.nonInteractiveMode = getBool("nonInteractiveMode").value_or(false);
config.failMode = getFailMode("failMode").value_or(FailMode::deny);
// reading proxy configuration
config.proxyEnabled = getBool("proxyEnabled").value_or(false);
config.proxyType = getStringOpt("proxyType");
config.proxyHost = getStringOpt("proxyHost");
// Apply fallback if no config is set
if(config.proxyEnabled && (!config.proxyType || config.proxyType->empty()) && (!config.proxyHost || config.proxyHost->empty())) {
log(LogLevel::Info, "Proxy is enabled but no configuration for it is provided, trying to read from env");
if(auto https_proxy = std::getenv("https_proxy"); https_proxy && *https_proxy) {
if(parseProxyURL(https_proxy)) {
log(LogLevel::Info, "Loaded proxy config from HTTPS_PROXY");
}
} else if(auto http_proxy = std::getenv("http_proxy"); http_proxy && *http_proxy) {
if(parseProxyURL(http_proxy)) {
log(LogLevel::Info, "Loaded proxy config from HTTP_PROXY");
}
}
}
if(config.proxyEnabled) {
if(not config.proxyType or config.proxyType->empty()) {
log(LogLevel::Error, "Proxy is enabled but proxy type is not present or empty");
return tl::unexpected{ConfigurationError::ErrorClass::BadConfiguration};
}
if(not config.proxyHost or config.proxyHost->empty()) {
log(LogLevel::Error, "Proxy is enabled but proxy server is not present or empty");
return tl::unexpected{ConfigurationError::ErrorClass::BadConfiguration};
}
}
config.proxyAuthRequired = getBool("proxyAuthRequired").value_or(false);
config.proxyUsername = getStringOpt("proxyUsername");
config.proxyPass = getStringOpt("proxyPassword");
if(config.proxyAuthRequired) {
if(not config.proxyUsername or config.proxyUsername->empty()) {
log(LogLevel::Error, "Proxy auth is required but proxy proxy username is not present or empty");
return tl::unexpected{ConfigurationError::ErrorClass::BadConfiguration};
}
if(not config.proxyPass) {
log(LogLevel::Error,
"Proxy is enabled but proxy password is not present, "
"if no password is required add an empty entry to configuration file");
return tl::unexpected{ConfigurationError::ErrorClass::BadConfiguration};
}
}
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

View File

@ -1,9 +1,5 @@
#pragma once
#include "rublon/error.hpp"
#include "rublon/static_string.hpp"
#include <memory>
#include <optional>
#include <rublon/configuration.hpp>
#include <rublon/core_handler_interface.hpp>
#include <rublon/curl.hpp>
@ -13,19 +9,22 @@
#include <rublon/utils.hpp>
#include <rublon/websockets.hpp>
#include <string_view>
#include <tl/expected.hpp>
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<WebSocket>(_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

View File

@ -1,5 +1,6 @@
#pragma once
#include <rublon/configuration.hpp>
#include <rublon/error.hpp>
#include <rublon/utils.hpp>
@ -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());

View File

@ -1,7 +1,6 @@
#pragma once
#include <string_view>
#include <tl/expected.hpp>
#include <rublon/non_owning_ptr.hpp>
#include <rublon/stdlib.hpp>
@ -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";

View File

@ -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;

View File

@ -4,16 +4,17 @@
#include <rublon/configuration.hpp>
#include <rublon/json.hpp>
#include <rublon/method/method_select.hpp>
#include <string_view>
#include <sys/utsname.h>
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 {

View File

@ -1,20 +1,14 @@
#pragma once
#include "rublon/memory.hpp"
#include "rublon/utils.hpp"
#include <rublon/bits.hpp>
#include <rublon/authentication_step_interface.hpp>
#include <rublon/bits.hpp>
#include <rublon/configuration.hpp>
#include <rublon/json.hpp>
#include <rublon/method/method_select.hpp>
#include <rublon/session.hpp>
#include <sys/utsname.h>
[[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 >

View File

@ -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);

View File

@ -1,9 +1,16 @@
#pragma once
#include <memory_resource>
#include <rublon/stdlib.hpp>
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 {

View File

@ -1,5 +1,6 @@
#pragma once
#include <algorithm>
#include <memory_resource>
#include <set>
#include <string_view>
#include <tl/expected.hpp>
@ -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};

View File

@ -1,5 +1,6 @@
#pragma once
#include "rublon/memory.hpp"
#include <tl/expected.hpp>
#include <rublon/authentication_step_interface.hpp>
@ -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); };

View File

@ -1,15 +1,17 @@
#pragma once
#include <rublon/stdlib.hpp>
#include <rublon/static_string.hpp>
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

View File

@ -1,5 +1,6 @@
#pragma once
#include "rublon/memory.hpp"
#include <tl/expected.hpp>
#include <rublon/bits.hpp>
@ -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 {};
}
};

View File

@ -1,43 +1,120 @@
#pragma once
#include "rublon/utils.hpp"
#include <rapidjson/document.h>
#include <rublon/memory.hpp>
#include <rublon/bits.hpp>
#include <rublon/configuration.hpp>
#include <rublon/json.hpp>
#include <string>
#include <string_view>
#include <rublon/utils.hpp>
#include <rapidjson/document.h>
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<std::mutex> 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<std::mutex> 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<void*, std::size_t> 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();
}
};

View File

@ -1,5 +1,6 @@
#pragma once
#include <memory>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/sha.h>
@ -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, EVP_MD_CTX_deleter>{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);

View File

@ -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

View File

@ -4,12 +4,16 @@
#include <rublon/static_string.hpp>
#include <rublon/stdlib.hpp>
#include <fcntl.h>
#include <cstdint>
#include <limits>
#include <string>
#include <utility>
#include <tl/expected.hpp>
#include <fcntl.h>
#include <sys/stat.h>
#include <syslog.h>
#include <unistd.h>
#include <utility>
#include <security/pam_appl.h>
#include <security/pam_modules.h>
@ -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<uint32_t>::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

View File

@ -1,19 +1,20 @@
#pragma once
#include "rublon/json.hpp"
#include "rublon/memory.hpp"
#include "rublon/static_string.hpp"
#include <array>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <rublon/error.hpp>
#include <rublon/pam_action.hpp>
#include <rublon/utils.hpp>
#include <functional>
#include <string>
#include <string_view>
#include <rublon/configuration.hpp>
#include <rublon/error.hpp>
#include <rublon/json.hpp>
#include <rublon/memory.hpp>
#include <rublon/pam_action.hpp>
#include <rublon/static_string.hpp>
#include <rublon/utils.hpp>
#include <libwebsockets.h>
#include <unistd.h>
@ -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;
}

View File

@ -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)

View File

@ -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

View File

@ -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*