| 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 |