Skip to content

Commit

Permalink
Merge pull request #628 from GDATASoftwareAG/cpp/various-fixes
Browse files Browse the repository at this point in the history
C++: Fix some bugs + improve error messages, add more tests
  • Loading branch information
GermanCoding authored Oct 22, 2024
2 parents 31fd122 + 42480fc commit 3cb8d85
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 27 deletions.
2 changes: 1 addition & 1 deletion cpp/dotenv.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class Dotenv {

std::string get(const std::string& key) {
if (std::getenv(key.c_str())) {
return std::getenv(key.c_str());
return std::string(std::getenv(key.c_str()));
}

if (envFromFile.find(key) != envFromFile.end()) {
Expand Down
45 changes: 24 additions & 21 deletions cpp/vaas.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
namespace vaas {

static const char* USER_AGENT = "C++ SDK 0.1.0";
const long CURL_VERBOSE = 0;
constexpr long CURL_VERBOSE = 0;

/// An AuthenticationException indicates that the credentials are incorrect. Manual intervention may be required.
class AuthenticationException final : public std::runtime_error {
Expand Down Expand Up @@ -83,37 +83,37 @@ static long getServerResponse(CURL* curl, Json::Value& jsonResponse) {
long response_code;
ensureCurlOk(curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code));

if (response_code < 200 || response_code >= 300) {
return response_code;
}

const Json::CharReaderBuilder readerBuilder;
std::string errs;

const std::unique_ptr<Json::CharReader> reader(readerBuilder.newCharReader());
if (!response.empty()) {
if (!reader->parse(response.c_str(), response.c_str() + response.size(), &jsonResponse, &errs)) {
if (response_code < 200 || response_code >= 300) {
// If the response code was an error anyway, return the error response instead of an exception
return response_code;
}
throw vaas::VaasException("Failed to parse JSON response: " + errs);
}
}

return response_code;
}

std::string bytesToHex(const std::vector<unsigned char>& bytes) {
static const char hexDigits[] = "0123456789abcdef";
inline std::string bytesToHex(const std::vector<unsigned char>& bytes) {
static constexpr char hexDigits[] = "0123456789abcdef";
std::string hexStr;
hexStr.reserve(bytes.size() * 2);

for (unsigned char byte : bytes) {
for (const unsigned char byte : bytes) {
hexStr.push_back(hexDigits[byte >> 4]);
hexStr.push_back(hexDigits[byte & 0x0F]);
}

return hexStr;
}

std::string calculateSHA256(const std::filesystem::path& filePath) {
inline std::string calculateSHA256(const std::filesystem::path& filePath) {
// Open the file in binary mode
std::ifstream file(filePath, std::ios::binary);
if (!file) {
Expand All @@ -131,7 +131,7 @@ std::string calculateSHA256(const std::filesystem::path& filePath) {
}

// Read the file in chunks and update the digest
const std::size_t bufferSize = 4096;
constexpr std::size_t bufferSize = 4096;
char buffer[bufferSize];
while (file.read(buffer, bufferSize)) {
if (EVP_DigestUpdate(context, buffer, file.gcount()) != 1) {
Expand Down Expand Up @@ -160,7 +160,7 @@ std::string calculateSHA256(const std::filesystem::path& filePath) {
return bytesToHex(hash);
}

std::string getLastSegmentOfUrl(const std::string& url) {
inline std::string getLastSegmentOfUrl(const std::string& url) {
size_t lastSlashPos = url.find_last_of('/');

if (lastSlashPos != std::string::npos) {
Expand All @@ -186,8 +186,8 @@ class OIDCClient {
}

OIDCClient(OIDCClient&& other) noexcept
: tokenEndpoint(std::move(tokenEndpoint)), clientId(std::move(clientId)),
clientSecret(std::move(clientSecret)),
: tokenEndpoint(other.tokenEndpoint), clientId(other.clientId),
clientSecret(other.clientSecret),
curl(other.curl) {
other.curl = nullptr;
}
Expand All @@ -202,7 +202,7 @@ class OIDCClient {
/// Retrieve a new access token from the identity provider, or return a cached token that is still valid.
/// </summary>
std::string getAccessToken() {
std::lock_guard<std::mutex> lock(mtx);
std::lock_guard lock(mtx);
const auto now = std::chrono::system_clock::now();
if (now < tokenExpiry) {
return accessToken;
Expand All @@ -225,13 +225,13 @@ class OIDCClient {
const auto response_code = vaas_internals::getServerResponse(curl, jsonResponse);
if (!(response_code == 200 || response_code == 401)) {
throw AuthenticationException(
"Server replied with unexpected HTTP response code " + std::to_string(response_code));
"Authentication Server replied with unexpected HTTP response code " + std::to_string(response_code));
}

if (jsonResponse.isMember("error") || response_code != 200) {
const auto errorMsg = jsonResponse.isMember("error_description")
? jsonResponse.get("error_description", "")
: jsonResponse.get("error", "unknown error");
: jsonResponse.get("error", "authentication server did not return an error message");
throw AuthenticationException(errorMsg.asString());
}

Expand Down Expand Up @@ -289,8 +289,8 @@ class VaasReport {
verdict = Pup;
}

explicit VaasReport(const std::string& sha256, Verdict verdict)
: sha256{sha256}, verdict{verdict} {
explicit VaasReport(std::string sha256, const Verdict verdict)
: sha256{std::move(sha256)}, verdict{verdict} {
}
};

Expand All @@ -304,15 +304,18 @@ class Vaas {
public:
Vaas(const std::string& serverEndpoint, const std::string& tokenEndpoint, const std::string& clientId,
const std::string& clientSecret)
: serverEndpoint(serverEndpoint), authenticator(tokenEndpoint, clientId, clientSecret),
: Vaas(serverEndpoint, OIDCClient(tokenEndpoint, clientId, clientSecret)) {}

Vaas(std::string serverEndpoint, OIDCClient&& authenticator)
: serverEndpoint(std::move(serverEndpoint)), authenticator(std::move(authenticator)),
curl(curl_easy_init()) {
if (!curl) {
throw std::runtime_error("Failed to initialize CURL");
}
}

Vaas(Vaas&& other) noexcept
: serverEndpoint(std::move(other.serverEndpoint)), // Can't actually move because it's const
: serverEndpoint(other.serverEndpoint), // Can't actually move because it's const
authenticator(std::move(other.authenticator)),
curl(other.curl) {
other.curl = nullptr;
Expand All @@ -329,7 +332,7 @@ class Vaas {
/// </summary>
VaasReport forFile(const std::filesystem::path& filePath) {
const auto sha256 = vaas_internals::calculateSHA256(filePath);
const auto report = forHash(sha256);
auto report = forHash(sha256);
if (report.verdict != VaasReport::Verdict::Unknown) {
return report;
}
Expand Down
41 changes: 36 additions & 5 deletions cpp/vaas_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,55 @@ int main(int argc, char** argv) {
return 0;
}

vaas::OIDCClient initAuthenticator() {
auto dotenv = dotenv::Dotenv();
const auto tokenUrl = dotenv.get("TOKEN_URL");
const auto clientId = dotenv.get("CLIENT_ID");
const auto clientSecret = dotenv.get("CLIENT_SECRET");
return vaas::OIDCClient(tokenUrl, clientId, clientSecret);
}

vaas::Vaas initVaas() {
auto dotenv = dotenv::Dotenv();
auto vaasUrl = dotenv.get("VAAS_URL");
auto tokenUrl = dotenv.get("TOKEN_URL");
auto clientId = dotenv.get("CLIENT_ID");
auto clientSecret = dotenv.get("CLIENT_SECRET");

return vaas::Vaas(vaasUrl, tokenUrl, clientId, clientSecret);
auto authenticator = initAuthenticator();
return vaas::Vaas(vaasUrl, std::move(authenticator));
}

class VaasTestFixture {
protected:
protected:
vaas::Vaas vaas;

VaasTestFixture() : vaas(initVaas()) {
}
};

class AuthenticatorTestFixture {
protected:
vaas::OIDCClient authenticator;

AuthenticatorTestFixture() : authenticator(initAuthenticator()) {
}
};

TEST_CASE_FIXTURE(AuthenticatorTestFixture, "OIDCClient::getAccessToken_withValidCredentials_returnsToken") {
auto token = authenticator.getAccessToken();
CHECK(!token.empty());
}

TEST_CASE("OIDCClient::getAccessToken_withGarbageCredentials_throwsAuthenticationException") {
const auto tokenUrl = std::getenv("TOKEN_URL")
? std::getenv("TOKEN_URL")
: "https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token";
const auto clientId = std::getenv("CLIENT_ID")
? std::getenv("CLIENT_ID")
: "auth-test-client-id";
// Intentionally incorrect credentials
auto authenticator = vaas::OIDCClient(tokenUrl, clientId, "incorrect-client-secret");
CHECK_THROWS_WITH_AS(authenticator.getAccessToken(), "Invalid client or Invalid client credentials", vaas::AuthenticationException);
}

TEST_CASE_FIXTURE(VaasTestFixture, "forFile_withCleanFile_returnsClean") {
auto report = vaas.forFile(program);
CHECK(report.verdict == vaas::VaasReport::Verdict::Clean);
Expand Down

0 comments on commit 3cb8d85

Please sign in to comment.