This commit is contained in:
Bartosz Wieczorek 2023-07-05 11:40:54 +02:00
commit dcab16178d
26 changed files with 4065 additions and 0 deletions

68
.clang-format Executable file
View File

@ -0,0 +1,68 @@
AccessModifierOffset: -2
AlignAfterOpenBracket: false
AlignConsecutiveAssignments: true
# AlignConsecutiveDeclarations: false
AlignEscapedNewlinesLeft: true
AlignOperands: false
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
# AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: true
BinPackArguments: false
BinPackParameters: false
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: false
BreakConstructorInitializersBeforeComma: false
# BreakStringLiterals: false
ColumnLimit: 140
# CommentPragmas:
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 1
ContinuationIndentWidth: 2
Cpp11BracedListStyle: true
DerivePointerAlignment : false
DisableFormat: false
# ExperimentalAutoDetectBinPacking: false
ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ]
# IncludeCategories:
# IncludeIsMainRegex:
IndentCaseLabels: true
IndentWidth: 4
# IndentWrappedFunctionNames:
KeepEmptyLinesAtTheStartOfBlocks: false
Language: Cpp
# MacroBlockBegin:
# MacroBlockEnd:
MaxEmptyLinesToKeep: 1
NamespaceIndentation: Inner
# PenaltyBreakBeforeFirstCallParameter:
# PenaltyBreakComment:
# PenaltyBreakFirstLessLess:
# PenaltyBreakString:
# PenaltyExcessCharacter:
# PenaltyReturnTypeOnItsOwnLine:
PointerAlignment: Middle
# ReflowComments: false
SortIncludes: true
SpaceAfterCStyleCast: true
# SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: Never
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: true
SpacesInCStyleCastParentheses: true
SpacesInContainerLiterals: true
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Cpp11
TabWidth: 4
UseTab: Never

74
.gitignore vendored Normal file
View File

@ -0,0 +1,74 @@
# This file is used to ignore files which are generated
# ----------------------------------------------------------------------------
*~
*.autosave
*.a
*.core
*.moc
*.o
*.obj
*.orig
*.rej
*.so
*.so.*
*_pch.h.cpp
*_resource.rc
*.qm
.#*
*.*#
core
!core/
tags
.DS_Store
.directory
*.debug
Makefile*
*.prl
*.app
moc_*.cpp
ui_*.h
qrc_*.cpp
Thumbs.db
*.res
*.rc
/.qmake.cache
/.qmake.stash
# qtcreator generated files
*.pro.user*
CMakeLists.txt.user*
# xemacs temporary files
*.flc
# Vim temporary files
.*.swp
# Visual Studio generated files
*.ib_pdb_index
*.idb
*.ilk
*.pdb
*.sln
*.suo
*.vcproj
*vcproj.*.*.user
*.ncb
*.sdf
*.opensdf
*.vcxproj
*vcxproj.*
# MinGW generated files
*.Debug
*.Release
# Python byte code
*.pyc
# Binaries
# --------
*.dll
*.exe

10
CMakeLists.txt Normal file
View File

