From 3fb2a64957514b8421a30533898ba8d5c9362145 Mon Sep 17 00:00:00 2001 From: Vratislav Podzimek Date: Thu, 18 Jul 2024 13:22:18 +0200 Subject: [PATCH 1/5] feat: Add support for user:password@host in BreakDownUrl() And some basic unit tests for the function. Ticket: MEN-7402 Changelog: none Signed-off-by: Vratislav Podzimek --- src/common/http.hpp | 4 ++- src/common/http/http.cpp | 29 ++++++++++++++----- tests/src/common/http_test.cpp | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/common/http.hpp b/src/common/http.hpp index 202b79599..f67e6f9b7 100644 --- a/src/common/http.hpp +++ b/src/common/http.hpp @@ -127,9 +127,11 @@ struct BrokenDownUrl { string host; int port {-1}; string path; + string username; + string password; }; -error::Error BreakDownUrl(const string &url, BrokenDownUrl &address); +error::Error BreakDownUrl(const string &url, BrokenDownUrl &address, bool with_auth = false); string URLEncode(const string &value); diff --git a/src/common/http/http.cpp b/src/common/http/http.cpp index bce00619d..b87fde748 100644 --- a/src/common/http/http.cpp +++ b/src/common/http/http.cpp @@ -92,7 +92,7 @@ string MethodToString(Method method) { return "INVALID_METHOD"; } -error::Error BreakDownUrl(const string &url, BrokenDownUrl &address) { +error::Error BreakDownUrl(const string &url, BrokenDownUrl &address, bool with_auth) { const string url_split {"://"}; auto split_index = url.find(url_split); @@ -115,11 +115,24 @@ error::Error BreakDownUrl(const string &url, BrokenDownUrl &address) { address.path = tmp.substr(split_index); } - if (address.host.find("@") != string::npos) { - address = {}; - return error::Error( - make_error_condition(errc::not_supported), - "URL Username and password is not supported"); + auto auth_index = address.host.find("@"); + if (auth_index != string::npos) { + if (!with_auth) { + address = {}; + return error::Error( + make_error_condition(errc::not_supported), + "URL Username and password is not supported"); + } + auto user_password = address.host.substr(0, auth_index); + address.host = address.host.substr(auth_index + 1); + auto u_pw_sep_index = user_password.find(":"); + if (u_pw_sep_index == string::npos) { + // no password + address.username = std::move(user_password); + } else { + address.username = user_password.substr(0, u_pw_sep_index); + address.password = user_password.substr(u_pw_sep_index + 1); + } } split_index = address.host.find(":"); @@ -149,7 +162,9 @@ error::Error BreakDownUrl(const string &url, BrokenDownUrl &address) { log::Trace( "URL broken down into (protocol: " + address.protocol + "), (host: " + address.host - + "), (port: " + to_string(address.port) + "), (path: " + address.path + ")"); + + "), (port: " + to_string(address.port) + "), (path: " + address.path + ")," + + "(username: " + address.username + + "), (password: " + (address.password == "" ? "" : "OMITTED") + ")"); return error::NoError; } diff --git a/tests/src/common/http_test.cpp b/tests/src/common/http_test.cpp index 447ca6189..d39c29096 100644 --- a/tests/src/common/http_test.cpp +++ b/tests/src/common/http_test.cpp @@ -100,6 +100,59 @@ TEST(URLTest, URLEncode) { EXPECT_EQ(ret, "so%2Fare%2Fslashes"); } +TEST(URLTest, BreakDownUrl) { + { + http::BrokenDownUrl url; + auto err = http::BreakDownUrl("https://easy.example.com/trivial", url); + EXPECT_EQ(err, error::NoError); + EXPECT_EQ(url.protocol, "https"); + EXPECT_EQ(url.host, "easy.example.com"); + EXPECT_EQ(url.path, "/trivial"); + } + + { + http::BrokenDownUrl url; + auto err = http::BreakDownUrl("https://cfengine.example.com:5308/trivial", url); + EXPECT_EQ(err, error::NoError); + EXPECT_EQ(url.protocol, "https"); + EXPECT_EQ(url.host, "cfengine.example.com"); + EXPECT_EQ(url.path, "/trivial"); + EXPECT_EQ(url.port, 5308); + } + + { + http::BrokenDownUrl url; + auto err = http::BreakDownUrl("https://admin:admin@cfengine.example.com:5308/trivial", url); + ASSERT_NE(err, error::NoError); + EXPECT_THAT(err.String(), testing::HasSubstr("Username and password is not supported")); + } + + { + http::BrokenDownUrl url; + auto err = http::BreakDownUrl( + "https://admin:cfeadmin@cfengine.example.com:5308/trivial", url, true); + EXPECT_EQ(err, error::NoError); + EXPECT_EQ(url.protocol, "https"); + EXPECT_EQ(url.host, "cfengine.example.com"); + EXPECT_EQ(url.path, "/trivial"); + EXPECT_EQ(url.port, 5308); + EXPECT_EQ(url.username, "admin"); + EXPECT_EQ(url.password, "cfeadmin"); + } + + { + http::BrokenDownUrl url; + auto err = http::BreakDownUrl("https://admin@cfengine.example.com:5308/trivial", url, true); + EXPECT_EQ(err, error::NoError); + EXPECT_EQ(url.protocol, "https"); + EXPECT_EQ(url.host, "cfengine.example.com"); + EXPECT_EQ(url.path, "/trivial"); + EXPECT_EQ(url.port, 5308); + EXPECT_EQ(url.username, "admin"); + EXPECT_EQ(url.password, ""); + } +} + void TestBasicRequestAndResponse() { TestEventLoop loop; From 5cfc99d0ae8eeb2b61af1a12095b9203dd57a9c7 Mon Sep 17 00:00:00 2001 From: Vratislav Podzimek Date: Thu, 18 Jul 2024 13:37:51 +0200 Subject: [PATCH 2/5] fix: Enable support for user:password@host in proxy connections Ticket: MEN-7402 Changelog: Basic authentication (https://user:password@host/) is now supported for proxy URLs and connections Signed-off-by: Vratislav Podzimek --- src/common/http/platform/beast/http.cpp | 29 ++++- tests/src/common/http_proxy_test.cpp | 164 +++++++++++++++++++++++- 2 files changed, 184 insertions(+), 9 deletions(-) diff --git a/src/common/http/platform/beast/http.cpp b/src/common/http/platform/beast/http.cpp index 3675fa18a..8b9ef1e7f 100644 --- a/src/common/http/platform/beast/http.cpp +++ b/src/common/http/platform/beast/http.cpp @@ -439,6 +439,21 @@ error::Error Client::AsyncCall( return error::NoError; } +static inline error::Error AddProxyAuthHeader(OutgoingRequest &req, BrokenDownUrl &proxy_address) { + if (proxy_address.username == "") { + // nothing to do + return error::NoError; + } + auto creds = proxy_address.username + ":" + proxy_address.password; + auto ex_encoded_creds = crypto::EncodeBase64(common::ByteVectorFromString(creds)); + if (!ex_encoded_creds) { + return ex_encoded_creds.error(); + } + req.SetHeader("Proxy-Authorization", "Basic " + ex_encoded_creds.value()); + + return error::NoError; +} + error::Error Client::HandleProxySetup() { secondary_req_.reset(); @@ -448,7 +463,7 @@ error::Error Client::HandleProxySetup() { if (http_proxy_ != "" && !HostNameMatchesNoProxy(request_->address_.host, no_proxy_)) { // Make a modified proxy request. BrokenDownUrl proxy_address; - auto err = BreakDownUrl(http_proxy_, proxy_address); + auto err = BreakDownUrl(http_proxy_, proxy_address, true); if (err != error::NoError) { return err.WithContext("HTTP proxy URL is invalid"); } @@ -464,6 +479,11 @@ error::Error Client::HandleProxySetup() { request_->address_.port = proxy_address.port; request_->address_.protocol = proxy_address.protocol; + err = AddProxyAuthHeader(*request_, proxy_address); + if (err != error::NoError) { + return err; + } + if (proxy_address.protocol == "https") { socket_mode_ = SocketMode::Tls; } else if (proxy_address.protocol == "http") { @@ -484,7 +504,7 @@ error::Error Client::HandleProxySetup() { request_ = make_shared(); request_->SetMethod(Method::CONNECT); BrokenDownUrl proxy_address; - auto err = BreakDownUrl(https_proxy_, proxy_address); + auto err = BreakDownUrl(https_proxy_, proxy_address, true); if (err != error::NoError) { return err.WithContext("HTTPS proxy URL is invalid"); } @@ -499,6 +519,11 @@ error::Error Client::HandleProxySetup() { request_->address_.port = proxy_address.port; request_->address_.protocol = proxy_address.protocol; + err = AddProxyAuthHeader(*request_, proxy_address); + if (err != error::NoError) { + return err; + } + if (proxy_address.protocol == "https") { socket_mode_ = SocketMode::Tls; } else if (proxy_address.protocol == "http") { diff --git a/tests/src/common/http_proxy_test.cpp b/tests/src/common/http_proxy_test.cpp index 7b7f8de5c..e6ddf9e20 100644 --- a/tests/src/common/http_proxy_test.cpp +++ b/tests/src/common/http_proxy_test.cpp @@ -35,6 +35,8 @@ using namespace std; #define TEST_PROXY_PORT "8003" #define TEST_TLS_PROXY_PORT "8004" #define TEST_CLOSED_PORT "8005" +#define TEST_PROXY_USER "testuser" +#define TEST_PROXY_PASSWORD "T3stP433w0rd" namespace common = mender::common; namespace error = mender::common::error; @@ -93,7 +95,7 @@ class HttpProxyTest : public testing::Test { ASSERT_EQ(error::NoError, err); } - void StartProxy() { + void StartProxy(bool authenticating) { const string tiny_proxy = "/usr/bin/tinyproxy"; const string nc = "/bin/nc"; @@ -113,6 +115,9 @@ Allow 127.0.0.1 MaxClients 10 StartServers 1 )"; + if (authenticating) { + config << "BasicAuth " TEST_PROXY_USER " " TEST_PROXY_PASSWORD << endl; + } ASSERT_TRUE(config.good()); config.close(); @@ -189,8 +194,8 @@ connect = localhost:)" + connect_port EnsureUp(client); } - void StartTlsProxy() { - StartProxy(); + void StartTlsProxy(bool authenticating) { + StartProxy(authenticating); StartTlsTunnel(TEST_TLS_PROXY_PORT, TEST_PROXY_PORT); } @@ -239,7 +244,7 @@ class HttpProxyHttpTest : public HttpProxyTest { public: void SetUp() override { StartPlainServer(); - StartProxy(); + StartProxy(false); } }; @@ -420,7 +425,7 @@ TEST_F(HttpProxyHttpTest, WrongTarget) { class HttpProxyHttpsTest : public HttpProxyTest { public: void SetUp() override { - StartProxy(); + StartProxy(false); StartTlsServer(); } }; @@ -627,7 +632,7 @@ class HttpsProxyHttpTest : public HttpProxyTest { public: void SetUp() override { StartPlainServer(); - StartTlsProxy(); + StartTlsProxy(false); } }; @@ -795,7 +800,7 @@ class HttpsProxyHttpsTest : public HttpProxyTest { public: void SetUp() override { StartTlsServer(); - StartTlsProxy(); + StartTlsProxy(false); } }; @@ -984,3 +989,148 @@ TEST_F(HttpsProxyHttpsTest, WrongCertificate) { EXPECT_TRUE(client_hit_header); } + +class HttpAuthProxyHttpTest : public HttpProxyTest { +public: + void SetUp() override { + StartPlainServer(); + StartProxy(true); + } +}; + +TEST_F(HttpAuthProxyHttpTest, BasicRequestAndResponse) { + bool client_hit_header = false; + bool client_hit_body = false; + + http::ClientConfig client_config { + .http_proxy = + "http://" TEST_PROXY_USER ":" TEST_PROXY_PASSWORD "@127.0.0.1:" TEST_PROXY_PORT, + }; + http::Client client(client_config, loop); + auto req = make_shared(); + req->SetMethod(http::Method::GET); + req->SetAddress("http://127.0.0.1:" TEST_PORT "/index.html"); + vector received; + auto err = client.AsyncCall( + req, + [&client_hit_header, &received](http::ExpectedIncomingResponsePtr exp_resp) { + ASSERT_TRUE(exp_resp) << exp_resp.error().String(); + auto resp = exp_resp.value(); + EXPECT_EQ(resp->GetStatusCode(), 200); + client_hit_header = true; + + auto body_writer = make_shared(received); + body_writer->SetUnlimited(true); + resp->SetBodyWriter(body_writer); + }, + [&client_hit_body, this](http::ExpectedIncomingResponsePtr exp_resp) { + ASSERT_TRUE(exp_resp) << exp_resp.error().String(); + client_hit_body = true; + loop.Stop(); + }); + ASSERT_EQ(error::NoError, err); + + loop.Run(); + + EXPECT_TRUE(plain_server_hit_header); + EXPECT_TRUE(plain_server_hit_body); + EXPECT_TRUE(client_hit_header); + EXPECT_TRUE(client_hit_body); + EXPECT_EQ(common::StringFromByteVector(received), "Test\r\n"); +} + +class HttpAuthProxyHttpsTest : public HttpProxyTest { +public: + void SetUp() override { + StartProxy(true); + StartTlsServer(); + } +}; + +TEST_F(HttpAuthProxyHttpsTest, BasicRequestAndResponse) { + bool client_hit_header = false; + bool client_hit_body = false; + + http::ClientConfig client_config { + .server_cert_path = "server.localhost.crt", + .https_proxy = + "http://" TEST_PROXY_USER ":" TEST_PROXY_PASSWORD "@localhost:" TEST_PROXY_PORT, + }; + http::Client client(client_config, loop); + auto req = make_shared(); + req->SetMethod(http::Method::GET); + req->SetAddress("https://localhost:" TEST_TLS_PORT "/index.html"); + vector received; + auto err = client.AsyncCall( + req, + [&client_hit_header, &received](http::ExpectedIncomingResponsePtr exp_resp) { + ASSERT_TRUE(exp_resp) << exp_resp.error().String(); + auto resp = exp_resp.value(); + EXPECT_EQ(resp->GetStatusCode(), 200); + client_hit_header = true; + + auto body_writer = make_shared(received); + body_writer->SetUnlimited(true); + resp->SetBodyWriter(body_writer); + }, + [&client_hit_body, this](http::ExpectedIncomingResponsePtr exp_resp) { + ASSERT_TRUE(exp_resp) << exp_resp.error().String(); + client_hit_body = true; + loop.Stop(); + }); + ASSERT_EQ(error::NoError, err); + + loop.Run(); + + EXPECT_TRUE(client_hit_header); + EXPECT_TRUE(client_hit_body); + EXPECT_EQ(common::StringFromByteVector(received), "Test\r\n"); +} + +class HttpsAuthProxyHttpsTest : public HttpProxyTest { +public: + void SetUp() override { + StartTlsServer(); + StartTlsProxy(true); + } +}; + +TEST_F(HttpsAuthProxyHttpsTest, BasicRequestAndResponse) { + bool client_hit_header = false; + bool client_hit_body = false; + + http::ClientConfig client_config { + .server_cert_path = "server.localhost.crt", + .https_proxy = + "https://" TEST_PROXY_USER ":" TEST_PROXY_PASSWORD "@localhost:" TEST_TLS_PROXY_PORT, + }; + http::Client client(client_config, loop); + auto req = make_shared(); + req->SetMethod(http::Method::GET); + req->SetAddress("https://localhost:" TEST_TLS_PORT "/index.html"); + vector received; + auto err = client.AsyncCall( + req, + [&client_hit_header, &received](http::ExpectedIncomingResponsePtr exp_resp) { + ASSERT_TRUE(exp_resp) << exp_resp.error().String(); + auto resp = exp_resp.value(); + EXPECT_EQ(resp->GetStatusCode(), 200); + client_hit_header = true; + + auto body_writer = make_shared(received); + body_writer->SetUnlimited(true); + resp->SetBodyWriter(body_writer); + }, + [&client_hit_body, this](http::ExpectedIncomingResponsePtr exp_resp) { + ASSERT_TRUE(exp_resp) << exp_resp.error().String(); + client_hit_body = true; + loop.Stop(); + }); + ASSERT_EQ(error::NoError, err); + + loop.Run(); + + EXPECT_TRUE(client_hit_header); + EXPECT_TRUE(client_hit_body); + EXPECT_EQ(common::StringFromByteVector(received), "Test\r\n"); +} From 8574d4a5e6f955a46438e545bc4198a47304edf7 Mon Sep 17 00:00:00 2001 From: Vratislav Podzimek Date: Tue, 23 Jul 2024 14:27:45 +0200 Subject: [PATCH 3/5] fix: Support '@' in proxy authentication data To be backward compatible with Mender client 3. Ticket: MEN-7402 Changelog: none Signed-off-by: Vratislav Podzimek --- src/common/http/http.cpp | 2 +- tests/src/common/http_test.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/common/http/http.cpp b/src/common/http/http.cpp index b87fde748..205af93f3 100644 --- a/src/common/http/http.cpp +++ b/src/common/http/http.cpp @@ -115,7 +115,7 @@ error::Error BreakDownUrl(const string &url, BrokenDownUrl &address, bool with_a address.path = tmp.substr(split_index); } - auto auth_index = address.host.find("@"); + auto auth_index = address.host.rfind("@"); if (auth_index != string::npos) { if (!with_auth) { address = {}; diff --git a/tests/src/common/http_test.cpp b/tests/src/common/http_test.cpp index d39c29096..8b4cb86af 100644 --- a/tests/src/common/http_test.cpp +++ b/tests/src/common/http_test.cpp @@ -151,6 +151,32 @@ TEST(URLTest, BreakDownUrl) { EXPECT_EQ(url.username, "admin"); EXPECT_EQ(url.password, ""); } + + { + http::BrokenDownUrl url; + auto err = http::BreakDownUrl( + "https://admin:cfe@dmin@cfengine.example.com:5308/trivial", url, true); + EXPECT_EQ(err, error::NoError); + EXPECT_EQ(url.protocol, "https"); + EXPECT_EQ(url.host, "cfengine.example.com"); + EXPECT_EQ(url.path, "/trivial"); + EXPECT_EQ(url.port, 5308); + EXPECT_EQ(url.username, "admin"); + EXPECT_EQ(url.password, "cfe@dmin"); + } + + { + http::BrokenDownUrl url; + auto err = http::BreakDownUrl( + "https://admin@cfengine.com:cfe@dmin@cfengine.example.com:5308/trivial", url, true); + EXPECT_EQ(err, error::NoError); + EXPECT_EQ(url.protocol, "https"); + EXPECT_EQ(url.host, "cfengine.example.com"); + EXPECT_EQ(url.path, "/trivial"); + EXPECT_EQ(url.port, 5308); + EXPECT_EQ(url.username, "admin@cfengine.com"); + EXPECT_EQ(url.password, "cfe@dmin"); + } } void TestBasicRequestAndResponse() { From 659122070a7d3651945eab46af84e26117095cfe Mon Sep 17 00:00:00 2001 From: Vratislav Podzimek Date: Tue, 23 Jul 2024 14:47:47 +0200 Subject: [PATCH 4/5] chore: Add http::URLDecode() function Ticket: MEN-7402 Changelog: none Signed-off-by: Vratislav Podzimek --- src/common/http.hpp | 1 + src/common/http/http.cpp | 20 ++++++++++++++++++++ tests/src/common/http_test.cpp | 23 ++++++++++++++++++++++- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/common/http.hpp b/src/common/http.hpp index f67e6f9b7..a94a6331b 100644 --- a/src/common/http.hpp +++ b/src/common/http.hpp @@ -134,6 +134,7 @@ struct BrokenDownUrl { error::Error BreakDownUrl(const string &url, BrokenDownUrl &address, bool with_auth = false); string URLEncode(const string &value); +expected::ExpectedString URLDecode(const string &value); string JoinOneUrl(const string &prefix, const string &url); diff --git a/src/common/http/http.cpp b/src/common/http/http.cpp index 205af93f3..01c90791a 100644 --- a/src/common/http/http.cpp +++ b/src/common/http/http.cpp @@ -188,6 +188,26 @@ string URLEncode(const string &value) { return escaped.str(); } +expected::ExpectedString URLDecode(const string &value) { + stringstream unescaped; + + auto len = value.length(); + for (size_t i = 0; i < len; i++) { + if (value[i] != '%') { + unescaped << value[i]; + } else { + if ((i + 2 >= len) || !isxdigit(value[i + 1]) || !(isxdigit(value[i + 2]))) { + return expected::unexpected(MakeError(InvalidUrlError, "Incomplete % sequence in '" + value + "'")); + } + unsigned int num; + sscanf(value.substr(i + 1, 2).c_str(), "%x", &num); + unescaped << static_cast(num); + i += 2; + } + } + return unescaped.str(); +} + string JoinOneUrl(const string &prefix, const string &suffix) { auto prefix_end = prefix.cend(); while (prefix_end != prefix.cbegin() && prefix_end[-1] == '/') { diff --git a/tests/src/common/http_test.cpp b/tests/src/common/http_test.cpp index 8b4cb86af..3b724b4c5 100644 --- a/tests/src/common/http_test.cpp +++ b/tests/src/common/http_test.cpp @@ -89,15 +89,36 @@ class TestServer : public Server { } // namespace common } // namespace mender -TEST(URLTest, URLEncode) { +TEST(URLTest, URLEncodeDecode) { auto ret = http::URLEncode("all-supported_so~no~change.expected"); EXPECT_EQ(ret, "all-supported_so~no~change.expected"); + auto ex_dec = http::URLDecode(ret); + ASSERT_TRUE(ex_dec); + EXPECT_EQ(ex_dec.value(), "all-supported_so~no~change.expected"); ret = http::URLEncode("spaces are bad"); EXPECT_EQ(ret, "spaces%20are%20bad"); + ex_dec = http::URLDecode(ret); + ASSERT_TRUE(ex_dec); + EXPECT_EQ(ex_dec.value(), "spaces are bad"); ret = http::URLEncode("so/are/slashes"); EXPECT_EQ(ret, "so%2Fare%2Fslashes"); + ex_dec = http::URLDecode(ret); + ASSERT_TRUE(ex_dec); + EXPECT_EQ(ex_dec.value(), "so/are/slashes"); + + ex_dec = http::URLDecode("notrailing%"); + ASSERT_FALSE(ex_dec); + + ex_dec = http::URLDecode("notrailingshortcode%2"); + ASSERT_FALSE(ex_dec); + + ex_dec = http::URLDecode("noshortcode%2somewhere"); + ASSERT_FALSE(ex_dec); + + ex_dec = http::URLDecode("no%alone"); + ASSERT_FALSE(ex_dec); } TEST(URLTest, BreakDownUrl) { From 819d3d1b2fe1fa493006190285f5cd4466f9706a Mon Sep 17 00:00:00 2001 From: Vratislav Podzimek Date: Tue, 23 Jul 2024 14:58:58 +0200 Subject: [PATCH 5/5] fix: URL-decode proxy username and password In cases where the proxy username or password contains characters that cannot show up in URLs, they should be URL-encoded. This is to ensure backwards compatibility with the Mender client 3. Unfortunately, tinyproxy considers all special characters in the BasicAuth configuration entry as syntax error so we have no way to test this. Ticket: MEN-7402 Changelog: none Signed-off-by: Vratislav Podzimek --- src/common/http/http.cpp | 3 ++- src/common/http/platform/beast/http.cpp | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/common/http/http.cpp b/src/common/http/http.cpp index 01c90791a..0fa592611 100644 --- a/src/common/http/http.cpp +++ b/src/common/http/http.cpp @@ -197,7 +197,8 @@ expected::ExpectedString URLDecode(const string &value) { unescaped << value[i]; } else { if ((i + 2 >= len) || !isxdigit(value[i + 1]) || !(isxdigit(value[i + 2]))) { - return expected::unexpected(MakeError(InvalidUrlError, "Incomplete % sequence in '" + value + "'")); + return expected::unexpected( + MakeError(InvalidUrlError, "Incomplete % sequence in '" + value + "'")); } unsigned int num; sscanf(value.substr(i + 1, 2).c_str(), "%x", &num); diff --git a/src/common/http/platform/beast/http.cpp b/src/common/http/platform/beast/http.cpp index 8b9ef1e7f..3ea949416 100644 --- a/src/common/http/platform/beast/http.cpp +++ b/src/common/http/platform/beast/http.cpp @@ -444,7 +444,15 @@ static inline error::Error AddProxyAuthHeader(OutgoingRequest &req, BrokenDownUr // nothing to do return error::NoError; } - auto creds = proxy_address.username + ":" + proxy_address.password; + auto ex_dec_username = URLDecode(proxy_address.username); + auto ex_dec_password = URLDecode(proxy_address.password); + if (!ex_dec_username) { + return ex_dec_username.error(); + } + if (!ex_dec_password) { + return ex_dec_password.error(); + } + auto creds = ex_dec_username.value() + ":" + ex_dec_password.value(); auto ex_encoded_creds = crypto::EncodeBase64(common::ByteVectorFromString(creds)); if (!ex_encoded_creds) { return ex_encoded_creds.error();