@@ -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
3336using 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+
101238int 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