@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.5)
project(rublon-ssh LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_compile_options(-Wall -Wextra -Wpedantic)
add_subdirectory(PAM/ssh)

9
PAM/ssh/CMakeLists.txt Normal file
View File

@ -0,0 +1,9 @@
add_subdirectory(extern/rapidjson)
include_directories(
extern/rapidjson/include
)
add_subdirectory(src)
add_subdirectory(tests)

1
PAM/ssh/extern/rapidjson vendored Submodule

@ -0,0 +1 @@
Subproject commit 973dc9c06dcd3d035ebd039cfb9ea457721ec213

2444
PAM/ssh/extern/tl/expected.hpp vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
set(CMAKE_SHARED_LINKER_FLAGS "-fpic -static-libstdc++ -fvisibility=hidden -ffunction-sections -fdata-sections -fwhole-program ")
add_library(
rublon-ssh SHARED pam.cpp pam.hpp rublon.hpp curl.hpp span.hpp sign.hpp CoreHandler.hpp configuration.hpp
)
target_link_libraries(rublon-ssh -lcurl -lssl -lcrypto)

104
PAM/ssh/src/CoreHandler.hpp Normal file
View File

@ -0,0 +1,104 @@
#pragma once
#include <memory_resource>
#include <string>
#include "configuration.hpp"
#include "curl.hpp"
#include "json.hpp"
#include "sign.hpp"
#include "../extern/tl/expected.hpp"
namespace rublon {
class CoreHandlerError {
public:
enum ErrorClass { BadSigature, CoreException, ConnectionError, BrokenData };
CoreHandlerError(ErrorClass e) : errorClass{e} {}
CoreHandlerError(ErrorClass e, std::string r) : errorClass{e}, reson{std::move(r)} {}
ErrorClass errorClass;
std::string reson;
};
template < typename Impl >
class CoreHandlerInterface {
public:
tl::expected< rublon::Document, CoreHandlerError > request(std::string_view path, const rublon::Document & body) const {
return static_cast< const Impl * >(this)->request(path, body);
}
};
template < typename HttpHandler = CURL >
class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > {
std::string secretKey;
std::string url;
std::pmr::string xRublonSignature(std::pmr::memory_resource * mr, std::string_view body) const {
return signData(body, secretKey.c_str()).data();
}
void signRequest(std::pmr::monotonic_buffer_resource & mr, Request & request) const {
request.headers["X-Rublon-Signature"] = xRublonSignature(&mr, request.body);
}
bool responseSigned(const Response & response) const {
auto xRubResp = response.headers.at("x-rublon-signature");
auto sign = signData(response.body, secretKey);
return xRubResp == sign.data();
}
protected:
HttpHandler http{};
public:
void debugLog(const char *, const char *) {}
CoreHandler(const rublon::Configuration & config) : secretKey{config.parameters.secretKey}, url{config.parameters.apiServer} {}
tl::expected< rublon::Document, CoreHandlerError > request(std::string_view path, const rublon::Document & body) const {
std::byte _buffer[16 * 1024];
std::pmr::monotonic_buffer_resource mr{_buffer, sizeof(_buffer)};
rublon::RapidJSONPMRAlloc alloc{&mr};
rublon::StringBuffer jsonStr{&alloc};
rublon::Writer writer{jsonStr, &alloc};
body.Accept(writer);
Request request;
request.headers["Content-Type"] = "application/json";
request.headers["Accept"] = "application/json";
request.body = jsonStr.GetString();
signRequest(mr, request);
std::pmr::string uri{url + path.data(), &mr};
auto response = http.request(uri, request);
if(not response.has_value()) {
return tl::unexpected{CoreHandlerError::ConnectionError};
}
if(not responseSigned(*response)) {
return tl::unexpected{CoreHandlerError::BadSigature};
}
rublon::Document resp{&alloc};
resp.Parse(response->body.c_str());
if(resp.HasParseError() or not resp.HasMember("result")) {
return tl::unexpected{CoreHandlerError::BrokenData};
}
if(resp["result"].HasMember("exception")) {
return tl::unexpected{CoreHandlerError{CoreHandlerError::CoreException, resp["result"]["exception"].GetString()}};
}
return resp;
}
};
} // namespace rublon

View File

@ -0,0 +1,105 @@
#pragma once
#include <algorithm>
#include <cctype>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <functional>
#include <map>
#include <memory_resource>
#include <optional>
#include <sstream>
#include <string>
#include "utils.hpp"
namespace rublon {
class ConfigurationFactory;
class Configuration {
public:
struct Parameters {
std::string systemToken;
std::string secretKey;
std::string apiServer;
int prompt;
bool enablePasswdEmail;
bool logging;
bool autopushPrompt;
};
Parameters parameters;
};
class ConfigurationFactory {
public:
ConfigurationFactory(){};
std::optional< Configuration > systemConfig() {
std::array< char, 8 * 1024 > configBuffer;
std::pmr::monotonic_buffer_resource mr{configBuffer.data(), configBuffer.size()};
using Params = Configuration::Parameters;
Params configValues;
/// TODO wypadałoby zmienić ścieżkę
std::ifstream file(std::filesystem::path{"/etc/rublon.config"});
std::pmr::string line{&mr};
line.reserve(100);
std::pmr::map< std::pmr::string, std::pmr::string > parameters{&mr};
while(std::getline(file, line)) {
std::pmr::string key{&mr};
std::pmr::string value{&mr};
if(!line.length())
continue;
if(line[0] == '#' || line[0] == ';')
continue;
auto posEqual = line.find('=');
key = line.substr(0, posEqual);
value = line.substr(posEqual + 1);
parameters.emplace(std::move(key), std::move(value));
}
auto saveStr = [&](auto member) { //
return [member](Params * params, std::string_view value) { params->*member = value; };
};
auto saveInt = [](auto member) { //
return [member](Params * params, std::string_view value) { params->*member = std::stoi(value.data()); };
};
auto saveBool = [](auto member) {
return [member](Params * params, std::string_view value) { params->*member = details::to_bool(value); };
};
std::tuple< const char *, std::function< void(Params *, const char *) >, const char * > checks[]{//
{"logging", saveBool(&Params::logging), "true"},
{"systemToken", saveStr(&Params::systemToken), nullptr},
{"secretKey", saveStr(&Params::secretKey), nullptr},
{"rublonApiServer", saveStr(&Params::apiServer), nullptr},
{"prompt", saveInt(&Params::prompt), "1"},
{"enablePasswdEmail", saveBool(&Params::enablePasswdEmail), "true"},
{"autopushPrompt", saveBool(&Params::autopushPrompt), "false"}};
for(const auto & [key, set_func, default_value] : checks) {
if(auto it = parameters.find(key); it != parameters.end()) {
set_func(&configValues, it->second.data());
} else {
if(default_value) {
/// TODO exit??
} else { // not required
set_func(&configValues, default_value);
}
}
}
return Configuration{std::move(configValues)};
}
};
} // namespace rublon

84
PAM/ssh/src/curl.hpp Normal file
View File

@ -0,0 +1,84 @@
#pragma once
#include <algorithm>
#include <cstdlib>
#include <curl/curl.h>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <memory_resource>
#include "span.hpp"
#include "utils.hpp"
/// TODO rename file to rublon/Core.hpp
/// TODO Create rublon utils
namespace rublon {
static size_t WriteMemoryCallback(void * contents, size_t size, size_t nmemb, void * userp) {
size_t realsize = size * nmemb;
reinterpret_cast< std::string * >(userp)->append(static_cast< const char * >(contents), realsize);
return realsize;
}
struct Request{
std::map<std::string, std::string> headers;
std::string body;
};
struct Response{
std::map<std::string, std::string> headers;
std::string body;
};
class CURL {
std::unique_ptr< ::CURL, void (*)(::CURL *) > curl;
public:
CURL() : curl{std::unique_ptr< ::CURL, void (*)(::CURL *) >(curl_easy_init(), curl_easy_cleanup)} {}
std::optional< Response > request(std::string_view uri, const Request &request) const {
std::string response_data;
response_data.reserve(1000);
Response response;
/// TODO this can be done on stack using pmr
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) {
curl_headers.reset(curl_slist_append(curl_headers.release(),(header.first + ": " + header.second).c_str()));
});
curl_easy_setopt(curl.get(), CURLOPT_VERBOSE, 1);
curl_easy_setopt(curl.get(), CURLOPT_URL, uri.data());
curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, curl_headers.get());
curl_easy_setopt(curl.get(), CURLOPT_POST, 1);
curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, request.body.data());
curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE, static_cast< u_int32_t >(request.body.size()));
curl_easy_setopt(curl.get(), CURLOPT_HEADER, 1);
curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response_data);
auto res = curl_easy_perform(curl.get());
if(res != CURLE_OK) {
// debugLog("No response from Rublon server (perform)", "");
return std::nullopt;
}
long size;
curl_easy_getinfo(curl.get(), CURLINFO_HEADER_SIZE, &size);
///TODO ogarnąć alokację pamięci
response.headers = details::headers({response_data.data(), static_cast<std::size_t>(size)});
response.body = response_data.substr(size);
return response;
}
};
} // namespace rublon

