cc -Wall -Wextra -O2 minimal_h3_client.c -o minimal_h3_client $(pkg-config --cflags --libs libngtcp2 libngtcp2_crypto_gnutls libnghttp3 gnutls)
Last active
February 27, 2026 19:12
-
-
Save masakielastic/1c79e18c524ec9d6b147e8c64705db30 to your computer and use it in GitHub Desktop.
ngtcp2 と nghttp3 で HTTP/3 クライアント (GnuTLS)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <arpa/inet.h> | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <gnutls/gnutls.h> | |
| #include <gnutls/crypto.h> | |
| #include <inttypes.h> | |
| #include <netdb.h> | |
| #include <poll.h> | |
| #include <stdbool.h> | |
| #include <stdint.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/socket.h> | |
| #include <sys/types.h> | |
| #include <time.h> | |
| #include <unistd.h> | |
| #include <nghttp3/nghttp3.h> | |
| #include <ngtcp2/ngtcp2.h> | |
| #include <ngtcp2/ngtcp2_crypto.h> | |
| #include <ngtcp2/ngtcp2_crypto_gnutls.h> | |
| #define REMOTE_HOST "nghttp2.org" | |
| #define REMOTE_PORT "443" | |
| #define REQ_PATH "/httpbin/get" | |
| #define RXBUF_SIZE 65536 | |
| #define TXBUF_SIZE 2048 | |
| typedef struct client { | |
| int fd; | |
| struct sockaddr_storage local_addr; | |
| socklen_t local_addrlen; | |
| struct sockaddr_storage remote_addr; | |
| socklen_t remote_addrlen; | |
| ngtcp2_path_storage ps; | |
| ngtcp2_conn *qconn; | |
| nghttp3_conn *h3conn; | |
| gnutls_certificate_credentials_t cred; | |
| gnutls_session_t tls; | |
| ngtcp2_crypto_conn_ref conn_ref; | |
| uint8_t rxbuf[RXBUF_SIZE]; | |
| uint8_t txbuf[TXBUF_SIZE]; | |
| int handshake_completed; | |
| int h3_streams_bound; | |
| int request_submitted; | |
| int response_complete; | |
| int64_t req_stream_id; | |
| ngtcp2_tstamp last_ts; | |
| int had_error; | |
| uint64_t tx_pkt_count; | |
| uint64_t rx_pkt_count; | |
| } client; | |
| static ngtcp2_tstamp timestamp_now_raw(void) { | |
| struct timespec ts; | |
| clock_gettime(CLOCK_MONOTONIC, &ts); | |
| return (ngtcp2_tstamp)ts.tv_sec * NGTCP2_SECONDS + (ngtcp2_tstamp)ts.tv_nsec; | |
| } | |
| static ngtcp2_tstamp timestamp_now(client *c) { | |
| ngtcp2_tstamp ts = timestamp_now_raw(); | |
| if (ts <= c->last_ts) { | |
| ts = c->last_ts + 1; | |
| } | |
| c->last_ts = ts; | |
| return ts; | |
| } | |
| static void fail(client *c, const char *where) { | |
| if (!c->had_error) { | |
| fprintf(stderr, "error: %s\n", where); | |
| } | |
| c->had_error = 1; | |
| } | |
| static const char *ccerr_type_name(ngtcp2_ccerr_type t) { | |
| switch (t) { | |
| case NGTCP2_CCERR_TYPE_TRANSPORT: | |
| return "transport"; | |
| case NGTCP2_CCERR_TYPE_APPLICATION: | |
| return "application"; | |
| case NGTCP2_CCERR_TYPE_VERSION_NEGOTIATION: | |
| return "version_negotiation"; | |
| case NGTCP2_CCERR_TYPE_IDLE_CLOSE: | |
| return "idle_close"; | |
| case NGTCP2_CCERR_TYPE_DROP_CONN: | |
| return "drop_conn"; | |
| case NGTCP2_CCERR_TYPE_RETRY: | |
| return "retry"; | |
| default: | |
| return "unknown"; | |
| } | |
| } | |
| static void log_connection_close_reason(client *c, const char *where) { | |
| const ngtcp2_ccerr *ccerr = ngtcp2_conn_get_ccerr(c->qconn); | |
| if (!ccerr) { | |
| fprintf(stderr, "%s: no ccerr\n", where); | |
| return; | |
| } | |
| fprintf(stderr, | |
| "%s: ccerr type=%s error_code=0x%llx frame_type=0x%llx reason=%.*s\n", | |
| where, ccerr_type_name(ccerr->type), | |
| (unsigned long long)ccerr->error_code, | |
| (unsigned long long)ccerr->frame_type, (int)ccerr->reasonlen, | |
| ccerr->reason ? (const char *)ccerr->reason : ""); | |
| } | |
| static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *ref) { | |
| client *c = (client *)ref->user_data; | |
| return c->qconn; | |
| } | |
| static void rand_cb(uint8_t *dest, size_t destlen, | |
| const ngtcp2_rand_ctx *rand_ctx) { | |
| (void)rand_ctx; | |
| if (gnutls_rnd(GNUTLS_RND_NONCE, dest, destlen) != GNUTLS_E_SUCCESS) { | |
| memset(dest, 0, destlen); | |
| } | |
| } | |
| static int get_new_connection_id_cb(ngtcp2_conn *conn, ngtcp2_cid *cid, | |
| uint8_t *token, size_t cidlen, | |
| void *user_data) { | |
| (void)conn; | |
| (void)user_data; | |
| if (gnutls_rnd(GNUTLS_RND_NONCE, cid->data, cidlen) != GNUTLS_E_SUCCESS) { | |
| return NGTCP2_ERR_CALLBACK_FAILURE; | |
| } | |
| cid->datalen = cidlen; | |
| if (gnutls_rnd(GNUTLS_RND_NONCE, token, NGTCP2_STATELESS_RESET_TOKENLEN) != | |
| GNUTLS_E_SUCCESS) { | |
| return NGTCP2_ERR_CALLBACK_FAILURE; | |
| } | |
| return 0; | |
| } | |
| static void extend_flow_control(client *c, int64_t stream_id, uint64_t n) { | |
| if (!n) { | |
| return; | |
| } | |
| if (ngtcp2_conn_extend_max_stream_offset(c->qconn, stream_id, n) != 0) { | |
| fail(c, "ngtcp2_conn_extend_max_stream_offset"); | |
| return; | |
| } | |
| ngtcp2_conn_extend_max_offset(c->qconn, n); | |
| } | |
| static int h3_recv_data_cb(nghttp3_conn *conn, int64_t stream_id, | |
| const uint8_t *data, size_t datalen, | |
| void *conn_user_data, void *stream_user_data) { | |
| (void)conn; | |
| (void)stream_user_data; | |
| client *c = (client *)conn_user_data; | |
| if (datalen && fwrite(data, 1, datalen, stdout) != datalen) { | |
| return NGHTTP3_ERR_CALLBACK_FAILURE; | |
| } | |
| fflush(stdout); | |
| /* DATA frame payload bytes are not included in nghttp3_conn_read_stream's | |
| consumed value, so return credit explicitly. */ | |
| extend_flow_control(c, stream_id, (uint64_t)datalen); | |
| return c->had_error ? NGHTTP3_ERR_CALLBACK_FAILURE : 0; | |
| } | |
| static int h3_deferred_consume_cb(nghttp3_conn *conn, int64_t stream_id, | |
| size_t consumed, void *conn_user_data, | |
| void *stream_user_data) { | |
| (void)conn; | |
| (void)stream_user_data; | |
| client *c = (client *)conn_user_data; | |
| extend_flow_control(c, stream_id, (uint64_t)consumed); | |
| return c->had_error ? NGHTTP3_ERR_CALLBACK_FAILURE : 0; | |
| } | |
| static int h3_recv_header_cb(nghttp3_conn *conn, int64_t stream_id, | |
| int32_t token, nghttp3_rcbuf *name, | |
| nghttp3_rcbuf *value, uint8_t flags, | |
| void *conn_user_data, void *stream_user_data) { | |
| (void)conn; | |
| (void)stream_id; | |
| (void)token; | |
| (void)flags; | |
| (void)conn_user_data; | |
| (void)stream_user_data; | |
| nghttp3_vec n = nghttp3_rcbuf_get_buf(name); | |
| nghttp3_vec v = nghttp3_rcbuf_get_buf(value); | |
| if (n.len == 7 && memcmp(n.base, ":status", 7) == 0) { | |
| fprintf(stderr, "HTTP status: %.*s\n", (int)v.len, v.base); | |
| } | |
| return 0; | |
| } | |
| static int h3_end_stream_cb(nghttp3_conn *conn, int64_t stream_id, | |
| void *conn_user_data, void *stream_user_data) { | |
| (void)conn; | |
| (void)stream_user_data; | |
| client *c = (client *)conn_user_data; | |
| if (stream_id == c->req_stream_id) { | |
| c->response_complete = 1; | |
| } | |
| return 0; | |
| } | |
| static int h3_stream_close_cb(nghttp3_conn *conn, int64_t stream_id, | |
| uint64_t app_error_code, void *conn_user_data, | |
| void *stream_user_data) { | |
| (void)conn; | |
| (void)stream_user_data; | |
| client *c = (client *)conn_user_data; | |
| if (stream_id == c->req_stream_id) { | |
| fprintf(stderr, "HTTP/3 stream closed (app_error=%" PRIu64 ")\n", | |
| app_error_code); | |
| } | |
| return 0; | |
| } | |
| static int h3_stop_sending_cb(nghttp3_conn *conn, int64_t stream_id, | |
| uint64_t app_error_code, void *conn_user_data, | |
| void *stream_user_data) { | |
| (void)conn; | |
| (void)stream_user_data; | |
| client *c = (client *)conn_user_data; | |
| if (ngtcp2_conn_shutdown_stream_read(c->qconn, 0, stream_id, | |
| app_error_code) < 0) { | |
| return NGHTTP3_ERR_CALLBACK_FAILURE; | |
| } | |
| return 0; | |
| } | |
| static int h3_reset_stream_cb(nghttp3_conn *conn, int64_t stream_id, | |
| uint64_t app_error_code, void *conn_user_data, | |
| void *stream_user_data) { | |
| (void)conn; | |
| (void)stream_user_data; | |
| client *c = (client *)conn_user_data; | |
| if (ngtcp2_conn_shutdown_stream_write(c->qconn, 0, stream_id, | |
| app_error_code) < 0) { | |
| return NGHTTP3_ERR_CALLBACK_FAILURE; | |
| } | |
| return 0; | |
| } | |
| static int q_recv_stream_data_cb(ngtcp2_conn *conn, uint32_t flags, | |
| int64_t stream_id, uint64_t offset, | |
| const uint8_t *data, size_t datalen, | |
| void *user_data, void *stream_user_data) { | |
| (void)conn; | |
| (void)offset; | |
| (void)stream_user_data; | |
| client *c = (client *)user_data; | |
| int fin = !!(flags & NGTCP2_STREAM_DATA_FLAG_FIN); | |
| nghttp3_ssize nconsumed = | |
| nghttp3_conn_read_stream(c->h3conn, stream_id, data, datalen, fin); | |
| if (nconsumed < 0) { | |
| fprintf(stderr, "nghttp3_conn_read_stream: %s\n", nghttp3_strerror((int)nconsumed)); | |
| return NGTCP2_ERR_CALLBACK_FAILURE; | |
| } | |
| /* These consumed bytes are frame/QPACK/control bytes, not body bytes. */ | |
| extend_flow_control(c, stream_id, (uint64_t)nconsumed); | |
| return c->had_error ? NGTCP2_ERR_CALLBACK_FAILURE : 0; | |
| } | |
| static int q_acked_stream_data_offset_cb(ngtcp2_conn *conn, int64_t stream_id, | |
| uint64_t offset, uint64_t datalen, | |
| void *user_data, | |
| void *stream_user_data) { | |
| (void)conn; | |
| (void)offset; | |
| (void)stream_user_data; | |
| client *c = (client *)user_data; | |
| if (nghttp3_conn_add_ack_offset(c->h3conn, stream_id, datalen) != 0) { | |
| return NGTCP2_ERR_CALLBACK_FAILURE; | |
| } | |
| return 0; | |
| } | |
| static int q_stream_close_cb(ngtcp2_conn *conn, uint32_t flags, | |
| int64_t stream_id, uint64_t app_error_code, | |
| void *user_data, void *stream_user_data) { | |
| (void)conn; | |
| (void)flags; | |
| (void)stream_user_data; | |
| client *c = (client *)user_data; | |
| if (nghttp3_conn_close_stream(c->h3conn, stream_id, app_error_code) != 0) { | |
| return NGTCP2_ERR_CALLBACK_FAILURE; | |
| } | |
| return 0; | |
| } | |
| static int q_handshake_completed_cb(ngtcp2_conn *conn, void *user_data) { | |
| (void)conn; | |
| client *c = (client *)user_data; | |
| c->handshake_completed = 1; | |
| fprintf(stderr, "QUIC handshake completed\n"); | |
| return 0; | |
| } | |
| static int socket_connect(client *c) { | |
| struct addrinfo hints; | |
| struct addrinfo *res = NULL; | |
| struct addrinfo *rp; | |
| int rv; | |
| memset(&hints, 0, sizeof(hints)); | |
| /* Prefer IPv4 in this minimal sample: UDP connect() on IPv6 can succeed | |
| even when path is not actually reachable in some environments. */ | |
| hints.ai_family = AF_INET; | |
| hints.ai_socktype = SOCK_DGRAM; | |
| rv = getaddrinfo(REMOTE_HOST, REMOTE_PORT, &hints, &res); | |
| if (rv != 0) { | |
| fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); | |
| return -1; | |
| } | |
| c->fd = -1; | |
| for (rp = res; rp; rp = rp->ai_next) { | |
| int fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); | |
| if (fd < 0) { | |
| continue; | |
| } | |
| if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) { | |
| c->fd = fd; | |
| memcpy(&c->remote_addr, rp->ai_addr, (size_t)rp->ai_addrlen); | |
| c->remote_addrlen = (socklen_t)rp->ai_addrlen; | |
| break; | |
| } | |
| close(fd); | |
| } | |
| freeaddrinfo(res); | |
| if (c->fd < 0) { | |
| perror("connect"); | |
| return -1; | |
| } | |
| int fl = fcntl(c->fd, F_GETFL, 0); | |
| if (fl < 0 || fcntl(c->fd, F_SETFL, fl | O_NONBLOCK) < 0) { | |
| perror("fcntl(O_NONBLOCK)"); | |
| return -1; | |
| } | |
| c->local_addrlen = sizeof(c->local_addr); | |
| if (getsockname(c->fd, (struct sockaddr *)&c->local_addr, &c->local_addrlen) != | |
| 0) { | |
| perror("getsockname"); | |
| return -1; | |
| } | |
| ngtcp2_path_storage_init(&c->ps, | |
| (struct sockaddr *)&c->local_addr, c->local_addrlen, | |
| (struct sockaddr *)&c->remote_addr, | |
| c->remote_addrlen, | |
| NULL); | |
| fprintf(stderr, "connected UDP socket (IPv4) to " REMOTE_HOST ":" REMOTE_PORT "\n"); | |
| return 0; | |
| } | |
| static int setup_tls(client *c) { | |
| int rv; | |
| gnutls_datum_t alpn; | |
| rv = gnutls_global_init(); | |
| if (rv != GNUTLS_E_SUCCESS) { | |
| fprintf(stderr, "gnutls_global_init: %s\n", gnutls_strerror(rv)); | |
| return -1; | |
| } | |
| rv = gnutls_certificate_allocate_credentials(&c->cred); | |
| if (rv != GNUTLS_E_SUCCESS) { | |
| fprintf(stderr, "gnutls_certificate_allocate_credentials: %s\n", | |
| gnutls_strerror(rv)); | |
| return -1; | |
| } | |
| rv = gnutls_certificate_set_x509_system_trust(c->cred); | |
| if (rv < 0) { | |
| fprintf(stderr, "gnutls_certificate_set_x509_system_trust: %s\n", | |
| gnutls_strerror(rv)); | |
| return -1; | |
| } | |
| rv = gnutls_init(&c->tls, GNUTLS_CLIENT | GNUTLS_NO_END_OF_EARLY_DATA); | |
| if (rv != GNUTLS_E_SUCCESS) { | |
| fprintf(stderr, "gnutls_init: %s\n", gnutls_strerror(rv)); | |
| return -1; | |
| } | |
| rv = gnutls_priority_set_direct( | |
| c->tls, "NORMAL:-VERS-ALL:+VERS-TLS1.3:%DISABLE_TLS13_COMPAT_MODE", NULL); | |
| if (rv != GNUTLS_E_SUCCESS) { | |
| fprintf(stderr, "gnutls_priority_set_direct: %s\n", gnutls_strerror(rv)); | |
| return -1; | |
| } | |
| rv = gnutls_credentials_set(c->tls, GNUTLS_CRD_CERTIFICATE, c->cred); | |
| if (rv != GNUTLS_E_SUCCESS) { | |
| fprintf(stderr, "gnutls_credentials_set: %s\n", gnutls_strerror(rv)); | |
| return -1; | |
| } | |
| rv = gnutls_server_name_set(c->tls, GNUTLS_NAME_DNS, REMOTE_HOST, | |
| strlen(REMOTE_HOST)); | |
| if (rv != GNUTLS_E_SUCCESS) { | |
| fprintf(stderr, "gnutls_server_name_set: %s\n", gnutls_strerror(rv)); | |
| return -1; | |
| } | |
| gnutls_session_set_verify_cert(c->tls, REMOTE_HOST, 0); | |
| alpn.data = (unsigned char *)"h3"; | |
| alpn.size = 2; | |
| rv = gnutls_alpn_set_protocols(c->tls, &alpn, 1, 0); | |
| if (rv != GNUTLS_E_SUCCESS) { | |
| fprintf(stderr, "gnutls_alpn_set_protocols: %s\n", gnutls_strerror(rv)); | |
| return -1; | |
| } | |
| c->conn_ref.get_conn = get_conn; | |
| c->conn_ref.user_data = c; | |
| gnutls_session_set_ptr(c->tls, &c->conn_ref); | |
| rv = ngtcp2_crypto_gnutls_configure_client_session(c->tls); | |
| if (rv != 0) { | |
| fprintf(stderr, "ngtcp2_crypto_gnutls_configure_client_session failed\n"); | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| static int setup_http3(client *c) { | |
| nghttp3_callbacks h3cb; | |
| nghttp3_settings h3settings; | |
| memset(&h3cb, 0, sizeof(h3cb)); | |
| h3cb.stream_close = h3_stream_close_cb; | |
| h3cb.recv_data = h3_recv_data_cb; | |
| h3cb.deferred_consume = h3_deferred_consume_cb; | |
| h3cb.recv_header = h3_recv_header_cb; | |
| h3cb.stop_sending = h3_stop_sending_cb; | |
| h3cb.reset_stream = h3_reset_stream_cb; | |
| h3cb.end_stream = h3_end_stream_cb; | |
| nghttp3_settings_default(&h3settings); | |
| int rv = nghttp3_conn_client_new(&c->h3conn, &h3cb, &h3settings, NULL, c); | |
| if (rv != 0) { | |
| fprintf(stderr, "nghttp3_conn_client_new: %s\n", nghttp3_strerror(rv)); | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| static int setup_quic(client *c) { | |
| ngtcp2_callbacks cb; | |
| ngtcp2_settings settings; | |
| ngtcp2_transport_params params; | |
| uint8_t dcid_data[18]; | |
| uint8_t scid_data[18]; | |
| ngtcp2_cid dcid; | |
| ngtcp2_cid scid; | |
| if (gnutls_rnd(GNUTLS_RND_NONCE, dcid_data, sizeof(dcid_data)) != | |
| GNUTLS_E_SUCCESS || | |
| gnutls_rnd(GNUTLS_RND_NONCE, scid_data, sizeof(scid_data)) != | |
| GNUTLS_E_SUCCESS) { | |
| fprintf(stderr, "failed to create CID entropy\n"); | |
| return -1; | |
| } | |
| ngtcp2_cid_init(&dcid, dcid_data, sizeof(dcid_data)); | |
| ngtcp2_cid_init(&scid, scid_data, sizeof(scid_data)); | |
| memset(&cb, 0, sizeof(cb)); | |
| cb.client_initial = ngtcp2_crypto_client_initial_cb; | |
| cb.recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb; | |
| cb.handshake_completed = q_handshake_completed_cb; | |
| cb.encrypt = ngtcp2_crypto_encrypt_cb; | |
| cb.decrypt = ngtcp2_crypto_decrypt_cb; | |
| cb.hp_mask = ngtcp2_crypto_hp_mask_cb; | |
| cb.recv_stream_data = q_recv_stream_data_cb; | |
| cb.acked_stream_data_offset = q_acked_stream_data_offset_cb; | |
| cb.stream_close = q_stream_close_cb; | |
| cb.recv_retry = ngtcp2_crypto_recv_retry_cb; | |
| cb.rand = rand_cb; | |
| cb.get_new_connection_id = get_new_connection_id_cb; | |
| cb.update_key = ngtcp2_crypto_update_key_cb; | |
| cb.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb; | |
| cb.delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb; | |
| cb.get_path_challenge_data = ngtcp2_crypto_get_path_challenge_data_cb; | |
| cb.version_negotiation = ngtcp2_crypto_version_negotiation_cb; | |
| ngtcp2_settings_default(&settings); | |
| settings.initial_ts = timestamp_now(c); | |
| ngtcp2_transport_params_default(¶ms); | |
| params.initial_max_data = 1024 * 1024; | |
| params.initial_max_stream_data_bidi_local = 512 * 1024; | |
| params.initial_max_stream_data_bidi_remote = 256 * 1024; | |
| params.initial_max_stream_data_uni = 256 * 1024; | |
| params.initial_max_streams_bidi = 16; | |
| params.initial_max_streams_uni = 16; | |
| int rv = ngtcp2_conn_client_new(&c->qconn, &dcid, &scid, &c->ps.path, | |
| NGTCP2_PROTO_VER_V1, &cb, &settings, | |
| ¶ms, NULL, c); | |
| if (rv != 0) { | |
| fprintf(stderr, "ngtcp2_conn_client_new: %s\n", ngtcp2_strerror(rv)); | |
| return -1; | |
| } | |
| ngtcp2_conn_set_tls_native_handle(c->qconn, c->tls); | |
| return 0; | |
| } | |
| static int bind_h3_unidirectional_streams(client *c) { | |
| int rv; | |
| int64_t ctrl_id; | |
| int64_t qenc_id; | |
| int64_t qdec_id; | |
| if (c->h3_streams_bound) { | |
| return 0; | |
| } | |
| rv = ngtcp2_conn_open_uni_stream(c->qconn, &ctrl_id, NULL); | |
| if (rv == NGTCP2_ERR_STREAM_ID_BLOCKED) { | |
| return 0; | |
| } | |
| if (rv != 0) { | |
| fprintf(stderr, "open control stream: %s\n", ngtcp2_strerror(rv)); | |
| return -1; | |
| } | |
| rv = ngtcp2_conn_open_uni_stream(c->qconn, &qenc_id, NULL); | |
| if (rv != 0) { | |
| fprintf(stderr, "open qpack encoder stream: %s\n", ngtcp2_strerror(rv)); | |
| return -1; | |
| } | |
| rv = ngtcp2_conn_open_uni_stream(c->qconn, &qdec_id, NULL); | |
| if (rv != 0) { | |
| fprintf(stderr, "open qpack decoder stream: %s\n", ngtcp2_strerror(rv)); | |
| return -1; | |
| } | |
| rv = nghttp3_conn_bind_control_stream(c->h3conn, ctrl_id); | |
| if (rv != 0) { | |
| fprintf(stderr, "nghttp3_conn_bind_control_stream: %s\n", nghttp3_strerror(rv)); | |
| return -1; | |
| } | |
| rv = nghttp3_conn_bind_qpack_streams(c->h3conn, qenc_id, qdec_id); | |
| if (rv != 0) { | |
| fprintf(stderr, "nghttp3_conn_bind_qpack_streams: %s\n", nghttp3_strerror(rv)); | |
| return -1; | |
| } | |
| c->h3_streams_bound = 1; | |
| fprintf(stderr, "bound H3 control stream=%" PRId64 ", qenc=%" PRId64 | |
| ", qdec=%" PRId64 "\n", | |
| ctrl_id, qenc_id, qdec_id); | |
| return 0; | |
| } | |
| static int submit_request(client *c) { | |
| int rv; | |
| int64_t stream_id; | |
| static const uint8_t method[] = "GET"; | |
| static const uint8_t scheme[] = "https"; | |
| static const uint8_t authority[] = REMOTE_HOST; | |
| static const uint8_t path[] = REQ_PATH; | |
| nghttp3_nv nva[] = { | |
| {(uint8_t *)":method", (uint8_t *)method, 7, sizeof(method) - 1, | |
| NGHTTP3_NV_FLAG_NONE}, | |
| {(uint8_t *)":scheme", (uint8_t *)scheme, 7, sizeof(scheme) - 1, | |
| NGHTTP3_NV_FLAG_NONE}, | |
| {(uint8_t *)":authority", (uint8_t *)authority, 10, | |
| sizeof(authority) - 1, NGHTTP3_NV_FLAG_NONE}, | |
| {(uint8_t *)":path", (uint8_t *)path, 5, sizeof(path) - 1, | |
| NGHTTP3_NV_FLAG_NONE}, | |
| {(uint8_t *)"user-agent", (uint8_t *)"minimal-ngtcp2-nghttp3-client", 10, | |
| 29, NGHTTP3_NV_FLAG_NONE}, | |
| }; | |
| if (c->request_submitted) { | |
| return 0; | |
| } | |
| rv = ngtcp2_conn_open_bidi_stream(c->qconn, &stream_id, NULL); | |
| if (rv == NGTCP2_ERR_STREAM_ID_BLOCKED) { | |
| return 0; | |
| } | |
| if (rv != 0) { | |
| fprintf(stderr, "ngtcp2_conn_open_bidi_stream: %s\n", ngtcp2_strerror(rv)); | |
| return -1; | |
| } | |
| rv = nghttp3_conn_submit_request(c->h3conn, stream_id, nva, | |
| sizeof(nva) / sizeof(nva[0]), NULL, NULL); | |
| if (rv != 0) { | |
| fprintf(stderr, "nghttp3_conn_submit_request: %s\n", nghttp3_strerror(rv)); | |
| return -1; | |
| } | |
| c->req_stream_id = stream_id; | |
| c->request_submitted = 1; | |
| fprintf(stderr, "submitted GET request on stream=%" PRId64 "\n", stream_id); | |
| return 0; | |
| } | |
| static int send_quic_packet(client *c, ngtcp2_ssize nwrite, ngtcp2_tstamp ts) { | |
| if (nwrite <= 0) { | |
| return 0; | |
| } | |
| ssize_t nw = send(c->fd, c->txbuf, (size_t)nwrite, 0); | |
| if (nw < 0) { | |
| if (errno == EAGAIN || errno == EWOULDBLOCK) { | |
| fprintf(stderr, "send would block; aborting this minimal client\n"); | |
| return -1; | |
| } | |
| perror("send"); | |
| return -1; | |
| } | |
| if ((ngtcp2_ssize)nw != nwrite) { | |
| fprintf(stderr, "short UDP send\n"); | |
| return -1; | |
| } | |
| ++c->tx_pkt_count; | |
| ngtcp2_conn_update_pkt_tx_time(c->qconn, ts); | |
| return 0; | |
| } | |
| static int flush_h3_to_quic(client *c) { | |
| size_t sent_pkts = 0; | |
| for (;;) { | |
| if (sent_pkts >= 32) { | |
| return 0; | |
| } | |
| int64_t stream_id = -1; | |
| int fin = 0; | |
| nghttp3_vec h3v[8]; | |
| nghttp3_ssize veccnt = | |
| nghttp3_conn_writev_stream(c->h3conn, &stream_id, &fin, h3v, | |
| sizeof(h3v) / sizeof(h3v[0])); | |
| if (veccnt < 0) { | |
| fprintf(stderr, "nghttp3_conn_writev_stream: %s\n", | |
| nghttp3_strerror((int)veccnt)); | |
| return -1; | |
| } | |
| if (veccnt == 0 && stream_id == -1) { | |
| return 0; | |
| } | |
| ngtcp2_vec qv[8]; | |
| for (nghttp3_ssize i = 0; i < veccnt; ++i) { | |
| qv[i].base = h3v[i].base; | |
| qv[i].len = h3v[i].len; | |
| } | |
| ngtcp2_tstamp ts = timestamp_now(c); | |
| ngtcp2_ssize pdatalen = -1; | |
| ngtcp2_ssize nwrite = ngtcp2_conn_writev_stream( | |
| c->qconn, &c->ps.path, NULL, c->txbuf, sizeof(c->txbuf), &pdatalen, | |
| fin ? NGTCP2_WRITE_STREAM_FLAG_FIN : NGTCP2_WRITE_STREAM_FLAG_NONE, | |
| stream_id, veccnt ? qv : NULL, (size_t)veccnt, ts); | |
| if (nwrite < 0) { | |
| if (nwrite == NGTCP2_ERR_STREAM_DATA_BLOCKED || | |
| nwrite == NGTCP2_ERR_STREAM_SHUT_WR || | |
| nwrite == NGTCP2_ERR_STREAM_NOT_FOUND) { | |
| return 0; | |
| } | |
| fprintf(stderr, "ngtcp2_conn_writev_stream(H3): %s\n", | |
| ngtcp2_strerror((int)nwrite)); | |
| return -1; | |
| } | |
| if (pdatalen >= 0) { | |
| int rv = nghttp3_conn_add_write_offset(c->h3conn, stream_id, | |
| (size_t)pdatalen); | |
| if (rv != 0) { | |
| fprintf(stderr, "nghttp3_conn_add_write_offset: %s\n", | |
| nghttp3_strerror(rv)); | |
| return -1; | |
| } | |
| } | |
| if (send_quic_packet(c, nwrite, ts) != 0) { | |
| return -1; | |
| } | |
| ++sent_pkts; | |
| if (nwrite == 0) { | |
| return 0; | |
| } | |
| } | |
| } | |
| static int flush_quic_control(client *c) { | |
| size_t sent_pkts = 0; | |
| for (;;) { | |
| if (sent_pkts >= 32) { | |
| return 0; | |
| } | |
| ngtcp2_tstamp ts = timestamp_now(c); | |
| ngtcp2_ssize pdatalen = -1; | |
| ngtcp2_ssize nwrite = ngtcp2_conn_writev_stream( | |
| c->qconn, &c->ps.path, NULL, c->txbuf, sizeof(c->txbuf), &pdatalen, | |
| NGTCP2_WRITE_STREAM_FLAG_NONE, -1, NULL, 0, ts); | |
| if (nwrite < 0) { | |
| fprintf(stderr, "ngtcp2_conn_writev_stream(ctrl): %s\n", | |
| ngtcp2_strerror((int)nwrite)); | |
| return -1; | |
| } | |
| if (nwrite == 0) { | |
| return 0; | |
| } | |
| if (send_quic_packet(c, nwrite, ts) != 0) { | |
| return -1; | |
| } | |
| ++sent_pkts; | |
| } | |
| } | |
| static int drive_tx(client *c) { | |
| if (flush_h3_to_quic(c) != 0) { | |
| return -1; | |
| } | |
| if (flush_quic_control(c) != 0) { | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| static int read_udp_once(client *c) { | |
| ssize_t nread = recv(c->fd, c->rxbuf, sizeof(c->rxbuf), 0); | |
| if (nread < 0) { | |
| if (errno == EAGAIN || errno == EWOULDBLOCK) { | |
| return 0; | |
| } | |
| perror("recv"); | |
| return -1; | |
| } | |
| ngtcp2_tstamp ts = timestamp_now(c); | |
| ++c->rx_pkt_count; | |
| int rv = ngtcp2_conn_read_pkt(c->qconn, &c->ps.path, NULL, | |
| c->rxbuf, (size_t)nread, ts); | |
| if (rv != 0) { | |
| if (rv == NGTCP2_ERR_DRAINING || rv == NGTCP2_ERR_CLOSING) { | |
| log_connection_close_reason(c, "ngtcp2_conn_read_pkt"); | |
| } | |
| fprintf(stderr, "ngtcp2_conn_read_pkt: %s\n", ngtcp2_strerror(rv)); | |
| return -1; | |
| } | |
| return 1; | |
| } | |
| static int run(client *c) { | |
| struct pollfd pfd; | |
| ngtcp2_tstamp started_ts = timestamp_now(c); | |
| pfd.fd = c->fd; | |
| pfd.events = POLLIN; | |
| /* Kick off Initial packets / TLS ClientHello. */ | |
| if (drive_tx(c) != 0) { | |
| return -1; | |
| } | |
| while (!c->had_error && !c->response_complete) { | |
| ngtcp2_tstamp now = timestamp_now(c); | |
| ngtcp2_tstamp expiry = ngtcp2_conn_get_expiry(c->qconn); | |
| int timeout_ms = 50; | |
| if (expiry <= now) { | |
| timeout_ms = 0; | |
| } else { | |
| uint64_t diff_ns = expiry - now; | |
| timeout_ms = (int)(diff_ns / NGTCP2_MILLISECONDS); | |
| if (timeout_ms > 50) { | |
| timeout_ms = 50; | |
| } | |
| } | |
| int n = poll(&pfd, 1, timeout_ms); | |
| if (n < 0) { | |
| if (errno == EINTR) { | |
| continue; | |
| } | |
| perror("poll"); | |
| return -1; | |
| } | |
| if (n > 0 && (pfd.revents & POLLIN)) { | |
| for (;;) { | |
| int rr = read_udp_once(c); | |
| if (rr < 0) { | |
| return -1; | |
| } | |
| if (rr == 0) { | |
| break; | |
| } | |
| } | |
| } | |
| now = timestamp_now(c); | |
| if (ngtcp2_conn_handle_expiry(c->qconn, now) != 0) { | |
| fprintf(stderr, "ngtcp2_conn_handle_expiry failed\n"); | |
| return -1; | |
| } | |
| if (ngtcp2_conn_in_closing_period(c->qconn) || | |
| ngtcp2_conn_in_draining_period(c->qconn)) { | |
| fprintf(stderr, "QUIC connection is closing/draining\n"); | |
| return -1; | |
| } | |
| if (c->handshake_completed) { | |
| if (bind_h3_unidirectional_streams(c) != 0) { | |
| return -1; | |
| } | |
| if (c->h3_streams_bound && submit_request(c) != 0) { | |
| return -1; | |
| } | |
| } | |
| if (drive_tx(c) != 0) { | |
| return -1; | |
| } | |
| if (timestamp_now(c) - started_ts > 30 * NGTCP2_SECONDS) { | |
| fprintf(stderr, | |
| "timeout: no complete response in 30s " | |
| "(handshake=%d, bound=%d, submitted=%d, tx_pkts=%" PRIu64 | |
| ", rx_pkts=%" PRIu64 ")\n", | |
| c->handshake_completed, c->h3_streams_bound, c->request_submitted, | |
| c->tx_pkt_count, c->rx_pkt_count); | |
| return -1; | |
| } | |
| } | |
| return c->had_error ? -1 : 0; | |
| } | |
| static void cleanup(client *c) { | |
| if (c->h3conn) { | |
| nghttp3_conn_del(c->h3conn); | |
| } | |
| if (c->qconn) { | |
| ngtcp2_conn_del(c->qconn); | |
| } | |
| if (c->tls) { | |
| gnutls_deinit(c->tls); | |
| } | |
| if (c->cred) { | |
| gnutls_certificate_free_credentials(c->cred); | |
| } | |
| gnutls_global_deinit(); | |
| if (c->fd >= 0) { | |
| close(c->fd); | |
| } | |
| } | |
| int main(void) { | |
| client c; | |
| memset(&c, 0, sizeof(c)); | |
| c.fd = -1; | |
| c.req_stream_id = -1; | |
| if (socket_connect(&c) != 0) { | |
| cleanup(&c); | |
| return 1; | |
| } | |
| if (setup_tls(&c) != 0) { | |
| cleanup(&c); | |
| return 1; | |
| } | |
| if (setup_http3(&c) != 0) { | |
| cleanup(&c); | |
| return 1; | |
| } | |
| if (setup_quic(&c) != 0) { | |
| cleanup(&c); | |
| return 1; | |
| } | |
| int rv = run(&c); | |
| cleanup(&c); | |
| if (rv != 0) { | |
| return 1; | |
| } | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment