Skip to content

Commit 787bfd0

Browse files
[fix] Add the curl wrapper to avoid inconsistent curl options (#313)
### Motivation When libcurl is used in `AuthOauth2`, the `CURLOPT_NOSIGNAL` option is not set, i.e. it will be the default value so that the `Curl_resolv_timeout` function might crash in multi-threading environment. ``` #2 0xf630 in _L_unlock_13 from /lib64/libpthread.so.0 (0x34) #3 0x2e6c7f in Curl_failf from /usr/local/bin/***/libpulsar.so (0x6f) #4 0x30a285 in Curl_resolv_timeout from /usr/local/bin/***/libpulsar.so (0x95) ``` Since there are many duplicated code when calling curl C APIs, it's hard to notice that `CURLOPT_NOSIGNAL` is not configured in `AuthOauth2`. ### Modifications Introduce a `CurlWrapper` class that sets the same options to reduce the duplicated code and adapting consistent behaviors unless a few options.
1 parent 84ac6fb commit 787bfd0

File tree

5 files changed

+283
-237
lines changed

5 files changed

+283
-237
lines changed

lib/CurlWrapper.h

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
#pragma once
20+
21+
#include <assert.h>
22+
#include <curl/curl.h>
23+
24+
#include <string>
25+
26+
namespace pulsar {
27+
28+
struct CurlInitializer {
29+
CurlInitializer() { curl_global_init(CURL_GLOBAL_ALL); }
30+
~CurlInitializer() { curl_global_cleanup(); }
31+
};
32+
static CurlInitializer curlInitializer;
33+
34+
class CurlWrapper {
35+
public:
36+
CurlWrapper() noexcept {}
37+
~CurlWrapper() {
38+
if (handle_) {
39+
curl_easy_cleanup(handle_);
40+
}
41+
}
42+
43+
char* escape(const std::string& s) const {
44+
assert(handle_);
45+
return curl_easy_escape(handle_, s.c_str(), s.length());
46+
}
47+
48+
// It must be called before calling other methods
49+
bool init() {
50+
handle_ = curl_easy_init();
51+
return handle_ != nullptr;
52+
}
53+
54+
struct Options {
55+
std::string method;
56+
std::string postFields;
57+
std::string userAgent;
58+
int timeoutInSeconds{0};
59+
int maxLookupRedirects{-1};
60+
};
61+
62+
struct TlsContext {
63+
std::string trustCertsFilePath;
64+
bool validateHostname{true};
65+
bool allowInsecure{false};
66+
std::string certPath;
67+
std::string keyPath;
68+
};
69+
70+
struct Result {
71+
CURLcode code;
72+
std::string responseData;
73+
long responseCode;
74+
std::string redirectUrl;
75+
std::string error;
76+
std::string serverError;
77+
};
78+
79+
Result get(const std::string& url, const std::string& header, const Options& options,
80+
const TlsContext* tlsContext) const;
81+
82+
private:
83+
CURL* handle_;
84+
85+
struct CurlListGuard {
86+
curl_slist*& headers;
87+
88+
CurlListGuard(curl_slist*& headers) : headers(headers) {}
89+
~CurlListGuard() {
90+
if (headers) {
91+
curl_slist_free_all(headers);
92+
}
93+
}
94+
};
95+
};
96+
97+
inline CurlWrapper::Result CurlWrapper::get(const std::string& url, const std::string& header,
98+
const Options& options, const TlsContext* tlsContext) const {
99+
assert(handle_);
100+
curl_easy_setopt(handle_, CURLOPT_URL, url.c_str());
101+
102+
if (!options.postFields.empty()) {
103+
curl_easy_setopt(handle_, CURLOPT_CUSTOMREQUEST, "POST");
104+
curl_easy_setopt(handle_, CURLOPT_POSTFIELDS, options.postFields.c_str());
105+
}
106+
107+
// Write response
108+
curl_easy_setopt(
109+
handle_, CURLOPT_WRITEFUNCTION,
110+
+[](char* buffer, size_t size, size_t nitems, void* outstream) -> size_t {
111+
static_cast<std::string*>(outstream)->append(buffer, size * nitems);
112+
return size * nitems;
113+
});
114+
std::string response;
115+
curl_easy_setopt(handle_, CURLOPT_WRITEDATA, &response);
116+
117+
// New connection is made for each call
118+
curl_easy_setopt(handle_, CURLOPT_FRESH_CONNECT, 1L);
119+
curl_easy_setopt(handle_, CURLOPT_FORBID_REUSE, 1L);
120+
121+
// Skipping signal handling - results in timeouts not honored during the DNS lookup
122+
// Without this config, Curl_resolv_timeout might crash in multi-threads environment
123+
curl_easy_setopt(handle_, CURLOPT_NOSIGNAL, 1L);
124+
125+
curl_easy_setopt(handle_, CURLOPT_TIMEOUT, options.timeoutInSeconds);
126+
if (!options.userAgent.empty()) {
127+
curl_easy_setopt(handle_, CURLOPT_USERAGENT, options.userAgent.c_str());
128+
}
129+
curl_easy_setopt(handle_, CURLOPT_FAILONERROR, 1L);
130+
131+
// Redirects
132+
curl_easy_setopt(handle_, CURLOPT_FOLLOWLOCATION, 1L);
133+
curl_easy_setopt(handle_, CURLOPT_MAXREDIRS, options.maxLookupRedirects);
134+
135+
char errorBuffer[CURL_ERROR_SIZE] = "";
136+
curl_easy_setopt(handle_, CURLOPT_ERRORBUFFER, errorBuffer);
137+
138+
curl_slist* headers = nullptr;
139+
CurlListGuard headersGuard{headers};
140+
if (!header.empty()) {
141+
headers = curl_slist_append(headers, header.c_str());
142+
curl_easy_setopt(handle_, CURLOPT_HTTPHEADER, headers);
143+
}
144+
145+
if (tlsContext) {
146+
CURLcode code;
147+
code = curl_easy_setopt(handle_, CURLOPT_SSLENGINE, nullptr);
148+
if (code != CURLE_OK) {
149+
return {code, "", -1, "",
150+
"Unable to load SSL engine for url " + url + ": " + curl_easy_strerror(code)};
151+
}
152+
code = curl_easy_setopt(handle_, CURLOPT_SSLENGINE_DEFAULT, 1L);
153+
if (code != CURLE_OK) {
154+
return {code, "", -1, "",
155+
"Unable to load SSL engine as default for url " + url + ": " + curl_easy_strerror(code)};
156+
}
157+
curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYHOST, tlsContext->validateHostname ? 1L : 0L);
158+
curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYPEER, tlsContext->allowInsecure ? 0L : 1L);
159+
if (!tlsContext->trustCertsFilePath.empty()) {
160+
curl_easy_setopt(handle_, CURLOPT_CAINFO, tlsContext->trustCertsFilePath.c_str());
161+
}
162+
if (!tlsContext->certPath.empty() && !tlsContext->keyPath.empty()) {
163+
curl_easy_setopt(handle_, CURLOPT_SSLCERT, tlsContext->certPath.c_str());
164+
curl_easy_setopt(handle_, CURLOPT_SSLKEY, tlsContext->keyPath.c_str());
165+
}
166+
}
167+
168+
auto res = curl_easy_perform(handle_);
169+
long responseCode;
170+
curl_easy_getinfo(handle_, CURLINFO_RESPONSE_CODE, &responseCode);
171+
Result result{res, response, responseCode, "", "", std::string(errorBuffer)};
172+
if (responseCode == 307 || responseCode == 302 || responseCode == 301) {
173+
char* url;
174+
curl_easy_getinfo(handle_, CURLINFO_REDIRECT_URL, &url);
175+
result.redirectUrl = url;
176+
}
177+
return result;
178+
}
179+
180+
} // namespace pulsar

