First release of open-appsec source code

This commit is contained in:
roybarda
2022-10-26 19:33:19 +03:00
parent 3883109caf
commit a883352f79
1353 changed files with 276290 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
ADD_DEFINITIONS(-Wno-deprecated-declarations -Dalpine)
add_library(orchestration_downloader curl_client.cc downloader.cc http_client.cc https_client.cc)
add_subdirectory(downloader_ut)

View File

@@ -0,0 +1,438 @@
// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "curl_client.h"
#include <curl/curl.h>
#if defined(alpine)
#include <openssl/ossl_typ.h>
#else
#include <openssl/types.h>
#endif //ifdef alpine
#include <openssl/x509.h>
#include <openssl/x509_vfy.h>
#include <openssl/ssl.h>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <random>
#include <memory>
#include <algorithm>
#include "config.h"
#include "url_parser.h"
#include "debug.h"
#include "sasal.h"
#include "scope_exit.h"
USE_DEBUG_FLAG(D_HTTP_REQUEST);
using namespace std;
SASAL_START // Orchestration - Communication
// LCOV_EXCL_START Reason: Depends on real download server.
class CurlGlobalInit
{
public:
CurlGlobalInit() { curl_global_init(CURL_GLOBAL_DEFAULT); }
~CurlGlobalInit() { curl_global_cleanup(); }
};
static CurlGlobalInit global_curl_handle;
HttpCurl::HttpCurl(
const URLParser &_url,
ofstream &_out_file,
const string &_bearer,
const Maybe<string> &proxy_url,
const Maybe<uint16_t> &proxy_port,
const Maybe<string> &proxy_auth)
:
url(_url),
out_file(_out_file),
bearer(_bearer),
curl(unique_ptr<CURL, function<void(CURL *)>>(curl_easy_init(), curl_easy_cleanup))
{
string port = url.getPort();
if (!port.empty())
{
curl_url = url.getBaseURL().unpack() + ":" + port + url.getQuery();
} else
{
curl_url = url.getBaseURL().unpack() + url.getQuery();
}
if (proxy_url.ok())
{
//Update curl proxy
if (!proxy_port.ok())
{
dbgWarning(D_HTTP_REQUEST)
<< "Invalid proxy port, CURL default port will be used instead. Error: "
<< proxy_port.getErr();
proxy = proxy_url.unpack();
} else
{
proxy = proxy_url.unpack() + ":" + to_string(proxy_port.unpack());
}
}
if (proxy_auth.ok())
{
I_Encryptor *encryptor = Singleton::Consume<I_Encryptor>::by<HttpCurl>();
proxy_credentials = "Proxy-Authorization: Basic " + encryptor->base64Encode(proxy_auth.unpack());
}
}
HttpCurl::HttpCurl(const HttpCurl &other)
:
url(other.url),
out_file(other.out_file),
bearer(other.bearer),
proxy(other.proxy),
proxy_credentials(other.proxy_credentials),
curl(unique_ptr<CURL, function<void(CURL *)>>(curl_easy_init(), curl_easy_cleanup))
{
}
void
HttpCurl::setCurlOpts(long timeout, HTTP_VERSION http_version)
{
struct curl_slist *headers = NULL;
struct curl_slist *proxy_headers = NULL;
CURL *curl_handle = curl.get();
//HTTP options
curl_easy_setopt(curl_handle, CURLOPT_HTTP_VERSION, http_version);
//SSL options
curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L);
//Header options
curl_easy_setopt(curl_handle, CURLOPT_URL, curl_url.c_str());
curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, writeResponseCallback);
curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &out_file);
curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, timeout);
headers = curl_slist_append(headers, "Accept: */*");
string auth = string("Authorization: Bearer ") + bearer;
headers = curl_slist_append(headers, auth.c_str());
headers = curl_slist_append(headers, "User-Agent: Infinity Next (a7030abf93a4c13)");
string uuid_header = string("X-Trace-Id: ") + TraceIdGenerator::generateTraceId();
headers = curl_slist_append(headers, "Connection: close");
headers = curl_slist_append(headers, uuid_header.c_str());
//Proxy options
if (!proxy.empty())
{
curl_easy_setopt(curl_handle, CURLOPT_PROXY, proxy.c_str());
if (!proxy_credentials.empty())
{
proxy_headers = curl_slist_append(proxy_headers, proxy_credentials.c_str());
//Apply proxy headers
curl_easy_setopt(curl_handle, CURLOPT_PROXYHEADER, proxy_headers);
}
dbgTrace(D_HTTP_REQUEST) << "Using Proxy: " << proxy;
}
//Apply headers
curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, headers);
}
bool
HttpCurl::connect()
{
// Response information.
long http_code;
char errorstr[CURL_ERROR_SIZE];
CURLcode res;
stringstream response_header;
CURL *curl_handle = curl.get();
auto __scope_exit = make_scope_exit(
[this] () {
out_file.flush();
out_file.close();
}
);
//Debug options
curl_easy_setopt(curl_handle, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curl_handle, CURLOPT_DEBUGFUNCTION, trace_http_request);
curl_easy_setopt(curl_handle, CURLOPT_DEBUGDATA, static_cast<void*>(&response_header));
curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, errorstr);
// Perform the request, res will get the return code
res = curl_easy_perform(curl_handle);
if (res != CURLE_OK) {
dbgWarning(D_HTTP_REQUEST) << "Failed to perform CURL request. CURL error " << string(errorstr);
dbgWarning(D_HTTP_REQUEST) << "CURL result " + string(curl_easy_strerror(res));
print_response_header(response_header);
return false;
}
curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code != 200){
dbgWarning(D_HTTP_REQUEST) << "Failed to connect. Error code: " + to_string(http_code);
print_response_header(response_header);
return false;
}
dbgTrace(D_HTTP_REQUEST) << "CURL HTTP request successfully completed.";
return true;
}
int
HttpCurl::trace_http_request(
CURL *,
curl_infotype type,
char *data,
size_t,
void *opq)
{
stringstream *response_header = static_cast<stringstream *>(opq);
switch (type)
{
case CURLINFO_HEADER_OUT:
dbgTrace(D_HTTP_REQUEST)
<< "=> Sending the following HTTP request:\n"
<< string(data);
break;
case CURLINFO_HEADER_IN:
if (!response_header)
{
// Should never reach this if. But just in case.
// The data will be printed at chunks in this case
dbgError(D_HTTP_REQUEST)
<< "<= Received the following HTTP response header (should not reach here):\n"
<< string(data);
} else
{
// The response header Will be printed at once later after curl_easy_perform.
// And after assembling all the response header chunks.
*response_header << string(data);
}
break;
default:
return 0;
}
return 0;
}
u_int
HttpCurl::writeResponseCallback(
const char *in_buf,
uint num_of_messages,
uint size_of_data,
ostream out_stream)
{
const unsigned long total_bytes(num_of_messages * size_of_data);
out_stream.write(in_buf, total_bytes);
return total_bytes;
}
void
HttpCurl::print_response_header(stringstream &stream)
{
string line;
istringstream header_stream(stream.str());
stringstream header_lines;
int lines_to_print = 10;
int i = 0;
while (getline(header_stream, line) && i < lines_to_print) {
header_lines << line << '\n';
++i;
}
dbgWarning(D_HTTP_REQUEST)
<< "<= Received the following HTTP response header:\n"
<< header_lines.str();
}
HttpsCurl::HttpsCurl(
const URLParser &_url,
ofstream &_out_file,
const string &_bearer,
const Maybe<string> &proxy_url,
const Maybe<uint16_t> &proxy_port,
const Maybe<string> &proxy_auth,
const string &_ca_path) :
HttpCurl(_url, _out_file, _bearer, proxy_url, proxy_port, proxy_auth),
ca_path(_ca_path) {}
HttpsCurl::HttpsCurl(const HttpsCurl &other) :
HttpCurl(other),
ca_path(other.ca_path) {}
void
HttpsCurl::setCurlOpts(long timeout, HTTP_VERSION http_version)
{
struct curl_slist *headers = NULL;
struct curl_slist *proxy_headers = NULL;
CURL *curl_handle = curl.get();
URLProtocol protocol = url.getProtocol();
if (protocol == URLProtocol::HTTPS)
{
if (curl_url.find("https://") == string::npos)
{
//Append https://
curl_url = "https://" + curl_url;
}
}
//HTTP options
curl_easy_setopt(curl_handle, CURLOPT_HTTP_VERSION, http_version);
//SSL options
if (getProfileAgentSettingWithDefault<bool>(
false,
"agent.config.message.ignoreSslValidation") == false)
{
curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl_handle, CURLOPT_SSL_CTX_FUNCTION, ssl_ctx_verify_certificate);
curl_easy_setopt(curl_handle, CURLOPT_SSL_CTX_DATA, static_cast<void*>(&bearer));
} else
{
curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L);
dbgWarning(D_HTTP_REQUEST) << "Ignoring SSL validation";
}
//Header options
curl_easy_setopt(curl_handle, CURLOPT_URL, curl_url.c_str());
curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, writeResponseCallback);
curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &out_file);
curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, timeout);
curl_easy_setopt(curl_handle, CURLOPT_CAINFO, ca_path.c_str());
headers = curl_slist_append(headers, "Accept: */*");
string auth = string("Authorization: Bearer ") + bearer;
headers = curl_slist_append(headers, auth.c_str());
headers = curl_slist_append(headers, "User-Agent: Infinity Next (a7030abf93a4c13)");
string uuid_header = string("X-Trace-Id: ") + TraceIdGenerator::generateTraceId();
headers = curl_slist_append(headers, "Connection: close");
headers = curl_slist_append(headers, uuid_header.c_str());
// Proxy options
if (!proxy.empty())
{
curl_easy_setopt(curl_handle, CURLOPT_PROXY, proxy.c_str());
if (!proxy_credentials.empty())
{
proxy_headers = curl_slist_append(proxy_headers, proxy_credentials.c_str());
//Apply proxy headers
curl_easy_setopt(curl_handle, CURLOPT_PROXYHEADER, proxy_headers);
}
dbgTrace(D_HTTP_REQUEST) << "Using Proxy : " << proxy;
}
//Apply headers
curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, headers);
}
int
HttpsCurl::verify_certificate(int preverify_ok, X509_STORE_CTX *ctx)
{
switch (X509_STORE_CTX_get_error(ctx))
{
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
dbgWarning(D_HTTP_REQUEST) << "SSL verification error: X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT";
break;
case X509_V_ERR_CERT_NOT_YET_VALID:
case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
dbgWarning(D_HTTP_REQUEST) << "SSL verification error: Certificate not yet valid";
break;
case X509_V_ERR_CERT_HAS_EXPIRED:
case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
dbgWarning(D_HTTP_REQUEST) << "Certificate expired";
break;
case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN:
dbgDebug(D_HTTP_REQUEST) << "Self signed certificate in chain";
if (getConfigurationWithDefault(false, "orchestration", "Self signed certificates acceptable")) {
preverify_ok = true;
}
break;
default:
if (!preverify_ok) {
dbgWarning(D_HTTP_REQUEST)
<< "Certificate verification error number: "
<< X509_STORE_CTX_get_error(ctx);
}
break;
}
return preverify_ok;
}
CURLcode
HttpsCurl::ssl_ctx_verify_certificate(CURL *, void *sslctx, void *opq)
{
SSL_CTX *ctx = (SSL_CTX *) sslctx;
string *token_ptr = static_cast<string*>(opq);
if(!token_ptr)
{
dbgWarning(D_HTTP_REQUEST) << "Invalid token (bearer) was received";
return CURLE_BAD_FUNCTION_ARGUMENT;
}
string token = *token_ptr;
if (token.empty())
{
return CURLE_OK;
}
SSL_CTX_set_verify(
ctx,
SSL_VERIFY_FAIL_IF_NO_PEER_CERT | SSL_VERIFY_PEER,
verify_certificate
);
return CURLE_OK;
}
string
TraceIdGenerator::generateRandomString(uint length)
{
string dst(length, 0);
static thread_local mt19937 range(random_device{}());
auto randchar = [&]() -> char
{
static const char charset[] = "0123456789abcdefghijklmnopqrstuvwxyz";
static const size_t size = (sizeof(charset) - 1);
return charset[ range() % size ];
};
generate_n(dst.begin(), length, randchar);
return dst;
}
string
TraceIdGenerator::generateTraceId()
{
string part1 = generateRandomString(8);
string part2 = generateRandomString(4);
string part3 = generateRandomString(4);
string part4 = generateRandomString(4);
string part5 = generateRandomString(12);
return string(part1 + "-" + part2 + "-" + part3 + "-" + part4 + "-" + part5);
}
SASAL_END