0
PAM/ssh/src/init.hpp Normal file
View File

88
PAM/ssh/src/json.hpp Normal file
View File

@ -0,0 +1,88 @@
#pragma once
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include <cstring>
#include <memory_resource>
namespace rublon {
struct RapidJSONPMRAlloc {
std::pmr::memory_resource * upstream = std::pmr::get_default_resource();
static constexpr bool kNeedFree = true;
static constexpr auto objectOffset = alignof(std::max_align_t);
static constexpr auto memPadding = objectOffset * 2;
void * Malloc(size_t size) {
if(size != 0) {
const auto allocated_size = size + memPadding;
std::byte * newPtr = static_cast< std::byte * >(upstream->allocate(allocated_size));
auto * ptrToReturn = newPtr + memPadding;
// placement new a pointer to ourselves at the first memory location
new(newPtr)(RapidJSONPMRAlloc *)(this);
// placement new the size in the second location
new(newPtr + objectOffset)(std::size_t)(size);
return ptrToReturn;
} else {
return nullptr;
}
}
void freePtr(void * origPtr, size_t originalSize) {
if(origPtr == nullptr) {
return;
}
upstream->deallocate(static_cast< std::byte * >(origPtr) - memPadding, originalSize + memPadding);
}
void * Realloc(void * origPtr, size_t originalSize, size_t newSize) {
if(newSize == 0) {
freePtr(origPtr, originalSize);
return nullptr;
}
if(newSize <= originalSize) {
return origPtr;
}
void * newPtr = Malloc(newSize);
std::memcpy(newPtr, origPtr, originalSize);
freePtr(origPtr, originalSize);
return newPtr;
}
// and Free needs to be static, which causes this whole thing
// to fall apart. This means that we have to keep our own list of allocated memory
// with our own pointers back to ourselves and our own list of sizes
// so we can push all of this back to the upstream allocator
static void Free(void * ptr) {
if(ptr == nullptr) {
return;
}
std::byte * startOfData = static_cast< std::byte * >(ptr) - memPadding;
auto * ptrToAllocator = *reinterpret_cast< RapidJSONPMRAlloc ** >(startOfData);
auto origAllocatedSize = *reinterpret_cast< std::size_t * >(startOfData + objectOffset);
ptrToAllocator->freePtr(ptr, origAllocatedSize);
}
};
using Document = rapidjson::GenericDocument< rapidjson::UTF8<>, RapidJSONPMRAlloc >;
using Value = rapidjson::GenericValue< rapidjson::UTF8<>, RapidJSONPMRAlloc >;
using StringBuffer = rapidjson::GenericStringBuffer< rapidjson::UTF8<>, RapidJSONPMRAlloc >;
using Writer = rapidjson::Writer< StringBuffer, rapidjson::UTF8<>, rapidjson::UTF8<>, RapidJSONPMRAlloc >;
// using value = GenericValue< UTF8<>, RapidJSONPMRAlloc >;
// RapidJSONPMRAlloc alloc{&mr};
// GenericDocument< UTF8<>, RapidJSONPMRAlloc > d(&alloc);
// d.SetObject();
// GenericDocument< UTF8<>, RapidJSONPMRAlloc >::AllocatorType & allocator = d.GetAllocator();
// d.AddMember("systemToken", "DUPA", allocator);
// d.AddMember("username", "$USER", allocator);
// d.AddMember("userEmail", "$USER_EMAIL", allocator);
} // namespace rublon

36
PAM/ssh/src/pam.cpp Normal file
View File

@ -0,0 +1,36 @@
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <security/pam_misc.h>
#include <security/pam_client.h>
#include <security/pam_ext.h>
#include <rapidjson/rapidjson.h>
#include <iostream>
#include "curl.hpp"
#include "rublon.hpp"
using namespace std;
PAM_EXTERN int pam_sm_setcred( pam_handle_t *pamh, int flags, int argc, const char **argv ) {
return PAM_SUCCESS;
}
PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) {
auto rublonConfig = rublon::ConfigurationFactory{}.systemConfig();
rublon::CoreHandler CH{rublonConfig.value()};
rublon::Init{rublonConfig.value()}.fire(CH);
return PAM_SUCCESS;
}
PAM_EXTERN int pam_sm_authenticate( pam_handle_t *pamh, int flags,int argc, const char **argv ) {
return PAM_SUCCESS;
}
int main(){
pam_sm_acct_mgmt(nullptr, 1, 0, nullptr);
}

63
PAM/ssh/src/pam.hpp Normal file
View File

