lib/CurlWrapper.h (132 lines of code) (raw):
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with 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.
*/
#pragma once
#include <assert.h>
#include <curl/curl.h>
#include <string>
namespace pulsar {
struct CurlInitializer {
CurlInitializer() { curl_global_init(CURL_GLOBAL_ALL); }
~CurlInitializer() { curl_global_cleanup(); }
};
static CurlInitializer curlInitializer;
class CurlWrapper {
public:
CurlWrapper() noexcept {}
~CurlWrapper() {
if (handle_) {
curl_easy_cleanup(handle_);
}
}
char* escape(const std::string& s) const {
assert(handle_);
return curl_easy_escape(handle_, s.c_str(), s.length());
}
// It must be called before calling other methods
bool init() {
handle_ = curl_easy_init();
return handle_ != nullptr;
}
struct Options {
std::string method;
std::string postFields;
std::string userAgent;
int timeoutInSeconds{0};
int maxLookupRedirects{-1};
};
struct TlsContext {
std::string trustCertsFilePath;
bool validateHostname{true};
bool allowInsecure{false};
std::string certPath;
std::string keyPath;
};
struct Result {
CURLcode code;
std::string responseData;
long responseCode;
std::string redirectUrl;
std::string error;
std::string serverError;
};
Result get(const std::string& url, const std::string& header, const Options& options,
const TlsContext* tlsContext) const;
private:
CURL* handle_;
struct CurlListGuard {
curl_slist*& headers;
CurlListGuard(curl_slist*& headers) : headers(headers) {}
~CurlListGuard() {
if (headers) {
curl_slist_free_all(headers);
}
}
};
};
inline CurlWrapper::Result CurlWrapper::get(const std::string& url, const std::string& header,
const Options& options, const TlsContext* tlsContext) const {
assert(handle_);
curl_easy_setopt(handle_, CURLOPT_URL, url.c_str());
if (!options.postFields.empty()) {
curl_easy_setopt(handle_, CURLOPT_CUSTOMREQUEST, "POST");
curl_easy_setopt(handle_, CURLOPT_POSTFIELDS, options.postFields.c_str());
}
// Write response
curl_easy_setopt(
handle_, CURLOPT_WRITEFUNCTION,
+[](char* buffer, size_t size, size_t nitems, void* outstream) -> size_t {
static_cast<std::string*>(outstream)->append(buffer, size * nitems);
return size * nitems;
});
std::string response;
curl_easy_setopt(handle_, CURLOPT_WRITEDATA, &response);
// New connection is made for each call
curl_easy_setopt(handle_, CURLOPT_FRESH_CONNECT, 1L);
curl_easy_setopt(handle_, CURLOPT_FORBID_REUSE, 1L);
// Skipping signal handling - results in timeouts not honored during the DNS lookup
// Without this config, Curl_resolv_timeout might crash in multi-threads environment
curl_easy_setopt(handle_, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(handle_, CURLOPT_TIMEOUT, options.timeoutInSeconds);
if (!options.userAgent.empty()) {
curl_easy_setopt(handle_, CURLOPT_USERAGENT, options.userAgent.c_str());
}
curl_easy_setopt(handle_, CURLOPT_FAILONERROR, 1L);
// Redirects
curl_easy_setopt(handle_, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(handle_, CURLOPT_MAXREDIRS, options.maxLookupRedirects);
char errorBuffer[CURL_ERROR_SIZE] = "";
curl_easy_setopt(handle_, CURLOPT_ERRORBUFFER, errorBuffer);
curl_slist* headers = nullptr;
CurlListGuard headersGuard{headers};
if (!header.empty()) {
headers = curl_slist_append(headers, header.c_str());
curl_easy_setopt(handle_, CURLOPT_HTTPHEADER, headers);
}
if (tlsContext) {
CURLcode code;
code = curl_easy_setopt(handle_, CURLOPT_SSLENGINE, nullptr);
if (code != CURLE_OK) {
return {code, "", -1, "",
"Unable to load SSL engine for url " + url + ": " + curl_easy_strerror(code)};
}
code = curl_easy_setopt(handle_, CURLOPT_SSLENGINE_DEFAULT, 1L);
if (code != CURLE_OK) {
return {code, "", -1, "",
"Unable to load SSL engine as default for url " + url + ": " + curl_easy_strerror(code)};
}
curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYHOST, tlsContext->validateHostname ? 1L : 0L);
curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYPEER, tlsContext->allowInsecure ? 0L : 1L);
if (!tlsContext->trustCertsFilePath.empty()) {
curl_easy_setopt(handle_, CURLOPT_CAINFO, tlsContext->trustCertsFilePath.c_str());
}
if (!tlsContext->certPath.empty() && !tlsContext->keyPath.empty()) {
curl_easy_setopt(handle_, CURLOPT_SSLCERT, tlsContext->certPath.c_str());
curl_easy_setopt(handle_, CURLOPT_SSLKEY, tlsContext->keyPath.c_str());
}
}
auto res = curl_easy_perform(handle_);
long responseCode;
curl_easy_getinfo(handle_, CURLINFO_RESPONSE_CODE, &responseCode);
Result result{res, response, responseCode, "", "", std::string(errorBuffer)};
if (responseCode == 307 || responseCode == 302 || responseCode == 301) {
char* url;
curl_easy_getinfo(handle_, CURLINFO_REDIRECT_URL, &url);
// `url` is null when the host of the redirect URL cannot be resolved
if (url) {
result.redirectUrl = url;
}
}
return result;
}
} // namespace pulsar