View File

@@ -0,0 +1,115 @@
// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <curl/curl.h>
#if defined(alpine)
#include <openssl/ossl_typ.h>
#else
#include <openssl/types.h>
#endif //ifdef alpine
#include <string>
#include <memory>
#include <fstream>
#include <iostream>
#include <ostream>
#include "i_encryptor.h"
#include "scope_exit.h"
#include "url_parser.h"
#include "sasal.h"
USE_DEBUG_FLAG(D_HTTP_REQUEST);
SASAL_START // Orchestration - Communication
// LCOV_EXCL_START Reason: Depends on real download server.
enum class HTTP_VERSION
{
HTTP_VERSION_NONE = CURL_HTTP_VERSION_NONE, //libcurl will use whatever it thinks fit.
HTTP_VERSION_1_0 = CURL_HTTP_VERSION_1_0,
HTTP_VERSION_1_1 = CURL_HTTP_VERSION_1_1,
HTTP_VERSION_2_0 = CURL_HTTP_VERSION_2_0
};
class TraceIdGenerator
{
public:
static std::string generateTraceId();
private:
static std::string generateRandomString(uint length);
};
class HttpCurl : public Singleton::Consume<I_Encryptor>
{
public:
HttpCurl(
const URLParser &_url,
std::ofstream &_out_file,
const std::string &_bearer,
const Maybe<std::string> &proxy_url,
const Maybe<uint16_t> &proxy_port,
const Maybe<std::string> &proxy_auth);
HttpCurl(const HttpCurl &other);
virtual void setCurlOpts(long timeout = 60L, HTTP_VERSION http_version = HTTP_VERSION::HTTP_VERSION_1_1);
virtual bool connect();
protected:
static int trace_http_request(
CURL *handle,
curl_infotype type,
char *data,
size_t size,
void *userptr);
static u_int writeResponseCallback(
const char *in_buf,
uint num_of_messages,
uint size_of_data,
std::ostream out_stream);
void print_response_header(std::stringstream &stream);
const URLParser& url;
std::ofstream &out_file;
std::string bearer;
std::string proxy;
std::string proxy_credentials;
std::unique_ptr<CURL, std::function<void(CURL *)>> curl;
std::string curl_url;
};
class HttpsCurl : public HttpCurl
{
public:
HttpsCurl(
const URLParser &_url,
std::ofstream &_out_file,
const std::string &_bearer,
const Maybe<std::string> &proxy_url,
const Maybe<uint16_t> &proxy_port,
const Maybe<std::string> &proxy_auth,
const std::string &_ca_path);
HttpsCurl(const HttpsCurl& other);
static CURLcode ssl_ctx_verify_certificate(CURL *curl, void *ssl_ctx, void *opq);
static int verify_certificate(int preverify_ok, X509_STORE_CTX *ctx);
void setCurlOpts(long timeout = 60L, HTTP_VERSION http_version = HTTP_VERSION::HTTP_VERSION_1_1) override;
private:
std::string ca_path;
};
SASAL_END

View File

