emme coverage


Directory: src/
File: src/http2_client.c
Date: 2026-05-14 14:35:13
Exec Total Coverage
Lines: 22 292 7.5%
Functions: 3 20 15.0%
Branches: 8 164 4.9%

Line Branch Exec Source
1 /* http2_client.c - HTTP/2 client implementation using nghttp2
2 *
3 * This module implements an HTTP/2 client for making requests to upstream backends.
4 * It handles:
5 * - TLS connection establishment
6 * - nghttp2 session management (client mode)
7 * - HTTP/2 request/response framing
8 * - Stream multiplexing (single request per client for simplicity)
9 */
10
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <string.h>
14 #include <unistd.h>
15 #include <fcntl.h>
16 #include <errno.h>
17 #include <poll.h>
18 #include <sys/time.h>
19 #include <sys/socket.h>
20 #include <netinet/in.h>
21 #include <arpa/inet.h>
22 #include <netdb.h>
23 #include <openssl/ssl.h>
24 #include <openssl/err.h>
25 #include "http2_client.h"
26 #include "log.h"
27 #include "tls.h"
28 #include "metrics.h"
29
30 #define HTTP2_ALPN "h2"
31 #define HTTP2_ALPN_LEN 2
32 #define HTTP2_POLL_TIMEOUT_MS 5000
33
34 #define MAKE_NV(NAME, VALUE) \
35 (nghttp2_nv){(uint8_t *)(NAME), (uint8_t *)(VALUE), strlen(NAME), strlen(VALUE), NGHTTP2_NV_FLAG_NONE}
36 #define HTTP2_ALPN_LEN 2
37 #define HTTP2_POLL_TIMEOUT_MS 5000
38
39 #ifndef DEBUG_H2C
40 #define DEBUG_H2C 0
41 #endif
42
43 #define H2C_LOG(...) \
44 do { \
45 if (DEBUG_H2C) \
46 log_message(LOG_LEVEL_DEBUG, __VA_ARGS__); \
47 } while (0)
48
49 // nghttp2 callback: send data
50 static ssize_t http2_client_send_callback(nghttp2_session *session,
51 const uint8_t *data, size_t length,
52 int flags, void *user_data)
53 {
54 http2_client_t *client = (http2_client_t *)user_data;
55 (void)session;
56 (void)flags;
57
58 int n = SSL_write(client->ssl, data, (int)length);
59 if (n <= 0) {
60 int ssl_err = SSL_get_error(client->ssl, n);
61 if (ssl_err == SSL_ERROR_WANT_READ) {
62 client->want_read = 1;
63 return NGHTTP2_ERR_WOULDBLOCK;
64 } else if (ssl_err == SSL_ERROR_WANT_WRITE) {
65 client->want_write = 1;
66 return NGHTTP2_ERR_WOULDBLOCK;
67 }
68 log_message(LOG_LEVEL_ERROR, "HTTP/2 client SSL_write failed: %d", ssl_err);
69 return NGHTTP2_ERR_CALLBACK_FAILURE;
70 }
71
72 H2C_LOG("http2_client: sent %zu bytes", (size_t)n);
73 return n;
74 }
75
76 // nghttp2 callback: receive data
77 static ssize_t http2_client_recv_callback(nghttp2_session *session,
78 uint8_t *buf, size_t length,
79 int flags, void *user_data)
80 {
81 http2_client_t *client = (http2_client_t *)user_data;
82 (void)session;
83 (void)flags;
84
85 int n = SSL_read(client->ssl, buf, (int)length);
86 if (n <= 0) {
87 int ssl_err = SSL_get_error(client->ssl, n);
88 if (ssl_err == SSL_ERROR_WANT_READ) {
89 client->want_read = 1;
90 return NGHTTP2_ERR_WOULDBLOCK;
91 } else if (ssl_err == SSL_ERROR_WANT_WRITE) {
92 client->want_write = 1;
93 return NGHTTP2_ERR_WOULDBLOCK;
94 }
95 if (ssl_err == SSL_ERROR_ZERO_RETURN) {
96 return NGHTTP2_ERR_EOF;
97 }
98 log_message(LOG_LEVEL_ERROR, "HTTP/2 client SSL_read failed: %d", ssl_err);
99 return NGHTTP2_ERR_CALLBACK_FAILURE;
100 }
101
102 H2C_LOG("http2_client: received %d bytes", n);
103 return n;
104 }
105
106 // nghttp2 callback: on frame received
107 static int http2_client_on_frame_recv(nghttp2_session *session,
108 const nghttp2_frame *frame,
109 void *user_data)
110 {
111 http2_client_t *client = (http2_client_t *)user_data;
112 (void)session;
113
114 if (frame->hd.type == NGHTTP2_HEADERS &&
115 frame->hd.flags & NGHTTP2_FLAG_END_HEADERS) {
116 client->response_status = frame->headers.cat;
117 H2C_LOG("http2_client: received HEADERS, status=%d", client->response_status);
118 } else if (frame->hd.type == NGHTTP2_DATA) {
119 H2C_LOG("http2_client: received DATA frame, length=%d", frame->hd.length);
120 }
121
122 return 0;
123 }
124
125 // nghttp2 callback: on data chunk received
126 static int http2_client_on_data_chunk_recv(nghttp2_session *session,
127 uint8_t flags, int32_t stream_id,
128 const uint8_t *data, size_t len,
129 void *user_data)
130 {
131 http2_client_t *client = (http2_client_t *)user_data;
132 (void)session;
133 (void)flags;
134 (void)stream_id;
135
136 if (client->response_received + len <= HTTP2_CLIENT_BUFFER_SIZE) {
137 memcpy(client->response_buffer + client->response_received, data, len);
138 client->response_received += len;
139 H2C_LOG("http2_client: accumulated %zu bytes", client->response_received);
140 } else {
141 log_message(LOG_LEVEL_ERROR, "HTTP/2 client response buffer overflow");
142 return -1;
143 }
144
145 return 0;
146 }
147
148 // nghttp2 callback: on stream close
149 static int http2_client_on_stream_close(nghttp2_session *session, int32_t stream_id,
150 uint32_t error_code, void *user_data)
151 {
152 http2_client_t *client = (http2_client_t *)user_data;
153 (void)session;
154 (void)stream_id;
155 (void)error_code;
156
157 client->done = 1;
158 client->error_code = error_code;
159 H2C_LOG("http2_client: stream closed, error_code=%u", error_code);
160
161 return 0;
162 }
163
164 // nghttp2 callback: data provider read
165 static ssize_t http2_client_data_read(nghttp2_session *session, int32_t stream_id,
166 uint8_t *buf, size_t length,
167 uint32_t *data_flags,
168 nghttp2_data_source *source,
169 void *user_data)
170 {
171 http2_client_t *client = (http2_client_t *)user_data;
172 (void)session;
173 (void)stream_id;
174 (void)source;
175
176 size_t remaining = client->request_body_len - client->body_sent;
177 size_t to_send = (remaining < length) ? remaining : length;
178
179 if (to_send > 0) {
180 memcpy(buf, client->request_body + client->body_sent, to_send);
181 client->body_sent += to_send;
182 H2C_LOG("http2_client: sent %zu body bytes", to_send);
183 }
184
185 if (remaining <= length) {
186 *data_flags |= NGHTTP2_DATA_FLAG_EOF;
187 }
188
189 return (ssize_t)to_send;
190 }
191
192 // Create SSL context for HTTP/2 client
193 static SSL_CTX* create_http2_client_ssl_ctx(bool verify_certs)
194 {
195 SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
196 if (!ctx) {
197 log_message(LOG_LEVEL_ERROR, "Failed to create SSL context for HTTP/2 client");
198 return NULL;
199 }
200
201 // Set ALPN protocols
202 unsigned char alpn_protos[] = {
203 2, 'h', '2'
204 };
205 SSL_CTX_set_alpn_protos(ctx, alpn_protos, sizeof(alpn_protos));
206
207 // Configure certificate verification
208 if (verify_certs) {
209 SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
210 SSL_CTX_set_default_verify_paths(ctx);
211 } else {
212 SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
213 }
214
215 return ctx;
216 }
217
218 // Connect to backend server
219 static int connect_to_backend(const char *host, int port)
220 {
221 int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
222 if (sock_fd < 0) {
223 log_message(LOG_LEVEL_ERROR, "Failed to create socket for backend %s:%d", host, port);
224 return -1;
225 }
226
227 // Set non-blocking
228 int flags = fcntl(sock_fd, F_GETFL, 0);
229 fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK);
230
231 struct sockaddr_in addr;
232 memset(&addr, 0, sizeof(addr));
233 addr.sin_family = AF_INET;
234 addr.sin_port = htons(port);
235
236 if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) {
237 // Try DNS resolution
238 struct hostent *he = gethostbyname(host);
239 if (!he) {
240 log_message(LOG_LEVEL_ERROR, "Failed to resolve backend host: %s", host);
241 close(sock_fd);
242 return -1;
243 }
244 memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
245 }
246
247 int ret = connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr));
248 if (ret < 0 && errno != EINPROGRESS) {
249 log_message(LOG_LEVEL_ERROR, "Failed to connect to backend %s:%d: %s",
250 host, port, strerror(errno));
251 close(sock_fd);
252 return -1;
253 }
254
255 return sock_fd;
256 }
257
258 // Initialize HTTP/2 client callbacks
259 29 static int init_client_callbacks(http2_client_t *client)
260 {
261
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 29 times.
29 if (nghttp2_session_callbacks_new(&client->callbacks) != 0) {
262 log_message(LOG_LEVEL_ERROR, "Failed to create HTTP/2 client callbacks");
263 return -1;
264 }
265
266 29 nghttp2_session_callbacks_set_send_callback(client->callbacks, http2_client_send_callback);
267 29 nghttp2_session_callbacks_set_recv_callback(client->callbacks, http2_client_recv_callback);
268 29 nghttp2_session_callbacks_set_on_frame_recv_callback(client->callbacks, http2_client_on_frame_recv);
269 29 nghttp2_session_callbacks_set_on_data_chunk_recv_callback(client->callbacks, http2_client_on_data_chunk_recv);
270 29 nghttp2_session_callbacks_set_on_stream_close_callback(client->callbacks, http2_client_on_stream_close);
271
272 29 return 0;
273 }
274
275 29 int http2_client_init(http2_client_t *client, const backend_config_t *backend)
276 {
277 (void)backend;
278
279
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (!client) {
280 return -1;
281 }
282
283 29 memset(client, 0, sizeof(http2_client_t));
284 29 client->socket_fd = -1;
285
286
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 29 times.
29 if (init_client_callbacks(client) != 0) {
287 return -1;
288 }
289
290 29 return 0;
291 }
292
293 int http2_client_connect(http2_client_t *client, const backend_config_t *backend)
294 {
295 if (!client || !backend) {
296 return -1;
297 }
298
299 // Connect to backend
300 client->socket_fd = connect_to_backend(backend->host, backend->port);
301 if (client->socket_fd < 0) {
302 return -1;
303 }
304
305 // Create SSL context
306 SSL_CTX *ssl_ctx = create_http2_client_ssl_ctx(backend->tls_verify);
307 if (!ssl_ctx) {
308 close(client->socket_fd);
309 return -1;
310 }
311
312 // Create SSL object
313 client->ssl = SSL_new(ssl_ctx);
314 if (!client->ssl) {
315 log_message(LOG_LEVEL_ERROR, "Failed to create SSL object for HTTP/2 client");
316 SSL_CTX_free(ssl_ctx);
317 close(client->socket_fd);
318 return -1;
319 }
320
321 SSL_set_fd(client->ssl, client->socket_fd);
322 SSL_set_connect_state(client->ssl);
323
324 // Perform non-blocking handshake
325 struct timeval start, end;
326 gettimeofday(&start, NULL);
327
328 int ret = SSL_connect(client->ssl);
329 while (ret <= 0) {
330 int err = SSL_get_error(client->ssl, ret);
331 if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
332 struct pollfd pfd = {
333 .fd = client->socket_fd,
334 .events = (err == SSL_ERROR_WANT_READ) ? POLLIN : POLLOUT
335 };
336 int poll_ret = poll(&pfd, 1, HTTP2_POLL_TIMEOUT_MS);
337 if (poll_ret <= 0) {
338 log_message(LOG_LEVEL_ERROR, "HTTP/2 client TLS handshake timeout");
339 SSL_free(client->ssl);
340 SSL_CTX_free(ssl_ctx);
341 close(client->socket_fd);
342 return -1;
343 }
344 ret = SSL_connect(client->ssl);
345 } else {
346 log_message(LOG_LEVEL_ERROR, "HTTP/2 client TLS handshake failed: %d", err);
347 SSL_free(client->ssl);
348 SSL_CTX_free(ssl_ctx);
349 close(client->socket_fd);
350 return -1;
351 }
352 }
353
354 gettimeofday(&end, NULL);
355 long handshake_ms = (end.tv_sec - start.tv_sec) * 1000 +
356 (end.tv_usec - start.tv_usec) / 1000;
357 log_message(LOG_LEVEL_INFO, "HTTP/2 client TLS handshake completed in %ld ms", handshake_ms);
358 metrics_increment_tls_handshake(1);
359 metrics_record_tls_handshake_duration(handshake_ms / 1000.0);
360
361 // Verify ALPN negotiation
362 const unsigned char *alpn = NULL;
363 unsigned int alpn_len = 0;
364 SSL_get0_alpn_selected(client->ssl, &alpn, &alpn_len);
365 if (!alpn || alpn_len != 2 || memcmp(alpn, "h2", 2) != 0) {
366 log_message(LOG_LEVEL_ERROR, "HTTP/2 ALPN negotiation failed");
367 SSL_free(client->ssl);
368 SSL_CTX_free(ssl_ctx);
369 close(client->socket_fd);
370 return -1;
371 }
372
373 log_message(LOG_LEVEL_INFO, "HTTP/2 client connected to %s:%d (ALPN: h2)",
374 backend->host, backend->port);
375
376 // Create nghttp2 session (client mode)
377 nghttp2_option *options;
378 if (nghttp2_option_new(&options) == 0) {
379 nghttp2_option_set_peer_max_concurrent_streams(options, 100);
380 }
381
382 if (nghttp2_session_client_new2(&client->session, client->callbacks,
383 client, options) != 0) {
384 log_message(LOG_LEVEL_ERROR, "Failed to create HTTP/2 client session");
385 nghttp2_option_del(options);
386 SSL_free(client->ssl);
387 SSL_CTX_free(ssl_ctx);
388 close(client->socket_fd);
389 return -1;
390 }
391
392 if (options) nghttp2_option_del(options);
393
394 // Send client connection preface and SETTINGS
395 if (nghttp2_submit_settings(client->session, NGHTTP2_FLAG_NONE, NULL, 0) != 0) {
396 log_message(LOG_LEVEL_ERROR, "Failed to submit HTTP/2 SETTINGS");
397 http2_client_cleanup(client);
398 return -1;
399 }
400
401 if (nghttp2_session_send(client->session) != 0) {
402 log_message(LOG_LEVEL_ERROR, "Failed to send HTTP/2 client preface");
403 http2_client_cleanup(client);
404 return -1;
405 }
406
407 return 0;
408 }
409
410 int http2_client_send_request(http2_client_t *client, const char *method,
411 const char *path, const char *host,
412 const char *body, size_t body_len)
413 {
414 if (!client || !client->session || !method || !path || !host) {
415 return -1;
416 }
417
418 client->method = method;
419 client->path = path;
420 client->host = host;
421 client->request_body = body;
422 client->request_body_len = body_len;
423 client->body_sent = 0;
424
425 // Build HTTP/2 headers
426 nghttp2_nv headers[HTTP2_CLIENT_MAX_HEADERS];
427 size_t num_headers = 0;
428
429 // :method
430 headers[num_headers++] = MAKE_NV(":method", method);
431
432 // :path
433 headers[num_headers++] = MAKE_NV(":path", path);
434
435 // :authority
436 headers[num_headers++] = MAKE_NV(":authority", host);
437
438 // :scheme
439 headers[num_headers++] = MAKE_NV(":scheme", "https");
440
441 // User-Agent
442 headers[num_headers++] = MAKE_NV("user-agent", "emme-http2-client/1.0");
443
444 // Content-Type (if body present)
445 if (body && body_len > 0) {
446 headers[num_headers++] = MAKE_NV("content-type", "application/json");
447 }
448
449 // Submit request
450 nghttp2_data_provider data_prd = {0};
451 if (body && body_len > 0) {
452 data_prd.source.ptr = NULL;
453 data_prd.read_callback = http2_client_data_read;
454 }
455
456 int stream_id = nghttp2_submit_request(client->session, NULL, headers,
457 num_headers, body ? &data_prd : NULL,
458 client);
459 if (stream_id < 0) {
460 log_message(LOG_LEVEL_ERROR, "Failed to submit HTTP/2 request: %s",
461 nghttp2_strerror(stream_id));
462 return -1;
463 }
464
465 H2C_LOG("http2_client: submitted request on stream %d", stream_id);
466
467 // Send the request
468 if (nghttp2_session_send(client->session) != 0) {
469 log_message(LOG_LEVEL_ERROR, "Failed to send HTTP/2 request");
470 return -1;
471 }
472
473 return stream_id;
474 }
475
476 int http2_client_recv_response(http2_client_t *client)
477 {
478 if (!client || !client->session) {
479 return -1;
480 }
481
482 struct timeval start = {0};
483 gettimeofday(&start, NULL);
484
485 while (!client->done) {
486 // Check timeout (30 seconds)
487 struct timeval now;
488 gettimeofday(&now, NULL);
489 long elapsed_ms = (now.tv_sec - start.tv_sec) * 1000 +
490 (now.tv_usec - start.tv_usec) / 1000;
491 if (elapsed_ms > 30000) {
492 log_message(LOG_LEVEL_ERROR, "HTTP/2 client response timeout");
493 return -1;
494 }
495
496 client->want_read = 0;
497 client->want_write = 0;
498
499 // Check if session wants read/write
500 short events = 0;
501 if (nghttp2_session_want_read(client->session)) events |= POLLIN;
502 if (nghttp2_session_want_write(client->session)) events |= POLLOUT;
503
504 if (events == 0) break;
505
506 struct pollfd pfd = {
507 .fd = client->socket_fd,
508 .events = events
509 };
510
511 int poll_ret = poll(&pfd, 1, 100); // 100ms poll timeout
512 if (poll_ret < 0) {
513 log_message(LOG_LEVEL_ERROR, "HTTP/2 client poll failed: %s", strerror(errno));
514 return -1;
515 }
516 if (poll_ret == 0) continue;
517
518 if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) {
519 log_message(LOG_LEVEL_ERROR, "HTTP/2 client socket error");
520 return -1;
521 }
522
523 // Receive data
524 if (pfd.revents & POLLIN) {
525 int ret = nghttp2_session_recv(client->session);
526 if (ret < 0 && ret != NGHTTP2_ERR_WOULDBLOCK) {
527 log_message(LOG_LEVEL_ERROR, "HTTP/2 session recv failed: %s",
528 nghttp2_strerror(ret));
529 return -1;
530 }
531 }
532
533 // Send data
534 if (pfd.revents & POLLOUT) {
535 int ret = nghttp2_session_send(client->session);
536 if (ret < 0 && ret != NGHTTP2_ERR_WOULDBLOCK) {
537 log_message(LOG_LEVEL_ERROR, "HTTP/2 session send failed: %s",
538 nghttp2_strerror(ret));
539 return -1;
540 }
541 }
542 }
543
544 return client->response_status;
545 }
546
547 29 void http2_client_cleanup(http2_client_t *client)
548 {
549
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (!client) return;
550
551
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (client->session) {
552 nghttp2_session_del(client->session);
553 client->session = NULL;
554 }
555
556
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 if (client->callbacks) {
557 29 nghttp2_session_callbacks_del(client->callbacks);
558 29 client->callbacks = NULL;
559 }
560
561
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (client->ssl) {
562 SSL *ssl = client->ssl;
563 SSL_CTX *ctx = SSL_get_SSL_CTX(ssl);
564 SSL_free(ssl);
565 if (ctx) SSL_CTX_free(ctx);
566 client->ssl = NULL;
567 }
568
569
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (client->socket_fd >= 0) {
570 close(client->socket_fd);
571 client->socket_fd = -1;
572 }
573 }
574
575 const char* http2_client_get_response_body(http2_client_t *client)
576 {
577 if (!client) return NULL;
578 return client->response_buffer;
579 }
580
581 size_t http2_client_get_response_length(http2_client_t *client)
582 {
583 if (!client) return 0;
584 return client->response_received;
585 }
586
587 int http2_client_get_response_status(http2_client_t *client)
588 {
589 if (!client) return 0;
590 return client->response_status;
591 }
592
593 bool http2_client_is_done(http2_client_t *client)
594 {
595 if (!client) return true;
596 return client->done;
597 }
598
599 bool http2_client_has_error(http2_client_t *client)
600 {
601 if (!client) return true;
602 return client->error_code != 0 || client->response_status == 0;
603 }
604
605 int http2_client_get_error_code(http2_client_t *client)
606 {
607 if (!client) return -1;
608 return client->error_code;
609 }
610