@ -0,0 +1,63 @@
#pragma once
#include <security/pam_ext.h>
#include <security/pam_appl.h>
#include <security/pam_client.h>
#include <security/pam_ext.h>
#include <security/pam_misc.h>
#include <security/pam_modules.h>
#include <optional>
#include <string>
#include <system_error>
#include <type_traits>
#include <utility>
#include <memory>
#include "utils.hpp"
class PamAction {};
class PamAccept {};
class PamDecline {};
class PAMPrompt {
pam_handle_t * pamh;
public:
template < typename... Ti >
void print(const char * fmt, Ti... ti) const noexcept {
// pam_prompt(pamh, PAM_TEXT_INFO, nullptr, fmt, std::forward<Ti...>(ti...));
}
template < typename Fun, typename... Ti >
[[nodiscard]] auto scan(Fun && f, const char * fmt, Ti... ti) const noexcept {
char * responseBuffer = nullptr;
pam_prompt(pamh, PAM_TEXT_INFO, &responseBuffer, fmt, std::forward< Ti... >(ti...));
if(responseBuffer) {
auto ret = f(responseBuffer);
free(responseBuffer);
return std::optional{ret};
}
return std::optional< std::result_of_t< Fun(char *) > >();
}
};
class LinuxPam {
pam_handle_t * pamh;
public:
rublon::NonOwningPtr< const char > ip() const {
const char * ip = NULL;
pam_get_item(pamh, PAM_RHOST, ( const void ** ) &ip);
if(ip == NULL)
ip = "";
return ip;
}
rublon::NonOwningPtr< const char > username() const {
// pam_get_user
}
};

119
PAM/ssh/src/rublon.hpp Normal file
View File

@ -0,0 +1,119 @@
#pragma once
#include <algorithm>
#include <cctype>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <functional>
#include <map>
#include <memory_resource>
#include <optional>
#include <sstream>
#include <string>
#include "CoreHandler.hpp"
#include "json.hpp"
#include "pam.hpp"
#include <iostream>
#include <variant>
namespace rublon {
enum class PamAction { accept, decline };
template < typename Impl >
class AuthenticationStep {
public:
template < typename Hander_t >
auto fire(const CoreHandlerInterface< Hander_t > & coreHandler) {
// log step
// debugLog("Starting %s step", static_cast<Impl*>(this)->stepName );
return static_cast< Impl * >(this)->handle(coreHandler);
}
};
template < typename PamInfo_t = PAMInfo >
class Method : public AuthenticationStep< Method< PamInfo_t > > {
std::string tid;
public:
Method() {}
};
template < typename PamInfo_t = PAMInfo >
class Init : public AuthenticationStep< Init< PamInfo_t > > {
const char * apiPath = "/api/transaction/init";
const std::string & systemToken;
PamInfo_t pamInfo;
public:
const char * stepName = "Initialization";
Init(const rublon::Configuration & config) : systemToken{config.parameters.systemToken} {}
/// TODO add core handler interface
template < typename Hander_t >
std::variant< Method< PamInfo_t >, PamAction > handle(const CoreHandlerInterface< Hander_t > & handler) const {
char _buffer[1024];
std::pmr::monotonic_buffer_resource mr{_buffer, 1024};
RapidJSONPMRAlloc alloc{&mr};
Document body{rapidjson::kObjectType, &alloc};
body.AddMember("systemToken", Value{systemToken.c_str(), alloc}, alloc);
body.AddMember("username", Value{pamInfo.username().get(), alloc}, alloc);
body.AddMember("userEmail", "bwi@rublon.com", alloc);
Value params{rapidjson::kObjectType};
params.AddMember("userIP", Value{pamInfo.ip().get(), alloc}, alloc);
params.AddMember("appVer", "v.1.6", alloc); /// TODO add version to cmake
params.AddMember("os", "Ubuntu 23.04", alloc); /// TODO add version to cmake
body.AddMember("params", std::move(params), alloc);
auto response = handler.request(apiPath, body);
if(response.has_value()) {
std::cout << response.value()["response"]["tid"].GetString();
return Method< PamInfo_t >{
};
} else {
// mostly connectio errors
switch(response.error().errorClass) {
case CoreHandlerError::ErrorClass::BadSigature:
return PamAction::decline; /// TODO accept?
case CoreHandlerError::ErrorClass::CoreException:
return PamAction::decline; /// TODO accept?
case CoreHandlerError::ErrorClass::ConnectionError:
return PamAction::decline; /// TODO accept?
case CoreHandlerError::ErrorClass::BrokenData:
return PamAction::decline;
}
}
return {PamAction::decline};
}
};
class ConfirmCode : public AuthenticationStep< ConfirmCode > {
public:
ConfirmCode(const Configuration & /*config*/) {}
};
class VerifySSH : public AuthenticationStep< VerifySSH > {
public:
VerifySSH(const Configuration & /*config*/) {}
};
class Credentials : public AuthenticationStep< Credentials > {
public:
Credentials(const Configuration & /*config*/) {}
};
} // namespace rublon

25
PAM/ssh/src/sign.hpp Normal file
View File

@ -0,0 +1,25 @@
#pragma once
#include <cstring>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <string_view>
#include <array>
namespace rublon {
inline std::array< char, 64 > signData(std::string_view data, std::string_view secretKey) {
std::array< char, 64 > xRublon;
unsigned char md[EVP_MAX_MD_SIZE] = {0};
unsigned int md_len;
HMAC(EVP_sha256(), secretKey.data(), secretKey.size(), ( unsigned const char * ) data.data(), data.size(), md, &md_len);
int i;
for(i = 0; i < 32; i++)
sprintf(&xRublon[i * 2], "%02x", ( unsigned int ) md[i]);
return xRublon;
}
} // namespace rublon

329
PAM/ssh/src/span.hpp Normal file
View File