@@ -0,0 +1,387 @@
// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "downloader.h"
#include "i_orchestration_tools.h"
#include "singleton.h"
#include "http_client.h"
#include "debug.h"
#include "config.h"
#include "rest.h"
#include "sasal.h"
#include "cereal/external/rapidjson/document.h"
#include <fstream>
using namespace std;
using namespace rapidjson;
SASAL_START // Orchestration - Communication
USE_DEBUG_FLAG(D_ORCHESTRATOR);
class Downloader::Impl : Singleton::Provide<I_Downloader>::From<Downloader>
{
public:
void init();
Maybe<string> downloadFileFromFog(
const string &checksum,
Package::ChecksumTypes checksum_type,
const GetResourceFile &resourse_file
) const override;
Maybe<map<string, string>> downloadVirtualFileFromFog(
const GetResourceFile &resourse_file,
Package::ChecksumTypes checksum_type
) const override;
Maybe<string> downloadFileFromURL(
const string &url,
const string &checksum,
Package::ChecksumTypes checksum_type,
const string &service_name
) const override;
private:
Maybe<string> downloadFileFromFogByHTTP(
const GetResourceFile &resourse_file,
const string &file_name
) const;
Maybe<string> validateChecksum(
const string &checksum,
Package::ChecksumTypes checksum_type,
Maybe<string> &file_path
) const;
Maybe<string> getFileFromExternalURL(
const URLParser &url,
const string &file_name,
bool auth_required
) const;
Maybe<string> getFileFromLocal(const string &local_file_path, const string &file_name) const;
Maybe<string> getFileFromURL(const URLParser &url, const string &file_name, bool auth_required) const;
tuple<string, string> splitQuery(const string &query) const;
string vectorToPath(const vector<string> &vec) const;
string dir_path;
};
void
Downloader::Impl::init()
{
dir_path = getConfigurationWithDefault<string>(
"/tmp/orchestration_downloads",
"orchestration",
"Default file download path"
);
Singleton::Consume<I_OrchestrationTools>::by<Downloader>()->createDirectory(dir_path);
}
Maybe<string>
Downloader::Impl::downloadFileFromFog(
const string &checksum,
Package::ChecksumTypes checksum_type,
const GetResourceFile &resourse_file) const
{
auto file_path = downloadFileFromFogByHTTP(resourse_file, resourse_file.getFileName() + ".download");
if (!file_path.ok()) {
return file_path;
}
auto checksum_validation = validateChecksum(checksum, checksum_type, file_path);
if (!checksum_validation.ok()) return checksum_validation;
I_OrchestrationTools *orchestration_tools = Singleton::Consume<I_OrchestrationTools>::by<Downloader>();
if (!orchestration_tools->isNonEmptyFile(file_path.unpack())) {
return genError("Failed to download file " + resourse_file.getFileName());
}
return file_path;
}
Maybe<map<string, string>>
Downloader::Impl::downloadVirtualFileFromFog(
const GetResourceFile &resourse_file,
Package::ChecksumTypes) const
{
static const string tenand_id_key = "tenantId";
static const string policy_key = "policy";
static const string settings_key = "settings";
static const string tenants_key = "tenants";
static const string error_text = "error";
map<string, string> res;
I_UpdateCommunication *update_communication = Singleton::Consume<I_UpdateCommunication>::by<Downloader>();
auto downloaded_data = update_communication->downloadAttributeFile(resourse_file);
if (!downloaded_data.ok()) return downloaded_data.passErr();
Document document;
document.Parse(downloaded_data.unpack().c_str());
if (document.HasParseError()) return genError("JSON file is not valid.");
const Value &tenants_data = document[tenants_key.c_str()];
for (Value::ConstValueIterator itr = tenants_data.Begin(); itr != tenants_data.End(); ++itr) {
auto tenant_id_obj = itr->FindMember(tenand_id_key.c_str());
if (tenant_id_obj == itr->MemberEnd()) continue;
string tenant_id = tenant_id_obj->value.GetString();
Value::ConstMemberIterator artifact_data = itr->FindMember(policy_key.c_str());
if (artifact_data == itr->MemberEnd()) artifact_data = itr->FindMember(settings_key.c_str());
if (artifact_data != itr->MemberEnd()) {
string file_path = dir_path + "/" + resourse_file.getFileName() + "_" + tenant_id + ".download";
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
artifact_data->value.Accept(writer);
I_OrchestrationTools *orchestration_tools = Singleton::Consume<I_OrchestrationTools>::by<Downloader>();
if (orchestration_tools->writeFile(buffer.GetString(), file_path)) {
res.insert({tenant_id, file_path});
}
continue;
}
Value::ConstMemberIterator error_data = itr->FindMember(error_text.c_str());
if (error_data != itr->MemberEnd()) {
dbgDebug(D_ORCHESTRATOR)
<< "Failed to download artifact"
<< ", Tenant ID: " << tenant_id
<< ", Error message: " << error_data->value.FindMember("message")->value.GetString()
<< ", Error ID: " << error_data->value.FindMember("messageId")->value.GetString();
continue;
}
}
return res;
}
Maybe<string>
Downloader::Impl::downloadFileFromURL(
const string &url,
const string &checksum,
Package::ChecksumTypes checksum_type,
const string &service_name) const
{
dbgDebug(D_ORCHESTRATOR) << "Download file. URL: " << url;
string new_url = url;
bool auth_required = false;
auto custom_url = getConfiguration<string>("orchestration", "Custom download url");
if (custom_url.ok()) {
auto resource_index = url.find_last_of("/");
string error_msg = "Failed to parse custom URL. ";
if (resource_index == string::npos) {
return genError(error_msg + "URL: " + url);
}
new_url = custom_url.unpack();
if (new_url.empty()) {
return genError(error_msg + "URL is empty");
}
if (new_url.back() == '/') {
new_url = new_url.substr(0, new_url.size() - 1);
}
new_url.append(url.substr(resource_index));
}
// Workaround - only in staging we need to add the auth header
static const string jwt_word = "<JWT>";
if (new_url.find(jwt_word) != string::npos) {
new_url = new_url.substr(jwt_word.length());
auth_required = true;
}
URLParser parsed_url(new_url);
Maybe<string> base_url = parsed_url.getBaseURL();
if (!base_url.ok()) return base_url;
Maybe<string> file_path = genError("Empty path");
string file_name = service_name + ".download";
if (parsed_url.getProtocol() == URLProtocol::LOCAL_FILE) {
file_path = getFileFromLocal(base_url.unpack(), file_name);
} else {
file_path = getFileFromExternalURL(parsed_url, file_name, auth_required);
}
if (!file_path.ok()) {
return file_path;
}
auto checksum_validation = validateChecksum(checksum, checksum_type, file_path);
if (!checksum_validation.ok()) return checksum_validation;
I_OrchestrationTools *orchestration_tools = Singleton::Consume<I_OrchestrationTools>::by<Downloader>();
if (!orchestration_tools->isNonEmptyFile(file_path.unpack())) {
return genError("Failed to download file. URL: " + parsed_url.toString());
}
return file_path;
}
Maybe<string>
Downloader::Impl::validateChecksum(
const string &checksum,
Package::ChecksumTypes checksum_type,
Maybe<string> &file_path) const
{
if (file_path.ok()) {
I_OrchestrationTools *orchestration_tools = Singleton::Consume<I_OrchestrationTools>::by<Downloader>();
Maybe<string> file_checksum = orchestration_tools->calculateChecksum(checksum_type, file_path.unpack());
if (!file_checksum.ok() || checksum != file_checksum.unpack()) {
orchestration_tools->removeFile(file_path.unpack());
if (!file_checksum.ok()) {
return genError("Failed to calculate file checksum, with error: " + file_checksum.getErr());
}
return genError(
"The checksum calculation is not as the expected, " +
checksum + " != " + file_checksum.unpack()
);
}
}
return file_path;
}
Maybe<string>
Downloader::Impl::downloadFileFromFogByHTTP(const GetResourceFile &resourse_file, const string &file_name) const
{
string file_path = dir_path + "/" + file_name;
dbgInfo(D_ORCHESTRATOR) << "Downloading file from fog. File: " << resourse_file.getFileName();
I_UpdateCommunication *update_communication = Singleton::Consume<I_UpdateCommunication>::by<Downloader>();
auto downloaded_file = update_communication->downloadAttributeFile(resourse_file);
if (!downloaded_file.ok()) return genError(downloaded_file.getErr());
dbgInfo(D_ORCHESTRATOR) << "Download completed. File: " << resourse_file.getFileName();
I_OrchestrationTools *orchestration_tools = Singleton::Consume<I_OrchestrationTools>::by<Downloader>();
if (orchestration_tools->writeFile(downloaded_file.unpack(), file_path)) return file_path;
return genError("Failed to write the attribute file. File: " + file_name);
}
Maybe<string>
Downloader::Impl::getFileFromLocal(const string &local_file_path, const string &file_name) const
{
string file_path = dir_path + "/" + file_name;
I_OrchestrationTools *orchestration_tools = Singleton::Consume<I_OrchestrationTools>::by<Downloader>();
if (!orchestration_tools->copyFile(local_file_path, file_path)) {
return genError("Get file from local failed. File: " + local_file_path);
}
return file_path;
}
// LCOV_EXCL_START Reason: Depends on real download server.
Maybe<string>
Downloader::Impl::getFileFromURL(const URLParser &url, const string &file_path, bool auth_required) const
{
ofstream outFile(file_path, ofstream::out | ofstream::binary);
HTTPClient http_client;
dbgInfo(D_ORCHESTRATOR) << "Downloading file. URL: " << url;
auto get_file_response = http_client.getFile(url, outFile, auth_required);
if (!get_file_response.ok()) {
Maybe<string> error = genError("Failed to download file from " + url.getBaseURL().unpack() +
". Error: " + get_file_response.getErr());
dbgWarning(D_ORCHESTRATOR) << "Download failed";
return error;
}
outFile.close();
dbgInfo(D_ORCHESTRATOR) << "Download completed. URL: " << url;
return file_path;
}
Maybe<string>
Downloader::Impl::getFileFromExternalURL(
const URLParser &parsed_url,
const string &file_name,
bool auth_required
) const
{
string file_path = dir_path + "/" + file_name;
auto base_url = parsed_url.getBaseURL().unpack();
string query_path;
string query_file;
tie(query_path, query_file) = splitQuery(parsed_url.getQuery());
auto try_dirs = getConfigurationWithDefault<bool>(
false,
"orchestration",
"Add tenant suffix"
);
if (try_dirs) {
vector<string> sub_path;
auto agent_details = Singleton::Consume<I_AgentDetails>::by<Downloader>();
auto tenant_id = agent_details->getTenantId();
if (!tenant_id.empty()) {
sub_path.push_back(tenant_id);
auto profile_id = agent_details->getProfileId();
if (!profile_id.empty()) {
sub_path.push_back(profile_id);
auto agent_id = agent_details->getAgentId();
if(!agent_id.empty()) {
sub_path.push_back(agent_id);
}
}
}
URLParser currentUrl = parsed_url;
while (!sub_path.empty()) {
currentUrl.setQuery(query_path + vectorToPath(sub_path) + "/" + query_file);
if (getFileFromURL(currentUrl, file_path, auth_required).ok()) return file_path;
sub_path.pop_back();
}
}
return getFileFromURL(parsed_url, file_path, auth_required);
}
tuple<string, string>
Downloader::Impl::splitQuery(const string &query) const
{
size_t index = query.find_last_of("/");
if (index == string::npos) return make_tuple(string(), query);
return make_tuple(query.substr(0, index), query.substr(index + 1));
}
string
Downloader::Impl::vectorToPath(const vector<string> &vec) const
{
string s;
for (const auto &piece : vec) { s += ("/" + piece); }
return s;
}
Downloader::Downloader() : Component("Downloader"), pimpl(make_unique<Impl>()) {}
Downloader::~Downloader() {}
void
Downloader::init()
{
pimpl->init();
}
void
Downloader::preload()
{
registerExpectedConfiguration<string>("orchestration", "Custom download url");
registerExpectedConfiguration<string>("orchestration", "Default file download path");
registerExpectedConfiguration<string>("orchestration", "Self signed certificates acceptable");
registerExpectedConfiguration<bool>("orchestration", "Add tenant suffix");
}
SASAL_END