lib/HTTPLookupService.cc

Lines changed: 33 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@
1818
*/
1919
#include "HTTPLookupService.h"
2020

21-
#include <curl/curl.h>
2221
#include <pulsar/Version.h>
2322

2423
#include <boost/property_tree/json_parser.hpp>
2524
#include <boost/property_tree/ptree.hpp>
2625

26+
#include "CurlWrapper.h"
2727
#include "ExecutorService.h"
2828
#include "Int64SerDes.h"
2929
#include "LogUtils.h"
@@ -46,16 +46,6 @@ const static std::string ADMIN_PATH_V2 = "/admin/v2/";
4646
const static std::string PARTITION_METHOD_NAME = "partitions";
4747
const static int NUMBER_OF_LOOKUP_THREADS = 1;
4848

49-
static inline bool needRedirection(long code) { return (code == 307 || code == 302 || code == 301); }
50-
51-
HTTPLookupService::CurlInitializer::CurlInitializer() {
52-
// Once per application - https://curl.haxx.se/mail/lib-2015-11/0052.html
53-
curl_global_init(CURL_GLOBAL_ALL);
54-
}
55-
HTTPLookupService::CurlInitializer::~CurlInitializer() { curl_global_cleanup(); }
56-
57-
HTTPLookupService::CurlInitializer HTTPLookupService::curlInitializer;
58-
5949
HTTPLookupService::HTTPLookupService(ServiceNameResolver &serviceNameResolver,
6050
const ClientConfiguration &clientConfiguration,
6151
const AuthenticationPtr &authData)
@@ -182,11 +172,6 @@ Future<Result, SchemaInfo> HTTPLookupService::getSchema(const TopicNamePtr &topi
182172
return promise.getFuture();
183173
}
184174

