|
/* |
|
* Minimal HTTP/2 client with nghttp2 + OpenSSL (blocking I/O, no event loop lib). |
|
* |
|
* Build: |
|
* cc -O2 -Wall -Wextra -pedantic h2_client.c -o h2_client \ |
|
* $(pkg-config --cflags --libs libnghttp2 openssl) |
|
* |
|
* Run: |
|
* ./h2_client example.com 443 / |
|
* |
|
* Notes: |
|
* - Requires server to support HTTP/2 over TLS (ALPN "h2"). |
|
* - Prints response body to stdout; headers/status are printed to stderr. |
|
*/ |
|
|
|
#include <nghttp2/nghttp2.h> |
|
|
|
#include <openssl/ssl.h> |
|
#include <openssl/err.h> |
|
#include <openssl/x509v3.h> |
|
|
|
#include <arpa/inet.h> |
|
#include <netdb.h> |
|
#include <sys/socket.h> |
|
#include <unistd.h> |
|
|
|
#include <errno.h> |
|
#include <stdint.h> |
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
|
|
#define MAKE_NV(NAME, VALUE) \ |
|
(nghttp2_nv){ \ |
|
(uint8_t *)(NAME), (uint8_t *)(VALUE), \ |
|
(uint16_t)strlen(NAME), (uint16_t)strlen(VALUE), \ |
|
NGHTTP2_NV_FLAG_NONE \ |
|
} |
|
|
|
typedef struct { |
|
SSL *ssl; |
|
int32_t stream_id; |
|
int stream_closed; |
|
} client_ctx; |
|
|
|
/* ---- Utility: TCP connect ---- */ |
|
static int tcp_connect(const char *host, const char *port) { |
|
struct addrinfo hints; |
|
memset(&hints, 0, sizeof(hints)); |
|
hints.ai_family = AF_UNSPEC; |
|
hints.ai_socktype = SOCK_STREAM; |
|
|
|
struct addrinfo *res = NULL; |
|
int gai = getaddrinfo(host, port, &hints, &res); |
|
if (gai != 0) { |
|
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai)); |
|
return -1; |
|
} |
|
|
|
int fd = -1; |
|
for (struct addrinfo *rp = res; rp; rp = rp->ai_next) { |
|
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) break; |
|
close(fd); |
|
fd = -1; |
|
} |
|
|
|
freeaddrinfo(res); |
|
if (fd < 0) fprintf(stderr, "tcp_connect: failed\n"); |
|
return fd; |
|
} |
|
|
|
/* ---- TLS setup (ALPN h2) ---- */ |
|
static SSL_CTX *sslctx_create(void) { |
|
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); |
|
if (!ctx) return NULL; |
|
|
|
/* Verify server cert */ |
|
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); |
|
if (SSL_CTX_set_default_verify_paths(ctx) != 1) { |
|
SSL_CTX_free(ctx); |
|
return NULL; |
|
} |
|
|
|
/* ALPN: offer h2 */ |
|
static const unsigned char alpn_protos[] = { 2, 'h', '2' }; |
|
if (SSL_CTX_set_alpn_protos(ctx, alpn_protos, sizeof(alpn_protos)) != 0) { |
|
SSL_CTX_free(ctx); |
|
return NULL; |
|
} |
|
|
|
return ctx; |
|
} |
|
|
|
static int ssl_handshake(SSL_CTX *ctx, int fd, const char *host, SSL **out_ssl) { |
|
SSL *ssl = SSL_new(ctx); |
|
if (!ssl) return -1; |
|
|
|
/* SNI */ |
|
SSL_set_tlsext_host_name(ssl, host); |
|
|
|
SSL_set_fd(ssl, fd); |
|
|
|
if (SSL_connect(ssl) != 1) { |
|
fprintf(stderr, "SSL_connect failed\n"); |
|
SSL_free(ssl); |
|
return -1; |
|
} |
|
|
|
/* Check ALPN result is h2 */ |
|
const unsigned char *alpn = NULL; |
|
unsigned int alpn_len = 0; |
|
SSL_get0_alpn_selected(ssl, &alpn, &alpn_len); |
|
if (!(alpn_len == 2 && memcmp(alpn, "h2", 2) == 0)) { |
|
fprintf(stderr, "Server did not negotiate h2 via ALPN\n"); |
|
SSL_free(ssl); |
|
return -1; |
|
} |
|
|
|
/* Basic hostname verification (OpenSSL 1.1.0+) */ |
|
X509 *cert = SSL_get_peer_certificate(ssl); |
|
if (!cert) { |
|
fprintf(stderr, "No server certificate\n"); |
|
SSL_free(ssl); |
|
return -1; |
|
} |
|
long vr = SSL_get_verify_result(ssl); |
|
if (vr != X509_V_OK) { |
|
fprintf(stderr, "Certificate verify failed: %s\n", X509_verify_cert_error_string(vr)); |
|
X509_free(cert); |
|
SSL_free(ssl); |
|
return -1; |
|
} |
|
#if OPENSSL_VERSION_NUMBER >= 0x10002000L |
|
if (X509_check_host(cert, host, 0, 0, NULL) != 1) { |
|
fprintf(stderr, "Hostname verification failed\n"); |
|
X509_free(cert); |
|
SSL_free(ssl); |
|
return -1; |
|
} |
|
#endif |
|
X509_free(cert); |
|
|
|
*out_ssl = ssl; |
|
return 0; |
|
} |
|
|
|
/* ---- nghttp2 callbacks ---- */ |
|
static ssize_t send_cb(nghttp2_session *session, |
|
const uint8_t *data, size_t length, |
|
int flags, void *user_data) { |
|
(void)session; (void)flags; |
|
client_ctx *c = (client_ctx *)user_data; |
|
|
|
size_t off = 0; |
|
while (off < length) { |
|
int n = SSL_write(c->ssl, data + off, (int)(length - off)); |
|
if (n <= 0) { |
|
int err = SSL_get_error(c->ssl, n); |
|
fprintf(stderr, "SSL_write error: %d\n", err); |
|
return NGHTTP2_ERR_CALLBACK_FAILURE; |
|
} |
|
off += (size_t)n; |
|
} |
|
return (ssize_t)length; |
|
} |
|
|
|
static int on_header_cb(nghttp2_session *session, |
|
const nghttp2_frame *frame, |
|
const uint8_t *name, size_t namelen, |
|
const uint8_t *value, size_t valuelen, |
|
uint8_t flags, void *user_data) { |
|
(void)session; (void)flags; |
|
client_ctx *c = (client_ctx *)user_data; |
|
|
|
if (frame->hd.type == NGHTTP2_HEADERS && |
|
frame->headers.cat == NGHTTP2_HCAT_RESPONSE && |
|
frame->hd.stream_id == c->stream_id) { |
|
fprintf(stderr, "H: %.*s: %.*s\n", |
|
(int)namelen, (const char *)name, |
|
(int)valuelen, (const char *)value); |
|
} |
|
return 0; |
|
} |
|
|
|
static int on_frame_recv_cb(nghttp2_session *session, |
|
const nghttp2_frame *frame, |
|
void *user_data) { |
|
(void)session; |
|
client_ctx *c = (client_ctx *)user_data; |
|
|
|
if (frame->hd.stream_id == c->stream_id) { |
|
if (frame->hd.type == NGHTTP2_HEADERS && |
|
frame->headers.cat == NGHTTP2_HCAT_RESPONSE) { |
|
fprintf(stderr, "Received response headers (stream=%d)\n", c->stream_id); |
|
} |
|
if (frame->hd.type == NGHTTP2_DATA) { |
|
/* data chunks handled in on_data_chunk_recv_cb */ |
|
} |
|
} |
|
return 0; |
|
} |
|
|
|
static int on_data_chunk_recv_cb(nghttp2_session *session, |
|
uint8_t flags, int32_t stream_id, |
|
const uint8_t *data, size_t len, |
|
void *user_data) { |
|
(void)session; (void)flags; |
|
client_ctx *c = (client_ctx *)user_data; |
|
|
|
if (stream_id == c->stream_id) { |
|
/* Body to stdout */ |
|
fwrite(data, 1, len, stdout); |
|
fflush(stdout); |
|
} |
|
return 0; |
|
} |
|
|
|
static int on_stream_close_cb(nghttp2_session *session, |
|
int32_t stream_id, uint32_t error_code, |
|
void *user_data) { |
|
(void)session; |
|
client_ctx *c = (client_ctx *)user_data; |
|
|
|
if (stream_id == c->stream_id) { |
|
fprintf(stderr, "\nStream closed (id=%d, error=%u)\n", stream_id, error_code); |
|
c->stream_closed = 1; |
|
} |
|
return 0; |
|
} |
|
|
|
static void die(const char *msg) { |
|
fprintf(stderr, "%s\n", msg); |
|
exit(1); |
|
} |
|
|
|
int main(int argc, char **argv) { |
|
if (argc != 4) { |
|
fprintf(stderr, "Usage: %s <host> <port> <path>\n", argv[0]); |
|
return 2; |
|
} |
|
const char *host = argv[1]; |
|
const char *port = argv[2]; |
|
const char *path = argv[3]; |
|
|
|
/* OpenSSL init */ |
|
SSL_library_init(); |
|
SSL_load_error_strings(); |
|
|
|
int fd = tcp_connect(host, port); |
|
if (fd < 0) return 1; |
|
|
|
SSL_CTX *ssl_ctx = sslctx_create(); |
|
if (!ssl_ctx) die("SSL_CTX create failed"); |
|
|
|
SSL *ssl = NULL; |
|
if (ssl_handshake(ssl_ctx, fd, host, &ssl) != 0) { |
|
SSL_CTX_free(ssl_ctx); |
|
close(fd); |
|
return 1; |
|
} |
|
|
|
/* nghttp2 session init */ |
|
nghttp2_session_callbacks *cbs = NULL; |
|
if (nghttp2_session_callbacks_new(&cbs) != 0) die("callbacks_new failed"); |
|
|
|
nghttp2_session_callbacks_set_send_callback(cbs, send_cb); |
|
nghttp2_session_callbacks_set_on_header_callback(cbs, on_header_cb); |
|
nghttp2_session_callbacks_set_on_frame_recv_callback(cbs, on_frame_recv_cb); |
|
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(cbs, on_data_chunk_recv_cb); |
|
nghttp2_session_callbacks_set_on_stream_close_callback(cbs, on_stream_close_cb); |
|
|
|
client_ctx ctx; |
|
memset(&ctx, 0, sizeof(ctx)); |
|
ctx.ssl = ssl; |
|
ctx.stream_id = -1; |
|
ctx.stream_closed = 0; |
|
|
|
nghttp2_session *session = NULL; |
|
if (nghttp2_session_client_new(&session, cbs, &ctx) != 0) die("client_new failed"); |
|
|
|
nghttp2_session_callbacks_del(cbs); |
|
|
|
/* Send initial SETTINGS */ |
|
nghttp2_settings_entry iv[1]; |
|
iv[0].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS; |
|
iv[0].value = 100; |
|
|
|
if (nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, iv, 1) != 0) |
|
die("submit_settings failed"); |
|
|
|
/* Build request headers */ |
|
char authority[512]; |
|
snprintf(authority, sizeof(authority), "%s:%s", host, port); |
|
|
|
nghttp2_nv hdrs[] = { |
|
MAKE_NV(":method", "GET"), |
|
MAKE_NV(":scheme", "https"), |
|
MAKE_NV(":authority", authority), |
|
MAKE_NV(":path", path), |
|
MAKE_NV("user-agent", "nghttp2-blocking-client/0.1"), |
|
MAKE_NV("accept", "*/*"), |
|
}; |
|
|
|
ctx.stream_id = nghttp2_submit_request(session, NULL, hdrs, |
|
(size_t)(sizeof(hdrs) / sizeof(hdrs[0])), |
|
NULL, NULL); |
|
if (ctx.stream_id < 0) die("submit_request failed"); |
|
|
|
/* Main blocking loop: |
|
* - nghttp2_session_send() writes pending frames via send_cb (SSL_write) |
|
* - SSL_read() blocks until bytes arrive |
|
* - feed bytes into nghttp2_session_mem_recv() |
|
*/ |
|
uint8_t rbuf[16 * 1024]; |
|
|
|
while (!ctx.stream_closed) { |
|
int rv = nghttp2_session_send(session); |
|
if (rv != 0) { |
|
fprintf(stderr, "session_send: %s\n", nghttp2_strerror(rv)); |
|
break; |
|
} |
|
|
|
int n = SSL_read(ssl, rbuf, (int)sizeof(rbuf)); |
|
if (n <= 0) { |
|
int err = SSL_get_error(ssl, n); |
|
if (err == SSL_ERROR_ZERO_RETURN) { |
|
fprintf(stderr, "TLS connection closed\n"); |
|
} else { |
|
fprintf(stderr, "SSL_read error: %d\n", err); |
|
} |
|
break; |
|
} |
|
|
|
ssize_t fed = nghttp2_session_mem_recv(session, rbuf, (size_t)n); |
|
if (fed < 0) { |
|
fprintf(stderr, "mem_recv: %s\n", nghttp2_strerror((int)fed)); |
|
break; |
|
} |
|
} |
|
|
|
/* Flush any remaining outbound (e.g., GOAWAY/ACK) */ |
|
(void)nghttp2_session_send(session); |
|
|
|
nghttp2_session_del(session); |
|
SSL_shutdown(ssl); |
|
SSL_free(ssl); |
|
SSL_CTX_free(ssl_ctx); |
|
close(fd); |
|
|
|
return 0; |
|
} |