Skip to content

Commit c5f6c1b

Browse files
authored
feat(tls): add custom CA certificate support for TLSContext (alibaba#1529)
Add set_ca_cert() and set_ca_file() to TLSContext, allowing each HTTP client to specify custom CA certificates for peer verification, equivalent to curl's --cacert / --capath options. Changes: - tls-stream.h: add set_ca_cert(const char*) and set_ca_file(const char*, const char*) pure virtual methods to TLSContext interface - tls-stream.cpp: implement both methods using OpenSSL X509_STORE and SSL_CTX_load_verify_locations; add missing #include <openssl/pem.h>; add null check for set_ca_cert(nullptr) - client.h: document that HTTP clients within the same std::thread share TLS config and connection pool
1 parent 92ab5a2 commit c5f6c1b

6 files changed

Lines changed: 467 additions & 1 deletion

File tree

net/http/client.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,9 @@ class Client : public Object {
204204
std::vector<IPAddr> m_bind_ips;
205205
};
206206

207-
//A Client without cookie_jar would ignore all response-header "Set-Cookies"
207+
// Create an HTTP client. Without cookie_jar, "Set-Cookies" headers are ignored.
208+
// Note: HTTP clients within the same std::thread share TLS config and connection pool.
209+
// Use separate std::threads for different TLS configurations.
208210
Client* new_http_client(ICookieJar *cookie_jar = nullptr, TLSContext *tls_ctx = nullptr);
209211

210212
ICookieJar* new_simple_cookie_jar();

net/http/test/client_tls_test.cpp

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ limitations under the License.
1616

1717
#include <openssl/ssl.h>
1818

19+
#include <thread>
20+
1921
#include <photon/photon.h>
2022
#include <photon/common/alog.h>
2123
#include <photon/common/alog-stdstring.h>
@@ -29,6 +31,7 @@ limitations under the License.
2931
#include "to_url.h"
3032

3133
#include "../../test/cert-key.cpp"
34+
#include "../../security-context/test/test_cert_utils.h"
3235

3336
using namespace photon;
3437

@@ -98,6 +101,140 @@ TEST(http_client, DISABLED_SNI) {
98101
}
99102
#endif
100103

104+
// HTTP-level test: verify set_ca_cert works through the HTTP client.
105+
// Must run in a separate std::thread because the thread_local PooledDialer
106+
// caches the TLS context from previous tests; reusing it after the context
107+
// is freed would be a use-after-free.
108+
TEST(client_tls, http_with_ca_cert) {
109+
// Server: TLS + HTTP, using self-signed cert
110+
auto server_ctx = net::new_tls_context(cert_str, key_str, passphrase_str);
111+
DEFER(delete server_ctx);
112+
auto tcpserver = net::new_tls_server(server_ctx, net::new_tcp_socket_server(), true);
113+
DEFER(delete tcpserver);
114+
tcpserver->timeout(1000UL * 1000);
115+
ASSERT_EQ(0, tcpserver->bind_v4localhost());
116+
tcpserver->listen();
117+
118+
auto server = net::http::new_http_server();
119+
DEFER(delete server);
120+
server->add_handler({nullptr, &idiot_handler});
121+
tcpserver->set_handler(server->get_connection_handler());
122+
tcpserver->start_loop();
123+
124+
auto port = tcpserver->getsockname().port;
125+
int client_result = -1;
126+
int status_code = 0;
127+
std::string test_handle;
128+
photon::semaphore sem;
129+
130+
std::thread t([&, port] {
131+
photon::init(photon::INIT_EVENT_DEFAULT, photon::INIT_IO_NONE);
132+
DEFER(photon::fini());
133+
134+
// Client: separate TLSContext, load server's cert as CA
135+
auto client_ctx = net::new_tls_context();
136+
DEFER(delete client_ctx);
137+
if (client_ctx->set_ca_cert(cert_str) != 0) {
138+
sem.signal(1);
139+
return;
140+
}
141+
142+
auto client = net::http::new_http_client(nullptr, client_ctx);
143+
DEFER(delete client);
144+
145+
auto url = estring().appends("https://localhost:", port, "/test");
146+
auto op = client->new_operation(net::http::Verb::GET, url);
147+
DEFER(client->destroy_operation(op));
148+
op->req.headers.range(0, 19);
149+
client_result = op->call();
150+
if (client_result == 0) {
151+
status_code = op->resp.status_code();
152+
test_handle = std::string(op->resp.headers["Test_Handle"]);
153+
}
154+
sem.signal(1);
155+
});
156+
t.detach();
157+
sem.wait(1);
158+
159+
ASSERT_EQ(0, client_result);
160+
EXPECT_EQ(200, status_code);
161+
EXPECT_EQ("test", test_handle);
162+
}
163+
164+
// Verify HTTP clients with different CA configs are isolated across OS threads.
165+
// Each client runs in its own std::thread to get an independent PooledDialer,
166+
// avoiding use-after-free on the thread_local dialer's cached TLS context.
167+
TEST(client_tls, http_client_cross_thread_isolation) {
168+
auto server_ctx = net::new_tls_context(cert_str, key_str, passphrase_str);
169+
DEFER(delete server_ctx);
170+
auto tcpserver = net::new_tls_server(server_ctx, net::new_tcp_socket_server(), true);
171+
DEFER(delete tcpserver);
172+
tcpserver->timeout(1000UL * 1000);
173+
ASSERT_EQ(0, tcpserver->bind_v4localhost());
174+
tcpserver->listen();
175+
176+
auto server = net::http::new_http_server();
177+
DEFER(delete server);
178+
server->add_handler({nullptr, &idiot_handler});
179+
tcpserver->set_handler(server->get_connection_handler());
180+
tcpserver->start_loop();
181+
182+
auto port = tcpserver->getsockname().port;
183+
184+
// Client A with correct CA (separate thread for fresh PooledDialer)
185+
int thread_a_result = -1;
186+
int thread_a_status = 0;
187+
photon::semaphore sem_a;
188+
std::thread ta([&, port] {
189+
photon::init(photon::INIT_EVENT_DEFAULT, photon::INIT_IO_NONE);
190+
DEFER(photon::fini());
191+
192+
auto ctx_a = net::new_tls_context();
193+
DEFER(delete ctx_a);
194+
ctx_a->set_ca_cert(cert_str);
195+
auto client_a = net::http::new_http_client(nullptr, ctx_a);
196+
DEFER(delete client_a);
197+
198+
auto url = estring().appends("https://127.0.0.1:", port, "/test");
199+
auto op = client_a->new_operation(net::http::Verb::GET, url);
200+
DEFER(client_a->destroy_operation(op));
201+
op->retry = 0;
202+
thread_a_result = op->call();
203+
if (thread_a_result == 0)
204+
thread_a_status = op->resp.status_code();
205+
sem_a.signal(1);
206+
});
207+
ta.detach();
208+
sem_a.wait(1);
209+
ASSERT_EQ(0, thread_a_result);
210+
EXPECT_EQ(200, thread_a_status);
211+
212+
// Client B with wrong CA (separate thread)
213+
auto wrong_ca_pem = generate_different_self_signed_cert();
214+
int thread_b_result = 0;
215+
photon::semaphore sem_b;
216+
std::thread tb([&, port] {
217+
photon::init(photon::INIT_EVENT_DEFAULT, photon::INIT_IO_NONE);
218+
DEFER(photon::fini());
219+
220+
auto ctx_b = net::new_tls_context();
221+
DEFER(delete ctx_b);
222+
ctx_b->set_ca_cert(wrong_ca_pem.c_str());
223+
auto client_b = net::http::new_http_client(nullptr, ctx_b);
224+
DEFER(delete client_b);
225+
226+
auto url = estring().appends("https://127.0.0.1:", port, "/test");
227+
auto op = client_b->new_operation(net::http::Verb::GET, url);
228+
DEFER(client_b->destroy_operation(op));
229+
op->retry = 0;
230+
thread_b_result = op->call();
231+
sem_b.signal(1);
232+
});
233+
tb.detach();
234+
sem_b.wait(1);
235+
EXPECT_NE(0, thread_b_result);
236+
}
237+
101238
int main(int argc, char** arg) {
102239
LOG_DEBUG("Begin test");
103240
if (photon::init(photon::INIT_EVENT_DEFAULT, photon::INIT_IO_NONE))

0 commit comments

Comments
 (0)