185-
static size_t curlWriteCallback(void *contents, size_t size, size_t nmemb, void *responseDataPtr) {
186-
((std::string *)responseDataPtr)->append((char *)contents, size * nmemb);
187-
return size * nmemb;
188-
}
189-
190175
void HTTPLookupService::handleNamespaceTopicsHTTPRequest(NamespaceTopicsPromise promise,
191176
const std::string completeUrl) {
192177
std::string responseData;
@@ -209,111 +194,61 @@ Result HTTPLookupService::sendHTTPRequest(std::string completeUrl, std::string &
209194
uint16_t reqCount = 0;
210195
Result retResult = ResultOk;
211196
while (++reqCount <= maxLookupRedirects_) {
212-
CURL *handle;
213-
CURLcode res;
214-
std::string version = std::string("Pulsar-CPP-v") + PULSAR_VERSION_STR;
215-
handle = curl_easy_init();
216-
217-
if (!handle) {
218-
LOG_ERROR("Unable to curl_easy_init for url " << completeUrl);
219-
// No curl_easy_cleanup required since handle not initialized
220-
return ResultLookupError;
221-
}
222-
// set URL
223-
curl_easy_setopt(handle, CURLOPT_URL, completeUrl.c_str());
224-
225-
// Write callback
226-
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, curlWriteCallback);
227-
curl_easy_setopt(handle, CURLOPT_WRITEDATA, &responseData);
228-
229-
// New connection is made for each call
230-
curl_easy_setopt(handle, CURLOPT_FRESH_CONNECT, 1L);
231-
curl_easy_setopt(handle, CURLOPT_FORBID_REUSE, 1L);
232-
233-
// Skipping signal handling - results in timeouts not honored during the DNS lookup
234-
curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L);
235-
236-
// Timer
237-
curl_easy_setopt(handle, CURLOPT_TIMEOUT, lookupTimeoutInSeconds_);
238-
239-
// Set User Agent
240-
curl_easy_setopt(handle, CURLOPT_USERAGENT, version.c_str());
241-
242-
// Fail if HTTP return code >=400
243-
curl_easy_setopt(handle, CURLOPT_FAILONERROR, 1L);
244-
245197
// Authorization data
246198
AuthenticationDataPtr authDataContent;
247199
Result authResult = authenticationPtr_->getAuthData(authDataContent);
248200
if (authResult != ResultOk) {
249201
LOG_ERROR("Failed to getAuthData: " << authResult);
250-
curl_easy_cleanup(handle);
251202
return authResult;
252203
}
253-
struct curl_slist *list = NULL;
254-
if (authDataContent->hasDataForHttp()) {
255-
list = curl_slist_append(list, authDataContent->getHttpHeaders().c_str());
204+
205+
CurlWrapper curl;
206+
if (!curl.init()) {
207+
LOG_ERROR("Unable to curl_easy_init for url " << completeUrl);
208+
return ResultLookupError;
256209
}
257-
curl_easy_setopt(handle, CURLOPT_HTTPHEADER, list);
258210

259-
// TLS
211+
std::unique_ptr<CurlWrapper::TlsContext> tlsContext;
260212
if (isUseTls_) {
261-
if (curl_easy_setopt(handle, CURLOPT_SSLENGINE, NULL) != CURLE_OK) {
262-
LOG_ERROR("Unable to load SSL engine for url " << completeUrl);
263-
curl_easy_cleanup(handle);
264-
return ResultConnectError;
265-
}
266-
if (curl_easy_setopt(handle, CURLOPT_SSLENGINE_DEFAULT, 1L) != CURLE_OK) {
267-
LOG_ERROR("Unable to load SSL engine as default, for url " << completeUrl);
268-
curl_easy_cleanup(handle);
269-
return ResultConnectError;
270-
}
271-
curl_easy_setopt(handle, CURLOPT_SSLCERTTYPE, "PEM");
272-
273-
if (tlsAllowInsecure_) {
274-
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L);
275-
} else {
276-
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 1L);
277-
}
278-
279-
if (!tlsTrustCertsFilePath_.empty()) {
280-
curl_easy_setopt(handle, CURLOPT_CAINFO, tlsTrustCertsFilePath_.c_str());
281-
}
282-
283-
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, tlsValidateHostname_ ? 1L : 0L);
284-
213+
tlsContext.reset(new CurlWrapper::TlsContext);
214+
tlsContext->trustCertsFilePath = tlsTrustCertsFilePath_;
215+
tlsContext->validateHostname = tlsValidateHostname_;
216+
tlsContext->allowInsecure = tlsAllowInsecure_;
285217
if (authDataContent->hasDataForTls()) {
286-
curl_easy_setopt(handle, CURLOPT_SSLCERT, authDataContent->getTlsCertificates().c_str());
287-
curl_easy_setopt(handle, CURLOPT_SSLKEY, authDataContent->getTlsPrivateKey().c_str());
218+
tlsContext->certPath = authDataContent->getTlsCertificates();
219+
tlsContext->keyPath = authDataContent->getTlsPrivateKey();
288220
} else {
289-
if (!tlsPrivateFilePath_.empty() && !tlsCertificateFilePath_.empty()) {
290-
curl_easy_setopt(handle, CURLOPT_SSLCERT, tlsCertificateFilePath_.c_str());
291-
curl_easy_setopt(handle, CURLOPT_SSLKEY, tlsPrivateFilePath_.c_str());
292-
}
221+
tlsContext->certPath = tlsCertificateFilePath_;
222+
tlsContext->keyPath = tlsPrivateFilePath_;
293223
}
294224
}
295225

