Skip to content

Commit 36b804b

Browse files
Merge pull request #340 from mcdonaldseanp/PE36763
(PE-36763) Add backwards-compat support for CURLOPT_PROTOCOLS_STR
2 parents 00ae30e + f3f1f0b commit 36b804b

File tree

7 files changed

+144
-25
lines changed

7 files changed

+144
-25
lines changed

.github/actions/run_cmake_and_make/action.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: docker_pull_and_make
1+
name: run_cmake_and_make
22

33
inputs:
44
pkg_suffix:

CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
cmake_minimum_required(VERSION 3.2.2)
22
project(leatherman VERSION 1.12.12)
33

4+
# Set the cmake policy that allows -D<LIB>_ROOT settings, so that you can point to a specific boost or curl
5+
cmake_policy(SET CMP0074 NEW)
6+
47
option(DYNAMICBASE "Add dynamicbase linker option" ON)
58
if (WIN32)
69
if (DYNAMICBASE)

curl/inc/leatherman/curl/client.hpp

+16-1
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,21 @@ namespace leatherman { namespace curl {
366366

367367
/**
368368
* Set and limit what protocols curl will support
369+
* @param client_protocols String indicating which protocol will be supported
370+
* (see more: http://curl.haxx.se/libcurl/c/CURLOPT_PROTOCOLS_STR.html)
371+
*/
372+
void set_supported_protocols(std::string const& client_protocols);
373+
374+
/**
375+
* WARNING: USING THE BITMASK IS TECHNICALLY DEPRECATED, USE THE STRING VERSION OF
376+
* THIS FUNCTION FOR ANY FUTURE USES OF set_supported_protocols.
377+
*
378+
* The bitmask version of this function is left here for compatibility purposes,
379+
* and only supports passing the CURLPROTO_HTTP/HTTPS bitmasks. DO NOT ADD SUPPORT FOR ANY OTHER
380+
* BITMASKS, NEW PROTOCOLS SHOULD BE USING cURL 8 AND PASSING THE STRING VERSION OF THE PROTOCOLS
381+
*
382+
* Parse the bitmask and convert to string, then use the string version of
383+
* set_supported_protocols to set and limit what protocols curl will support
369384
* @param client_protocols bitmask of CURLPROTO_*
370385
* (see more: http://curl.haxx.se/libcurl/c/CURLOPT_PROTOCOLS.html)
371386
*/
@@ -403,7 +418,7 @@ namespace leatherman { namespace curl {
403418
std::string _client_key;
404419
std::string _client_crl;
405420
std::string _proxy;
406-
long _client_protocols = CURLPROTO_ALL;
421+
std::string _client_protocols = "all";
407422

408423
response perform(http_method method, request const& req);
409424
void download_file_helper(request const& req,

curl/src/client.cc

+45-2
Original file line numberDiff line numberDiff line change
@@ -322,10 +322,30 @@ namespace leatherman { namespace curl {
322322
_client_key = client_key;
323323
}
324324

325-
void client::set_supported_protocols(long client_protocols) {
325+
void client::set_supported_protocols(string const& client_protocols) {
326326
_client_protocols = client_protocols;
327327
}
328328

329+
void client::set_supported_protocols(long client_protocols) {
330+
std::vector<std::string> protocols;
331+
332+
if (client_protocols == CURLPROTO_ALL) {
333+
_client_protocols = "all";
334+
} else {
335+
if (client_protocols & CURLPROTO_HTTPS) {
336+
protocols.push_back("https");
337+
}
338+
if (client_protocols & CURLPROTO_HTTP) {
339+
protocols.push_back("http");
340+
}
341+
// If there was anything that _wasn't_ http or https we throw
342+
if (client_protocols & ~(CURLPROTO_HTTP | CURLPROTO_HTTPS)) {
343+
throw http_exception(_("Passing CURLPROTO_* bitmasks to set supported protocols is deprecated! Upgrade to cURL 8 and use the string version of set_supported_protocols instead"));
344+
}
345+
}
346+
_client_protocols = boost::join(protocols, ",");
347+
}
348+
329349
void client::set_method(context& ctx, http_method method)
330350
{
331351
switch (method) {
@@ -469,7 +489,30 @@ namespace leatherman { namespace curl {
469489
}
470490

471491
void client::set_client_protocols(context& ctx) {
472-
curl_easy_setopt_maybe(ctx, CURLOPT_PROTOCOLS, _client_protocols);
492+
#ifdef CURLOPT_PROTOCOLS_STR
493+
curl_easy_setopt_maybe(ctx, CURLOPT_PROTOCOLS_STR, _client_protocols.c_str());
494+
#else
495+
// If leatherman is still being compiled with cURL versions less than 8, parse
496+
// the string versions of the protocols back to bit masks. Allow use of HTTP/HTTPS and
497+
// ALL, but otherwise fail and warn the user they need to upgrade cURL.
498+
long protocols = 0;
499+
if (_client_protocols == "all") {
500+
curl_easy_setopt_maybe(ctx, CURLOPT_PROTOCOLS, CURLPROTO_ALL);
501+
} else {
502+
std::vector<std::string> split_protocols;
503+
boost::split(split_protocols, _client_protocols, boost::is_any_of(","));
504+
for (auto proto = split_protocols.begin(); proto != split_protocols.end(); proto++) {
505+
if (*proto == "https") {
506+
protocols |= CURLPROTO_HTTPS;
507+
} else if (*proto == "http") {
508+
protocols |= CURLPROTO_HTTP;
509+
} else {
510+
throw http_exception(_("Passing CURLPROTO_* bitmasks to set supported protocols is deprecated! Upgrade to cURL 8 and use the string version of set_supported_protocols instead"));
511+
}
512+
}
513+
curl_easy_setopt_maybe(ctx, CURLOPT_PROTOCOLS, protocols);
514+
}
515+
#endif
473516
}
474517

475518
size_t client::read_body(char* buffer, size_t size, size_t count, void* ptr)

curl/tests/client_test.cc

+42-17
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fs::path find_matching_file(const boost::regex& re) {
3434
fs::recursive_directory_iterator(fs::current_path()),
3535
fs::recursive_directory_iterator(),
3636
[re](const fs::path& f) { return boost::regex_match(f.filename().string(), re); });
37-
37+
3838
// throw exception, as this means that the matching file does not exist.
3939
if (file == fs::recursive_directory_iterator()) {
4040
throw std::runtime_error("matching file not found");
@@ -297,18 +297,43 @@ TEST_CASE("curl::client CA bundle and SSL setup") {
297297
}
298298

299299
SECTION("cURL should make an HTTP request with the specified HTTP protocol") {
300-
test_client.set_supported_protocols(CURLPROTO_HTTP);
300+
test_client.set_supported_protocols("http");
301+
auto resp = test_client.get(test_request);
302+
CURL* const& handle = test_client.get_handle();
303+
auto test_impl = reinterpret_cast<curl_impl* const>(handle);
304+
REQUIRE(test_impl->protocols == "http");
305+
}
306+
307+
SECTION("cURL should make an HTTP request with multiple specified protocols") {
308+
test_client.set_supported_protocols("http,https");
309+
auto resp = test_client.get(test_request);
310+
CURL* const& handle = test_client.get_handle();
311+
auto test_impl = reinterpret_cast<curl_impl* const>(handle);
312+
// Either order is fine
313+
REQUIRE((test_impl->protocols == "http,https" || test_impl->protocols == "https,http"));
314+
}
315+
316+
SECTION("leatherman should throw an error when multiple specified protocols include anything other than http, https") {
317+
REQUIRE_THROWS_AS(test_client.set_supported_protocols(CURLPROTO_HTTP | CURLPROTO_HTTPS | CURLPROTO_SFTP), http_exception);
318+
}
319+
320+
SECTION("cURL should make an HTTP request with the HTTPS protocol when given the CURLPROTO_HTTPS bitmask") {
321+
test_client.set_supported_protocols(CURLPROTO_HTTPS);
301322
auto resp = test_client.get(test_request);
302323
CURL* const& handle = test_client.get_handle();
303324
auto test_impl = reinterpret_cast<curl_impl* const>(handle);
304-
REQUIRE(test_impl->protocols == CURLPROTO_HTTP);
325+
REQUIRE(test_impl->protocols == "https");
326+
}
327+
328+
SECTION("leatherman should throw an error when any bitmask other than CURLPROTO_HTTP/HTTPS/ALL is given to set_supported_protocols") {
329+
REQUIRE_THROWS_AS(test_client.set_supported_protocols(CURLPROTO_SCP), http_exception);
305330
}
306331

307332
SECTION("cURL defaults to all protocols if no protocols are specified") {
308333
auto resp = test_client.get(test_request);
309334
CURL* const& handle = test_client.get_handle();
310335
auto test_impl = reinterpret_cast<curl_impl* const>(handle);
311-
REQUIRE(test_impl->protocols == CURLPROTO_ALL);
336+
REQUIRE(test_impl->protocols == "all");
312337
}
313338
}
314339

@@ -418,7 +443,7 @@ TEST_CASE("curl::client errors") {
418443
}
419444

420445
SECTION("client fails to make http call with https protocol only enabled") {
421-
test_client.set_supported_protocols(CURLPROTO_HTTPS);
446+
test_client.set_supported_protocols("https");
422447
test_impl->test_failure_mode = curl_impl::error_mode::protocol_error;
423448
REQUIRE_THROWS_AS(test_client.get(test_request), http_curl_setup_exception);
424449
}
@@ -427,7 +452,7 @@ TEST_CASE("curl::client errors") {
427452
TEST_CASE("curl::client download_file") {
428453
mock_client test_client;
429454
temp_directory temp_dir;
430-
fs::path temp_dir_path = fs::path(temp_dir.get_dir_name());
455+
fs::path temp_dir_path = fs::path(temp_dir.get_dir_name());
431456
CURL* const& handle = test_client.get_handle();
432457
auto test_impl = reinterpret_cast<curl_impl* const>(handle);
433458
std::string url = "https://download.com";
@@ -440,7 +465,7 @@ TEST_CASE("curl::client errors") {
440465

441466
test_client.set_ca_cert(ca_file);
442467
test_client.set_client_cert(cert_file, key_file);
443-
test_client.set_supported_protocols(CURLPROTO_HTTPS);
468+
test_client.set_supported_protocols("https");
444469

445470
std::string file_path = (temp_dir_path / "test_file").string();
446471
std::string token = "token";
@@ -455,7 +480,7 @@ TEST_CASE("curl::client errors") {
455480
REQUIRE(test_impl->cacert == ca_file);
456481
REQUIRE(test_impl->client_cert == cert_file);
457482
REQUIRE(test_impl->client_key == key_file);
458-
REQUIRE(test_impl->protocols == CURLPROTO_HTTPS);
483+
REQUIRE(test_impl->protocols == "https");
459484
REQUIRE(test_impl->connect_timeout == connect_timeout);
460485
REQUIRE(std::string(test_impl->header->data) == ("X-Authentication: " + token));
461486
if (test_impl->header->next) {
@@ -487,7 +512,7 @@ TEST_CASE("curl::client errors") {
487512
std::string url = "https://download_trigger_404.com";
488513
auto file_path = (temp_dir_path / "404_test_file").string();
489514
request req(url);
490-
test_client.download_file(req, file_path);
515+
test_client.download_file(req, file_path);
491516

492517
// now check that the file was actually downloaded and written with the right
493518
// contents.
@@ -507,7 +532,7 @@ TEST_CASE("curl::client errors") {
507532

508533
test_client.set_ca_cert(ca_file);
509534
test_client.set_client_cert(cert_file, key_file);
510-
test_client.set_supported_protocols(CURLPROTO_HTTPS);
535+
test_client.set_supported_protocols("https");
511536

512537
std::string file_path = (temp_dir_path / "test_file").string();
513538
std::string token = "token";
@@ -523,7 +548,7 @@ TEST_CASE("curl::client errors") {
523548
REQUIRE(test_impl->cacert == ca_file);
524549
REQUIRE(test_impl->client_cert == cert_file);
525550
REQUIRE(test_impl->client_key == key_file);
526-
REQUIRE(test_impl->protocols == CURLPROTO_HTTPS);
551+
REQUIRE(test_impl->protocols == "https");
527552
REQUIRE(test_impl->connect_timeout == connect_timeout);
528553
REQUIRE(std::string(test_impl->header->data) == ("X-Authentication: " + token));
529554
if (test_impl->header->next) {
@@ -561,11 +586,11 @@ TEST_CASE("curl::client errors") {
561586
auto file_path = (temp_dir_path / "404_test_file").string();
562587
request req(url);
563588
response res;
564-
test_client.download_file(req, file_path, res);
589+
test_client.download_file(req, file_path, res);
565590

566591
REQUIRE(res.status_code() == 404);
567592
REQUIRE(res.body() == "Not found");
568-
// check that the file was not downloaded
593+
// check that the file was not downloaded
569594
REQUIRE(!fs::exists(file_path));
570595
}
571596
}
@@ -574,7 +599,7 @@ TEST_CASE("curl::client errors") {
574599
TEST_CASE("curl::client download_file errors") {
575600
mock_client test_client;
576601
temp_directory temp_dir;
577-
fs::path temp_dir_path = fs::path(temp_dir.get_dir_name());
602+
fs::path temp_dir_path = fs::path(temp_dir.get_dir_name());
578603
CURL* const& handle = test_client.get_handle();
579604
auto test_impl = reinterpret_cast<curl_impl* const>(handle);
580605

@@ -600,7 +625,7 @@ TEST_CASE("curl::client download_file errors") {
600625
SECTION("when curl_easy_perform fails due to a CURLE_WRITE_ERROR, but the temporary file is removed, an http_file_operation_exception is thrown") {
601626
std::string file_path = (temp_dir_path / "file").string();
602627
request req("");
603-
test_impl->test_failure_mode = curl_impl::error_mode::easy_perform_write_error;
628+
test_impl->test_failure_mode = curl_impl::error_mode::easy_perform_write_error;
604629
REQUIRE_THROWS_AS_WITH(
605630
test_client.download_file(req, file_path),
606631
http_file_operation_exception,
@@ -610,7 +635,7 @@ TEST_CASE("curl::client download_file errors") {
610635
SECTION("when curl_easy_perform fails for reasons other than a CURLE_WRITE_ERROR, but the temporary file is removed, only the errbuf message is contained in the thrown http_file_download_exception") {
611636
std::string file_path = (temp_dir_path / "file").string();
612637
request req("");
613-
test_impl->test_failure_mode = curl_impl::error_mode::easy_perform_error;
638+
test_impl->test_failure_mode = curl_impl::error_mode::easy_perform_error;
614639
REQUIRE_THROWS_AS_WITH(
615640
test_client.download_file(req, file_path),
616641
http_file_download_exception,
@@ -623,7 +648,7 @@ TEST_CASE("curl::client download_file errors") {
623648
SECTION("when renaming the temporary file to the user-provided file path fails, an http_file_operation_exception is thrown") {
624649
std::string file_path = (temp_dir_path / "file").string();
625650
request req("https://download.com");
626-
test_impl->trigger_external_failure = remove_temp_file;
651+
test_impl->trigger_external_failure = remove_temp_file;
627652
REQUIRE_THROWS_AS_WITH(
628653
test_client.download_file(req, file_path),
629654
http_file_operation_exception,

curl/tests/mock_curl.cc

+36-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
#include <stdio.h>
55
#include <array>
66
#include <algorithm>
7+
#include <vector>
78
#include "mock_curl.hpp"
9+
#include <boost/algorithm/string.hpp>
810

911
using namespace std;
1012

@@ -105,6 +107,8 @@ CURL *curl_easy_init()
105107
CURLcode curl_easy_setopt(CURL *handle, CURLoption option, ...)
106108
{
107109
auto h = reinterpret_cast<curl_impl*>(handle);
110+
long curlopt;
111+
std::vector<std::string> given_protocols;
108112
va_list vl;
109113
#pragma clang diagnostic push
110114
#pragma clang diagnostic ignored "-Wvarargs"
@@ -259,10 +263,39 @@ CURLcode curl_easy_setopt(CURL *handle, CURLoption option, ...)
259263
va_end(vl);
260264
return CURLE_COULDNT_CONNECT;
261265
}
262-
h->protocols = va_arg(vl, long);
266+
// If still compiling with cURL versions less than 8, CURLOPT_PROTOCOLS
267+
// will recieve a bitmask as the param, but the protocol entry type is
268+
// a string. Using the string type avoids the need for additional special
269+
// logic on creating and checking the value of this entry, but it also
270+
// means now we need to parse the bitmasks back to strings.
271+
//
272+
// When compiling with cURL 8 or newer the client code will use the string
273+
// version of the CURLOPT_PROTOCOLS function, so this logic won't be used.
274+
curlopt = va_arg(vl, int);
275+
276+
if (curlopt == CURLPROTO_ALL) {
277+
h->protocols = "all";
278+
} else {
279+
if (curlopt & CURLPROTO_HTTPS) {
280+
given_protocols.push_back("https");
281+
}
282+
if (curlopt & CURLPROTO_HTTP) {
283+
given_protocols.push_back("http") ;
284+
}
285+
h->protocols = boost::join(given_protocols,",");
286+
}
287+
break;
288+
#ifdef CURLOPT_PROTOCOLS_STR
289+
case CURLOPT_PROTOCOLS_STR:
290+
if (h->test_failure_mode == curl_impl::error_mode::protocol_error) {
291+
va_end(vl);
292+
return CURLE_COULDNT_CONNECT;
293+
}
294+
h->protocols = va_arg(vl, char*);
263295
break;
296+
#endif
264297
case CURLOPT_ERRORBUFFER:
265-
h->errbuf = va_arg(vl, char*);
298+
h->errbuf = va_arg(vl, char*);
266299
break;
267300
default:
268301
break;
@@ -283,7 +316,7 @@ CURLcode curl_easy_perform(CURL * easy_handle)
283316
}
284317
if (h->test_failure_mode == curl_impl::error_mode::easy_perform_error) {
285318
if (h->errbuf) {
286-
strcpy(h->errbuf, "easy perform failed");
319+
strcpy(h->errbuf, "easy perform failed");
287320
}
288321
return CURLE_COULDNT_CONNECT;
289322
}

curl/tests/mock_curl.hpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ struct curl_impl
5959
void* read_data; // Where to read the request body from
6060

6161
std::string request_url, cookie, cacert, client_cert, client_key, client_crl, proxy;
62-
long protocols;
62+
std::string protocols;
6363
long connect_timeout;
6464
http_method method = http_method::get;
6565

0 commit comments

Comments
 (0)