View File

@@ -0,0 +1,7 @@
link_directories(${BOOST_ROOT}/lib)
add_unit_test(
downloader_ut
"downloader_ut.cc"
"orchestration;orchestration_downloader;orchestration_modules;orchestration_tools;environment;config;update_communication;metric;event_is;-lcurl;-lcrypto;-lssl;-lboost_regex"
)

View File

@@ -0,0 +1,431 @@
#include "cptest.h"
#include "config.h"
#include "config_component.h"
#include "downloader.h"
#include "enum_range.h"
#include "environment.h"
#include "mock/mock_mainloop.h"
#include "mock/mock_time_get.h"
#include "mock/mock_update_communication.h"
#include "mock/mock_orchestration_tools.h"
using namespace std;
using namespace testing;
class DownloaderTest : public Test
{
public:
DownloaderTest()
{
setConfiguration<string>("/tmp", "orchestration", "Default file download path");
EXPECT_CALL(mock_orchestration_tools, createDirectory("/tmp")).WillOnce(Return(true));
downloader.init();
}
NiceMock<MockMainLoop> mock_mainloop;
NiceMock<MockTimeGet> mock_timer;
::Environment env;
ConfigComponent config_component;
StrictMock<MockUpdateCommunication> mock_communication;
StrictMock<MockOrchestrationTools> mock_orchestration_tools;
Downloader downloader;
I_Downloader *i_downloader = Singleton::Consume<I_Downloader>::from(downloader);
};
TEST_F(DownloaderTest, doNothing)
{
}
TEST_F(DownloaderTest, downloadFileFromFog)
{
string fog_response = "bla bla";
string checksum = "123";
GetResourceFile resourse_file(GetResourceFile::ResourceFileType::VIRTUAL_SETTINGS);
EXPECT_CALL(mock_communication, downloadAttributeFile(resourse_file)).WillOnce(Return(fog_response));
EXPECT_CALL(
mock_orchestration_tools,
calculateChecksum(Package::ChecksumTypes::SHA256, "/tmp/virtualSettings.download")
).WillOnce(Return(string("123")));
EXPECT_CALL(mock_orchestration_tools, writeFile(fog_response, "/tmp/virtualSettings.download"))
.WillOnce(Return(true));
EXPECT_CALL(mock_orchestration_tools, isNonEmptyFile("/tmp/virtualSettings.download")).WillOnce(Return(true));
Maybe<string> downloaded_file = i_downloader->downloadFileFromFog(
checksum,
Package::ChecksumTypes::SHA256,
resourse_file
);
EXPECT_THAT(downloaded_file, IsValue("/tmp/virtualSettings.download"));
}
TEST_F(DownloaderTest, downloadFileFromFogFailure)
{
string checksum = "123";
Maybe<string> fog_response(genError("Failed to download"));
GetResourceFile resourse_file(GetResourceFile::ResourceFileType::SETTINGS);
EXPECT_CALL(mock_communication, downloadAttributeFile(resourse_file)).WillOnce(Return(fog_response));
Maybe<string> downloaded_file = i_downloader->downloadFileFromFog(
checksum,
Package::ChecksumTypes::SHA256,
resourse_file
);
EXPECT_FALSE(downloaded_file.ok());
EXPECT_THAT(downloaded_file, IsError("Failed to download"));
}
TEST_F(DownloaderTest, registerConfig)
{
string file_path_value = "/tmp";
string signed_certificates_value = "bla";
string config_json =
"{\n"
" \"orchestration\": {\n"
" \"Default file download path\": [\n"
" {\n"
" \"context\": \"All()\",\n"
" \"value\": \"" + file_path_value + "\"\n"
" }\n"
" ],\n"
" \"Self signed certificates acceptable\": [\n"
" {\n"
" \"context\": \"All()\",\n"
" \"value\": \"" + signed_certificates_value + "\"\n"
" }\n"
" ]\n"
" }\n"
"}\n";
env.preload();
env.init();
downloader.preload();
istringstream stringstream(config_json);
Singleton::Consume<Config::I_Config>::from(config_component)->loadConfiguration(stringstream);
EXPECT_THAT(
getConfiguration<string>("orchestration", "Default file download path"),
IsValue(file_path_value)
);
EXPECT_THAT(
getConfiguration<string>("orchestration", "Self signed certificates acceptable"),
IsValue(signed_certificates_value)
);
env.fini();
}
TEST_F(DownloaderTest, downloadWithBadChecksum)
{
string local_file_path = "/tmp/test_file.sh";
string url = "file://" + local_file_path;
string dir_path = getConfigurationWithDefault<string>(
"/tmp/orchestration_downloads",
"orchestration",
"Default file download path"
);
string service_name = "test";
string file_name = service_name + ".download";
string file_path = dir_path + "/" + file_name;
string checksum = "1234";
Package::ChecksumTypes checksum_type = Package::ChecksumTypes::MD5;
EXPECT_CALL(
mock_orchestration_tools,
calculateChecksum(checksum_type, file_path)
).WillOnce(Return(checksum + "5"));
EXPECT_CALL(mock_orchestration_tools, copyFile(local_file_path, file_path)).WillOnce(Return(true));
EXPECT_CALL(mock_orchestration_tools, removeFile("/tmp/test.download")).WillOnce(Return(true));
EXPECT_THAT(
i_downloader->downloadFileFromURL(url, checksum, checksum_type, service_name),
IsError("The checksum calculation is not as the expected, 1234 != 12345")
);
}
TEST_F(DownloaderTest, downloadFromLocal)
{
string local_file_path = "/tmp/test_file.sh";
string url = "file://" + local_file_path;
string dir_path = getConfigurationWithDefault<string>(
"/tmp/orchestration_downloads",
"orchestration",
"Default file download path"
);
string service_name = "test";
string file_name = service_name + ".download";
string file_path = dir_path + "/" + file_name;
string checksum = "1234";
Package::ChecksumTypes checksum_type = Package::ChecksumTypes::MD5;
EXPECT_CALL(mock_orchestration_tools, calculateChecksum(checksum_type, file_path)).WillOnce(Return(checksum));
EXPECT_CALL(mock_orchestration_tools, copyFile(local_file_path, file_path)).WillOnce(Return(true));
EXPECT_CALL(mock_orchestration_tools, isNonEmptyFile(file_path)).WillOnce(Return(true));
i_downloader->downloadFileFromURL(url, checksum, checksum_type, service_name);
}
TEST_F(DownloaderTest, downloadEmptyFileFromFog)
{
string fog_response = "bla bla";
string checksum = "123";
string service_name = "manifest";
string empty_str = "";
GetResourceFile resourse_file(GetResourceFile::ResourceFileType::MANIFEST);
EXPECT_CALL(mock_communication, downloadAttributeFile(resourse_file)).WillOnce(Return(fog_response));
EXPECT_CALL(mock_orchestration_tools, writeFile(fog_response, "/tmp/manifest.download"))
.WillOnce(Return(true));
EXPECT_CALL(mock_orchestration_tools, isNonEmptyFile("/tmp/manifest.download")).WillOnce(Return(false));
EXPECT_CALL(
mock_orchestration_tools,
calculateChecksum(Package::ChecksumTypes::SHA256, "/tmp/manifest.download")
).WillOnce(Return(checksum));
Maybe<string> downloaded_file = i_downloader->downloadFileFromFog(
checksum,
Package::ChecksumTypes::SHA256,
resourse_file
);
EXPECT_FALSE(downloaded_file.ok());
EXPECT_THAT(downloaded_file, IsError("Failed to download file manifest"));
}
TEST_F(DownloaderTest, downloadFromCustomURL)
{
string file_prefix = "file://";
string file_name = "/test_file.sh";
string local_file_path = "/tmp" + file_name;
string url = file_prefix + local_file_path;
string custom_URL = "/custom";
setConfiguration<string>(
string(file_prefix + custom_URL),
"orchestration",
"Custom download url"
);
string dir_path = getConfigurationWithDefault<string>(
"/tmp/orchestration_downloads",
"orchestration",
"Default file download path"
);
string service_name = "test";
string download_file_name = service_name + ".download";
string download_file_path = dir_path + "/" + download_file_name;
string checksum = "1234";
Package::ChecksumTypes checksum_type = Package::ChecksumTypes::MD5;
EXPECT_CALL(mock_orchestration_tools, copyFile(custom_URL + file_name, download_file_path))
.WillOnce(Return(true));
EXPECT_CALL(mock_orchestration_tools, isNonEmptyFile(download_file_path)).WillOnce(Return(true));
EXPECT_CALL(mock_orchestration_tools, calculateChecksum(checksum_type, download_file_path))
.WillOnce(Return(checksum));
i_downloader->downloadFileFromURL(url, checksum, checksum_type, service_name);
}
TEST_F(DownloaderTest, CustomURLBackBackslash)
{
string file_prefix = "file://";
string file_name = "test_file.sh";
string local_file_path = "/tmp/" + file_name;
string url = file_prefix + local_file_path;
string custom_URL = "/custom/";
setConfiguration<string>(
string(file_prefix + custom_URL),
"orchestration",
"Custom download url"
);
string dir_path = getConfigurationWithDefault<string>(
"/tmp/orchestration_downloads",
"orchestration",
"Default file download path"
);
string service_name = "test";
string download_file_name = service_name + ".download";
string download_file_path = dir_path + "/" + download_file_name;
string checksum = "1234";
Package::ChecksumTypes checksum_type = Package::ChecksumTypes::MD5;
EXPECT_CALL(mock_orchestration_tools, copyFile(custom_URL + file_name, download_file_path))
.WillOnce(Return(true));
EXPECT_CALL(mock_orchestration_tools, isNonEmptyFile(download_file_path)).WillOnce(Return(true));
EXPECT_CALL(mock_orchestration_tools, calculateChecksum(checksum_type, download_file_path))
.WillOnce(Return(checksum));
i_downloader->downloadFileFromURL(url, checksum, checksum_type, service_name);
}
TEST_F(DownloaderTest, EmptyCustomURL)
{
string file_prefix = "file://";
string file_name = "/test_file.sh";
string local_file_path = "/tmp" + file_name;
string url = file_prefix + local_file_path;
string custom_URL = "";
setConfiguration<string>(
string(custom_URL),
"orchestration",
"Custom download url"
);
string dir_path = getConfigurationWithDefault<string>(
"/tmp/orchestration_downloads",
"orchestration",
"Default file download path"
);
string service_name = "test";
string download_file_name = service_name + ".download";
string download_file_path = dir_path + "/" + download_file_name;
string checksum = "1234";
Package::ChecksumTypes checksum_type = Package::ChecksumTypes::MD5;
EXPECT_THAT(
i_downloader->downloadFileFromURL(url, checksum, checksum_type, service_name),
IsError("Failed to parse custom URL. URL is empty")
);
}
TEST_F(DownloaderTest, download_virtual_policy)
{
GetResourceFile resourse_file(GetResourceFile::ResourceFileType::VIRTUAL_POLICY);
resourse_file.addTenant("0000", "1", "checksum0000");
resourse_file.addTenant("1111", "2", "checksum1111");
string tenant_0000_file =
"{"
"\"waap\":\"108-005\","
"\"accessControl\":\"Internal error, check logs\","
"\"idk\":\"ed5ac9a6-6924-4ebc-9ace-971896ca33c5\","
"\"something\":\"Low\""
"}";
string tenant_1111_file =
"{"
"\"messageId\":\"108-005\","
"\"message\":\"Internal error, check logs\","
"\"referenceId\":\"ed5ac9a6-6924-4ebc-9ace-971896ca33c5\","
"\"severity\":\"Low\""
"}";
string fog_response =
"{\n"
" \"tenants\": [\n"
" {\n"
" \"tenantId\": \"0000\",\n"
" \"policy\": {\n"
" \"waap\": \"108-005\",\n"
" \"accessControl\": \"Internal error, check logs\",\n"
" \"idk\": \"ed5ac9a6-6924-4ebc-9ace-971896ca33c5\",\n"
" \"something\": \"Low\"\n"
" }\n"
" },\n"
" {\n"
" \"tenantId\": \"1111\",\n"
" \"policy\": {\n"
" \"messageId\": \"108-005\",\n"
" \"message\": \"Internal error, check logs\",\n"
" \"referenceId\": \"ed5ac9a6-6924-4ebc-9ace-971896ca33c5\",\n"
" \"severity\": \"Low\"\n"
" }\n"
" }\n"
" ]\n"
"}";
EXPECT_CALL(mock_communication, downloadAttributeFile(resourse_file)).WillOnce(Return(fog_response));
EXPECT_CALL(mock_orchestration_tools, writeFile(tenant_0000_file, "/tmp/virtualPolicy_0000.download"))
.WillOnce(Return(true));
EXPECT_CALL(mock_orchestration_tools, writeFile(tenant_1111_file, "/tmp/virtualPolicy_1111.download"))
.WillOnce(Return(true));
map<string, string> expected_downloaded_files =
{
{ "0000", "/tmp/virtualPolicy_0000.download" },
{ "1111", "/tmp/virtualPolicy_1111.download" }
};
EXPECT_EQ(
i_downloader->downloadVirtualFileFromFog(
resourse_file,
Package::ChecksumTypes::SHA256
),
expected_downloaded_files
);
}
TEST_F(DownloaderTest, download_virtual_settings)
{
GetResourceFile resourse_file(GetResourceFile::ResourceFileType::VIRTUAL_SETTINGS);
resourse_file.addTenant("4c721b40-85df-4364-be3d-303a10ee9789", "1", "checksum0000");
string tenant_0000_file =
"{"
"\"agentSettings\":["
"{"
"\"id\":\"f0bd081b-175a-2fb6-c6de-d05d62fdcadf\","
"\"key\":\"\","
"\"value\":\"\""
"}"
"],"
"\"allowOnlyDefinedApplications\":false,"
"\"anyFog\":true,"
"\"reverseProxy\":{"
"\"assets\":[]"
"},"
"\"upgradeMode\":\"automatic\""
"}";
string fog_response =
"{\n"
" \"tenants\": [\n"
" {\n"
" \"tenantId\": \"4c721b40-85df-4364-be3d-303a10ee9789\",\n"
" \"settings\": {\n"
" \"agentSettings\": [\n"
" {\n"
" \"id\": \"f0bd081b-175a-2fb6-c6de-d05d62fdcadf\",\n"
" \"key\": \"\",\n"
" \"value\": \"\"\n"
" }\n"
" ],\n"
" \"allowOnlyDefinedApplications\": false,\n"
" \"anyFog\": true,\n"
" \"reverseProxy\": {\n"
" \"assets\": []\n"
" },\n"
" \"upgradeMode\": \"automatic\"\n"
" }\n"
" }\n"
" ]\n"
"}";
EXPECT_CALL(mock_communication, downloadAttributeFile(resourse_file)).WillOnce(Return(fog_response));
EXPECT_CALL(
mock_orchestration_tools,
writeFile(tenant_0000_file, "/tmp/virtualSettings_4c721b40-85df-4364-be3d-303a10ee9789.download")
).WillOnce(Return(true));
map<string, string> expected_downloaded_files = {
{ "4c721b40-85df-4364-be3d-303a10ee9789",
"/tmp/virtualSettings_4c721b40-85df-4364-be3d-303a10ee9789.download"
}
};
EXPECT_EQ(
i_downloader->downloadVirtualFileFromFog(
resourse_file,
Package::ChecksumTypes::SHA256
),
expected_downloaded_files
);
}

