Skip to content

Commit

Permalink
Merge pull request #1647 from vpodzime/master-proxy_auth
Browse files Browse the repository at this point in the history
Proxy basic authentication support
  • Loading branch information
oldgiova committed Jul 25, 2024
2 parents 50aa411 + 819d3d1 commit 5d86ffd
Show file tree
Hide file tree
Showing 5 changed files with 340 additions and 18 deletions.
5 changes: 4 additions & 1 deletion src/common/http.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,14 @@ 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);
expected::ExpectedString URLDecode(const string &value);

string JoinOneUrl(const string &prefix, const string &url);

Expand Down
50 changes: 43 additions & 7 deletions src/common/http/http.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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.rfind("@");
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(":");
Expand Down Expand Up @@ -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;
}
Expand All @@ -173,6 +188,27 @@ 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<char>(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] == '/') {
Expand Down
37 changes: 35 additions & 2 deletions src/common/http/platform/beast/http.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,29 @@ 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 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();
}
req.SetHeader("Proxy-Authorization", "Basic " + ex_encoded_creds.value());

return error::NoError;
}

error::Error Client::HandleProxySetup() {
secondary_req_.reset();

Expand All @@ -448,7 +471,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");
}
Expand All @@ -464,6 +487,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") {
Expand All @@ -484,7 +512,7 @@ error::Error Client::HandleProxySetup() {
request_ = make_shared<OutgoingRequest>();
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");
}
Expand All @@ -499,6 +527,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") {
Expand Down
164 changes: 157 additions & 7 deletions tests/src/common/http_proxy_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";

Expand All @@ -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();

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -239,7 +244,7 @@ class HttpProxyHttpTest : public HttpProxyTest {
public:
void SetUp() override {
StartPlainServer();
StartProxy();
StartProxy(false);
}
};

Expand Down Expand Up @@ -420,7 +425,7 @@ TEST_F(HttpProxyHttpTest, WrongTarget) {
class HttpProxyHttpsTest : public HttpProxyTest {
public:
void SetUp() override {
StartProxy();
StartProxy(false);
StartTlsServer();
}
};
Expand Down Expand Up @@ -627,7 +632,7 @@ class HttpsProxyHttpTest : public HttpProxyTest {
public:
void SetUp() override {
StartPlainServer();
StartTlsProxy();
StartTlsProxy(false);
}
};

Expand Down Expand Up @@ -795,7 +800,7 @@ class HttpsProxyHttpsTest : public HttpProxyTest {
public:
void SetUp() override {
StartTlsServer();
StartTlsProxy();
StartTlsProxy(false);
}
};

Expand Down Expand Up @@ -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<http::OutgoingRequest>();
req->SetMethod(http::Method::GET);
req->SetAddress("http://127.0.0.1:" TEST_PORT "/index.html");
vector<uint8_t> 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<io::ByteWriter>(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<http::OutgoingRequest>();
req->SetMethod(http::Method::GET);
req->SetAddress("https://localhost:" TEST_TLS_PORT "/index.html");
vector<uint8_t> 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<io::ByteWriter>(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<http::OutgoingRequest>();
req->SetMethod(http::Method::GET);
req->SetAddress("https://localhost:" TEST_TLS_PORT "/index.html");
vector<uint8_t> 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<io::ByteWriter>(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");
}
Loading

0 comments on commit 5d86ffd

Please sign in to comment.