@ -0,0 +1,329 @@
#pragma once
#include <array> // for std::array, etc.
#include <cassert> // for assert
#include <cstddef> // for std::size_t, etc.
#include <iterator> // for std::reverse_iterator, etc.
#include <type_traits> // for std::enable_if, etc.
namespace rublon {
#define CONSTRAINT(...) std::enable_if_t< (__VA_ARGS__), int > = 0
#define EXPECTS(...) assert((__VA_ARGS__))
// constants
// equivalent to std::numeric_limits<std::size_t>::max()
inline constexpr std::size_t dynamic_extent = -1;
// class template span
template < class T, std::size_t N = dynamic_extent >
class span;
namespace span_detail {
// detect specializations of span
template < class T >
struct is_span : std::false_type {};
template < class T, std::size_t N >
struct is_span< span< T, N > > : std::true_type {};
template < class T >
inline constexpr bool is_span_v = is_span< T >::value;
// detect specializations of std::array
template < class T >
struct is_array : std::false_type {};
template < class T, std::size_t N >
struct is_array< std::array< T, N > > : std::true_type {};
template < class T >
inline constexpr bool is_array_v = is_array< T >::value;
// ADL-aware data() and size()
template < class C >
constexpr decltype(auto) my_data(C & c) {
return std::data(c);
}
template < class C >
constexpr decltype(auto) my_size(C & c) {
return std::size(c);
}
// detect container
template < class C, class = void >
struct is_cont : std::false_type {};
template < class C >
struct is_cont< C,
std::void_t< std::enable_if_t< !is_span_v< C > >,
std::enable_if_t< !is_array_v< C > >,
std::enable_if_t< !std::is_array_v< C > >,
decltype(std::data(std::declval< C >())),
decltype(std::size(std::declval< C >())) > > : std::true_type {};
template < class C >
inline constexpr bool is_cont_v = is_cont< C >::value;
} // namespace span_detail
template < class T, std::size_t N >
class span {
public:
// constants and types
using element_type = T;
using value_type = std::remove_cv_t< T >;
using index_type = std::size_t;
using difference_type = std::ptrdiff_t;
using pointer = T *;
using const_pointer = const T *;
using reference = T &;
using const_reference = const T &;
using iterator = T *;
using const_iterator = const T *;
using reverse_iterator = std::reverse_iterator< iterator >;
using const_reverse_iterator = std::reverse_iterator< const_iterator >;
static constexpr index_type extent = N;
// constructors, copy, and assignment
// LWG 3198 applied
constexpr span() noexcept : size_{0}, data_{nullptr} {
static_assert(N == dynamic_extent || N == 0);
}
constexpr span(T * ptr, index_type n) : size_{n}, data_{ptr} {
EXPECTS(N == dynamic_extent || N == n);
}
constexpr span(T * first, T * last) : size_{last - first}, data_{first} {
EXPECTS(N == dynamic_extent || last - first = N);
}
template < std::size_t M,
CONSTRAINT(N == dynamic_extent ||
N == M &&
std::is_convertible_v< std::remove_pointer_t< decltype(span_detail::my_data(std::declval< T (&)[M] >())) > (*)[], T (*)[] >) >
constexpr span(T (&arr)[M]) noexcept : size_{M}, data_{arr} {}
template < std::size_t M,
CONSTRAINT(N == dynamic_extent ||
N == M &&
std::is_convertible_v< std::remove_pointer_t< decltype(span_detail::my_data(std::declval< T (&)[M] >())) > (*)[], T (*)[] >) >
constexpr span(std::array< value_type, M > & arr) noexcept : size_{M}, data_{arr.data()} {}
template < std::size_t M,
CONSTRAINT(N == dynamic_extent ||
N == M &&
std::is_convertible_v< std::remove_pointer_t< decltype(span_detail::my_data(std::declval< T (&)[M] >())) > (*)[], T (*)[] >) >
constexpr span(const std::array< value_type, M > & arr) noexcept : size_{M}, data_{arr.data()} {}
template < class Cont,
CONSTRAINT(N == dynamic_extent && span_detail::is_cont_v< Cont > &&
std::is_convertible_v< std::remove_pointer_t< decltype(span_detail::my_data(std::declval< Cont >())) > (*)[], T (*)[] >) >
constexpr span(Cont & c) : size_{span_detail::my_size(c)}, data_{span_detail::my_data(c)} {}
template < class Cont,
CONSTRAINT(N == dynamic_extent && span_detail::is_cont_v< Cont > &&
std::is_convertible_v< std::remove_pointer_t< decltype(span_detail::my_data(std::declval< Cont >())) > (*)[], T (*)[] >) >
constexpr span(const Cont & c) : size_{span_detail::my_size(c)}, data_{span_detail::my_data(c)} {}
constexpr span(const span & other) noexcept = default;
// template < class U, std::size_t M, CONSTRAINT(N == dynamic_extent || N == M && std::is_convertible_v< U (*)[], T (*)[] >) >
// constexpr span(const span< U, M > & s) noexcept : size_{s.size()}, data_{s.data()} {}
~span() noexcept = default;
constexpr span & operator=(const span & other) noexcept = default;
// subviews
template < std::size_t Cnt >
constexpr span< T, Cnt > first() const {
assert(Cnt <= size());
return {data(), Cnt};
}
template < std::size_t Cnt >
constexpr span< T, Cnt > last() const {
assert(Cnt <= size());
return {data() + (size() - Cnt), Cnt};
}
template < std::size_t Off, std::size_t Cnt = dynamic_extent >
constexpr auto subspan() const {
assert(Off <= size() && (Cnt == dynamic_extent || Off + Cnt <= size()));
if constexpr(Cnt != dynamic_extent)
return span< T, Cnt >{data() + Off, Cnt};
else if constexpr(N != dynamic_extent)
return span< T, N - Off >{data() + Off, size() - Off};
else
return span< T, dynamic_extent >{data() + Off, size() - Off};
}
constexpr span< T, dynamic_extent > first(index_type cnt) const {
assert(cnt <= size());
return {data(), cnt};
}
constexpr span< T, dynamic_extent > last(index_type cnt) const {
assert(cnt <= size());
return {data() + (size() - cnt), cnt};
}
constexpr span< T, dynamic_extent > subspan(index_type off, index_type cnt = dynamic_extent) const {
assert(off <= size() && (cnt == dynamic_extent || off + cnt <= size()));
return {data() + off, cnt == dynamic_extent ? size() - off : cnt};
}
// observers
constexpr index_type size() const noexcept {
return size_;
}
constexpr index_type size_bytes() const noexcept {
return size() * sizeof(T);
}
[[nodiscard]] constexpr bool empty() const noexcept {
return size() == 0;
}
// element access
constexpr reference operator[](index_type idx) const {
assert(idx < size());
return *(data() + idx);
}
constexpr reference front() const {
assert(!empty());
return *data();
}
constexpr reference back() const {
assert(!empty());
return *(data() + (size() - 1));
}
constexpr pointer data() const noexcept {
return data_;
}
// iterator support
constexpr iterator begin() const noexcept {
return data();
}
constexpr iterator end() const noexcept {
return data() + size();
}
constexpr const_iterator cbegin() const noexcept {
return data();
}
constexpr const_iterator cend() const noexcept {
return data() + size();
}
constexpr reverse_iterator rbegin() const noexcept {
return reverse_iterator{end()};
}
constexpr reverse_iterator rend() const noexcept {
return reverse_iterator{begin()};
}
constexpr const_reverse_iterator crbegin() const noexcept {
return reverse_iterator{cend()};
}
constexpr const_reverse_iterator crend() const noexcept {
return reverse_iterator{cbegin()};
}
friend constexpr iterator begin(span s) noexcept {
return s.begin();
}
friend constexpr iterator end(span s) noexcept {
return s.end();
}
private:
pointer data_;
index_type size_;
};
// deduction guide
template < class T, std::size_t N >
span(T (&)[N]) -> span< T, N >;
template < class T, std::size_t N >
span(std::array< T, N > &) -> span< T, N >;
template < class T, std::size_t N >
span(const std::array< T, N > &) -> span< const T, N >;
template < class Cont >
span(Cont &) -> span< typename Cont::value_type >;
template < class Cont >
span(const Cont &) -> span< const typename Cont::value_type >;
// views of objects representation
template < class T, std::size_t N >
auto as_bytes(span< T, N > s) noexcept -> span< const std::byte, N == dynamic_extent ? dynamic_extent : sizeof(T) * N > {
return {reinterpret_cast< const std::byte * >(s.data()), s.size_bytes()};
}
template < class T, std::size_t N, CONSTRAINT(!std::is_const_v< T >) >
auto as_writable_bytes(span< T, N > s) noexcept -> span< std::byte, N == dynamic_extent ? dynamic_extent : sizeof(T) * N > {
return {reinterpret_cast< std::byte * >(s.data()), s.size_bytes()};
}
} // namespace rublon
namespace std {
// tuple interface
// the primary template declarations are included in <array>
template < class T, std::size_t N >
struct tuple_size< rublon::span< T, N > > : std::integral_constant< std::size_t, N > {};
// not defined
template < class T >
struct tuple_size< rublon::span< T, rublon::dynamic_extent > >;
template < std::size_t I, class T, std::size_t N >
struct tuple_element< I, rublon::span< T, N > > {
static_assert(N != rublon::dynamic_extent && I < N);
using type = T;
};
template < std::size_t I, class T, std::size_t N >
constexpr T & get(rublon::span< T, N > s) noexcept {
static_assert(N != rublon::dynamic_extent && I < N);
return s[I];
}
} // namespace std
#undef CONSTRAINT
#undef EXPECTS