View File

@@ -0,0 +1,276 @@
// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "http_client.h"
#include "curl_client.h"
#include "downloader.h"
#include "debug.h"
#include "i_encryptor.h"
#include "url_parser.h"
#include "sasal.h"
#include "config.h"
#include "i_environment.h"
#include "orchestration_comp.h"
#include <fstream>
#include <string>
#include <iostream>
#include <chrono>
#include <boost/asio/ip/tcp.hpp>
using boost::asio::ip::tcp;
using namespace std;
SASAL_START // Orchestration - Communication
USE_DEBUG_FLAG(D_ORCHESTRATOR);
USE_DEBUG_FLAG(D_HTTP_REQUEST);
// LCOV_EXCL_START Reason: Depends on real download server.
class ClientConnection
{
public:
ClientConnection(
const URLParser &_url,
const Maybe<string> &_proxy_url,
const Maybe<uint16_t> &_proxy_port,
const Maybe<string> &_proxy_auth,
const string &_token)
:
url(_url),
proxy_url(_proxy_url),
proxy_port(_proxy_port),
proxy_auth(_proxy_auth),
token(_token)
{
}
Maybe<void>
handleConnect()
{
if (!url.getBaseURL().ok()) {
return genError("Failed to handle connection. Error: " + url.getBaseURL().getErr());
}
string server_name = url.getBaseURL().unpack();
string port = url.getPort();
string query = url.getQuery();
string host = server_name;
try {
if (stoi(port) != 80) {
host = host + ":" + port;
}
} catch (const exception &err) {
return genError("Failed to parse port to a number. Port: " + port );
}
chrono::duration<unsigned int, ratio<1>> sleep_time(60);
io_stream.expires_from_now(sleep_time);
if (proxy_url.ok()) {
if (!proxy_port.ok()) {
return genError(
"Failed to handle connection to server. proxy domain is defined with invalid port, Error: " +
proxy_port.getErr()
);
}
io_stream.connect(proxy_url.unpack(), to_string(proxy_port.unpack()));
} else {
io_stream.connect(server_name, port);
}
if (!io_stream) {
return genError("Failed to handle connection to server. Error: " + io_stream.error().message());
}
string request_url = query;
if (proxy_url.ok()) {
request_url = host + query;
}
stringstream http_request;
http_request << "GET http://" << request_url << " HTTP/1.1\r\n";
http_request << "Host: " << host << "\r\n";
if (!token.empty()) {
http_request << "Authorization: " << "Bearer " << token << "\r\n";
}
http_request << "User-Agent: Infinity Next (a7030abf93a4c13)\r\n";
auto i_trace_env = Singleton::Consume<I_Environment>::by<OrchestrationComp>();
http_request << i_trace_env->getCurrentHeaders();
http_request << "Accept: */*\r\n";
if (proxy_url.ok()) {
http_request << "Accept-Encoding: identity";
http_request << "Connection: close\r\n";
http_request << "Proxy-Connection: Keep-Alive\r\n";
if (proxy_auth.ok()) {
I_Encryptor *encryptor = Singleton::Consume<I_Encryptor>::by<Downloader>();
http_request << "Proxy-Authorization: Basic " + encryptor->base64Encode(proxy_auth.unpack()) + "\r\n";
}
http_request << "\r\n";
} else {
http_request << "Connection: close\r\n\r\n";
}
dbgTrace(D_HTTP_REQUEST) << "Sending the following HTTP Request: " << endl << http_request.str();
io_stream << http_request.str();
return Maybe<void>();
}
Maybe<void>
handleResponse(ofstream &out_file)
{
string response_http_version;
io_stream >> response_http_version;
unsigned int status_code;
io_stream >> status_code;
string status_message;
getline(io_stream, status_message);
if (!io_stream || response_http_version.substr(0, 5) != "HTTP/") {
return genError("Invalid response");
}
if (status_code != 200) {
return genError("HTTP response returned with status code " + status_code);
}
string header;
vector<string> headers;
while (getline(io_stream, header) && header != "\r") {
headers.push_back(header);
}
out_file << io_stream.rdbuf();
dbgTrace(D_HTTP_REQUEST)
<< "Received HTTP Response with the following data (downloaded file will not be printed):"
<< endl
<< response_http_version
<< " "
<< status_code
<< " "
<< status_message
<< endl
<< makeSeparatedStr(headers, "\n");
return Maybe<void>();
}
private:
const URLParser url;
const Maybe<string> proxy_url;
const Maybe<uint16_t> proxy_port;
const Maybe<string> proxy_auth;
const string &token;
boost::asio::ip::tcp::iostream io_stream;
};
Maybe<void>
HTTPClient::getFile(const URLParser &url, ofstream &out_file, bool auth_required)
{
auto message = Singleton::Consume<I_Messaging>::by<HTTPClient>();
auto load_env_proxy = message->loadProxy();
if (!load_env_proxy.ok()) return load_env_proxy;
string token = "";
if (auth_required) {
auto message = Singleton::Consume<I_Messaging>::by<HTTPClient>();
token = message->getAccessToken();
}
if (url.isOverSSL()) {
auto get_file_over_ssl_res = getFileSSL(url, out_file, token);
if (!get_file_over_ssl_res.ok())
{
//CURL fallback
dbgWarning(D_ORCHESTRATOR) << "Failed to get file over SSL. Trying via CURL (SSL).";
return curlGetFileOverSSL(url, out_file, token);
}
return get_file_over_ssl_res;
}
auto get_file_http_res = getFileHttp(url, out_file, token);
if (!get_file_http_res.ok())
{
//CURL fallback
dbgWarning(D_ORCHESTRATOR) << "Failed to get file over HTTP. Trying via CURL (HTTP).";
return curlGetFileOverHttp(url, out_file, token);
}
return get_file_http_res;
}
Maybe<void>
HTTPClient::curlGetFileOverHttp(const URLParser &url, ofstream &out_file, const string &token)
{
try {
auto message = Singleton::Consume<I_Messaging>::by<HTTPClient>();
HttpCurl http_curl_client(
url,
out_file,
token,
message->getProxyDomain(ProxyProtocol::HTTPS),
message->getProxyPort(ProxyProtocol::HTTPS),
message->getProxyCredentials(ProxyProtocol::HTTPS));
http_curl_client.setCurlOpts();
bool connection_ok = http_curl_client.connect();
if (!connection_ok)
{
stringstream url_s;
url_s << url;
string err_msg = string("Failed to get file over HTTP. URL: ") + url_s.str();
return genError(err_msg);
}
// As this class is a temporal solution catch all exception types is enabled.
} catch (const exception &e) {
string err_msg = "Failed to get file over HTTP. Exception: " + string(e.what());
return genError(err_msg);
}
return Maybe<void>();
}
Maybe<void>
HTTPClient::getFileHttp(const URLParser &url, ofstream &out_file, const string &token)
{
try {
auto message = Singleton::Consume<I_Messaging>::by<HTTPClient>();
ClientConnection client_connection(
url,
message->getProxyDomain(ProxyProtocol::HTTP),
message->getProxyPort(ProxyProtocol::HTTP),
message->getProxyCredentials(ProxyProtocol::HTTP),
token
);
auto handle_connect_res = client_connection.handleConnect();
if (!handle_connect_res.ok()) return handle_connect_res;
return client_connection.handleResponse(out_file);
// As this class is a temporal solution catch all exception types is enabled.
} catch (const exception &e) {
string err_msg = "Failed to get file over HTTP. Exception: " + string(e.what());
return genError(err_msg);
}
return Maybe<void>();
}
// LCOV_EXCL_STOP
SASAL_END

