mirror of https://github.com/oxen-io/oxen-core.git
252 lines
7.5 KiB
C++
252 lines
7.5 KiB
C++
#include "http_client.h"
|
|
#include <chrono>
|
|
#include <regex>
|
|
#include <cpr/cpr.h>
|
|
#include "common/string_util.h"
|
|
#include "cpr/ssl_options.h"
|
|
|
|
#undef LOKI_DEFAULT_LOG_CATEGORY
|
|
#define LOKI_DEFAULT_LOG_CATEGORY "rpc.http_client"
|
|
|
|
namespace cryptonote::rpc {
|
|
|
|
http_client_connect_error::http_client_connect_error(const cpr::Error& err, const std::string& prefix) :
|
|
http_client_error{prefix + err.message},
|
|
code{err.code}
|
|
{}
|
|
|
|
void http_client::set_base_url(std::string base_url_) {
|
|
std::lock_guard lock{params_mutex};
|
|
if (!base_url_.empty() && base_url_.back() != '/')
|
|
base_url_ += '/';
|
|
base_url = cpr::Url{std::move(base_url_)};
|
|
}
|
|
std::string http_client::get_base_url() const {
|
|
std::shared_lock lock{params_mutex};
|
|
return base_url.str();
|
|
}
|
|
|
|
void http_client::set_timeout(std::chrono::milliseconds timeout_) {
|
|
std::lock_guard lock{params_mutex};
|
|
if (timeout_ > 0s)
|
|
timeout = timeout_;
|
|
else
|
|
timeout.reset();
|
|
apply_timeout = true;
|
|
}
|
|
|
|
std::chrono::milliseconds http_client::get_timeout() const {
|
|
std::shared_lock lock{params_mutex};
|
|
return timeout ? timeout->ms : 0s;
|
|
}
|
|
|
|
void http_client::cancel() {
|
|
// We *don't* take a session lock here, which is a little dirty, but resetting the timeout seems
|
|
// small enough as to not cause issues when done from another thread.
|
|
session.SetTimeout(1ms);
|
|
}
|
|
|
|
http_client::WipedAuth::WipedAuth(std::string_view username, std::string_view password)
|
|
: Authentication("", "")
|
|
{
|
|
auth_string_.clear();
|
|
auth_string_.reserve(username.size() + 1 + password.size());
|
|
auth_string_ += username;
|
|
auth_string_ += ':';
|
|
auth_string_ += password;
|
|
}
|
|
|
|
http_client::WipedAuth::~WipedAuth() {
|
|
memwipe(auth_string_.data(), auth_string_.size());
|
|
}
|
|
|
|
void http_client::set_auth(std::string_view username, std::string_view password) {
|
|
std::lock_guard lock{params_mutex};
|
|
if (username.empty() && password.empty()) {
|
|
if (auth) {
|
|
auth.reset();
|
|
apply_auth = true;
|
|
}
|
|
} else {
|
|
auth.emplace(username, password);
|
|
apply_auth = true;
|
|
}
|
|
}
|
|
|
|
void http_client::set_proxy(std::string proxy_) {
|
|
std::lock_guard lock{params_mutex};
|
|
if (proxy != proxy_) {
|
|
proxy = std::move(proxy_);
|
|
apply_proxy = true;
|
|
}
|
|
}
|
|
|
|
std::string http_client::get_proxy() const {
|
|
std::shared_lock lock{params_mutex};
|
|
return proxy;
|
|
}
|
|
|
|
void http_client::set_https_client_cert(std::string cert_path, std::string key_path) {
|
|
std::lock_guard lock{params_mutex};
|
|
if (cert_path.empty() || key_path.empty()) {
|
|
if (client_cert) {
|
|
client_cert.reset();
|
|
apply_ssl = true;
|
|
}
|
|
} else {
|
|
client_cert.emplace(std::move(cert_path), std::move(key_path));
|
|
apply_ssl = true;
|
|
}
|
|
}
|
|
|
|
void http_client::set_insecure_https(bool insecure) {
|
|
std::lock_guard lock{params_mutex};
|
|
if (insecure != !verify_https) {
|
|
verify_https = !insecure;
|
|
apply_ssl = true;
|
|
}
|
|
}
|
|
|
|
void http_client::set_https_cainfo(std::string cainfo_bundle_path) {
|
|
std::lock_guard lock{params_mutex};
|
|
if (cainfo_bundle_path.empty()) {
|
|
if (ca_info) {
|
|
ca_info.reset();
|
|
apply_ssl = true;
|
|
}
|
|
} else {
|
|
ca_info.emplace(std::move(cainfo_bundle_path));
|
|
}
|
|
}
|
|
|
|
|
|
void http_client::copy_params_from(const http_client& other) {
|
|
std::unique_lock lock{params_mutex, std::defer_lock};
|
|
std::shared_lock olock{other.params_mutex, std::defer_lock};
|
|
std::lock(lock, olock);
|
|
|
|
base_url = other.base_url;
|
|
timeout = other.timeout;
|
|
auth = other.auth;
|
|
}
|
|
|
|
|
|
cpr::Response http_client::post(const std::string& uri, cpr::Body body, cpr::Header header) {
|
|
if (base_url.str().empty())
|
|
throw http_client_error{"Cannot submit request: no base url has been set"};
|
|
|
|
cpr::Response res;
|
|
{
|
|
std::shared_lock plock{params_mutex};
|
|
std::chrono::steady_clock::time_point start;
|
|
if (LOG_ENABLED(Debug))
|
|
start = std::chrono::steady_clock::now();
|
|
auto url = base_url + uri;
|
|
|
|
// See if we need to update any of the session paramters; we do it here with only the parameter
|
|
// lock, then actually load the setting below once we have the session lock.
|
|
std::optional<cpr::Timeout> new_timeout;
|
|
if (apply_timeout) {
|
|
new_timeout = timeout ? *timeout : cpr::Timeout{0ms};
|
|
apply_timeout = false;
|
|
}
|
|
std::optional<cpr::Authentication> new_auth;
|
|
if (apply_auth) {
|
|
new_auth = auth ? *auth : cpr::Authentication{"", ""};
|
|
apply_auth = false;
|
|
}
|
|
std::optional<cpr::Proxies> new_proxy;
|
|
if (apply_proxy) {
|
|
if (proxy.empty())
|
|
new_proxy.emplace();
|
|
else
|
|
new_proxy = cpr::Proxies{{{"http", proxy}, {"https", proxy}}};
|
|
apply_proxy = false;
|
|
}
|
|
|
|
std::optional<cpr::SslOptions> new_ssl_opts;
|
|
if (apply_ssl) {
|
|
new_ssl_opts.emplace();
|
|
if (client_cert) {
|
|
new_ssl_opts->SetOption(client_cert->first);
|
|
new_ssl_opts->SetOption(client_cert->second);
|
|
}
|
|
if (!verify_https) {
|
|
MWARNING("HTTPS certificate verification disabled; this connection is not secure");
|
|
new_ssl_opts->SetOption(cpr::ssl::VerifyHost(false));
|
|
new_ssl_opts->SetOption(cpr::ssl::VerifyPeer(false));
|
|
new_ssl_opts->SetOption(cpr::ssl::VerifyStatus(false));
|
|
}
|
|
if (ca_info) {
|
|
new_ssl_opts->SetOption(*ca_info);
|
|
}
|
|
}
|
|
|
|
plock.unlock();
|
|
|
|
{
|
|
std::lock_guard slock{session_mutex};
|
|
|
|
if (new_auth) session.SetAuth(*new_auth);
|
|
if (new_timeout) session.SetTimeout(*new_timeout);
|
|
if (new_proxy) session.SetProxies(*std::move(new_proxy));
|
|
if (new_ssl_opts) session.SetSslOptions(*new_ssl_opts);
|
|
|
|
MDEBUG("Submitting post request to " << url);
|
|
session.SetUrl(url);
|
|
session.SetHeader(header);
|
|
session.SetBody(std::move(body));
|
|
|
|
res = session.Post();
|
|
}
|
|
|
|
MDEBUG(url << ": " <<
|
|
(res.error.code != cpr::ErrorCode::OK ? res.error.message : res.status_line) <<
|
|
", sent " << res.uploaded_bytes << " bytes, received " << res.downloaded_bytes << " bytes in " <<
|
|
tools::friendly_duration(std::chrono::steady_clock::now() - start));
|
|
|
|
bytes_sent += res.uploaded_bytes;
|
|
bytes_received += res.downloaded_bytes;
|
|
}
|
|
|
|
if (res.error.code != cpr::ErrorCode::OK)
|
|
throw http_client_connect_error(res.error, "HTTP request failed: ");
|
|
|
|
if (res.status_code != 200)
|
|
throw http_client_response_error(true, res.status_code, "HTTP request failed; server returned " +
|
|
(res.status_line.empty() ? std::to_string(res.status_code) : res.status_line));
|
|
|
|
return res;
|
|
}
|
|
|
|
static const std::regex rexp_match_url{R"(^(?:([a-zA-Z][a-zA-Z0-9.+-]*)://)?(?:(\[[0-9a-fA-F:.]*\])|([^\[\]/:?]*))(?::(\d+))?)", std::regex::optimize};
|
|
// proto ipv6 ipv4/host port
|
|
|
|
std::tuple<std::string, std::string, uint16_t, std::string> http_client::parse_url(const std::string& url) {
|
|
std::string proto, host, uri;
|
|
uint16_t port = 0;
|
|
|
|
std::smatch result;
|
|
if (!std::regex_search(url, result, rexp_match_url))
|
|
throw http_client_error{"Failed to parse URL: " + url};
|
|
|
|
if (result[1].matched)
|
|
proto = result[1];
|
|
host = result[result[2].matched ? 2 : 3];
|
|
if (result[4].matched) {
|
|
auto port_str = result[4].str();
|
|
if (!tools::parse_int(port_str, port))
|
|
throw http_client_error{"Failed to parse URL: invalid port '" + port_str + "'"}; // i.e. most likely some port value > 65535
|
|
}
|
|
uri = result.suffix();
|
|
|
|
return {std::move(proto), std::move(host), port, std::move(uri)};
|
|
}
|
|
|
|
std::tuple<std::string, std::string, uint16_t, std::string> http_client::parse_base_url() const {
|
|
std::shared_lock lock{params_mutex};
|
|
return parse_url(base_url.str());
|
|
}
|
|
|
|
}
|