107
PAM/ssh/src/utils.hpp Normal file
View File

@ -0,0 +1,107 @@
#pragma once
#include <algorithm>
#include <cctype>
#include <cstring>
#include <map>
#include <memory_resource>
#include <sstream>
#include <string_view>
#include <alloca.h>
namespace rublon {
template < typename T >
class NonOwningPtr {
T * object;
public:
constexpr NonOwningPtr(T * obj) : object{obj} {}
constexpr T * get() noexcept {
assert(object != nullptr);
return object;
}
constexpr const T * get() const noexcept {
assert(object != nullptr);
return object;
}
constexpr operator const T *() const noexcept {
return get();
}
constexpr operator T *() noexcept {
return get();
}
};
namespace details {
inline bool to_bool(std::string_view value) {
auto * buf = ( char * ) alloca(value.size());
buf[value.size()] = '\0';
auto asciitolower = [](char in) { return in - ((in <= 'Z' && in >= 'A') ? ('Z' - 'z') : 0); };
std::transform(value.cbegin(), value.cend(), buf, asciitolower);
return strcmp(buf, "true") == 0;
};
static inline std::string_view ltrim(std::string_view s) {
while(s.length() && std::isspace(*s.begin()))
s.remove_prefix(1);
return s;
}
static inline std::string_view rtrim(std::string_view s) {
while(s.length() && std::isspace(*s.rbegin()))
s.remove_suffix(1);
return s;
}
static inline std::string_view trim(std::string_view s) {
return ltrim(rtrim(s));
}
inline std::map< std::string, std::string > headers(std::string_view data) {
std::map< std::string, std::string > headers{};
std::string tmp{};
std::istringstream resp{};
resp.rdbuf()->pubsetbuf(const_cast< char * >(data.data()), data.size());
while(std::getline(resp, tmp)) {
auto line = std::string_view(tmp);
auto index = tmp.find(':', 0);
if(index != std::string::npos) {
headers.insert({std::string{trim(line.substr(0, index))}, std::string{trim(line.substr(index + 1))}});
}
}
return headers;
}
namespace pmr {
inline std::pmr::map< std::pmr::string, std::pmr::string > headers(std::pmr::memory_resource * mr, std::string_view data) {
char _buf[1024];
std::pmr::monotonic_buffer_resource tmr{_buf, sizeof(_buf)};
std::pmr::map< std::pmr::string, std::pmr::string > headers{mr};
std::pmr::string tmp{&tmr};
std::istringstream resp{};
resp.rdbuf()->pubsetbuf(const_cast< char * >(data.data()), data.size());
while(std::getline(resp, tmp) && !(trim(tmp).empty())) {
auto line = std::string_view(tmp);
auto index = tmp.find(':', 0);
if(index != std::string::npos) {
headers.insert({std::pmr::string{trim(line.substr(0, index)), mr}, std::pmr::string{trim(line.substr(index + 1)), mr}});
}
}
return headers;
}
} // namespace pmr
} // namespace details
} // namespace rublon

