Skip to content

Commit f9a83ad

Browse files
photonlibospakkah
andauthored
feat(http): support per-operation proxy URL (alibaba#1201) (alibaba#1204) (alibaba#1205)
Signed-off-by: huangzhaoxiang.hzx <huangzhaoxiang.hzx@alibaba-inc.com> Co-authored-by: huangzhaoxiang.hzx <huangzhaoxiang.hzx@alibaba-inc.com> Co-authored-by: Zhaoxiang Huang <114486509+pakkah@users.noreply.github.com>
1 parent 26cf54c commit f9a83ad

4 files changed

Lines changed: 261 additions & 1 deletion

File tree

net/http/client.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ class ClientImpl : public Client {
214214
LOG_ERROR_RETURN(ETIMEDOUT, ROUNDTRIP_FAILED, "connection timedout");
215215
auto &req = op->req;
216216
ISocketStream* s;
217-
if (op->enable_proxy && !m_proxy_url.empty())
217+
if (op->enable_proxy && !op->proxy_url.empty())
218+
s = get_dialer().dial(op->proxy_url, tmo.timeout());
219+
else if (op->enable_proxy && !m_proxy_url.empty())
218220
s = get_dialer().dial(m_proxy_url, tmo.timeout());
219221
else if (!op->uds_path.empty())
220222
s = get_dialer().dial(op->uds_path, tmo.timeout());

net/http/client.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,24 @@ class Client : public Object {
8585
void set_enable_proxy(bool enable) {
8686
enable_proxy = enable;
8787
}
88+
// Set per-operation proxy URL, takes precedence over client-level proxy.
89+
// Automatically enables proxy and rebuilds request line to absolute URI format.
90+
void set_proxy(std::string_view proxy) {
91+
proxy_url.from_string(proxy);
92+
if (!enable_proxy) {
93+
// redirect() handles scheme+host completion and absolute URI rebuild
94+
req.redirect(req.verb(), req.target(), true);
95+
enable_proxy = true;
96+
}
97+
}
98+
const StoredURL* get_proxy() {
99+
return &proxy_url;
100+
}
101+
// Clear per-operation proxy, fall back to client-level proxy.
102+
// Does not change enable_proxy or request line format.
103+
void clear_proxy() {
104+
proxy_url.clear();
105+
}
88106
int call() {
89107
if (!_client) return -1;
90108
return _client->call(this);
@@ -107,6 +125,8 @@ class Client : public Object {
107125

108126
protected:
109127
Client* _client;
128+
StoredURL proxy_url; // Per-operation proxy URL (takes precedence over
129+
// client-level proxy)
110130
const void *body_buffer = nullptr;
111131
size_t body_buffer_size = 0;
112132

net/http/test/client_function_test.cpp

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,240 @@ TEST(http_client, set_proxy_with_auth) {
808808
EXPECT_EQ(client->get_proxy()->port(), 8080);
809809
}
810810

811+
// Verify that Operation::set_proxy/clear_proxy manage operation-level proxy state
812+
TEST(http_client, operation_set_clear_proxy) {
813+
// Client without proxy
814+
auto client = new_http_client();
815+
DEFER(delete client);
816+
817+
auto op = client->new_operation(Verb::GET, "http://example.com/path");
818+
DEFER(client->destroy_operation(op));
819+
820+
EXPECT_TRUE(op->get_proxy()->empty());
821+
EXPECT_FALSE(op->enable_proxy);
822+
823+
// Operation::set_proxy should enable proxy at operation level
824+
op->set_proxy("http://proxy1:8080");
825+
EXPECT_FALSE(op->get_proxy()->empty());
826+
EXPECT_EQ(op->get_proxy()->host(), "proxy1");
827+
EXPECT_EQ(op->get_proxy()->port(), 8080);
828+
EXPECT_TRUE(op->enable_proxy);
829+
830+
// clear_proxy should clear operation-level proxy but keep enable_proxy
831+
op->clear_proxy();
832+
EXPECT_TRUE(op->get_proxy()->empty());
833+
EXPECT_TRUE(op->enable_proxy);
834+
}
835+
836+
// Verify set_proxy/clear_proxy keep request line unchanged when enable_proxy is already true
837+
TEST(http_client, operation_set_clear_proxy_no_rebuild) {
838+
auto client = new_http_client();
839+
DEFER(delete client);
840+
client->set_proxy("http://default-proxy:8080");
841+
842+
auto op = client->new_operation(Verb::GET, "http://example.com/path");
843+
DEFER(client->destroy_operation(op));
844+
845+
// Client-level proxy is set, so enable_proxy is already true
846+
EXPECT_TRUE(op->enable_proxy);
847+
auto target_before = std::string(op->req.target());
848+
849+
// Set operation-level proxy; should not rebuild request line
850+
op->set_proxy("http://op-proxy:9090");
851+
EXPECT_EQ(op->req.target(), target_before);
852+
EXPECT_FALSE(op->get_proxy()->empty());
853+
EXPECT_EQ(op->get_proxy()->host(), "op-proxy");
854+
EXPECT_EQ(op->get_proxy()->port(), 9090);
855+
856+
// Clear operation-level proxy; request line still unchanged
857+
op->clear_proxy();
858+
EXPECT_TRUE(op->get_proxy()->empty());
859+
EXPECT_EQ(op->req.target(), target_before);
860+
EXPECT_TRUE(op->enable_proxy);
861+
}
862+
863+
// Verify set_proxy/clear_proxy rebuild request line between relative and absolute URI
864+
TEST(http_client, operation_set_proxy_rebuilds_request_line) {
865+
auto client = new_http_client();
866+
DEFER(delete client);
867+
// No client-level proxy
868+
869+
auto op = client->new_operation(Verb::GET, "http://example.com/path?query=1");
870+
DEFER(client->destroy_operation(op));
871+
872+
// Before set_proxy: request line should be relative path
873+
EXPECT_FALSE(op->enable_proxy);
874+
EXPECT_EQ(op->req.target(), "/path?query=1");
875+
876+
// After set_proxy: request line should be absolute URI
877+
op->set_proxy("http://myproxy:3128");
878+
EXPECT_TRUE(op->enable_proxy);
879+
EXPECT_EQ(op->req.target(), "http://example.com/path?query=1");
880+
881+
// After clear_proxy: enable_proxy still true (request line unchanged),
882+
// but no proxy is configured
883+
op->clear_proxy();
884+
EXPECT_TRUE(op->enable_proxy);
885+
EXPECT_EQ(op->req.target(), "http://example.com/path?query=1");
886+
EXPECT_TRUE(op->get_proxy()->empty());
887+
}
888+
889+
// E2E test: different operations use different proxies through the same client
890+
namespace {
891+
int op_proxy_director(void* src_, Request& src, Request& dst) {
892+
auto source_server = (ISocketServer*)src_;
893+
// src.target() may be absolute URI (via proxy) or relative path (direct)
894+
// Parse it to extract the path portion for forwarding
895+
URL src_url(src.target());
896+
auto url = to_url(source_server, src_url.path());
897+
dst.reset(src.verb(), url);
898+
for (auto kv = src.headers.begin(); kv != src.headers.end(); kv++) {
899+
if (kv.first() != "Host")
900+
dst.headers.insert(kv.first(), kv.second(), 1);
901+
}
902+
return 0;
903+
}
904+
int op_proxy_modifier(void* ctx, Response& src, Response& dst) {
905+
dst.set_result(src.status_code());
906+
for (auto kv : src.headers) {
907+
dst.headers.insert(kv.first, kv.second);
908+
}
909+
// Add X-Via header to identify which proxy handled the request
910+
if (ctx) {
911+
dst.headers.insert("X-Via", static_cast<const char*>(ctx));
912+
}
913+
return 0;
914+
}
915+
int op_proxy_echo_handler(void*, Request& req, Response& resp, std::string_view) {
916+
// Verify the request path was correctly forwarded
917+
EXPECT_EQ(req.target(), "/echo");
918+
// Echo: read the request body and send it back as-is
919+
auto body_len = req.headers.content_length();
920+
resp.set_result(200);
921+
resp.headers.content_length(body_len);
922+
char buf[4096];
923+
ssize_t n;
924+
while ((n = req.read(buf, sizeof(buf))) > 0) {
925+
resp.write(buf, n);
926+
}
927+
return 0;
928+
}
929+
} // anonymous namespace
930+
931+
TEST(http_client, operation_level_proxy_e2e) {
932+
//--------start source server ------------
933+
auto source_server = new_tcp_socket_server();
934+
source_server->timeout(1000UL*1000);
935+
source_server->bind_v4localhost();
936+
source_server->listen();
937+
DEFER(delete source_server);
938+
auto source_http_server = new_http_server();
939+
DEFER(delete source_http_server);
940+
source_http_server->add_handler({nullptr, &op_proxy_echo_handler});
941+
source_server->set_handler(source_http_server->get_connection_handler());
942+
source_server->start_loop();
943+
944+
photon::thread_sleep(1);
945+
946+
//--------start proxy server A ------------
947+
auto proxy_client_a = new_http_client();
948+
DEFER(delete proxy_client_a);
949+
auto proxy_server_a_tcp = new_tcp_socket_server();
950+
proxy_server_a_tcp->timeout(1000UL*1000);
951+
proxy_server_a_tcp->bind_v4localhost();
952+
proxy_server_a_tcp->listen();
953+
DEFER(delete proxy_server_a_tcp);
954+
auto proxy_server_a = new_http_server();
955+
DEFER(delete proxy_server_a);
956+
auto proxy_handler_a = new_proxy_handler(
957+
{source_server, &op_proxy_director},
958+
{(void*)"proxy-a", &op_proxy_modifier}, proxy_client_a);
959+
proxy_server_a->add_handler(proxy_handler_a);
960+
proxy_server_a_tcp->set_handler(proxy_server_a->get_connection_handler());
961+
proxy_server_a_tcp->start_loop();
962+
963+
//--------start proxy server B ------------
964+
auto proxy_client_b = new_http_client();
965+
DEFER(delete proxy_client_b);
966+
auto proxy_server_b_tcp = new_tcp_socket_server();
967+
proxy_server_b_tcp->timeout(1000UL*1000);
968+
proxy_server_b_tcp->bind_v4localhost();
969+
proxy_server_b_tcp->listen();
970+
DEFER(delete proxy_server_b_tcp);
971+
auto proxy_server_b = new_http_server();
972+
DEFER(delete proxy_server_b);
973+
auto proxy_handler_b = new_proxy_handler(
974+
{source_server, &op_proxy_director},
975+
{(void*)"proxy-b", &op_proxy_modifier}, proxy_client_b);
976+
proxy_server_b->add_handler(proxy_handler_b);
977+
proxy_server_b_tcp->set_handler(proxy_server_b->get_connection_handler());
978+
proxy_server_b_tcp->start_loop();
979+
980+
//--------client with no default proxy --------
981+
auto client = new_http_client();
982+
DEFER(delete client);
983+
984+
// Operation 1: use proxy A
985+
auto op1 = client->new_operation(Verb::POST, to_url(source_server, "/echo"));
986+
DEFER(client->destroy_operation(op1));
987+
op1->set_proxy(to_url(proxy_server_a_tcp, "/"));
988+
std::string body1 = "1234567890";
989+
op1->req.headers.content_length(body1.size());
990+
auto writer1 = [&](Request* req) -> ssize_t {
991+
return req->write(body1.data(), body1.size());
992+
};
993+
op1->body_writer = writer1;
994+
int ret1 = op1->call();
995+
EXPECT_EQ(0, ret1);
996+
// Verify request went through proxy A
997+
EXPECT_EQ(op1->resp.headers["X-Via"], "proxy-a");
998+
// Verify echo body matches request body
999+
char buf1[4096] = {};
1000+
ret1 = op1->resp.read(buf1, sizeof(buf1));
1001+
EXPECT_EQ(ret1, (ssize_t)body1.size());
1002+
EXPECT_EQ(std::string_view(buf1, ret1), body1);
1003+
1004+
// Operation 2: use proxy B
1005+
auto op2 = client->new_operation(Verb::POST, to_url(source_server, "/echo"));
1006+
DEFER(client->destroy_operation(op2));
1007+
op2->set_proxy(to_url(proxy_server_b_tcp, "/"));
1008+
std::string body2 = "abcdefghij";
1009+
op2->req.headers.content_length(body2.size());
1010+
auto writer2 = [&](Request* req) -> ssize_t {
1011+
return req->write(body2.data(), body2.size());
1012+
};
1013+
op2->body_writer = writer2;
1014+
int ret2 = op2->call();
1015+
EXPECT_EQ(0, ret2);
1016+
// Verify request went through proxy B
1017+
EXPECT_EQ(op2->resp.headers["X-Via"], "proxy-b");
1018+
// Verify echo body matches request body
1019+
char buf2[4096] = {};
1020+
ret2 = op2->resp.read(buf2, sizeof(buf2));
1021+
EXPECT_EQ(ret2, (ssize_t)body2.size());
1022+
EXPECT_EQ(std::string_view(buf2, ret2), body2);
1023+
1024+
// Operation 3: direct connection (no proxy)
1025+
auto op3 = client->new_operation(Verb::POST, to_url(source_server, "/echo"));
1026+
DEFER(client->destroy_operation(op3));
1027+
std::string body3 = "ABCDEFGHIJ";
1028+
op3->req.headers.content_length(body3.size());
1029+
auto writer3 = [&](Request* req) -> ssize_t {
1030+
return req->write(body3.data(), body3.size());
1031+
};
1032+
op3->body_writer = writer3;
1033+
int ret3 = op3->call();
1034+
EXPECT_EQ(0, ret3);
1035+
// Verify request went directly (no X-Via header)
1036+
EXPECT_TRUE(op3->resp.headers["X-Via"].empty());
1037+
EXPECT_EQ(200, op3->resp.status_code());
1038+
// Verify echo body matches request body
1039+
char buf3[4096] = {};
1040+
ret3 = op3->resp.read(buf3, sizeof(buf3));
1041+
EXPECT_EQ(ret3, (ssize_t)body3.size());
1042+
EXPECT_EQ(std::string_view(buf3, ret3), body3);
1043+
}
1044+
8111045
// Only for manual test
8121046
// TEST(http_client, proxy) {
8131047
// auto client = new_http_client();

net/http/url.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ class StoredURL : public URL {
103103
auto u = strndup(url.data(), url.size());
104104
return URL::from_string({u, url.size()});
105105
}
106+
void clear() {
107+
free((void*)m_url);
108+
m_url = nullptr;
109+
}
106110
~StoredURL() {
107111
free((void*)m_url);
108112
}

0 commit comments

Comments
 (0)