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