View File

@ -0,0 +1,24 @@
enable_testing()
include(FetchContent)
## Project-wide setup
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS NO)
# Externally provided libraries
FetchContent_Declare(googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG main)
FetchContent_Declare(googlebenchmark
GIT_REPOSITORY https://github.com/google/benchmark.git
GIT_TAG main)
FetchContent_MakeAvailable(
googletest
googlebenchmark)
add_executable(rublon-tests utilsTests.cpp rublonTests.cpp core_handler_tests.cpp init_test.cpp)
target_link_libraries(rublon-tests GTest::gmock_main -lssl -lcrypto)

View File

View File

@ -0,0 +1,67 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <numeric>
#include <set>
#include "../src/CoreHandler.hpp"
#include "http_mock.hpp"
class CoreHandlerTestable : public rublon::CoreHandler< HttpHandlerMock > {
public:
CoreHandlerTestable() : rublon::CoreHandler< HttpHandlerMock >{conf} {}
auto & _http() {
return http;
}
};
class CoreHandlerTests : public testing::Test {
public:
CoreHandlerTests() : http{sut._http()} {
doc.SetObject();
}
CoreHandlerTestable sut;
HttpHandlerMock & http;
rublon::Document doc;
};
using namespace testing;
MATCHER(HasValue, "") {
return arg.has_value();
}
bool operator==(const rublon::CoreHandlerError & lhs, const rublon::CoreHandlerError & rhs) {
return lhs.errorClass == rhs.errorClass;
}
MATCHER_P(Unexpected, error, "") {
return arg.error() == error;
}
TEST_F(CoreHandlerTests, coreShouldSetConnectionErrorOnBrokenConnection) {
EXPECT_CALL(http, request(_, _)).WillOnce(Return(std::optional< rublon::Response >{}));
EXPECT_THAT(sut.request("", doc), //
AllOf(Not(HasValue()), Unexpected(rublon::CoreHandlerError::ConnectionError)));
}
TEST_F(CoreHandlerTests, coreShouldCheckSignatureAndReturnBadSignatureBeforeAnythingElse) {
EXPECT_CALL(http, request(_, _)).WillOnce(Return((rublon::Response) http.brokenSignature().brokenBody()));
EXPECT_THAT(sut.request("", doc), //
AllOf(Not(HasValue()), Unexpected(rublon::CoreHandlerError::BadSigature)));
}
TEST_F(CoreHandlerTests, coreShouldSetBrokenDataWhenResultIsNotAvailable) {
EXPECT_CALL(http, request(_, _)).WillOnce(Return(( rublon::Response ) http.brokenBody()));
EXPECT_THAT(sut.request("", doc), //
AllOf(Not(HasValue()), Unexpected(rublon::CoreHandlerError::BrokenData)));
}
TEST_F(CoreHandlerTests, coreSignatureIsBeingChecked) {
EXPECT_CALL(http, request(Eq(conf.parameters.apiServer + "/path"), _)).WillOnce(Return(( rublon::Response ) http.statusPending()));
auto val = sut.request("/path", doc);
EXPECT_TRUE(val.value().IsObject());
}

View File

