Skip to content

Commit

Permalink
Remove cpr. Use pure libcurl. (#17)
Browse files Browse the repository at this point in the history
* Remove cpr. Use pure libcurl.

* remove cpr from README.md

* CI: gnumake

* CI: remove gnumake check

* proxy test

* server side etag test

* bump version 3.1.0
  • Loading branch information
kp-cat authored Nov 7, 2023
1 parent feda8e5 commit e2d3f1e
Show file tree
Hide file tree
Showing 11 changed files with 250 additions and 97 deletions.
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ include(CMakePackageConfigHelpers)

option(CONFIGCAT_BUILD_TESTS "Build ConfigCat unittests." ON)

find_package(cpr REQUIRED)
find_package(CURL REQUIRED)
find_package(nlohmann_json REQUIRED)
find_package(unofficial-hash-library REQUIRED)
find_package(semver REQUIRED)
Expand Down Expand Up @@ -62,7 +62,7 @@ file(GLOB SOURCES "src/*")
add_library(configcat ${SOURCES})
add_library(configcat::configcat ALIAS configcat)

set(CONFIGCAT_LIBRARIES ${CONFIGCAT_LIBRARIES} cpr::cpr nlohmann_json::nlohmann_json unofficial::hash-library semver::semver)
set(CONFIGCAT_LIBRARIES ${CONFIGCAT_LIBRARIES} CURL::libcurl nlohmann_json::nlohmann_json unofficial::hash-library semver::semver)

target_link_libraries(configcat
PRIVATE ${CONFIGCAT_LIBRARIES}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,4 @@ Contributions are welcome. For more info please read the [Contribution Guideline
- [Blog](https://configcat.com/blog)
## External Libraries
ConfigCat C++ SDK uses these wonderful libraries: [cpr](https://github.com/libcpr/cpr), [hash-library](https://create.stephan-brumme.com/hash-library), [libcurl](https://curl.se/libcurl), [nlohmann-json](https://github.com/nlohmann/json), [z4kn4fein-semver](https://github.com/z4kn4fein/cpp-semver).
ConfigCat C++ SDK uses these wonderful libraries: [hash-library](https://create.stephan-brumme.com/hash-library), [libcurl](https://curl.se/libcurl), [nlohmann-json](https://github.com/nlohmann/json), [z4kn4fein-semver](https://github.com/z4kn4fein/cpp-semver).
6 changes: 1 addition & 5 deletions include/configcat/configcatoptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,10 @@
#include "flagoverrides.h"
#include "log.h"
#include "evaluationdetails.h"
#include "proxyauthentication.h"

namespace configcat {

struct ProxyAuthentication {
std::string user;
std::string password;
};

// Hooks for events sent by `ConfigCatClient`.
class Hooks {
public:
Expand Down
12 changes: 12 additions & 0 deletions include/configcat/proxyauthentication.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once

#include <string>

namespace configcat {

struct ProxyAuthentication {
std::string user;
std::string password;
};

} // namespace configcat
2 changes: 1 addition & 1 deletion samples/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ CPPFLAGS += -I$(VCPKG_ROOT)/installed/$(TRIPLET)/include
LDFLAGS += -L$(VCPKG_ROOT)/installed/$(TRIPLET)/lib

# Library flags
LDLIBS += -lconfigcat -lcpr -lcurl -lz -lssl -lcrypto -lpthread -lhash-library
LDLIBS += -lconfigcat -lcurl -lz -lssl -lcrypto -lpthread -lhash-library

# Build target
TARGET = example
Expand Down
241 changes: 162 additions & 79 deletions src/configfetcher.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#include "configfetcher.h"

#include <cpr/cpr.h>
#include "configcat/log.h"
#include "configcat/httpsessionadapter.h"
#include "configcat/configcatoptions.h"
Expand All @@ -13,78 +12,90 @@ using namespace std;

namespace configcat {

class SessionInterceptor : public cpr::Interceptor {
public:
SessionInterceptor(shared_ptr<HttpSessionAdapter> httpSessionAdapter):
httpSessionAdapter(httpSessionAdapter) {
class LibCurlResourceGuard {
private:
static std::shared_ptr<LibCurlResourceGuard> instance;
static std::mutex mutex;

LibCurlResourceGuard() {
curl_global_init(CURL_GLOBAL_DEFAULT);
}

virtual cpr::Response intercept(cpr::Session& session) {
configcat::Response adapterResponse = httpSessionAdapter->get(session.GetFullRequestUrl(), header);
header.clear();
cpr::Response response;
response.status_code = adapterResponse.status_code;
response.text = adapterResponse.text;
for (auto keyValue : adapterResponse.header) {
response.header.insert({keyValue.first, keyValue.second});
struct Deleter {
void operator()(LibCurlResourceGuard* libCurlResourceGuard) {
curl_global_cleanup();
delete libCurlResourceGuard;
LibCurlResourceGuard::instance.reset();
}
return response;
}
};

void setHeader(const cpr::Header& requestHeader) {
for (auto keyValue : requestHeader) {
header.insert({keyValue.first, keyValue.second});
public:
// Prevent copying
LibCurlResourceGuard(const LibCurlResourceGuard&) = delete;
LibCurlResourceGuard& operator=(const LibCurlResourceGuard&) = delete;

static std::shared_ptr<LibCurlResourceGuard> getInstance() {
std::lock_guard<std::mutex> lock(mutex);
if (!instance) {
instance = std::shared_ptr<LibCurlResourceGuard>(new LibCurlResourceGuard(), Deleter());
}
return instance;
}
};
std::shared_ptr<LibCurlResourceGuard> LibCurlResourceGuard::instance = nullptr;
std::mutex LibCurlResourceGuard::mutex;

void close() {
httpSessionAdapter->close();
}
int ProgressCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
return static_cast<ConfigFetcher*>(clientp)->ProgressFunction(dltotal, dlnow, ultotal, ulnow);
}

private:
shared_ptr<HttpSessionAdapter> httpSessionAdapter;
std::map<std::string, std::string> header;
};
int ConfigFetcher::ProgressFunction(curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
return closed ? 1 : 0; // Return 0 to continue, or 1 to abort
}

ConfigFetcher::ConfigFetcher(const string& sdkKey, shared_ptr<ConfigCatLogger> logger, const string& mode, const ConfigCatOptions& options):
sdkKey(sdkKey),
logger(logger),
mode(mode),
connectTimeoutMs(options.connectTimeoutMs),
readTimeoutMs(options.readTimeoutMs) {
if (options.httpSessionAdapter) {
sessionInterceptor = make_shared<SessionInterceptor>(options.httpSessionAdapter);
}
session = make_unique<cpr::Session>();
session->SetConnectTimeout(connectTimeoutMs);
session->SetTimeout(readTimeoutMs);
session->SetProxies(options.proxies);
std::map<std::string, cpr::EncodedAuthentication> proxyAuthentications;
for (auto& keyValue : options.proxyAuthentications) {
auto& authentications = keyValue.second;
proxyAuthentications.insert({keyValue.first, {authentications.user, authentications.password}});
}
session->SetProxyAuth(proxyAuthentications);
session->SetProgressCallback(cpr::ProgressCallback{
[&](size_t /*downloadTotal*/, size_t /*downloadNow*/, size_t /*uploadTotal*/, size_t /*uploadNow*/,
intptr_t /*userdata*/) -> bool {
return !closed;
}
});
readTimeoutMs(options.readTimeoutMs),
proxies(options.proxies),
proxyAuthentications(options.proxyAuthentications),
httpSessionAdapter(options.httpSessionAdapter) {
urlIsCustom = !options.baseUrl.empty();
url = urlIsCustom
? options.baseUrl
: options.dataGovernance == DataGovernance::Global
? kGlobalBaseUrl
: kEuOnlyBaseUrl;
userAgent = string("ConfigCat-Cpp/") + mode + "-" + CONFIGCAT_VERSION;

libCurlResourceGuard = LibCurlResourceGuard::getInstance();
curl = curl_easy_init();
if (!curl) {
LOG_ERROR(0) << "Cannot initialize CURL.";
return;
}

// Timeout setup
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, connectTimeoutMs);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, readTimeoutMs);

// Enable the progress callback function to be able to abort the request
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallback);
curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this);
}

ConfigFetcher::~ConfigFetcher() {
if (curl) {
curl_easy_cleanup(curl);
}
}

void ConfigFetcher::close() {
if (sessionInterceptor) {
sessionInterceptor->close();
if (httpSessionAdapter) {
httpSessionAdapter->close();
}
closed = true;
}
Expand Down Expand Up @@ -136,49 +147,122 @@ FetchResponse ConfigFetcher::executeFetch(const std::string& eTag, int executeCo
return response;
}

FetchResponse ConfigFetcher::fetch(const std::string& eTag) {
cpr::Header header = {
{kUserAgentHeaderName, string("ConfigCat-Cpp/") + mode + "-" + CONFIGCAT_VERSION},
{kPlatformHeaderName, getPlatformName()}
};
if (!eTag.empty()) {
header.insert({kIfNoneMatchHeaderName, eTag});
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, std::string *userp) {
userp->append((char*)contents, size * nmemb);
return size * nmemb;
}

std::map<std::string, std::string> ParseHeader(const std::string& headerString) {
std::map<std::string, std::string> header;

std::vector<std::string> lines;
std::istringstream stream(headerString);
std::string line;
while (std::getline(stream, line, '\n')) {
lines.push_back(line);
}
session->SetHeader(header);
session->SetUrl(url + "/configuration-files/" + sdkKey + "/" + kConfigJsonName);

// The session's interceptor will be unregistered after the cpr::Session::Get() call.
// We always register it before the call.
if (sessionInterceptor) {
sessionInterceptor->setHeader(header);
session->AddInterceptor(sessionInterceptor);

for (std::string& line : lines) {
if (line.length() > 0) {
const size_t colonIndex = line.find(':');
if (colonIndex != std::string::npos) {
std::string value = line.substr(colonIndex + 1);
value.erase(0, value.find_first_not_of("\t "));
value.resize(std::min<size_t>(value.size(), value.find_last_not_of("\t\n\r ") + 1));
header[line.substr(0, colonIndex)] = value;
}
}
}

auto response = session->Get();
return header;
}

FetchResponse ConfigFetcher::fetch(const std::string& eTag) {
string requestUrl(url + "/configuration-files/" + sdkKey + "/" + kConfigJsonName);
std::map<std::string, std::string> responseHeaders;
std::string readBuffer;
long response_code = 0;

if (!httpSessionAdapter) {
if (!curl) {
LogEntry logEntry = LogEntry(logger, LOG_LEVEL_ERROR, 1103) <<
"Unexpected error occurred while trying to fetch config JSON: CURL is not initialized.";
return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), true);
}

if (response.error.code != cpr::ErrorCode::OK) {
LogEntry logEntry = (response.error.code == cpr::ErrorCode::OPERATION_TIMEDOUT)
? LogEntry(logger, LOG_LEVEL_ERROR, 1102) <<
"Request timed out while trying to fetch config JSON. "
"Timeout values: [connect: " << connectTimeoutMs << "ms, read: " << readTimeoutMs << "ms]"
: LogEntry(logger, LOG_LEVEL_ERROR, 1103) <<
"Unexpected error occurred while trying to fetch config JSON: " << response.error.message;
CURLcode res;
std::string headerString;

return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), true);
// Update header
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, (string(kUserAgentHeaderName) + ": " + userAgent).c_str());
headers = curl_slist_append(headers, (string(kPlatformHeaderName) + ": " + getPlatformName()).c_str());
if (!eTag.empty()) {
headers = curl_slist_append(headers, (string(kIfNoneMatchHeaderName) + ": " + eTag).c_str());
}

curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_URL, requestUrl.c_str());

// Set the callback function to receive the response
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headerString);

// Proxy setup
const std::string protocol = url.substr(0, url.find(':'));
if (proxies.count(protocol) > 0) {
curl_easy_setopt(curl, CURLOPT_PROXY, proxies[protocol].c_str());
if (proxyAuthentications.count(protocol) > 0) {
curl_easy_setopt(curl, CURLOPT_PROXYUSERNAME, proxyAuthentications[protocol].user.c_str());
curl_easy_setopt(curl, CURLOPT_PROXYPASSWORD, proxyAuthentications[protocol].password.c_str());
}
}

// Perform the request
res = curl_easy_perform(curl);

if (res != CURLE_OK) {
LogEntry logEntry = (res == CURLE_OPERATION_TIMEDOUT)
? LogEntry(logger, LOG_LEVEL_ERROR, 1102) <<
"Request timed out while trying to fetch config JSON. "
"Timeout values: [connect: " << connectTimeoutMs << "ms, read: " << readTimeoutMs << "ms]"
: LogEntry(logger, LOG_LEVEL_ERROR, 1103) <<
"Unexpected error occurred while trying to fetch config JSON: " << curl_easy_strerror(res);

return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), true);
}

curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);

// Parse the headers from headerString
responseHeaders = ParseHeader(headerString);
} else {
std::map<std::string, std::string> requestHeader = {
{kUserAgentHeaderName, userAgent},
{kPlatformHeaderName, getPlatformName()}
};
if (!eTag.empty()) {
requestHeader.insert({kIfNoneMatchHeaderName, eTag});
}
auto response = httpSessionAdapter->get(requestUrl, requestHeader);
response_code = response.status_code;
readBuffer = response.text;
responseHeaders = response.header;
}

switch (response.status_code) {
switch (response_code) {
case 200:
case 201:
case 202:
case 203:
case 204: {
const auto it = response.header.find(kEtagHeaderName);
string eTag = it != response.header.end() ? it->second : "";
const auto it = responseHeaders.find(kEtagHeaderName);
string eTag = it != responseHeaders.end() ? it->second : "";
try {
auto config = Config::fromJson(response.text);
auto config = Config::fromJson(readBuffer);
LOG_DEBUG << "Fetch was successful: new config fetched.";
return FetchResponse(fetched, make_shared<ConfigEntry>(config, eTag, response.text, getUtcNowSecondsSinceEpoch()));
return FetchResponse(fetched, make_shared<ConfigEntry>(config, eTag, readBuffer, getUtcNowSecondsSinceEpoch()));
} catch (exception& exception) {
LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1105);
logEntry <<
Expand All @@ -192,19 +276,18 @@ FetchResponse ConfigFetcher::fetch(const std::string& eTag) {
LOG_DEBUG << "Fetch was successful: config not modified.";
return FetchResponse({notModified, ConfigEntry::empty});


case 403:
case 404: {
LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1100);
logEntry <<
"Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. "
"Received unexpected response: " << response.status_code;
"Received unexpected response: " << response_code;
return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), false);
}

default: {
LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1101);
logEntry << "Unexpected HTTP response was received while trying to fetch config JSON: " << response.status_code;
logEntry << "Unexpected HTTP response was received while trying to fetch config JSON: " << response_code;
return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), true);
}
}
Expand Down
Loading

0 comments on commit e2d3f1e

Please sign in to comment.