296226
LOG_INFO("Curl [" << reqCount << "] Lookup Request sent for " << completeUrl);
227+
CurlWrapper::Options options;
228+
options.timeoutInSeconds = lookupTimeoutInSeconds_;
229+
options.userAgent = std::string("Pulsar-CPP-v") + PULSAR_VERSION_STR;
230+
options.maxLookupRedirects = 1; // redirection is implemented by the outer loop
231+
auto result = curl.get(completeUrl, authDataContent->getHttpHeaders(), options, tlsContext.get());
232+
const auto &error = result.error;
233+
if (!error.empty()) {
234+
LOG_ERROR(completeUrl << " failed: " << error);
235+
return ResultConnectError;
236+
}
297237

298-
// Make get call to server
299-
res = curl_easy_perform(handle);
300-
301-
curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &responseCode);
238+
responseData = result.responseData;
239+
responseCode = result.responseCode;
240+
auto res = result.code;
302241
LOG_INFO("Response received for url " << completeUrl << " responseCode " << responseCode
303242
<< " curl res " << res);
304243

305-
// Free header list
306-
curl_slist_free_all(list);
307-
244+
const auto &redirectUrl = result.redirectUrl;
308245
switch (res) {
309246
case CURLE_OK:
310247
if (responseCode == 200) {
311248
retResult = ResultOk;
312-
} else if (needRedirection(responseCode)) {
313-
char *url = NULL;
314-
curl_easy_getinfo(handle, CURLINFO_REDIRECT_URL, &url);
315-
LOG_INFO("Response from url " << completeUrl << " to new url " << url);
316-
completeUrl = url;
249+
} else if (!redirectUrl.empty()) {
250+
LOG_INFO("Response from url " << completeUrl << " to new url " << redirectUrl);
251+
completeUrl = redirectUrl;
317252
retResult = ResultLookupError;
318253
} else {
319254
retResult = ResultLookupError;
@@ -342,8 +277,7 @@ Result HTTPLookupService::sendHTTPRequest(std::string completeUrl, std::string &
342277
retResult = ResultLookupError;
343278
break;
344279
}
345-
curl_easy_cleanup(handle);
346-
if (!needRedirection(responseCode)) {
280+
if (redirectUrl.empty()) {
347281
break;
348282
}
349283
}

lib/HTTPLookupService.h

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,6 @@ using NamespaceTopicsPromisePtr = std::shared_ptr<NamespaceTopicsPromise>;
3131
using GetSchemaPromise = Promise<Result, SchemaInfo>;
3232

3333
class HTTPLookupService : public LookupService, public std::enable_shared_from_this<HTTPLookupService> {
34-
class CurlInitializer {
35-
public:
36-
CurlInitializer();
37-
~CurlInitializer();
38-
};
39-
static CurlInitializer curlInitializer;
40-
4134
enum RequestType
4235
{
4336
Lookup,

0 commit comments

Comments
 (0)