@ -0,0 +1,86 @@
#pragma once
#include <algorithm>
#include <string>
#include <set>
#include <array>
namespace io {
template < class X >
using is_string = typename std::enable_if_t< std::is_same_v< X, std::string > >;
template < class X >
using is_not_string = typename std::enable_if_t< !std::is_same_v< X, std::string > >;
template < class T, class = is_not_string< T > >
constexpr T forward_or_transform(T t) {
return std::forward< T >(t);
}
template < class T, class = is_string< T > >
constexpr const char * forward_or_transform(T t) {
return t.c_str();
}
template < class... Ti >
int sprintf(char * _s, const std::string & format, Ti... t) {
return std::sprintf(_s, format.c_str(), forward_or_transform(t)...);
}
} // namespace io
namespace {
std::string gen_random(const int len) {
static const char alphanum[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
std::string tmp_s;
tmp_s.resize(len);
std::for_each(tmp_s.begin(), tmp_s.end(), [](auto & chr) { chr = alphanum[rand() % (sizeof(alphanum) - 1)]; });
return tmp_s;
}
static constexpr const char * result_ok_template =
R"json({"status":"OK","result":{"tid":"%s","status":"%s","companyName":"%s","applicationName":"%s","methods":[%s]}})json";
static constexpr const char * result_broken_template = R"json({"some":"random","json":"notrublon"})json";
} // namespace
class CoreResponseGenerator {
public:
std::string generateBody() {
std::array< char, 2048 > _buf;
io::sprintf(_buf.data(),
generateBrokenData ? result_broken_template : result_ok_template,
tid,
status,
companyName,
applicationName,
print_methods());
return _buf.data();
}
std::string print_methods() {
std::string ret;
for(const auto & m : methods)
ret += "\"" + m + "\",";
ret.pop_back();
return ret;
}
static std::string generateTid() {
return gen_random(32);
}
std::string tid{generateTid()};
std::string status;
std::string companyName{"rublon"};
std::string applicationName{"test_app"};
std::set< std::string > methods{"email", "totp", "qrcode", "push"};
bool skipSignatureGeneration{false};
bool generateBrokenData{false};
};

View File

@ -0,0 +1,60 @@
#pragma once
#include <gmock/gmock.h>
#include <set>
#include <string>
#include "../src/curl.hpp"
#include "../src/configuration.hpp"
#include "core_response_generator.hpp"
namespace {
rublon::Configuration conf{rublon::Configuration::Parameters{//
"320BAB778C4D4262B54CD243CDEFFAFD",
"39e8d771d83a2ed3cc728811911c25",
"https://staging-core.rublon.net",
1,
true,
true,
false}};
} // namespace
class HttpHandlerMock {
auto signResponse(rublon::Response & res) {
const auto & sign =
skipSignatureGeneration ? std::array< char, 64 >{} : rublon::signData(res.body, conf.parameters.secretKey.c_str());
res.headers["x-rublon-signature"] = sign.data();
}
public:
MOCK_METHOD(std::optional< rublon::Response >, request, ( std::string_view, const rublon::Request & ), (const));
HttpHandlerMock & statusPending() {
gen.status = "pending";
return *this;
}
HttpHandlerMock & brokenBody() {
gen.generateBrokenData = true;
return *this;
}
HttpHandlerMock & brokenSignature() {
skipSignatureGeneration = true;
return *this;
}
operator rublon::Response() {
rublon::Response res;
res.body = gen.generateBody();
signResponse(res);
return res;
}
bool skipSignatureGeneration;
CoreResponseGenerator gen;
};

View File

@ -0,0 +1,58 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "../src/rublon.hpp"
#include "core_response_generator.hpp"
using namespace rublon;
using namespace testing;
namespace {
Configuration conf;
}
class CoreHandlerMock : public CoreHandlerInterface< CoreHandlerMock > {
public:
CoreHandlerMock() {}
MOCK_METHOD(( tl::expected< Document, CoreHandlerError > ), request, ( std::string_view, const Document & ), (const));
CoreHandlerMock & statusPending() {
gen.status = "pending";
return *this;
}
CoreHandlerMock & brokenBody() {
gen.generateBrokenData = true;
return *this;
}
operator tl::expected< Document, CoreHandlerError >() {
auto body = gen.generateBody();
rublon::Document doc;
doc.Parse(body.c_str());
return doc;
}
CoreResponseGenerator gen;
};
class RublonHttpInitTest : public testing::Test {
public:
CoreHandlerMock coreHandler;
Init<> sut{conf};
};
using CoreReturn = tl::expected< Document, CoreHandlerError >;
TEST_F(RublonHttpInitTest, initializationSendsRequestOnGoodPath) {
EXPECT_CALL(coreHandler, request("/api/transaction/init", _)).WillOnce(Return(tl::unexpected{CoreHandlerError{CoreHandlerError::BadSigature}}));
sut.handle(coreHandler);
}
TEST_F(RublonHttpInitTest, returnMethods){
}

View File

@ -0,0 +1,7 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include "../src/rublon.hpp"
using namespace testing;

View File

@ -0,0 +1,90 @@
#include "gmock/gmock.h"
#include <cassert>
#include <gtest/gtest.h>
#include <memory_resource>
#include <string>
#include "../src/utils.hpp"
using namespace rublon;
using namespace testing;
TEST(Utils, toBoolReturnsTrueWhenStringIsPassed) {
EXPECT_TRUE(details::to_bool("TrUe"));
}
TEST(Utils, toBoolReturnsFalseWhenStringIsPassed) {
EXPECT_FALSE(details::to_bool("False"));
}
std::string response = R"http(HTTP/2 200
date: Thu, 22 Jun 2023 13:24:58 GMT
content-type: application/json
server: nginx
cache-control: no-cache, private
x-rublon-signature: 1a01558bedaa2dd92ff659fb8ee3c65a89163d63e312fcb9b6f60463cce864d7
x-ratelimit-limit: 300
x-ratelimit-remaining: 299
{"status":"OK","result":{"tid":"2039132542F6465691BF8C41D7CC38C5","status":"pending","companyName":"rublon","applicationName":"Bartoszek_SSH","methods":["email","totp","qrcode","push"]}})http";
static inline std::string_view ltrim(std::string_view s) {
while(std::isspace(*s.begin()))
s.remove_prefix(1);
return s;
}
static inline std::string_view rtrim(std::string_view s) {
while(std::isspace(*s.rbegin()))
s.remove_suffix(1);
return s;
}
static inline std::string_view trim(std::string_view s) {
return ltrim(rtrim(s));
}
std::pmr::map< std::pmr::string, std::pmr::string > headers(std::pmr::memory_resource * mr, std::string_view data) {
std::pmr::map< std::pmr::string, std::pmr::string > headers{mr};
std::pmr::string tmp{mr};
tmp.reserve(256);
std::istringstream resp{};
resp.rdbuf()->pubsetbuf(const_cast< char * >(data.data()), data.size());
while(std::getline(resp, tmp) && !(trim(tmp).empty())) {
auto line = std::string_view(tmp);
auto index = tmp.find(':', 0);
if(index != std::string::npos) {
headers.insert({std::pmr::string{trim(line.substr(0, index)), mr}, std::pmr::string{trim(line.substr(index + 1)), mr}});
}
}
return headers;
}
inline std::map< std::string, std::string > headers(std::string_view data) {
std::map< std::string, std::string > headers{};
std::string tmp{};
std::istringstream resp{};
resp.rdbuf()->pubsetbuf(const_cast< char * >(data.data()), data.size());
while(std::getline(resp, tmp) && !(trim(tmp).empty())) {
auto line = std::string_view(tmp);
auto index = tmp.find(':', 0);
if(index != std::string::npos) {
headers.insert({std::string{trim(line.substr(0, index))}, std::string{trim(line.substr(index + 1))}});
}
}
return headers;
}
TEST(Utils, responseParser) {
auto sut = headers(response);
EXPECT_THAT(sut,
AllOf(Contains(Pair("date", "Thu, 22 Jun 2023 13:24:58 GMT")),
Contains(Pair("x-rublon-signature", "1a01558bedaa2dd92ff659fb8ee3c65a89163d63e312fcb9b6f60463cce864d7")),
Contains(Pair("x-ratelimit-remaining", "299"))));
}