View File

@@ -0,0 +1,39 @@
// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef __HTTP_CLIENT_H__
#define __HTTP_CLIENT_H__
#include <string>
#include "maybe_res.h"
#include "url_parser.h"
#include "i_messaging.h"
// LCOV_EXCL_START Reason: Depends on real download server.
class HTTPClient : public Singleton::Consume<I_Messaging>
{
public:
HTTPClient() = default;
Maybe<void> getFile(const URLParser &url, std::ofstream &out_file, bool auth_required);
private:
std::string loadCAChainDir();
Maybe<void> getFileSSL(const URLParser &url, std::ofstream &out_file, const std::string &_token);
Maybe<void> getFileHttp(const URLParser &url, std::ofstream &out_file, const std::string &_token);
Maybe<void> curlGetFileOverSSL(const URLParser &url, std::ofstream &out_file, const std::string &_token);
Maybe<void> curlGetFileOverHttp(const URLParser &url, std::ofstream &out_file, const std::string &_token);
};
// LCOV_EXCL_STOP
#endif // __HTTP_CLIENT_H__

View File

@@ -0,0 +1,619 @@
// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "http_client.h"
#include "curl_client.h"
#include "debug.h"
#include "i_agent_details.h"
#include "i_encryptor.h"
#include "downloader.h"
#include "config.h"
#include "sasal.h"
#include "boost/uuid/uuid.hpp"
#include "boost/uuid/uuid_generators.hpp"
#include <boost/asio/deadline_timer.hpp>
#include "boost/uuid/uuid_io.hpp"
#include <string>
#include <iostream>
#include <istream>
#include <ostream>
#include <fstream>
#include <boost/asio.hpp>
#include <boost/bind/bind.hpp>
#include <boost/asio/ssl.hpp>
#include <exception>
using namespace boost::placeholders;
using boost::asio::ip::tcp;
using namespace std;
SASAL_START // Orchestration - Communication
USE_DEBUG_FLAG(D_COMMUNICATION);
USE_DEBUG_FLAG(D_HTTP_REQUEST);
USE_DEBUG_FLAG(D_ORCHESTRATOR);
// LCOV_EXCL_START Reason: Depends on real download server.
class BadResponseFromServer : public exception
{
public:
BadResponseFromServer() : message("Bad response returned from server") {}
BadResponseFromServer(const string &msg) : message(msg) {}
const char *
what() const throw()
{
return message.c_str();
}
private:
string message;
};
class Client
{
public:
Client(
ofstream &out_file,
boost::asio::io_service &io_service,
boost::asio::ssl::context &context,
const URLParser &_url,
const Maybe<string> &_proxy_url,
const Maybe<uint16_t> &_proxy_port,
const Maybe<string> &_proxy_auth,
const string &_token)
:
out_file(out_file),
url(_url),
proxy_url(_proxy_url),
proxy_port(_proxy_port),
proxy_auth(_proxy_auth),
resolver_(io_service),
deadline(io_service),
socket_(io_service),
ssl_socket(socket_, context),
token(_token)
{
}
Maybe<void>
handleConnection()
{
ostream request_stream(&request_);
stringstream http_request;
http_request << "GET " << url.getQuery() << " HTTP/1.1\r\n";
string host = url.getBaseURL().unpack();
string port = url.getPort();
int port_int;
try {
port_int = stoi(port);
} catch (const exception &err) {
dbgWarning(D_COMMUNICATION)
<< "Failed to convert port number from string. Port: "
<< port
<< ", Error: "
<< err.what();
return genError("Failed to parse port to a number. Port: " + port);
}
if (port_int != 443) {
host = host + ":" + port;
}
http_request << "Host: " << host << "\r\n";
if (!token.empty()) {
http_request << "Authorization: " << "Bearer " << token << "\r\n";
}
http_request << "User-Agent: Infinity Next (a7030abf93a4c13)\r\n";
boost::uuids::uuid correlation_id;
try {
correlation_id = uuid_random_gen();
} catch (const boost::uuids::entropy_error &) {
dbgWarning(D_COMMUNICATION) << "Failed to generate random correlation id - entropy exception";
}
http_request << "X-Trace-Id: " + boost::uuids::to_string(correlation_id) + "\r\n";
http_request << "Accept: */*\r\n";
http_request << "Connection: close\r\n\r\n";
request_stream << http_request.str();
deadline.expires_from_now(boost::posix_time::minutes(5));
deadline.async_wait(boost::bind(&Client::checkDeadline, this, _1));
if (proxy_url.ok()) {
if (!proxy_port.ok()) {
dbgWarning(D_COMMUNICATION)
<< "Failed to connect to proxy due to invalid port value, Error: "
<< proxy_port.getErr();
return genError(
"Failed to handle connection to server. proxy port is invalid, Error: " +
proxy_port.getErr()
);
}
if (port_int == 443) host = host + ":" + port;
ostream connect_request_stream(&connect_request);
stringstream proxy_request;
proxy_request << "CONNECT " << host << " HTTP/1.1\r\n";
proxy_request << "Host: " << host << "\r\n";
if (proxy_auth.ok()) {
I_Encryptor *encryptor = Singleton::Consume<I_Encryptor>::by<Downloader>();
proxy_request
<< "Proxy-Authorization: Basic "
<< encryptor->base64Encode(proxy_auth.unpack())
<< "\r\n";
}
proxy_request << "\r\n";
dbgTrace(D_HTTP_REQUEST) << "Connecting to proxy: " << endl << proxy_request.str();
connect_request_stream << proxy_request.str();
tcp::resolver::query query(proxy_url.unpack(), to_string(proxy_port.unpack()));
resolver_.async_resolve(
query,
boost::bind(
&Client::overProxyResolver,
this,
boost::asio::placeholders::error,
boost::asio::placeholders::iterator
)
);
} else {
tcp::resolver::query query(url.getBaseURL().unpack(), port);
resolver_.async_resolve(
query,
boost::bind(
&Client::handleResolve,
this,
boost::asio::placeholders::error,
boost::asio::placeholders::iterator
)
);
}
dbgTrace(D_HTTP_REQUEST) << "Sending the following HTTP Request: " << endl << http_request.str();
return Maybe<void>();
}
private:
void
checkDeadline(const boost::system::error_code &err)
{
if (err) return;
if (deadline.expires_at() <= boost::asio::deadline_timer::traits_type::now()) {
boost::system::error_code ignored_ec = boost::asio::error::operation_aborted;
socket_.close(ignored_ec);
deadline.expires_at(boost::posix_time::pos_infin);
return;
}
deadline.async_wait(boost::bind(&Client::checkDeadline, this, _1));
}
void
overProxyResolver(const boost::system::error_code &err, tcp::resolver::iterator endpoint_iterator)
{
if (!err) {
boost::asio::async_connect(socket_, endpoint_iterator,
boost::bind(&Client::overProxyHandleConnect, this,
boost::asio::placeholders::error));
} else {
string err_msg = "Failed to connect to proxy. Error: " + err.message();
throw BadResponseFromServer(err_msg);
}
}
void
overProxyHandleConnect(const boost::system::error_code &err)
{
if (!err) {
boost::asio::async_write(socket_, connect_request,
boost::bind(&Client::overProxyHandleWriteRequest, this,
boost::asio::placeholders::error));
} else {
string err_msg = "Failed to connect to proxy. Error: " + err.message();
throw BadResponseFromServer(err_msg);
}
}
void
overProxyHandleWriteRequest(const boost::system::error_code &err)
{
if (!err) {
boost::asio::async_read_until(
socket_,
response_,
"\r\n",
boost::bind(&Client::overProxyHandleReadStatusLine, this, boost::asio::placeholders::error)
);
} else {
string err_msg = "Failed to write over proxy. Error: " + err.message();
throw BadResponseFromServer(err_msg);
}
}
void
overProxyHandleReadStatusLine(const boost::system::error_code &err)
{
if (err) {
string err_msg = "Failed to read status line over proxy. Error: " + err.message();
throw BadResponseFromServer(err_msg);
}
// Check that response is OK.
istream response_stream(&response_);
string response_http_version;
response_stream >> response_http_version;
unsigned int status_code;
response_stream >> status_code;
string status_message;
getline(response_stream, status_message);
if (!response_stream || response_http_version.substr(0, 5) != "HTTP/") {
throw BadResponseFromServer("Invalid response");
return;
}
if (status_code != 200) {
string err_msg = "Response returned with status code " + status_code;
throw BadResponseFromServer(err_msg);
}
dbgTrace(D_HTTP_REQUEST)
<< "Received HTTP Response over proxied connection with the following data:"
<< endl
<< response_http_version
<< " "
<< status_code
<< " "
<< status_message;
if (getProfileAgentSettingWithDefault<bool>(false, "agent.config.message.ignoreSslValidation") == false) {
ssl_socket.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert);
ssl_socket.set_verify_callback(boost::bind(&Client::verifyCertificate, this, _1, _2));
} else {
dbgWarning(D_HTTP_REQUEST) << "Ignoring SSL validation";
}
ssl_socket.async_handshake(
boost::asio::ssl::stream_base::client,
boost::bind(&Client::handleHandshake, this, boost::asio::placeholders::error)
);
}
void
handleResolve(const boost::system::error_code &err, tcp::resolver::iterator endpoint_iterator)
{
if (!err) {
boost::asio::async_connect(ssl_socket.lowest_layer(), endpoint_iterator,
boost::bind(&Client::handleConnect, this,
boost::asio::placeholders::error));
} else {
string message = "Failed to connect. Error: " + err.message();
throw BadResponseFromServer(message);
}
}
bool
verifyCertificate(bool preverified, boost::asio::ssl::verify_context &ctx)
{
if (!token.empty()) {
X509_STORE_CTX *cts = ctx.native_handle();
switch (X509_STORE_CTX_get_error(cts))
{
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
dbgWarning(D_ORCHESTRATOR) << "SSL verification error: X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT";
break;
case X509_V_ERR_CERT_NOT_YET_VALID:
case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
dbgWarning(D_ORCHESTRATOR) << "SSL verification error: Certificate not yet valid";
break;
case X509_V_ERR_CERT_HAS_EXPIRED:
case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
dbgWarning(D_ORCHESTRATOR) << "Certificate expired";
break;
case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN:
dbgDebug(D_ORCHESTRATOR) << "Self signed certificate in chain";
if (getConfigurationWithDefault(false, "orchestration", "Self signed certificates acceptable")) {
preverified = true;
}
break;
default:
if (!preverified) {
dbgWarning(D_ORCHESTRATOR)
<< "Certificate verification error number: "
<< X509_STORE_CTX_get_error(cts);
}
break;
}
return preverified;
}
return true;
}
void
handleConnect(const boost::system::error_code &err)
{
if (!err) {
if (getProfileAgentSettingWithDefault<bool>(false, "agent.config.message.ignoreSslValidation") == false) {
ssl_socket.set_verify_mode(
boost::asio::ssl::verify_peer |
boost::asio::ssl::verify_fail_if_no_peer_cert
);
ssl_socket.set_verify_callback(boost::bind(&Client::verifyCertificate, this, _1, _2));
} else {
dbgWarning(D_HTTP_REQUEST) << "Ignoring SSL validation";
}
ssl_socket.async_handshake(boost::asio::ssl::stream_base::client,
boost::bind(&Client::handleHandshake, this,
boost::asio::placeholders::error));
} else {
string err_message = "Failed to connect. Error: " + err.message();
throw BadResponseFromServer(err_message);
}
}
void
handleHandshake(const boost::system::error_code &error)
{
if (!error) {
boost::asio::buffer_cast<const char*>(request_.data());
boost::asio::async_write(ssl_socket, request_,
boost::bind(&Client::handleWriteRequest, this,
boost::asio::placeholders::error));
} else {
string err_message = "Handshake failed. Error: " + error.message();
throw BadResponseFromServer(err_message);
}
}
void
handleWriteRequest(const boost::system::error_code &err)
{
if (!err) {
boost::asio::async_read_until(ssl_socket, resp, "\r\n",
boost::bind(&Client::handleReadStatusLine, this,
boost::asio::placeholders::error));
} else {
string err_message = "Failed to handle write request. Error: " + err.message();
throw BadResponseFromServer(err_message);
}
}
void
handleReadStatusLine(const boost::system::error_code &err)
{
if (!err) {
istream response_stream(&resp);
string http_version;
response_stream >> http_version;
unsigned int status_code;
response_stream >> status_code;
string status_message;
getline(response_stream, status_message);
dbgTrace(D_HTTP_REQUEST)
<< "Received HTTP Response with the following data:"
<< endl
<< http_version
<< " "
<< status_code;
if (!response_stream || http_version.substr(0, 5) != "HTTP/") {
string err_message = "Invalid response";
throw BadResponseFromServer(err_message);
}
if (status_code != 200) {
string err_message = "HTTPS response returned with status code " + to_string(status_code)
+ ". URL: " + url.toString();
throw BadResponseFromServer(err_message);
}
boost::asio::async_read_until(ssl_socket, resp, "\r\n\r\n",
boost::bind(&Client::handleReadHeaders, this,
boost::asio::placeholders::error));
} else {
dbgWarning(D_COMMUNICATION) << "Failed to read response status. Error:" << err.message();
string err_message = "Failed to read status. Error: " + err.message();
throw BadResponseFromServer(err_message);
}
}
void
handleReadHeaders(const boost::system::error_code &err)
{
if (!err) {
// Process the response headers.
istream response_stream(&resp);
string header;
vector<string> headers;
while (getline(response_stream, header) && header != "\r") {
headers.push_back(header);
}
dbgTrace(D_HTTP_REQUEST) << "Received Response headers:" << endl << makeSeparatedStr(headers, "\n");
// Write whatever content we already have to output.
if (resp.size() > 0)
out_file << &resp;
// Start reading remaining data until EOF.
boost::asio::async_read(ssl_socket, resp,
boost::asio::transfer_at_least(1),
boost::bind(&Client::handleReadContent, this,
boost::asio::placeholders::error));
} else {
dbgWarning(D_COMMUNICATION) << "Failed to read response headers. Error:" << err.message();
string err_message = "Failed to read headers. Error: " + err.message();
throw BadResponseFromServer(err_message);
}
}
void
handleReadContent(const boost::system::error_code &err)
{
if (!err) {
// Write all of the data that has been read so far.
out_file << &resp;
// Continue reading remaining data until EOF.
boost::asio::async_read(
ssl_socket,
resp,
boost::asio::transfer_at_least(1),
boost::bind(&Client::handleReadContent, this, boost::asio::placeholders::error)
);
} else if (err != boost::asio::error::eof && err != boost::asio::ssl::error::stream_truncated) {
dbgWarning(D_COMMUNICATION) << "Failed to read response body. Error:" << err.message();
string err_message = "Failed to read content. Error: " + err.message();
throw BadResponseFromServer(err_message);
} else if (err == boost::asio::ssl::error::stream_truncated) {
dbgError(D_COMMUNICATION) << "Had SSL warning during reading response body stage. Error:" << err.message();
deadline.cancel();
} else {
deadline.cancel();
}
}
ofstream &out_file;
const URLParser &url;
const Maybe<string> proxy_url;
const Maybe<uint16_t> proxy_port;
const Maybe<string> proxy_auth;
tcp::resolver resolver_;
boost::asio::deadline_timer deadline;
boost::asio::ip::tcp::socket socket_;
boost::asio::ssl::stream<boost::asio::ip::tcp::socket&> ssl_socket;
boost::asio::streambuf request_;
boost::asio::streambuf connect_request;
boost::asio::streambuf response_;
boost::asio::streambuf resp;
const string &token;
boost::uuids::random_generator uuid_random_gen;
};
string
HTTPClient::loadCAChainDir()
{
string ca_chain_dir;
auto agent_details = Singleton::Consume<I_AgentDetails>::by<Downloader>();
auto load_ca_chain_dir = agent_details->getOpenSSLDir();
if (load_ca_chain_dir.ok()) {
ca_chain_dir = load_ca_chain_dir.unpack();
}
return getConfigurationWithDefault<string>(ca_chain_dir, "message", "Certificate authority directory");
}
Maybe<void>
HTTPClient::getFileSSL(const URLParser &url, ofstream &out_file, const string &token)
{
try {
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
if (!token.empty()) {
string cert_file_path = getConfigurationWithDefault<string>(
getFilesystemPathConfig() + "/certs/fog.pem",
"message",
"Certificate chain file path"
);
dbgTrace(D_ORCHESTRATOR) << "Http client, cert file path: " << cert_file_path;
auto trusted_ca_directory = getConfiguration<string>("message", "Trusted CA directory");
if (trusted_ca_directory.ok() && !trusted_ca_directory.unpack().empty()) {
ctx.add_verify_path(trusted_ca_directory.unpack());
} else {
string cert_file_path = getConfigurationWithDefault<string>(
getFilesystemPathConfig() + "/certs/fog.pem",
"message",
"Certificate chain file path"
);
ctx.load_verify_file(cert_file_path);
}
}
boost::asio::io_service io_service;
auto message = Singleton::Consume<I_Messaging>::by<HTTPClient>();
Client client(
out_file,
io_service,
ctx,
url,
message->getProxyDomain(ProxyProtocol::HTTPS),
message->getProxyPort(ProxyProtocol::HTTPS),
message->getProxyCredentials(ProxyProtocol::HTTPS),
token
);
auto connection_result = client.handleConnection();
if (!connection_result.ok()) {
return connection_result;
};
auto mainloop = Singleton::Consume<I_MainLoop>::by<Downloader>();
while (!io_service.stopped()) {
io_service.poll_one();
mainloop->yield(true);
}
} catch (const exception &e) {
dbgWarning(D_COMMUNICATION) << "Failed to get file over HTTPS. Error:" << string(e.what());
string error_str = "Failed to get file over HTTPS, exception: " + string(e.what());
return genError(error_str);
}
return Maybe<void>();
}
Maybe<void>
HTTPClient::curlGetFileOverSSL(const URLParser &url, ofstream &out_file, const string &token)
{
try {
string cert_file_path;
if (!token.empty())
{
cert_file_path = getConfigurationWithDefault<string>(
getFilesystemPathConfig() + "/certs/fog.pem",
"message",
"Certificate chain file path"
);
}
auto message = Singleton::Consume<I_Messaging>::by<HTTPClient>();
HttpsCurl ssl_curl_client(
url,
out_file,
token,
message->getProxyDomain(ProxyProtocol::HTTPS),
message->getProxyPort(ProxyProtocol::HTTPS),
message->getProxyCredentials(ProxyProtocol::HTTPS),
cert_file_path);
ssl_curl_client.setCurlOpts();
bool connection_ok = ssl_curl_client.connect();
if (!connection_ok)
{
stringstream url_s;
url_s << url;
string err_msg = string("Failed to get file over HTTPS. URL: ") + url_s.str();
return genError(err_msg);
}
} catch (const exception &e) {
dbgWarning(D_COMMUNICATION) << "Failed to get file over HTTPS. Error:" << string(e.what());
string error_str = "Failed to get file over HTTPS, exception: " + string(e.what());
return genError(error_str);
}
return Maybe<void>();
}
// LCOV_EXCL_STOP
SASAL_END