Skip to content

Instantly share code, notes, and snippets.

@unasuke
Created December 29, 2025 20:03
Show Gist options
  • Select an option

  • Save unasuke/cadc6e9af0b2f5e2860fcdf3318bb517 to your computer and use it in GitHub Desktop.

Select an option

Save unasuke/cadc6e9af0b2f5e2860fcdf3318bb517 to your computer and use it in GitHub Desktop.
OpsnSSL's QUIC API and HTTP/3 integrations demo codes with JA descriptions by Claude Code
/*
* Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* ファイルパス: demos/http3/ossl-nghttp3-demo-client.c (OpenSSLリポジトリ内)
*
* 注意: このファイルの日本語コメントは Claude Code によって追加されました。
* オリジナルのソースコードは https://github.com/openssl/openssl にあります。
*/
/*
* 【ossl-nghttp3-demo-client.c - HTTP/3クライアントデモ】
*
* ※ 日本語コメントは Claude Code によって追加されました。
*
* このデモは、OpenSSL QUICとnghttp3ライブラリを組み合わせて
* HTTP/3クライアントを実装する方法を示します。
*
* ossl-nghttp3-demo-server.cと同様に、SSL_poll()を使用した
* 非ブロッキングイベント駆動型のアーキテクチャを採用しています。
*
* ============================================================================
* 【アーキテクチャ概要】
* ============================================================================
*
* ┌─────────────────────────────────────────────────────────────────────┐
* │ HTTP/3 Application │
* │ (このデモ: GETリクエスト送信) │
* └─────────────────────────────────────────────────────────────────────┘
* │
* ┌─────────────────────────────────────────────────────────────────────┐
* │ nghttp3 Library │
* │ ・HTTP/3フレーム処理 │
* │ ・QPACKヘッダ圧縮/展開 │
* │ ・リクエスト/レスポンス管理 │
* └─────────────────────────────────────────────────────────────────────┘
* │
* ┌─────────────────────────────────────────────────────────────────────┐
* │ OpenSSL QUIC │
* │ ・QUICプロトコル実装 │
* │ ・ストリーム多重化 │
* │ ・SSL_poll()によるイベント監視 │
* └─────────────────────────────────────────────────────────────────────┘
* │
* ┌─────────────────────────────────────────────────────────────────────┐
* │ UDP Socket │
* └─────────────────────────────────────────────────────────────────────┘
*
* ============================================================================
* 【処理フロー】
* ============================================================================
*
* main()
* │
* ├── コマンドライン引数解析 (host:port [path])
* │
* ├── BIO_parse_hostserv() ... ホスト名とポートを分離
* │
* ├── BIO_lookup_ex() ... DNSルックアップ
* │
* ├── create_ctx() ... SSL_CTX作成(OSSL_QUIC_client_method)
* │
* ├── create_socket_and_connect() ... UDPソケット作成・接続
* │
* ├── SSL_new() + SSL_connect() ... QUIC接続確立
* │
* ├── nghttp3_conn_client_new() ... HTTP/3クライアント接続作成
* │
* ├── quic_client_h3streams() ... HTTP/3制御ストリーム作成
* │ ├── 制御ストリーム (control)
* │ ├── QPACKエンコーダストリーム
* │ └── QPACKデコーダストリーム
* │
* ├── submit_request() ... GETリクエスト送信
* │
* └── for (;;) { ... メインループ
* │
* ├── wait_for_activity() ... select()で待機
* ├── handle_events() ... SSL_handle_events()
* ├── read_from_ssl_ids() ... SSL_poll()でイベント処理
* │ ├── SSL_POLL_EVENT_ISU → SSL_accept_stream() (サーバーのuni)
* │ ├── SSL_POLL_EVENT_R → quic_client_read()
* │ ├── SSL_POLL_EVENT_W → nghttp3_conn_writev_stream()
* │ └── SSL_POLL_EVENT_EC → 接続終了処理
* │
* └── write_from_h3() ... nghttp3データをQUICに送信
* }
*
* ============================================================================
* 【ossl-nghttp3-demo-server.cとの違い】
* ============================================================================
*
* 項目 サーバー クライアント
* ─────────────────────────────────────────────────────────────────────────
* SSL_METHOD OSSL_QUIC_server_method OSSL_QUIC_client_method
* 接続開始 SSL_new_listener SSL_connect
* + SSL_accept_connection
* nghttp3 nghttp3_conn_server_new nghttp3_conn_client_new
* 単方向ストリーム サーバーが作成 サーバーが作成
* (3, 7, 11) (受け入れる側: 3, 7, 11)
* 双方向ストリーム クライアントが作成 クライアントが作成
* (受け入れる側: 0, 4, 8) (0, 4, 8)
*
* ============================================================================
* 【使用方法】
* ============================================================================
*
* $ ./ossl-nghttp3-demo-client <host:port> [path]
*
* 例:
* ./ossl-nghttp3-demo-client localhost:4433
* ./ossl-nghttp3-demo-client localhost:4433 /index.html
*
* ============================================================================
* 【出力】
* ============================================================================
*
* stderr: デバッグ情報、HTTPレスポンスヘッダ
* stdout: HTTPレスポンスボディ
*
* ============================================================================
*/
#include <assert.h>
#include <netinet/in.h>
#include <nghttp3/nghttp3.h>
#include <openssl/err.h>
#include <openssl/quic.h>
#include <openssl/ssl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/select.h>
/*
* 【定数定義】
*/
#define nghttp3_arraylen(A) (sizeof(A) / sizeof(*(A)))
/*
* 【グローバル変数: nghttp3設定】
*
* settings: HTTP/3の設定(ヘッダリスト最大サイズ、QPACKパラメータ等)
* mem: nghttp3が使用するメモリアロケータ
* callbacks: nghttp3からのコールバック関数群
*/
static nghttp3_settings settings;
static const nghttp3_mem *mem;
static nghttp3_callbacks callbacks = {0};
/*
* 【ssl_id構造体 - ストリーム管理】
*
* OpenSSL QUICストリームとnghttp3のストリームIDを対応付ける構造体。
*
* 【QUICストリームID規則】
* - ビット0: 開始者 (0=クライアント, 1=サーバー)
* - ビット1: 方向性 (0=双方向, 1=単方向)
*
* クライアント開始 双方向: 0, 4, 8, 12, ... (リクエスト/レスポンス用)
* クライアント開始 単方向: 2, 6, 10, ... (HTTP/3制御ストリーム)
* サーバー開始 双方向: (HTTP/3では未使用)
* サーバー開始 単方向: 3, 7, 11, ... (HTTP/3制御ストリーム)
*/
struct ssl_id {
SSL *s; /* OpenSSLがSSL_read()/SSL_write()で使用するストリームSSL* */
uint64_t id; /* nghttp3が使用するストリーム識別子(QUIC stream ID) */
int status; /* ストリームの状態フラグ */
};
/*
* 【ストリーム状態フラグ】
*/
#define SERVERUNIOPEN 0x01 /* サーバーが開いた単方向ストリーム (ID: 3, 7, 11) */
#define SERVERCLOSED 0x02 /* サーバーがストリームを閉じた */
#define CLIENTBIDIOPEN 0x04 /* クライアントが開いた双方向ストリーム (ID: 0, 4, 8) */
#define CLIENTUNIOPEN 0x08 /* クライアントが開いた単方向ストリーム (ID: 2, 6, 10) */
#define CLIENTCLOSED 0x10 /* クライアント(自分)がストリームを閉じた */
#define TOBEREMOVED 0x20 /* 削除予約: 全イベント処理後に削除 */
#define ISCONNECTION 0x40 /* QUIC接続オブジェクト */
/*
* 【定数】
*/
#define MAXSSL_IDS 15
#define MAXURL 255
/*
* 【h3ssl構造体 - HTTP/3接続状態管理】
*
* 1つのHTTP/3接続に関する全ての状態を管理する構造体。
*/
struct h3ssl {
/* ストリーム管理配列 */
struct ssl_id ssl_ids[MAXSSL_IDS];
/* 状態フラグ群 */
int end_headers_received; /* on_end_headers()コールバックが呼ばれた */
int datadone; /* レスポンスボディ受信完了 */
int has_uni; /* HTTP/3必須の単方向ストリーム3本を作成済み */
int close_done; /* SSL_POLL_EVENT_ECを受信、接続終了開始 */
int done; /* レスポンス受信完了またはエラー */
/* リクエスト情報 */
uint64_t id_bidi; /* リクエスト用双方向ストリームID */
char url[MAXURL]; /* リクエストパス */
char hostname[MAXURL]; /* ホスト名(:authority用) */
};
/*
* ============================================================================
* 【ヘルパー関数】
* ============================================================================
*/
/*
* make_nv() - nghttp3用ヘッダ名前/値ペアを初期化
*/
static void make_nv(nghttp3_nv *nv, const char *name, const char *value)
{
nv->name = (uint8_t *)name;
nv->value = (uint8_t *)value;
nv->namelen = strlen(name);
nv->valuelen = strlen(value);
nv->flags = NGHTTP3_NV_FLAG_NONE;
}
/*
* init_ids() - h3ssl構造体を初期化
*/
static void init_ids(struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
memset(h3ssl, 0, sizeof(struct h3ssl));
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++)
ssl_ids[i].id = UINT64_MAX;
h3ssl->id_bidi = UINT64_MAX;
}
/*
* add_id_status() - ストリームをssl_ids配列に登録
*/
static void add_id_status(uint64_t id, SSL *ssl, struct h3ssl *h3ssl, int status)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].s == NULL) {
ssl_ids[i].s = ssl;
ssl_ids[i].id = id;
ssl_ids[i].status = status;
return;
}
}
printf("Oops too many streams to add!!!\n");
exit(1);
}
/*
* add_ids_connection() - 接続をssl_ids配列に登録
*/
static void add_ids_connection(struct h3ssl *h3ssl, SSL *ssl)
{
add_id_status(UINT64_MAX, ssl, h3ssl, ISCONNECTION);
}
/*
* get_ids_connection() - 現在の接続SSLを取得
*/
static SSL *get_ids_connection(struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].status & ISCONNECTION) {
return ssl_ids[i].s;
}
}
return NULL;
}
/*
* remove_marked_ids() - TOBEREMOVEDフラグが付いたストリームを削除
*/
static void remove_marked_ids(struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].status & TOBEREMOVED) {
printf("remove_id %llu\n", (unsigned long long) ssl_ids[i].id);
SSL_free(ssl_ids[i].s);
ssl_ids[i].s = NULL;
ssl_ids[i].id = UINT64_MAX;
ssl_ids[i].status = 0;
return;
}
}
}
/*
* set_id_status() - 指定IDのストリームにステータスフラグを追加
*/
static void set_id_status(uint64_t id, int status, struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].id == id) {
printf("set_id_status: %llu to %d\n", (unsigned long long) ssl_ids[i].id, status);
ssl_ids[i].status = ssl_ids[i].status | status;
return;
}
}
printf("Oops can't set status, can't find stream!!!\n");
}
/*
* get_id_status() - 指定IDのストリームのステータスを取得
*/
static int get_id_status(uint64_t id, struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].id == id) {
return ssl_ids[i].status;
}
}
return -1;
}
/*
* close_all_ids() - 全てのストリームを解放(接続以外)
*/
static void close_all_ids(struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].id == UINT64_MAX)
continue;
SSL_free(ssl_ids[i].s);
ssl_ids[i].s = NULL;
ssl_ids[i].id = UINT64_MAX;
}
}
/*
* ============================================================================
* 【nghttp3コールバック関数】
*
* nghttp3ライブラリからHTTP/3イベントを受け取るコールバック。
* ============================================================================
*/
/*
* on_recv_header() - HTTPレスポンスヘッダを1つ受信
*
* サーバーから1つのHTTPヘッダを受信するたびに呼び出される。
* ヘッダ名と値をstderrに出力する。
*/
static int on_recv_header(nghttp3_conn *conn, int64_t stream_id, int32_t token,
nghttp3_rcbuf *name, nghttp3_rcbuf *value,
uint8_t flags, void *user_data,
void *stream_user_data)
{
nghttp3_vec vname, vvalue;
vname = nghttp3_rcbuf_get_buf(name);
vvalue = nghttp3_rcbuf_get_buf(value);
/* ヘッダをstderrに出力: "name: value\n" */
fwrite(vname.base, vname.len, 1, stderr);
fprintf(stderr, ": ");
fwrite(vvalue.base, vvalue.len, 1, stderr);
fprintf(stderr, "\n");
return 0;
}
/*
* on_end_headers() - ヘッダ終了コールバック
*
* 全てのHTTPレスポンスヘッダを受信した後に呼び出される。
*/
static int on_end_headers(nghttp3_conn *conn, int64_t stream_id, int fin,
void *user_data, void *stream_user_data)
{
struct h3ssl *h3ssl = (struct h3ssl *)user_data;
fprintf(stderr, "\n"); /* ヘッダとボディの間に空行 */
h3ssl->end_headers_received = 1;
return 0;
}
/*
* on_recv_data() - HTTPレスポンスボディを受信
*
* サーバーからレスポンスボディのデータを受信するたびに呼び出される。
* 受信したデータをそのままstdoutに出力する。
*/
static int on_recv_data(nghttp3_conn *conn, int64_t stream_id,
const uint8_t *data, size_t datalen,
void *conn_user_data, void *stream_user_data)
{
size_t wr;
while (datalen > 0) {
wr = fwrite(data, 1, datalen, stdout);
if (ferror(stdout))
return 1;
data += wr;
datalen -= wr;
}
return 0;
}
/*
* on_end_stream() - ストリーム終了コールバック
*
* HTTPトランザクションが完了した際に呼び出される。
*/
static int on_end_stream(nghttp3_conn *h3conn, int64_t stream_id,
void *conn_user_data, void *stream_user_data)
{
struct h3ssl *h3ssl = (struct h3ssl *)conn_user_data;
printf("on_end_stream! stream_id=%lld\n", (long long)stream_id);
h3ssl->done = 1;
return 0;
}
/*
* ============================================================================
* 【ストリーム読み取り処理】
* ============================================================================
*/
/*
* quic_client_read() - QUICストリームからデータを読み取りnghttp3に渡す
*
* 【重要】
* nghttp3_conn_read_stream()の第4引数finは、このデータがストリームの
* 最後であることを示す。FINを正しく渡さないとon_end_stream()が呼ばれない。
*/
static int quic_client_read(nghttp3_conn *h3conn, SSL *stream, uint64_t id, struct h3ssl *h3ssl)
{
int r;
uint8_t msg2[16000];
size_t readbytes = 0;
int fin = 0;
/*
* SSL_read_ex()でデータを読み取る。
* ストリームの状態をチェックしてFINを検出する。
*/
if (!SSL_read_ex(stream, msg2, sizeof(msg2), &readbytes)) {
int err = SSL_get_error(stream, 0);
switch (err) {
case SSL_ERROR_WANT_READ:
return 0;
case SSL_ERROR_ZERO_RETURN:
/* ストリームが正常終了(FIN受信) */
fin = 1;
readbytes = 0;
break;
default:
ERR_print_errors_fp(stderr);
return -1;
}
}
/* ストリームの読み取り状態をチェック */
if (SSL_get_stream_read_state(stream) == SSL_STREAM_STATE_FINISHED) {
fin = 1;
}
/* nghttp3にデータを渡す(fin=1でストリーム終了を通知) */
r = nghttp3_conn_read_stream(h3conn, id, msg2, readbytes, fin);
printf("nghttp3_conn_read_stream used %d of %zu on %llu (fin=%d)\n", r,
readbytes, (unsigned long long) id, fin);
if (r < 0) {
if (!nghttp3_err_is_fatal(r)) {
printf("nghttp3_conn_read_stream: non-fatal error %d on %llu\n", r,
(unsigned long long) id);
return 1;
}
fprintf(stderr, "nghttp3_conn_read_stream: fatal error %d\n", r);
return -1;
}
return 1;
}
/*
* quic_client_h3streams() - HTTP/3必須の単方向ストリームを作成
*
* HTTP/3プロトコルでは、クライアントは以下の3つの単方向ストリームを
* サーバーに対して作成する必要がある:
*
* 1. 制御ストリーム (Control Stream)
* 2. QPACKエンコーダストリーム
* 3. QPACKデコーダストリーム
*
* クライアント開始の単方向ストリームは、ID 2, 6, 10, ... の順で割り当てられる。
*/
static int quic_client_h3streams(nghttp3_conn *h3conn, struct h3ssl *h3ssl)
{
SSL *rstream = NULL; /* QPACKデコーダストリーム */
SSL *pstream = NULL; /* QPACKエンコーダストリーム */
SSL *cstream = NULL; /* 制御ストリーム */
SSL *conn;
uint64_t r_streamid, p_streamid, c_streamid;
conn = get_ids_connection(h3ssl);
if (conn == NULL) {
fprintf(stderr, "quic_client_h3streams no connection\n");
return -1;
}
/* 3つの単方向ストリームを作成 */
rstream = SSL_new_stream(conn, SSL_STREAM_FLAG_UNI);
if (rstream == NULL) {
fprintf(stderr, "=> QPACKデコーダストリーム作成失敗\n");
goto err;
}
printf("=> Opened QPACK decoder stream on %llu\n",
(unsigned long long)SSL_get_stream_id(rstream));
pstream = SSL_new_stream(conn, SSL_STREAM_FLAG_UNI);
if (pstream == NULL) {
fprintf(stderr, "=> QPACKエンコーダストリーム作成失敗\n");
goto err;
}
printf("=> Opened QPACK encoder stream on %llu\n",
(unsigned long long)SSL_get_stream_id(pstream));
cstream = SSL_new_stream(conn, SSL_STREAM_FLAG_UNI);
if (cstream == NULL) {
fprintf(stderr, "=> 制御ストリーム作成失敗\n");
goto err;
}
printf("=> Opened control stream on %llu\n",
(unsigned long long)SSL_get_stream_id(cstream));
r_streamid = SSL_get_stream_id(rstream);
p_streamid = SSL_get_stream_id(pstream);
c_streamid = SSL_get_stream_id(cstream);
/* nghttp3にストリームをバインド */
if (nghttp3_conn_bind_qpack_streams(h3conn, p_streamid, r_streamid)) {
fprintf(stderr, "nghttp3_conn_bind_qpack_streams failed!\n");
goto err;
}
if (nghttp3_conn_bind_control_stream(h3conn, c_streamid)) {
fprintf(stderr, "nghttp3_conn_bind_control_stream failed!\n");
goto err;
}
printf("control: %llu enc %llu dec %llu\n",
(unsigned long long)c_streamid,
(unsigned long long)p_streamid,
(unsigned long long)r_streamid);
/* 作成したストリームをssl_ids配列に登録 */
add_id_status(r_streamid, rstream, h3ssl, CLIENTUNIOPEN);
add_id_status(p_streamid, pstream, h3ssl, CLIENTUNIOPEN);
add_id_status(c_streamid, cstream, h3ssl, CLIENTUNIOPEN);
return 0;
err:
SSL_free(rstream);
SSL_free(pstream);
SSL_free(cstream);
return -1;
}
/*
* ============================================================================
* 【SSL_poll()イベントループ】
* ============================================================================
*/
/*
* read_from_ssl_ids() - SSL_poll()でイベントを監視し処理
*/
static int read_from_ssl_ids(nghttp3_conn *h3conn, struct h3ssl *h3ssl)
{
int hassomething = 0, i;
struct ssl_id *ssl_ids = h3ssl->ssl_ids;
SSL_POLL_ITEM items[MAXSSL_IDS] = {0}, *item = items;
static const struct timeval nz_timeout = {0, 0};
size_t result_count = SIZE_MAX;
int numitem = 0, ret;
uint64_t processed_event = 0;
int has_ids_to_remove = 0;
/* SSL_POLL_ITEM配列を構築 */
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].s != NULL) {
item->desc = SSL_as_poll_descriptor(ssl_ids[i].s);
item->events = UINT64_MAX;
item->revents = UINT64_MAX;
numitem++;
item++;
}
}
/* SSL_poll()を呼び出し */
ret = SSL_poll(items, numitem, sizeof(SSL_POLL_ITEM), &nz_timeout,
SSL_POLL_FLAG_NO_HANDLE_EVENTS, &result_count);
if (!ret) {
fprintf(stderr, "SSL_poll failed\n");
return -1;
}
printf("read_from_ssl_ids %ld events\n", (unsigned long)result_count);
if (result_count == 0) {
return 0;
}
/* イベントをディスパッチ */
for (i = 0, item = items; i < numitem; i++, item++) {
SSL *s;
if (item->revents == SSL_POLL_EVENT_NONE)
continue;
processed_event = 0;
s = item->desc.value.ssl;
/*
* 【イベント: SSL_POLL_EVENT_ISU】単方向ストリーム受け入れ可能
*
* サーバーからの単方向ストリーム(制御、QPACKエンコーダ/デコーダ)を
* SSL_accept_stream()で受け入れる。
*/
if (item->revents & SSL_POLL_EVENT_ISU) {
SSL *stream = SSL_accept_stream(s, 0);
uint64_t new_id;
int r;
if (stream == NULL) {
continue;
}
new_id = SSL_get_stream_id(stream);
printf("=> Accepted server uni stream on %llu\n", (unsigned long long) new_id);
add_id_status(new_id, stream, h3ssl, SERVERUNIOPEN);
r = quic_client_read(h3conn, stream, new_id, h3ssl);
if (r == -1) {
ret = -1;
goto err;
}
if (r == 1)
hassomething++;
processed_event = processed_event | SSL_POLL_EVENT_ISU;
}
/*
* 【イベント: SSL_POLL_EVENT_OSU】単方向ストリーム作成可能
*
* クライアントから単方向ストリームを作成できる状態。
* HTTP/3の必須ストリームをまだ作成していなければ、ここで作成する。
*/
if (item->revents & SSL_POLL_EVENT_OSU) {
printf("OSU event - can create uni streams\n");
processed_event = processed_event | SSL_POLL_EVENT_OSU;
if (!h3ssl->has_uni) {
printf("Creating HTTP/3 control streams\n");
ret = quic_client_h3streams(h3conn, h3ssl);
if (ret == -1) {
fprintf(stderr, "quic_client_h3streams failed!\n");
goto err;
}
h3ssl->has_uni = 1;
hassomething++;
}
}
/*
* 【イベント: SSL_POLL_EVENT_EC】接続終了開始
*/
if (item->revents & SSL_POLL_EVENT_EC) {
printf("Connection terminating\n");
if (!h3ssl->close_done) {
h3ssl->close_done = 1;
} else {
h3ssl->done = 1;
}
hassomething++;
processed_event = processed_event | SSL_POLL_EVENT_EC;
}
/*
* 【イベント: SSL_POLL_EVENT_ECD】接続終了完了
*/
if (item->revents & SSL_POLL_EVENT_ECD) {
printf("Connection terminated\n");
h3ssl->done = 1;
hassomething++;
processed_event = processed_event | SSL_POLL_EVENT_ECD;
}
/*
* 【イベント: SSL_POLL_EVENT_R】読み取り可能
*/
if (item->revents & SSL_POLL_EVENT_R) {
uint64_t id = SSL_get_stream_id(s);
int r;
printf("revent READ on %llu\n", (unsigned long long)id);
r = quic_client_read(h3conn, s, id, h3ssl);
if (r == 0) {
uint8_t msg[1];
size_t l = sizeof(msg);
r = SSL_read(s, msg, l);
if (r > 0) {
ret = -1;
goto err;
}
r = SSL_get_error(s, r);
if (r != SSL_ERROR_ZERO_RETURN) {
ret = -1;
goto err;
}
set_id_status(id, TOBEREMOVED, h3ssl);
has_ids_to_remove++;
continue;
}
if (r == -1) {
ret = -1;
goto err;
}
hassomething++;
processed_event = processed_event | SSL_POLL_EVENT_R;
}
/*
* 【イベント: SSL_POLL_EVENT_ER】読み取り側例外
*/
if (item->revents & SSL_POLL_EVENT_ER) {
uint64_t id = SSL_get_stream_id(s);
int status = get_id_status(id, h3ssl);
printf("revent exception READ on %llu\n", (unsigned long long)id);
if (status != -1 && (status & SERVERUNIOPEN)) {
set_id_status(id, SERVERCLOSED, h3ssl);
hassomething++;
}
processed_event = processed_event | SSL_POLL_EVENT_ER;
}
/*
* 【イベント: SSL_POLL_EVENT_W】書き込み可能
*/
if (item->revents & SSL_POLL_EVENT_W) {
processed_event = processed_event | SSL_POLL_EVENT_W;
}
/*
* 【イベント: SSL_POLL_EVENT_EW】書き込み側例外
*/
if (item->revents & SSL_POLL_EVENT_EW) {
uint64_t id = SSL_get_stream_id(s);
int status = get_id_status(id, h3ssl);
if (status != -1 && (status & CLIENTCLOSED)) {
printf("both sides closed on %llu\n", (unsigned long long)id);
set_id_status(id, TOBEREMOVED, h3ssl);
has_ids_to_remove++;
hassomething++;
}
processed_event = processed_event | SSL_POLL_EVENT_EW;
}
if (item->revents != processed_event) {
uint64_t id = SSL_get_stream_id(s);
printf("revent %llu on %llu NOT PROCESSED!\n",
(unsigned long long)item->revents,
(unsigned long long)id);
}
}
ret = hassomething;
err:
if (has_ids_to_remove)
remove_marked_ids(h3ssl);
return ret;
}
/*
* handle_events_from_ids() - QUIC内部イベントを処理
*/
static void handle_events_from_ids(struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids = h3ssl->ssl_ids;
int i;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].s != NULL && (ssl_ids[i].status & ISCONNECTION)) {
SSL_handle_events(ssl_ids[i].s);
}
}
}
/*
* quic_client_write() - QUICストリームにデータを書き込む
*/
static int quic_client_write(struct h3ssl *h3ssl, uint64_t streamid,
uint8_t *buff, size_t len, uint64_t flags,
size_t *written)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].id == streamid) {
if (!SSL_write_ex2(ssl_ids[i].s, buff, len, flags, written) ||
*written != len) {
fprintf(stderr, "couldn't write on connection\n");
ERR_print_errors_fp(stderr);
return 0;
}
printf("written %lld on %lld flags %lld\n", (unsigned long long)len,
(unsigned long long)streamid, (unsigned long long)flags);
return 1;
}
}
printf("quic_client_write %lld on %lld (NOT FOUND!)\n", (unsigned long long)len,
(unsigned long long)streamid);
return 0;
}
/*
* write_from_h3() - nghttp3からデータを取得してQUICに書き込む
*
* nghttp3_conn_writev_stream()でnghttp3が送信したいデータを取得し、
* quic_client_write()でQUICストリームに書き込む。
*/
static int write_from_h3(nghttp3_conn *h3conn, struct h3ssl *h3ssl)
{
nghttp3_vec vec[16];
nghttp3_ssize sveccnt;
int64_t streamid;
int fin;
int ret = 0;
for (;;) {
sveccnt = nghttp3_conn_writev_stream(h3conn, &streamid, &fin, vec,
nghttp3_arraylen(vec));
printf("nghttp3_conn_writev_stream: %lld vectors, stream %lld, fin %d\n",
(long long)sveccnt, (long long)streamid, fin);
if (sveccnt < 0) {
fprintf(stderr, "nghttp3_conn_writev_stream failed: %lld\n",
(long long)sveccnt);
return -1;
}
if (sveccnt == 0 && streamid == -1) {
/* 送信するデータがない */
break;
}
if (sveccnt > 0) {
int i;
uint64_t flags = fin ? SSL_WRITE_FLAG_CONCLUDE : 0;
size_t written;
for (i = 0; i < sveccnt; i++) {
if (!quic_client_write(h3ssl, streamid, vec[i].base, vec[i].len,
(i == sveccnt - 1) ? flags : 0, &written)) {
return -1;
}
}
} else if (fin) {
/* データなしでFINのみ送信 */
size_t written;
if (!quic_client_write(h3ssl, streamid, NULL, 0,
SSL_WRITE_FLAG_CONCLUDE, &written)) {
return -1;
}
}
/* nghttp3に書き込み完了を通知 */
if (nghttp3_conn_add_write_offset(h3conn, streamid,
nghttp3_vec_len(vec, sveccnt))) {
fprintf(stderr, "nghttp3_conn_add_write_offset failed\n");
return -1;
}
ret = 1;
}
return ret;
}
/*
* ============================================================================
* 【リクエスト送信】
* ============================================================================
*/
/*
* submit_request() - HTTP/3 GETリクエストを送信
*/
static int submit_request(nghttp3_conn *h3conn, struct h3ssl *h3ssl)
{
nghttp3_nv nva[5];
size_t num_nv = 0;
SSL *conn;
SSL *stream;
uint64_t stream_id;
int rv;
conn = get_ids_connection(h3ssl);
if (conn == NULL) {
fprintf(stderr, "submit_request: no connection\n");
return -1;
}
/* リクエスト用の双方向ストリームを作成 */
stream = SSL_new_stream(conn, 0); /* 0 = 双方向ストリーム */
if (stream == NULL) {
fprintf(stderr, "Failed to create request stream\n");
ERR_print_errors_fp(stderr);
return -1;
}
stream_id = SSL_get_stream_id(stream);
printf("=> Created request stream on %llu\n", (unsigned long long)stream_id);
/* ストリームを登録 */
add_id_status(stream_id, stream, h3ssl, CLIENTBIDIOPEN);
h3ssl->id_bidi = stream_id;
/* HTTPリクエストヘッダを構築 */
make_nv(&nva[num_nv++], ":method", "GET");
make_nv(&nva[num_nv++], ":scheme", "https");
make_nv(&nva[num_nv++], ":authority", h3ssl->hostname);
make_nv(&nva[num_nv++], ":path", h3ssl->url);
make_nv(&nva[num_nv++], "user-agent", "OpenSSL-nghttp3-demo-client");
/* nghttp3にリクエストを登録 */
rv = nghttp3_conn_submit_request(h3conn, stream_id, nva, num_nv, NULL, NULL);
if (rv != 0) {
fprintf(stderr, "nghttp3_conn_submit_request failed: %d\n", rv);
return -1;
}
printf("Submitted GET request for %s%s\n", h3ssl->hostname, h3ssl->url);
return 0;
}
/*
* ============================================================================
* 【SSL_CTX / ソケット初期化】
* ============================================================================
*/
/*
* ALPN設定 (クライアント側)
*/
static const unsigned char alpn_h3[] = { 2, 'h', '3' };
/*
* create_ctx() - SSL_CTXを作成・設定
*
* パラメータ:
* insecure: 1の場合、証明書検証をスキップ(-kオプション)
*/
static SSL_CTX *create_ctx(int insecure)
{
SSL_CTX *ctx;
ctx = SSL_CTX_new(OSSL_QUIC_client_method());
if (ctx == NULL)
goto err;
if (insecure) {
/* 証明書検証を無効化(自己署名証明書用) */
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
} else {
/* サーバー証明書の検証を有効化 */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
/* システムデフォルトのCA証明書パスを使用 */
if (SSL_CTX_set_default_verify_paths(ctx) == 0)
goto err;
}
return ctx;
err:
SSL_CTX_free(ctx);
return NULL;
}
/*
* wait_for_activity() - ソケットアクティビティ待機
*/
static void wait_for_activity(SSL *ssl)
{
fd_set wfds, rfds;
int fd, width, ret;
struct timeval tv, *tvp = NULL;
int isinfinite;
fd = SSL_get_fd(ssl);
if (fd < 0)
return;
FD_ZERO(&wfds);
FD_ZERO(&rfds);
if (SSL_net_write_desired(ssl))
FD_SET(fd, &wfds);
if (SSL_net_read_desired(ssl))
FD_SET(fd, &rfds);
width = fd + 1;
if (SSL_get_event_timeout(ssl, &tv, &isinfinite) && !isinfinite)
tvp = &tv;
else {
tv.tv_sec = 0;
tv.tv_usec = 100000; /* 100ms */
tvp = &tv;
}
ret = select(width, &rfds, &wfds, NULL, tvp);
(void)ret;
}
/*
* ============================================================================
* 【メイン関数】
* ============================================================================
*/
static void print_usage(const char *prog)
{
fprintf(stderr, "usage: %s [-k] <host:port> [path]\n", prog);
fprintf(stderr, " -k Skip certificate verification (insecure)\n");
}
int main(int argc, char **argv)
{
int ret = 1;
SSL_CTX *ctx = NULL;
SSL *ssl = NULL;
nghttp3_conn *h3conn = NULL;
struct h3ssl h3ssl;
BIO *bio = NULL;
BIO_ADDRINFO *bai = NULL;
const BIO_ADDRINFO *bai_walk;
char *hostname = NULL, *service = NULL;
const char *path = "/";
const char *hostport = NULL;
int ok;
int insecure = 0;
int argidx = 1;
/* オプション解析 */
while (argidx < argc && argv[argidx][0] == '-') {
if (strcmp(argv[argidx], "-k") == 0) {
insecure = 1;
argidx++;
} else {
fprintf(stderr, "Unknown option: %s\n", argv[argidx]);
print_usage(argv[0]);
goto err;
}
}
/* コマンドライン引数の検証 */
if (argidx >= argc) {
print_usage(argv[0]);
goto err;
}
hostport = argv[argidx++];
if (argidx < argc) {
path = argv[argidx];
}
/* ホスト名とポートの分離 */
if (BIO_parse_hostserv(hostport, &hostname, &service, 0) != 1) {
fprintf(stderr, "Failed to parse host:port\n");
goto err;
}
if (hostname == NULL || service == NULL) {
fprintf(stderr, "usage: %s <host:port> [path]\n", argv[0]);
goto err;
}
printf("Connecting to %s:%s, path=%s\n", hostname, service, path);
/* DNSルックアップ */
ok = BIO_lookup_ex(hostname, service, BIO_LOOKUP_CLIENT,
0, SOCK_DGRAM, IPPROTO_UDP, &bai);
if (ok == 0) {
fprintf(stderr, "Host %s not found\n", hostname);
goto err;
}
/* SSL_CTX作成 */
ctx = create_ctx(insecure);
if (ctx == NULL) {
fprintf(stderr, "Failed to create SSL_CTX\n");
ERR_print_errors_fp(stderr);
goto err;
}
/* h3ssl構造体を初期化 */
init_ids(&h3ssl);
strncpy(h3ssl.hostname, hostname, MAXURL - 1);
strncpy(h3ssl.url, path, MAXURL - 1);
/* nghttp3コールバックを設定 */
callbacks.recv_header = on_recv_header;
callbacks.end_headers = on_end_headers;
callbacks.recv_data = on_recv_data;
callbacks.end_stream = on_end_stream;
/* DNSアドレスリストへの接続試行 */
for (bai_walk = bai; bai_walk != NULL;
bai_walk = BIO_ADDRINFO_next(bai_walk)) {
int fd;
/* UDPソケット作成 */
fd = BIO_socket(BIO_ADDRINFO_family(bai_walk), SOCK_DGRAM, IPPROTO_UDP, 0);
if (fd < 0)
continue;
/* ソケットを接続 */
if (!BIO_connect(fd, BIO_ADDRINFO_address(bai_walk), 0)) {
BIO_closesocket(fd);
continue;
}
/* 非ブロッキングモードに設定 */
if (!BIO_socket_nbio(fd, 1)) {
BIO_closesocket(fd);
continue;
}
/* BIO作成 */
bio = BIO_new(BIO_s_datagram());
if (bio == NULL) {
BIO_closesocket(fd);
continue;
}
BIO_set_fd(bio, fd, BIO_CLOSE);
/* SSL作成 */
ssl = SSL_new(ctx);
if (ssl == NULL) {
BIO_free(bio);
bio = NULL;
continue;
}
/* BIOを設定 */
SSL_set_bio(ssl, bio, bio);
bio = NULL; /* SSLが所有権を取得 */
/* ピアアドレスを設定 */
if (!SSL_set1_initial_peer_addr(ssl, BIO_ADDRINFO_address(bai_walk))) {
SSL_free(ssl);
ssl = NULL;
continue;
}
/* SNIを設定 */
if (!SSL_set_tlsext_host_name(ssl, hostname)) {
SSL_free(ssl);
ssl = NULL;
continue;
}
/* 証明書検証用ホスト名を設定 */
if (!SSL_set1_host(ssl, hostname)) {
SSL_free(ssl);
ssl = NULL;
continue;
}
/* ALPNを設定 */
if (SSL_set_alpn_protos(ssl, alpn_h3, sizeof(alpn_h3)) != 0) {
SSL_free(ssl);
ssl = NULL;
continue;
}
/* デフォルトストリームモードを無効化(HTTP/3では必須) */
if (!SSL_set_default_stream_mode(ssl, SSL_DEFAULT_STREAM_MODE_NONE)) {
SSL_free(ssl);
ssl = NULL;
continue;
}
/* 受信ストリームポリシーを設定 */
if (!SSL_set_incoming_stream_policy(ssl, SSL_INCOMING_STREAM_POLICY_ACCEPT, 0)) {
SSL_free(ssl);
ssl = NULL;
continue;
}
/* 非ブロッキングモードを設定 */
if (!SSL_set_blocking_mode(ssl, 0)) {
SSL_free(ssl);
ssl = NULL;
continue;
}
/* QUIC接続を開始 */
printf("Attempting QUIC connection...\n");
while (1) {
int connret = SSL_connect(ssl);
if (connret == 1) {
printf("QUIC connection established!\n");
break;
}
if (connret == 0) {
fprintf(stderr, "SSL_connect failed\n");
ERR_print_errors_fp(stderr);
SSL_free(ssl);
ssl = NULL;
break;
}
int err = SSL_get_error(ssl, connret);
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
wait_for_activity(ssl);
continue;
}
fprintf(stderr, "SSL_connect error: %d\n", err);
ERR_print_errors_fp(stderr);
SSL_free(ssl);
ssl = NULL;
break;
}
if (ssl != NULL)
break; /* 接続成功 */
}
if (ssl == NULL) {
fprintf(stderr, "Failed to connect to any address\n");
goto err;
}
/* 接続をssl_idsに登録 */
add_ids_connection(&h3ssl, ssl);
/* nghttp3クライアント接続を作成 */
mem = nghttp3_mem_default();
nghttp3_settings_default(&settings);
if (nghttp3_conn_client_new(&h3conn, &callbacks, &settings, mem, &h3ssl)) {
fprintf(stderr, "nghttp3_conn_client_new failed!\n");
goto err;
}
printf("nghttp3 client connection created\n");
/* HTTP/3制御ストリームを作成 */
if (quic_client_h3streams(h3conn, &h3ssl) != 0) {
fprintf(stderr, "quic_client_h3streams failed!\n");
goto err;
}
h3ssl.has_uni = 1;
/* 初期データを送信 */
if (write_from_h3(h3conn, &h3ssl) < 0) {
fprintf(stderr, "write_from_h3 failed!\n");
goto err;
}
/* リクエストを送信 */
if (submit_request(h3conn, &h3ssl) != 0) {
fprintf(stderr, "submit_request failed!\n");
goto err;
}
/* リクエストデータを送信 */
if (write_from_h3(h3conn, &h3ssl) < 0) {
fprintf(stderr, "write_from_h3 failed!\n");
goto err;
}
/* メインイベントループ */
printf("Entering main event loop...\n");
while (!h3ssl.done) {
int r;
/* ソケットアクティビティを待機 */
wait_for_activity(ssl);
/* QUIC内部イベントを処理 */
handle_events_from_ids(&h3ssl);
/* SSL_poll()でイベントを処理 */
r = read_from_ssl_ids(h3conn, &h3ssl);
if (r < 0) {
fprintf(stderr, "read_from_ssl_ids failed!\n");
goto err;
}
/* nghttp3のデータを書き込む */
if (write_from_h3(h3conn, &h3ssl) < 0) {
fprintf(stderr, "write_from_h3 failed!\n");
goto err;
}
}
printf("\nRequest completed successfully!\n");
ret = 0;
err:
if (ret != 0)
ERR_print_errors_fp(stderr);
nghttp3_conn_del(h3conn);
close_all_ids(&h3ssl);
SSL_free(ssl);
SSL_CTX_free(ctx);
BIO_ADDRINFO_free(bai);
OPENSSL_free(hostname);
OPENSSL_free(service);
return ret;
}
/*
* Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* ファイルパス: demos/http3/ossl-nghttp3-demo-server.c (OpenSSLリポジトリ内)
*
* 注意: このファイルの日本語コメントは Claude Code によって追加されました。
* オリジナルのソースコードは https://github.com/openssl/openssl にあります。
*/
/*
* 【ossl-nghttp3-demo-server.c - HTTP/3サーバーデモ】
*
* ※ 日本語コメントは Claude Code によって追加されました。
*
* このデモは、OpenSSL QUICとnghttp3ライブラリを組み合わせて
* HTTP/3サーバーを実装する方法を示します。
*
* ============================================================================
* 【アーキテクチャ概要】
* ============================================================================
*
* ┌─────────────────────────────────────────────────────────────────────┐
* │ HTTP/3 Application │
* │ (このデモ: ファイル配信) │
* └─────────────────────────────────────────────────────────────────────┘
* │
* ┌─────────────────────────────────────────────────────────────────────┐
* │ nghttp3 Library │
* │ ・HTTP/3フレーム処理 │
* │ ・QPACKヘッダ圧縮/展開 │
* │ ・リクエスト/レスポンス管理 │
* └─────────────────────────────────────────────────────────────────────┘
* │
* ┌─────────────────────────────────────────────────────────────────────┐
* │ OpenSSL QUIC │
* │ ・QUICプロトコル実装 │
* │ ・ストリーム多重化 │
* │ ・SSL_poll()によるイベント監視 │
* └─────────────────────────────────────────────────────────────────────┘
* │
* ┌─────────────────────────────────────────────────────────────────────┐
* │ UDP Socket │
* └─────────────────────────────────────────────────────────────────────┘
*
* ============================================================================
* 【処理フロー】
* ============================================================================
*
* main()
* │
* ├── create_ctx() ... SSL_CTX作成(OSSL_QUIC_server_method)
* ├── create_socket() ... UDPソケット作成・バインド
* └── run_quic_server() ... メインサーバーループ
* │
* ├── SSL_new_listener()
* ├── nghttp3コールバック設定
* │
* └── for (;;) { ... メインループ
* │
* ├── wait_for_activity() ... select()で待機
* ├── read_from_ssl_ids() ... SSL_poll()でイベント処理
* │ ├── SSL_POLL_EVENT_IC → SSL_accept_connection()
* │ ├── SSL_POLL_EVENT_ISB → SSL_accept_stream() (bidi)
* │ ├── SSL_POLL_EVENT_ISU → SSL_accept_stream() (uni)
* │ ├── SSL_POLL_EVENT_OSU → quic_server_h3streams()
* │ ├── SSL_POLL_EVENT_R → quic_server_read()
* │ └── SSL_POLL_EVENT_EC → 接続終了処理
* │
* ├── quic_server_h3streams() ... HTTP/3制御ストリーム作成
* │ ├── 制御ストリーム (control)
* │ ├── QPACKエンコーダストリーム
* │ └── QPACKデコーダストリーム
* │
* ├── nghttp3_conn_submit_response() ... レスポンス送信
* └── nghttp3_conn_writev_stream() ... データ書き込み
* }
*
* ============================================================================
* 【他のデモとの比較】
* ============================================================================
*
* demos/quic/server/server.c:
* - ブロッキングモード、HTTP無し、単純な文字列送信
* - 最もシンプルなQUICサーバー
*
* demos/quic/poll-server/quic-server-ssl-poll-http.c:
* - 非ブロッキング、HTTP/1.0、カスタムHTTPパーサー
* - poll_event構造体による高度な状態管理
*
* このファイル (ossl-nghttp3-demo-server.c):
* - 非ブロッキング、HTTP/3、nghttp3ライブラリ使用
* - 実際のHTTP/3実装に近いアプローチ
*
* ============================================================================
* 【HTTP/3必須ストリーム】
* ============================================================================
*
* HTTP/3では、以下の3つの単方向ストリームが必須:
*
* 1. 制御ストリーム (Control Stream)
* - SETTINGS、GOAWAY等のフレーム送受信
* - stream ID: サーバー開始のuni = 3, 7, 11, ...
*
* 2. QPACKエンコーダストリーム
* - 動的テーブル更新の送信
*
* 3. QPACKデコーダストリーム
* - 動的テーブル確認応答
*
* クライアントからも同様に3つのストリームが来る (ID: 2, 6, 10)
*
* ============================================================================
* 【使用方法】
* ============================================================================
*
* $ ./ossl-nghttp3-demo-server <port> <cert.pem> <key.pem>
*
* 環境変数:
* FILEPREFIX: 静的ファイルを配信するディレクトリのプレフィックス
*
* ============================================================================
*/
#include <assert.h>
#include <netinet/in.h>
#include <nghttp3/nghttp3.h>
#include <openssl/err.h>
#include <openssl/quic.h>
#include <openssl/ssl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/socket.h>
/*
* 【定数定義】
*/
#ifndef PATH_MAX
# define PATH_MAX 255
#endif
/* nghttp3_arraylen: 配列の要素数を計算するマクロ */
#define nghttp3_arraylen(A) (sizeof(A) / sizeof(*(A)))
/*
* 【デフォルトレスポンスデータ】
*
* ファイルが見つからない場合に返すデフォルトのペイロード。
* テスト用に20バイトの文字列を使用。
*/
/* The crappy test wants 20 bytes */
#define NULL_PAYLOAD "12345678901234567890"
static uint8_t *nulldata = (uint8_t *) NULL_PAYLOAD;
static size_t nulldata_sz = sizeof(NULL_PAYLOAD) - 1;
/*
* 【nghttp3グローバル変数】
*
* nghttp3ライブラリで使用するグローバル変数。
* これらはメインループとread_from_ssl_ids()の両方で参照される。
*
* settings: HTTP/3の設定(ヘッダリスト最大サイズ、QPACKパラメータ等)
* nghttp3_settings_default()でデフォルト値を設定
*
* mem: nghttp3が使用するメモリアロケータ
* nghttp3_mem_default()で標準のmalloc/freeを使用
*
* callbacks: nghttp3からのコールバック関数群
* recv_header, end_headers, recv_data, end_stream等を設定
*/
/* The nghttp3 variable we need in the main part and read_from_ssl_ids */
static nghttp3_settings settings;
static const nghttp3_mem *mem;
static nghttp3_callbacks callbacks = {0};
/*
* 【ssl_id構造体 - ストリーム管理】
*
* OpenSSL QUICストリームとnghttp3のストリームIDを対応付ける構造体。
*
* HTTP/3では、クライアントとサーバーがそれぞれストリームを作成する:
*
* 【QUICストリームID規則】
* - ビット0: 開始者 (0=クライアント, 1=サーバー)
* - ビット1: 方向性 (0=双方向, 1=単方向)
*
* クライアント開始 双方向: 0, 4, 8, 12, ... (リクエスト/レスポンス用)
* クライアント開始 単方向: 2, 6, 10, ... (HTTP/3制御ストリーム)
* サーバー開始 双方向: (HTTP/3では未使用)
* サーバー開始 単方向: 3, 7, 11, ... (HTTP/3制御ストリーム)
*
* 典型的なHTTP/3接続では:
* クライアント: ID 0 (bidi: リクエスト), 2, 6, 10 (uni: control, qpack enc/dec)
* サーバー: 3, 7, 11 (uni: control, qpack enc/dec)
*/
/* 3 streams created by the server and 4 by the client (one is bidi) */
struct ssl_id {
SSL *s; /* the stream openssl uses in SSL_read(), SSL_write etc */
/* OpenSSLがSSL_read()/SSL_write()で使用するストリームSSL* */
uint64_t id; /* the stream identifier the nghttp3 uses */
/* nghttp3が使用するストリーム識別子(QUIC stream ID) */
int status; /* 0 or one the below status and origin */
/* ストリームの状態フラグ(下記定数のビットOR) */
};
/*
* 【ストリーム状態フラグ】
*
* ssl_id.statusフィールドに設定するビットフラグ。
* 複数のフラグを組み合わせてストリームの現在状態を表す。
*/
/* status and origin of the streams the possible values are: */
#define CLIENTUNIOPEN 0x01 /* unidirectional open by the client (2, 6 and 10) */
/* クライアントが開いた単方向ストリーム (ID: 2, 6, 10) */
#define CLIENTCLOSED 0x02 /* closed by the client */
/* クライアントがストリームを閉じた */
#define CLIENTBIDIOPEN 0x04 /* bidirectional open by the client (something like 0, 4, 8 ...) */
/* クライアントが開いた双方向ストリーム (ID: 0, 4, 8, ...) */
#define SERVERUNIOPEN 0x08 /* unidirectional open by the server (3, 7 and 11) */
/* サーバーが開いた単方向ストリーム (ID: 3, 7, 11) */
#define SERVERCLOSED 0x10 /* closed by the server (us) */
/* サーバー(自分)がストリームを閉じた */
#define TOBEREMOVED 0x20 /* marked for removing in read_from_ssl_ids, */
/* it will be removed after processing all events */
/* 削除予約: 全イベント処理後にremove_marked_ids()で削除 */
#define ISLISTENER 0x40 /* the stream is a listener from SSL_new_listener() */
/* SSL_new_listener()で作成したリスナー */
#define ISCONNECTION 0x80 /* the stream is a connection from SSL_accept_connection() */
/* SSL_accept_connection()で受け入れた接続 */
/*
* 【定数】
*
* MAXSSL_IDS: 管理可能なストリームの最大数
* HTTP/3では通常、制御用3+3=6ストリーム + リクエストストリーム
*
* MAXURL: URLパスの最大長
*/
#define MAXSSL_IDS 20
#define MAXURL 255
/*
* 【h3ssl構造体 - HTTP/3接続状態管理】
*
* 1つのHTTP/3接続に関する全ての状態を管理する構造体。
* 接続ごとに1つのインスタンスを使用し、接続のライフサイクル全体を追跡。
*
* 【状態遷移の概要】
*
* 初期化 (init_ids)
* │
* ▼
* 接続受け入れ (new_conn = 1)
* │
* ▼
* HTTP/3制御ストリーム作成 (has_uni = 1)
* │
* ▼
* ヘッダ受信完了 (end_headers_received = 1)
* │
* ▼
* レスポンス送信 (datadone = 1)
* │
* ▼
* 終了待機 (close_wait = 1)
* │
* ├── 新しいリクエスト → restart = 1 → ヘッダ受信待機へ戻る
* │
* └── 接続終了 → close_done = 1 → done = 1
*/
struct h3ssl {
/*
* 【ストリーム管理配列】
* 全てのアクティブなストリーム(リスナー、接続、各種QUICストリーム)を管理。
* ssl_ids[i].s == NULLのエントリは空きスロット。
*/
struct ssl_id ssl_ids[MAXSSL_IDS];
/*
* 【状態フラグ群】
* HTTP/3リクエスト/レスポンスのライフサイクルを追跡。
*/
int end_headers_received; /* h3 header received call back called */
/* on_end_headers()コールバックが呼ばれた */
/* HTTPヘッダの受信が完了 */
int datadone; /* h3 has given openssl all the data of the response */
/* レスポンスデータの送信が完了 */
/* step_read_data()でNGHTTP3_DATA_FLAG_EOFを返した */
int has_uni; /* we have the 3 uni directional stream needed */
/* HTTP/3必須の単方向ストリーム3本を作成済み */
/* (control, qpack encoder, qpack decoder) */
int close_done; /* connection begins terminating EVENT_EC */
/* SSL_POLL_EVENT_ECを受信、接続終了開始 */
int close_wait; /* we are waiting for a close or a new request */
/* レスポンス送信後、クローズまたは次リクエスト待ち */
int done; /* connection terminated EVENT_ECD, after EVENT_EC */
/* SSL_POLL_EVENT_ECDを受信、接続完全終了 */
int new_conn; /* a new connection has been received */
/* 新しい接続をSSL_accept_connection()で受け入れた */
int received_from_two; /* workaround for -607 on nghttp3_conn_read_stream on stream 2 */
/* stream ID 2でnghttp3が-607エラーを返す問題の回避用 */
/* Chromeとの互換性のためのワークアラウンド */
int restart; /* new request/response cycle started */
/* 同一接続上で新しいリクエストが開始された */
/* HTTP/3のストリーム多重化による複数リクエスト対応 */
/*
* 【リクエスト/レスポンス処理用】
*/
uint64_t id_bidi; /* the id of the stream used to read request and send response */
/* 現在処理中のリクエスト/レスポンス用双方向ストリームID */
/* クライアント開始なので 0, 4, 8, ... */
char *fileprefix; /* prefix of the directory to fetch files from */
/* ファイル配信のベースディレクトリ */
/* 環境変数FILEPREFIXから設定 */
char url[MAXURL]; /* url to serve the request */
/* リクエストされたURLパス(:path疑似ヘッダの値) */
/*
* 【レスポンスデータ管理】
*/
uint8_t *ptr_data; /* pointer to the data to send */
/* 送信するレスポンスデータへのポインタ */
/* ファイル内容またはnulldata */
size_t ldata; /* amount of bytes to send */
/* 残り送信バイト数 */
int offset_data; /* offset to next data to send */
/* 次に送信するデータの開始オフセット */
/* チャンク送信時に使用 */
};
/*
* ============================================================================
* 【ヘルパー関数】
* ============================================================================
*/
/*
* make_nv() - nghttp3用ヘッダ名前/値ペアを初期化
*
* nghttp3_nv構造体は、HTTP/3ヘッダのname/valueペアを表す。
* HTTPヘッダ(例: "content-type: text/html")や
* HTTP/3疑似ヘッダ(例: ":status: 200")の設定に使用。
*
* 注意: nameとvalueは、nghttp3がこの構造体を使用する間、
* 有効なメモリを指し続ける必要がある(コピーされない)。
*/
static void make_nv(nghttp3_nv *nv, const char *name, const char *value)
{
nv->name = (uint8_t *)name; /* ヘッダ名(例: ":status", "content-type") */
nv->value = (uint8_t *)value; /* ヘッダ値(例: "200", "text/html") */
nv->namelen = strlen(name); /* 名前の長さ */
nv->valuelen = strlen(value); /* 値の長さ */
nv->flags = NGHTTP3_NV_FLAG_NONE; /* 特別なフラグなし */
}
/*
* init_ids() - h3ssl構造体を初期化
*
* 新しい接続を処理する前にh3ssl構造体をクリアする。
*
* 処理内容:
* 1. 前回のレスポンスデータを解放(nulldataでない場合)
* 2. 構造体全体をゼロクリア
* 3. 全ssl_idのIDをUINT64_MAX(未使用マーカー)に設定
* 4. fileprefixを復元(環境変数から設定されるため保持)
*
* 注意: SSL*ポインタは解放しない。それは別途close_all_ids()で行う。
*/
static void init_ids(struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
char *prior_fileprefix = h3ssl->fileprefix;
/* 前回のレスポンスデータを解放(動的に確保した場合のみ) */
if (h3ssl->ptr_data != NULL && h3ssl->ptr_data != nulldata)
free(h3ssl->ptr_data);
/* 構造体全体をゼロクリア */
memset(h3ssl, 0, sizeof(struct h3ssl));
/* 全スロットを未使用状態に初期化 */
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++)
ssl_ids[i].id = UINT64_MAX; /* UINT64_MAX = 未使用マーカー */
h3ssl->id_bidi = UINT64_MAX; /* 双方向ストリームも未設定 */
/* restore the fileprefix */
/* fileprefixは環境変数由来なので復元 */
h3ssl->fileprefix = prior_fileprefix;
}
/*
* reuse_h3ssl() - 同一接続上で新しいリクエストを処理するためにリセット
*
* HTTP/3では、1つの接続上で複数のリクエストを処理できる(ストリーム多重化)。
* 新しいリクエストを受け取った際に、リクエスト固有の状態のみをリセットし、
* 接続やストリーム管理の状態は維持する。
*
* init_ids()との違い:
* - init_ids(): 接続全体をリセット(新規接続時)
* - reuse_h3ssl(): リクエスト状態のみリセット(同一接続での次リクエスト時)
*/
static void reuse_h3ssl(struct h3ssl *h3ssl)
{
/* リクエスト処理の状態フラグをリセット */
h3ssl->end_headers_received = 0; /* ヘッダ未受信状態に戻す */
h3ssl->datadone = 0; /* データ送信未完了 */
h3ssl->close_done = 0; /* クローズ未開始 */
h3ssl->close_wait = 0; /* クローズ待ち解除 */
h3ssl->done = 0; /* 完了フラグリセット */
/* URL情報をクリア */
memset(h3ssl->url, '\0', sizeof(h3ssl->url));
/* 前回のレスポンスデータを解放 */
if (h3ssl->ptr_data != NULL && h3ssl->ptr_data != nulldata)
free(h3ssl->ptr_data);
h3ssl->ptr_data = NULL;
h3ssl->offset_data = 0;
h3ssl->ldata = 0;
}
/*
* add_id_status() - ストリームをssl_ids配列に登録
*
* 新しいストリームを空きスロットに追加する。
* QUIC stream IDとOpenSSL SSL*ポインタを対応付けて管理。
*
* パラメータ:
* id: QUICストリームID(nghttp3が使用)
* ssl: OpenSSL SSL*(SSL_read/SSL_writeが使用)
* h3ssl: 状態管理構造体
* status: 初期ステータスフラグ(CLIENTUNIOPEN等)
*/
static void add_id_status(uint64_t id, SSL *ssl, struct h3ssl *h3ssl, int status)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
/* 空きスロット(s == NULL)を探す */
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].s == NULL) {
ssl_ids[i].s = ssl;
ssl_ids[i].id = id;
ssl_ids[i].status = status;
return;
}
}
/* 空きスロットがない場合はエラー終了 */
printf("Oops too many streams to add!!!\n");
exit(1);
}
/*
* add_id() - ストリームをステータス0で登録
*
* add_id_status()のラッパー。通常のストリーム追加用。
*/
static void add_id(uint64_t id, SSL *ssl, struct h3ssl *h3ssl)
{
add_id_status(id, ssl, h3ssl, 0);
}
/*
* 【リスナー・コネクション管理関数群】
*
* QUICサーバーでは、以下の階層構造でオブジェクトを管理:
*
* リスナー (SSL_new_listener)
* │
* └── 接続 (SSL_accept_connection)
* │
* ├── 双方向ストリーム (リクエスト/レスポンス)
* └── 単方向ストリーム (HTTP/3制御)
*
* リスナーと接続は、通常のストリームとは別に特別なステータスで管理。
* IDはUINT64_MAX(ストリームIDを持たないため)。
*/
/* Add listener and connection */
/*
* add_ids_listener() - リスナーをssl_ids配列に登録
*
* SSL_new_listener()で作成したリスナーを追加。
* リスナーはSSL_poll()でSSL_POLL_EVENT_ICを監視するために必要。
*/
static void add_ids_listener(SSL *ssl, struct h3ssl *h3ssl)
{
add_id_status(UINT64_MAX, ssl, h3ssl, ISLISTENER);
}
/*
* add_ids_connection() - 接続をssl_ids配列に登録
*
* SSL_accept_connection()で受け入れた接続を追加。
* 接続からストリームを作成・受け入れるために必要。
*/
static void add_ids_connection(struct h3ssl *h3ssl, SSL *ssl)
{
add_id_status(UINT64_MAX, ssl, h3ssl, ISCONNECTION);
}
/*
* get_ids_connection() - 現在の接続SSLを取得
*
* ssl_ids配列からISCONNECTIONフラグが付いたエントリを探して返す。
* 新しいストリームを作成する際に接続SSLが必要。
*
* 戻り値: 接続SSL*、または見つからない場合はNULL
*/
static SSL *get_ids_connection(struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].status & ISCONNECTION) {
printf("get_ids_connection\n");
return ssl_ids[i].s;
}
}
return NULL;
}
/*
* replace_ids_connection() - 接続SSLを置換
*
* 古い接続を新しい接続で置き換える。
* 新しい接続が来た際に、古い接続をクローズして新しい接続で
* 継続する場合に使用。
*/
static void replace_ids_connection(struct h3ssl *h3ssl, SSL *oldstream, SSL *newstream)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].status & ISCONNECTION && ssl_ids[i].s == oldstream) {
printf("replace_ids_connection\n");
ssl_ids[i].s = newstream;
}
}
}
/*
* remove_marked_ids() - TOBEREMOVEDフラグが付いたストリームを削除
*
* イベント処理中にストリームを直接削除すると、SSL_poll()の
* 結果配列が不整合になる可能性がある。そのため、削除予定の
* ストリームにTOBEREMOVEDフラグを付け、全イベント処理後に
* この関数で実際の削除を行う。
*
* 処理内容:
* 1. TOBEREMOVEDフラグを持つエントリを探す
* 2. SSL_free()でストリームを解放
* 3. スロットを未使用状態にリセット
*
* 注意: 1回の呼び出しで1つのエントリのみ削除(returnで終了)
*/
/* remove the ids marked for removal */
static void remove_marked_ids(struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].status & TOBEREMOVED) {
printf("remove_id %llu\n", (unsigned long long) ssl_ids[i].id);
SSL_free(ssl_ids[i].s); /* ストリームを解放 */
ssl_ids[i].s = NULL; /* ポインタをクリア */
ssl_ids[i].id = UINT64_MAX; /* 未使用マーカーに戻す */
ssl_ids[i].status = 0; /* ステータスをクリア */
return; /* 1つ削除したら終了 */
}
}
}
/*
* set_id_status() - 指定IDのストリームにステータスフラグを追加
*
* 既存のstatusにビットORで新しいフラグを追加する。
* 例: CLIENTUNIOPEN状態のストリームにCLIENTCLOSEDを追加
*
* パラメータ:
* id: 対象のQUICストリームID
* status: 追加するステータスフラグ
* h3ssl: 状態管理構造体
*/
/* add the status bytes to the status */
static void set_id_status(uint64_t id, int status, struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].id == id) {
printf("set_id_status: %llu to %d\n", (unsigned long long) ssl_ids[i].id, status);
ssl_ids[i].status = ssl_ids[i].status | status; /* ビットORで追加 */
return;
}
}
/* 指定IDが見つからない場合はプログラムエラー */
printf("Oops can't set status, can't find stream!!!\n");
assert(0);
}
/*
* get_id_status() - 指定IDのストリームのステータスを取得
*
* パラメータ:
* id: 対象のQUICストリームID
* h3ssl: 状態管理構造体
*
* 戻り値: ステータスフラグのビットOR値
*/
static int get_id_status(uint64_t id, struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].id == id) {
printf("get_id_status: %llu to %d\n",
(unsigned long long) ssl_ids[i].id, ssl_ids[i].status);
return ssl_ids[i].status;
}
}
/* 指定IDが見つからない場合はプログラムエラー */
printf("Oops can't get status, can't find stream!!!\n");
assert(0);
return -1;
}
/*
* are_all_clientid_closed() - クライアントの全ストリームが閉じたか確認
*
* HTTP/3接続の正常終了を判定するために使用。
* クライアントが開いた単方向ストリーム(制御、QPACKエンコーダ、デコーダ)が
* 全て閉じられているかを確認する。
*
* 処理内容:
* 1. CLIENTUNIOPENフラグを持つストリームを探す
* 2. CLIENTCLOSEDフラグも付いていれば、そのストリームを解放
* 3. まだ開いているストリームがあれば0を返す
*
* 戻り値:
* 1: 全クライアントストリームが閉じている
* 0: まだ開いているストリームがある
*/
/* check that all streams opened by the client are closed */
static int are_all_clientid_closed(struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
/* 未使用スロットはスキップ */
if (ssl_ids[i].id == UINT64_MAX)
continue;
printf("are_all_clientid_closed: %llu status %d : %d\n",
(unsigned long long) ssl_ids[i].id, ssl_ids[i].status, CLIENTUNIOPEN | CLIENTCLOSED);
/* クライアントが開いた単方向ストリームをチェック */
if (ssl_ids[i].status & CLIENTUNIOPEN) {
if (ssl_ids[i].status & CLIENTCLOSED) {
/* 閉じられたストリームは解放 */
printf("are_all_clientid_closed: %llu closed\n",
(unsigned long long) ssl_ids[i].id);
SSL_free(ssl_ids[i].s);
ssl_ids[i].s = NULL;
ssl_ids[i].id = UINT64_MAX;
continue;
}
/* まだ開いているストリームがある */
printf("are_all_clientid_closed: %llu open\n", (unsigned long long) ssl_ids[i].id);
return 0;
}
}
return 1; /* 全てのクライアントストリームが閉じた */
}
/*
* close_all_ids() - 全てのストリームを解放(リスナー・接続以外)
*
* 接続終了時に呼び出し、管理中の全ストリームを解放する。
* リスナーと接続は別途管理されるため、ここでは解放しない。
*
* 注意: IDがUINT64_MAX以外の全エントリを解放。
* リスナー/接続はIDがUINT64_MAXなので影響を受けない。
*/
/* free all the ids except listener and connection */
static void close_all_ids(struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
/* 未使用スロット(リスナー/接続含む)はスキップ */
if (ssl_ids[i].id == UINT64_MAX)
continue;
/* ストリームを解放してスロットをクリア */
SSL_free(ssl_ids[i].s);
ssl_ids[i].s = NULL;
ssl_ids[i].id = UINT64_MAX;
}
}
/*
* ============================================================================
* 【nghttp3コールバック関数】
*
* nghttp3ライブラリからHTTP/3イベントを受け取るコールバック。
* これらはnghttp3_callbacksに設定し、nghttp3_conn_server_new()に渡す。
*
* 呼び出しタイミング:
* nghttp3_conn_read_stream() → データをパース
* → 適切なコールバックを呼び出し
*
* 【HTTP/3リクエスト受信の流れ】
*
* 1. on_recv_header() × N回 (各ヘッダごと)
* ↓
* 2. on_end_headers() × 1回 (ヘッダ終了)
* ↓
* 3. on_recv_data() × N回 (リクエストボディがある場合)
* ↓
* 4. on_end_stream() × 1回 (リクエスト終了)
*
* ============================================================================
*/
/*
* on_recv_header() - HTTPヘッダを1つ受信
*
* HTTPリクエストの各ヘッダ(疑似ヘッダ含む)を受信するたびに呼ばれる。
*
* パラメータ:
* conn: nghttp3接続
* stream_id: リクエストのQUICストリームID
* token: QPACKトークン(既知のヘッダ名の識別子)
* name: ヘッダ名(参照カウント付きバッファ)
* value: ヘッダ値(参照カウント付きバッファ)
* flags: フラグ
* user_data: h3ssl構造体
* stream_user_data: ストリーム固有データ(未使用)
*
* HTTP/3疑似ヘッダ:
* :method - HTTPメソッド(GET, POST等)
* :scheme - スキーム(https)
* :authority - ホスト名
* :path - リクエストパス ← この関数で抽出してurl[]に保存
*/
static int on_recv_header(nghttp3_conn *conn, int64_t stream_id, int32_t token,
nghttp3_rcbuf *name, nghttp3_rcbuf *value,
uint8_t flags, void *user_data,
void *stream_user_data)
{
nghttp3_vec vname, vvalue;
struct h3ssl *h3ssl = (struct h3ssl *)user_data;
/* Received a single HTTP header. */
/* 参照カウント付きバッファからデータポインタを取得 */
vname = nghttp3_rcbuf_get_buf(name);
vvalue = nghttp3_rcbuf_get_buf(value);
/* デバッグ用: 受信したヘッダを出力 */
fwrite(vname.base, vname.len, 1, stdout);
fprintf(stdout, ": ");
fwrite(vvalue.base, vvalue.len, 1, stdout);
fprintf(stdout, "\n");
/*
* :path疑似ヘッダの処理
*
* 例: :path: /index.html → url = "index.html"
* :path: / → url = "index.html"(デフォルト)
* :path: /foo/bar → url = "foo/bar"
*
* 先頭の'/'は削除して保存(ファイルパス構築時に便利)
*/
if (token == NGHTTP3_QPACK_TOKEN__PATH) {
int len = (((vvalue.len) < (MAXURL)) ? (vvalue.len) : (MAXURL));
memset(h3ssl->url, 0, sizeof(h3ssl->url));
if (vvalue.base[0] == '/') {
if (vvalue.base[1] == '\0') {
/* ルートパス "/" → デフォルトファイル */
strncpy(h3ssl->url, "index.html", MAXURL);
} else {
/* 先頭の'/'を除いてコピー */
memcpy(h3ssl->url, vvalue.base + 1, len - 1);
h3ssl->url[len - 1] = '\0';
}
} else {
/* '/'で始まらない場合はそのままコピー */
memcpy(h3ssl->url, vvalue.base, len);
}
}
return 0; /* 0 = 成功 */
}
/*
* on_end_headers() - HTTPヘッダの受信完了通知
*
* 全てのHTTPヘッダを受信し終わった時に呼ばれる。
* この時点でリクエストの内容(メソッド、パス等)が確定する。
*
* パラメータ:
* fin: ストリームが終了するか(ボディなしの場合は1)
*
* このデモでは:
* end_headers_receivedフラグを立てて、メインループに
* レスポンス送信の準備ができたことを通知する。
*/
static int on_end_headers(nghttp3_conn *conn, int64_t stream_id, int fin,
void *user_data, void *stream_user_data)
{
struct h3ssl *h3ssl = (struct h3ssl *)user_data;
fprintf(stderr, "on_end_headers!\n");
h3ssl->end_headers_received = 1; /* レスポンス送信可能になった */
return 0;
}
/*
* on_recv_data() - HTTPリクエストボディを受信
*
* POSTリクエスト等でリクエストボディがある場合に呼ばれる。
* GETリクエストでは通常呼ばれない。
*
* このデモでは:
* デバッグ出力のみ。実際の処理は行わない。
* ファイルアップロード等を実装する場合は、ここでデータを処理。
*/
static int on_recv_data(nghttp3_conn *conn, int64_t stream_id,
const uint8_t *data, size_t datalen,
void *conn_user_data, void *stream_user_data)
{
fprintf(stderr, "on_recv_data! %ld\n", (unsigned long)datalen);
fprintf(stderr, "on_recv_data! %.*s\n", (int)datalen, data);
return 0;
}
/*
* on_end_stream() - リクエストストリームの終了通知
*
* クライアントがリクエストストリームを閉じた時に呼ばれる。
* これは通常、リクエスト全体の送信が完了したことを意味する。
*
* このデモでは:
* doneフラグを立てて、接続終了処理を開始できることを通知。
*
* 注意: HTTP/3では、レスポンス送信後にこのコールバックが来る場合もある。
*/
static int on_end_stream(nghttp3_conn *h3conn, int64_t stream_id,
void *conn_user_data, void *stream_user_data)
{
struct h3ssl *h3ssl = (struct h3ssl *)conn_user_data;
printf("on_end_stream!\n");
h3ssl->done = 1; /* ストリーム終了 */
return 0;
}
/*
* ============================================================================
* 【ストリーム読み取り処理】
*
* OpenSSL QUICからデータを読み取り、nghttp3に渡すブリッジ関数群。
*
* 【データフロー】
*
* [QUIC Packet] → SSL_read() → quic_server_read()
* ↓
* nghttp3_conn_read_stream()
* ↓
* [コールバック呼び出し]
* on_recv_header(), on_end_headers(), etc.
*
* ============================================================================
*/
/*
* quic_server_read() - QUICストリームからデータを読み取りnghttp3に渡す
*
* OpenSSL SSL_read()でQUICストリームからデータを読み取り、
* nghttp3_conn_read_stream()に渡してHTTP/3フレームをパースさせる。
*
* パラメータ:
* h3conn: nghttp3接続
* stream: OpenSSL SSL*(読み取り元ストリーム)
* id: QUICストリームID
* h3ssl: 状態管理構造体
*
* 戻り値:
* 1: データを読み取った
* 0: 読み取るデータがない(SSL_ERROR_WANT_READ)
* -1: エラー
*/
/* Read from the stream and push to the h3conn */
static int quic_server_read(nghttp3_conn *h3conn, SSL *stream, uint64_t id, struct h3ssl *h3ssl)
{
int ret, r;
uint8_t msg2[16000]; /* 読み取りバッファ */
size_t l = sizeof(msg2);
/* 読み取り可能なデータがあるかチェック */
if (!SSL_has_pending(stream))
return 0; /* Nothing to read */
/* QUICストリームからデータを読み取る */
ret = SSL_read(stream, msg2, l);
if (ret <= 0) {
fprintf(stderr, "SSL_read %d on %llu failed\n",
SSL_get_error(stream, ret),
(unsigned long long) id);
switch (SSL_get_error(stream, ret)) {
case SSL_ERROR_WANT_READ:
/* 非ブロッキングモードで、読み取るデータがない */
return 0;
case SSL_ERROR_ZERO_RETURN:
/* ストリームが正常終了(FIN受信) */
return 1;
default:
/* その他のエラー */
ERR_print_errors_fp(stderr);
return -1;
}
return -1;
}
/*
* nghttp3にデータを渡してパースさせる
*
* 【ワークアラウンド】
* stream ID 2(クライアントのQPACKエンコーダストリーム)で
* nghttp3が-607エラー(NGHTTP3_ERR_QPACK_ENCODER_STREAM_ERROR)を
* 返すことがある(特にChromeで発生)。
*
* 一度エラーが発生したら、以降はそのストリームからの
* データをnghttp3に渡さないようにする。
*/
/* XXX: work around nghttp3_conn_read_stream returning -607 on stream 2 */
if (!h3ssl->received_from_two && id != 2) {
/* 通常のストリーム処理 */
r = nghttp3_conn_read_stream(h3conn, id, msg2, ret, 0);
} else {
/* stream 2は無視(ワークアラウンド) */
r = ret; /* ignore it for the moment ... */
}
printf("nghttp3_conn_read_stream used %d of %d on %llu\n", r,
ret, (unsigned long long) id);
if (r != ret) {
/* chrome returns -607 on stream 2 */
/* nghttp3が全てのデータを消費しなかった場合 */
if (!nghttp3_err_is_fatal(r)) {
/* 致命的でないエラーは警告のみで続行 */
printf("nghttp3_conn_read_stream used %d of %d (not fatal) on %llu\n", r,
ret, (unsigned long long) id);
if (id == 2)
h3ssl->received_from_two = 1; /* stream 2のワークアラウンドフラグ */
return 1;
}
/* 致命的エラー */
return -1;
}
return 1; /* 正常に読み取り完了 */
}
/*
* quic_server_h3streams() - HTTP/3必須の単方向ストリームを作成
*
* HTTP/3プロトコルでは、サーバーは以下の3つの単方向ストリームを
* クライアントに対して作成する必要がある:
*
* 1. 制御ストリーム (Control Stream)
* - SETTINGSフレーム、GOAWAYフレーム等を送受信
* - 接続の設定情報を交換
*
* 2. QPACKエンコーダストリーム (QPACK Encoder Stream)
* - QPACKヘッダ圧縮の動的テーブル更新を送信
* - ヘッダブロックで参照される前に動的テーブルエントリを挿入
*
* 3. QPACKデコーダストリーム (QPACK Decoder Stream)
* - 動的テーブルエントリの受信確認を送信
* - 相手のエンコーダに対してACKを返す
*
* 【ストリームID】
* サーバー開始の単方向ストリームは、ID 3, 7, 11, ... の順で割り当てられる。
* (ビット0=1: サーバー開始, ビット1=1: 単方向)
*
* 戻り値:
* 0: 成功
* -1: エラー
*/
/*
* creates the control stream, the encoding and decoding streams.
* nghttp3_conn_bind_control_stream() is for the control stream.
*/
static int quic_server_h3streams(nghttp3_conn *h3conn, struct h3ssl *h3ssl)
{
SSL *rstream = NULL; /* QPACKデコーダストリーム (receiver/decoder) */
SSL *pstream = NULL; /* QPACKエンコーダストリーム (pusher/encoder) */
SSL *cstream = NULL; /* 制御ストリーム (control) */
SSL *conn;
uint64_t r_streamid, p_streamid, c_streamid;
/* QUIC接続を取得 */
conn = get_ids_connection(h3ssl);
if (conn == NULL) {
fprintf(stderr, "quic_server_h3streams no connection\n");
fflush(stderr);
return -1;
}
/*
* 3つの単方向ストリームを作成
* SSL_STREAM_FLAG_UNI: 単方向ストリーム(送信のみ可能)
*/
/* QPACKデコーダストリーム */
rstream = SSL_new_stream(conn, SSL_STREAM_FLAG_UNI);
if (rstream != NULL) {
printf("=> Opened on %llu\n",
(unsigned long long)SSL_get_stream_id(rstream));
} else {
fprintf(stderr, "=> Stream == NULL!\n");
goto err;
}
/* QPACKエンコーダストリーム */
pstream = SSL_new_stream(conn, SSL_STREAM_FLAG_UNI);
if (pstream != NULL) {
printf("=> Opened on %llu\n",
(unsigned long long)SSL_get_stream_id(pstream));
} else {
fprintf(stderr, "=> Stream == NULL!\n");
goto err;
}
/* 制御ストリーム */
cstream = SSL_new_stream(conn, SSL_STREAM_FLAG_UNI);
if (cstream != NULL) {
fprintf(stderr, "=> Opened on %llu\n",
(unsigned long long)SSL_get_stream_id(cstream));
fflush(stderr);
} else {
fprintf(stderr, "=> Stream == NULL!\n");
goto err;
}
/* 各ストリームのIDを取得 */
r_streamid = SSL_get_stream_id(rstream);
p_streamid = SSL_get_stream_id(pstream);
c_streamid = SSL_get_stream_id(cstream);
/*
* nghttp3にストリームをバインド
*
* nghttp3_conn_bind_qpack_streams():
* QPACKエンコーダ/デコーダストリームを関連付け
* これによりnghttp3がQPACKの動的テーブル管理を行える
*
* nghttp3_conn_bind_control_stream():
* 制御ストリームを関連付け
* これによりSETTINGS等のフレームを送受信できる
*/
if (nghttp3_conn_bind_qpack_streams(h3conn, p_streamid, r_streamid)) {
fprintf(stderr, "nghttp3_conn_bind_qpack_streams failed!\n");
goto err;
}
if (nghttp3_conn_bind_control_stream(h3conn, c_streamid)) {
fprintf(stderr, "nghttp3_conn_bind_qpack_streams failed!\n");
goto err;
}
printf("control: %llu enc %llu dec %llu\n",
(unsigned long long)c_streamid,
(unsigned long long)p_streamid,
(unsigned long long)r_streamid);
/* 作成したストリームをssl_ids配列に登録 */
add_id(SSL_get_stream_id(rstream), rstream, h3ssl);
add_id(SSL_get_stream_id(pstream), pstream, h3ssl);
add_id(SSL_get_stream_id(cstream), cstream, h3ssl);
return 0;
err:
fflush(stderr);
SSL_free(rstream);
SSL_free(pstream);
SSL_free(cstream);
return -1;
}
/*
* ============================================================================
* 【SSL_poll()イベントループ】
*
* OpenSSL SSL_poll()を使用して、複数のQUICオブジェクトを効率的に監視する。
*
* 【SSL_poll()の概要】
*
* SSL_poll()は、複数のSSL/QUICオブジェクトの状態変化を一度に監視できる
* OpenSSL 3.3で追加されたAPI。UNIX poll(2)に似たセマンティクス。
*
* 【SSL_POLL_ITEM構造体】
*
* struct SSL_POLL_ITEM {
* SSL_POLL_DESCRIPTOR desc; // 監視対象のSSLオブジェクト
* uint64_t events; // 監視したいイベント
* uint64_t revents; // 発生したイベント(出力)
* };
*
* 【主要なイベントタイプ】
*
* SSL_POLL_EVENT_IC: Incoming Connection - 新規接続受け入れ可能
* SSL_POLL_EVENT_ISB: Incoming Stream Bidi - 双方向ストリーム受け入れ可能
* SSL_POLL_EVENT_ISU: Incoming Stream Uni - 単方向ストリーム受け入れ可能
* SSL_POLL_EVENT_OSB: Outgoing Stream Bidi - 双方向ストリーム作成可能
* SSL_POLL_EVENT_OSU: Outgoing Stream Uni - 単方向ストリーム作成可能
* SSL_POLL_EVENT_R: Readable - 読み取り可能データあり
* SSL_POLL_EVENT_W: Writable - 書き込み可能
* SSL_POLL_EVENT_ER: Exception Read - 読み取り側でエラー/終了
* SSL_POLL_EVENT_EW: Exception Write - 書き込み側でエラー/終了
* SSL_POLL_EVENT_EC: Exception Connection - 接続終了開始
* SSL_POLL_EVENT_ECD: Exception Connection Done - 接続終了完了
*
* ============================================================================
*/
/*
* read_from_ssl_ids() - SSL_poll()でイベントを監視し処理
*
* このデモのメインイベント処理関数。ssl_ids配列内の全オブジェクトを
* SSL_poll()で監視し、発生したイベントに応じた処理を行う。
*
* パラメータ:
* curh3conn: nghttp3接続へのポインタ(新規接続時に再作成される)
* h3ssl: 状態管理構造体
*
* 戻り値:
* >0: 何らかのイベントを処理した
* 0: イベントなし(タイムアウト)
* -1: エラー
*/
/* Try to read from the streams we have */
static int read_from_ssl_ids(nghttp3_conn **curh3conn, struct h3ssl *h3ssl)
{
int hassomething = 0, i;
struct ssl_id *ssl_ids = h3ssl->ssl_ids;
SSL_POLL_ITEM items[MAXSSL_IDS] = {0}, *item = items;
static const struct timeval nz_timeout = {0, 0}; /* 即時リターン(ノンブロッキング) */
size_t result_count = SIZE_MAX;
int numitem = 0, ret;
uint64_t processed_event = 0;
int has_ids_to_remove = 0;
nghttp3_conn *h3conn = *curh3conn;
/*
* 【ステップ1】SSL_POLL_ITEM配列を構築
*
* ssl_ids配列内の全アクティブなSSLオブジェクトを
* SSL_poll()に渡すための配列に追加する。
*/
/*
* Process all the streams
* the first one is the connection if we get something here is a new stream
*/
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].s != NULL) {
/* SSL*をSSL_POLL_DESCRIPTORに変換 */
item->desc = SSL_as_poll_descriptor(ssl_ids[i].s);
item->events = UINT64_MAX; /* TODO adjust to the event we need process */
/* 全イベントを監視(本番では必要なもののみ) */
item->revents = UINT64_MAX; /* TODO adjust to the event we need process */
numitem++;
item++;
}
}
/*
* 【ステップ2】SSL_poll()を呼び出し
*
* SSL_POLL_FLAG_NO_HANDLE_EVENTS:
* SSL_poll()内部でSSL_handle_events()を自動呼び出ししない。
* 代わりにhandle_events_from_ids()で明示的に呼び出す。
*
* これにより、イベント処理のタイミングをより細かく制御できる。
* 通常はSSL_poll()が内部で自動的にtickingを行う。
*/
/*
* SSL_POLL_FLAG_NO_HANDLE_EVENTS would require to use:
* SSL_get_event_timeout on the connection stream
* select/wait using the timeout value (which could be no wait time)
* SSL_handle_events
* SSL_poll
* for the moment we let SSL_poll to performs ticking internally
* on an automatic basis.
*/
ret = SSL_poll(items, numitem, sizeof(SSL_POLL_ITEM), &nz_timeout,
SSL_POLL_FLAG_NO_HANDLE_EVENTS, &result_count);
if (!ret) {
fprintf(stderr, "SSL_poll failed\n");
printf("SSL_poll failed\n");
return -1; /* something is wrong */
}
printf("read_from_ssl_ids %ld events\n", (unsigned long)result_count);
if (result_count == 0) {
/* Timeout may be something somewhere */
/* イベントなし - タイムアウト */
return 0;
}
/*
* 【ステップ3】状態フラグをリセット
*
* イベント処理中に設定される可能性のあるフラグを
* 事前にリセットしておく。
*/
/* reset the states */
h3ssl->new_conn = 0;
h3ssl->restart = 0;
h3ssl->done = 0;
/*
* 【ステップ4】イベントをディスパッチ
*
* SSL_poll()の結果を順に処理し、各イベントタイプに応じた
* 処理を実行する。
*/
/* Process all the item we have polled */
for (i = 0, item = items; i < numitem; i++, item++) {
SSL *s;
/* イベントが発生していないアイテムはスキップ */
if (item->revents == SSL_POLL_EVENT_NONE)
continue;
processed_event = 0;
/* get the stream */
s = item->desc.value.ssl;
/*
* 【イベント: SSL_POLL_EVENT_IC】新規接続受け入れ可能
*
* リスナーに対して発生するイベント。クライアントからの
* 新しいQUIC接続をSSL_accept_connection()で受け入れる。
*/
/* New connection */
if (item->revents & SSL_POLL_EVENT_IC) {
SSL *conn = SSL_accept_connection(item->desc.value.ssl, 0);
SSL *oldconn;
printf("SSL_accept_connection\n");
if (conn == NULL) {
fprintf(stderr, "error while accepting connection\n");
ret = -1;
goto err;
}
/* the previous might be still there */
oldconn = get_ids_connection(h3ssl);
if (oldconn != NULL) {
/* XXX we support only one connection for the moment */
printf("SSL_accept_connection closing previous\n");
SSL_free(oldconn);
replace_ids_connection(h3ssl, oldconn, conn);
reuse_h3ssl(h3ssl);
close_all_ids(h3ssl);
h3ssl->id_bidi = UINT64_MAX;
h3ssl->has_uni = 0;
} else {
printf("SSL_accept_connection first connection\n");
add_ids_connection(h3ssl, conn);
}
h3ssl->new_conn = 1;
/* create the new h3conn */
nghttp3_conn_del(*curh3conn);
nghttp3_settings_default(&settings);
if (nghttp3_conn_server_new(curh3conn, &callbacks, &settings, mem,
h3ssl)) {
fprintf(stderr, "nghttp3_conn_client_new failed!\n");
exit(1);
}
h3conn = *curh3conn;
hassomething++;
if (!SSL_set_incoming_stream_policy(conn,
SSL_INCOMING_STREAM_POLICY_ACCEPT, 0)) {
fprintf(stderr, "error while setting inccoming stream policy\n");
ret = -1;
goto err;
}
printf("SSL_accept_connection\n");
processed_event = processed_event | SSL_POLL_EVENT_IC;
}
/*
* 【イベント: SSL_POLL_EVENT_ISB / SSL_POLL_EVENT_ISU】
* 新規ストリーム受け入れ可能
*
* ISB: 双方向ストリーム(Bidirectional)- リクエスト/レスポンス用
* ISU: 単方向ストリーム(Unidirectional)- HTTP/3制御用
*
* SSL_accept_stream()で新しいストリームを受け入れる。
*/
/* SSL_accept_stream if SSL_POLL_EVENT_ISB or SSL_POLL_EVENT_ISU */
if ((item->revents & SSL_POLL_EVENT_ISB) ||
(item->revents & SSL_POLL_EVENT_ISU)) {
SSL *stream = SSL_accept_stream(item->desc.value.ssl, 0);
uint64_t new_id;
int r;
if (stream == NULL) {
ret = -1;
goto err;
}
new_id = SSL_get_stream_id(stream);
printf("=> Received connection on %lld %d\n", (unsigned long long) new_id,
SSL_get_stream_type(stream));
add_id(new_id, stream, h3ssl);
if (h3ssl->close_wait) {
printf("in close_wait so we will have a new request\n");
reuse_h3ssl(h3ssl);
h3ssl->restart = 1; /* Checked in wait_close loop */
}
if (SSL_get_stream_type(stream) == SSL_STREAM_TYPE_BIDI) {
/* bidi that is the id where we have to send the response */
if (h3ssl->id_bidi != UINT64_MAX) {
set_id_status(h3ssl->id_bidi, TOBEREMOVED, h3ssl);
has_ids_to_remove++;
}
h3ssl->id_bidi = new_id;
reuse_h3ssl(h3ssl);
h3ssl->restart = 1;
} else {
set_id_status(new_id, CLIENTUNIOPEN, h3ssl);
}
r = quic_server_read(h3conn, stream, new_id, h3ssl);
if (r == -1) {
ret = -1;
goto err;
}
if (r == 1)
hassomething++;
if (item->revents & SSL_POLL_EVENT_ISB)
processed_event = processed_event | SSL_POLL_EVENT_ISB;
if (item->revents & SSL_POLL_EVENT_ISU)
processed_event = processed_event | SSL_POLL_EVENT_ISU;
}
/*
* 【イベント: SSL_POLL_EVENT_OSB】双方向ストリーム作成可能
*
* サーバーから双方向ストリームを作成できる状態。
* HTTP/3では通常サーバーからの双方向ストリームは使用しないため、
* このイベントは無視する。
*/
if (item->revents & SSL_POLL_EVENT_OSB) {
/* Create new streams when allowed */
/* at least one bidi */
processed_event = processed_event | SSL_POLL_EVENT_OSB;
printf("Create bidi?\n");
}
/*
* 【イベント: SSL_POLL_EVENT_OSU】単方向ストリーム作成可能
*
* サーバーから単方向ストリームを作成できる状態。
* HTTP/3の必須ストリーム(制御、QPACKエンコーダ/デコーダ)を
* まだ作成していなければ、ここで作成する。
*/
if (item->revents & SSL_POLL_EVENT_OSU) {
/* at least one uni */
/* we have 4 streams from the client 2, 6 , 10 and 0 */
/* need 3 streams to the client */
printf("Create uni?\n");
processed_event = processed_event | SSL_POLL_EVENT_OSU;
if (!h3ssl->has_uni) {
/* HTTP/3必須の3つの単方向ストリームを作成 */
printf("Create uni\n");
ret = quic_server_h3streams(h3conn, h3ssl);
if (ret == -1) {
fprintf(stderr, "quic_server_h3streams failed!\n");
goto err;
}
h3ssl->has_uni = 1; /* 作成済みフラグを立てる */
hassomething++;
}
}
/*
* 【イベント: SSL_POLL_EVENT_EC】接続終了開始
*
* QUIC接続の終了処理が開始された。
* CONNECTION_CLOSE フレームを受信した、または送信した状態。
*
* 終了処理は2段階:
* 1. EC (Connection closing) - 終了開始
* 2. ECD (Connection done) - 終了完了
*/
if (item->revents & SSL_POLL_EVENT_EC) {
/* the connection begins terminating */
printf("Connection terminating\n");
printf("Connection terminating restart %d\n", h3ssl->restart);
if (!h3ssl->close_done) {
h3ssl->close_done = 1; /* 終了開始をマーク */
} else {
h3ssl->done = 1; /* 既にclose_doneなら完全終了 */
}
hassomething++;
processed_event = processed_event | SSL_POLL_EVENT_EC;
}
/*
* 【イベント: SSL_POLL_EVENT_ECD】接続終了完了
*
* QUIC接続の終了処理が完了した。
* この後、接続に関連するリソースを解放できる。
*/
if (item->revents & SSL_POLL_EVENT_ECD) {
/* the connection is terminated */
printf("Connection terminated\n");
h3ssl->done = 1; /* 完全終了 */
hassomething++;
processed_event = processed_event | SSL_POLL_EVENT_ECD;
}
/*
* 【イベント: SSL_POLL_EVENT_R】読み取り可能
*
* ストリームから読み取り可能なデータがある。
* quic_server_read()でデータを読み取り、nghttp3に渡す。
*/
if (item->revents & SSL_POLL_EVENT_R) {
/* try to read */
uint64_t id = UINT64_MAX;
int r;
/* get the id, well the connection has no id... */
id = SSL_get_stream_id(item->desc.value.ssl);
printf("revent READ on %llu\n", (unsigned long long)id);
r = quic_server_read(h3conn, s, id, h3ssl);
if (r == 0) {
/* 読み取るデータがない - ストリーム終了を確認 */
uint8_t msg[1];
size_t l = sizeof(msg);
/* check that the other side is closed */
r = SSL_read(s, msg, l);
printf("SSL_read tells %d\n", r);
if (r > 0) {
ret = -1;
goto err;
}
r = SSL_get_error(s, r);
if (r != SSL_ERROR_ZERO_RETURN) {
/* SSL_ERROR_ZERO_RETURN以外はエラー */
ret = -1;
goto err;
}
/* ストリームが正常終了 - 削除予約 */
set_id_status(id, TOBEREMOVED, h3ssl);
has_ids_to_remove++;
continue;
}
if (r == -1) {
ret = -1;
goto err;
}
hassomething++;
processed_event = processed_event | SSL_POLL_EVENT_R;
}
/*
* 【イベント: SSL_POLL_EVENT_ER】読み取り側例外
*
* ストリームの読み取り側が閉じられた。
* クライアントがFINを送信した状態。
*
* クライアントの単方向ストリームの場合、
* CLIENTCLOSEDフラグを追加する。
*/
if (item->revents & SSL_POLL_EVENT_ER) {
/* mark it closed */
uint64_t id = UINT64_MAX;
int status;
id = SSL_get_stream_id(item->desc.value.ssl);
status = get_id_status(id, h3ssl);
printf("revent exception READ on %llu\n", (unsigned long long)id);
if (status & CLIENTUNIOPEN) {
/* クライアントの単方向ストリームが閉じた */
set_id_status(id, CLIENTCLOSED, h3ssl);
hassomething++;
}
processed_event = processed_event | SSL_POLL_EVENT_ER;
}
/*
* 【イベント: SSL_POLL_EVENT_W】書き込み可能
*
* ストリームに書き込み可能。
* このデモでは無視(レスポンス送信は別の場所で行う)。
*/
if (item->revents & SSL_POLL_EVENT_W) {
/* we ignore those for the moment */
processed_event = processed_event | SSL_POLL_EVENT_W;
}
/*
* 【イベント: SSL_POLL_EVENT_EW】書き込み側例外
*
* ストリームの書き込み側で例外が発生。
* 通常はSTOP_SENDINGフレームを受信した場合。
*
* サーバー側が既に閉じていれば、両方向とも閉じたので
* ストリームを削除できる。
*/
if (item->revents & SSL_POLL_EVENT_EW) {
/* write part received a STOP_SENDING */
uint64_t id = UINT64_MAX;
int status;
id = SSL_get_stream_id(item->desc.value.ssl);
status = get_id_status(id, h3ssl);
if (status & SERVERCLOSED) {
/* サーバーも閉じていれば完全に削除可能 */
printf("both sides closed on %llu\n", (unsigned long long)id);
set_id_status(id, TOBEREMOVED, h3ssl);
has_ids_to_remove++;
hassomething++;
}
processed_event = processed_event | SSL_POLL_EVENT_EW;
}
/* 未処理のイベントがあれば警告を出力 */
if (item->revents != processed_event) {
/* Figure out ??? */
uint64_t id = UINT64_MAX;
id = SSL_get_stream_id(item->desc.value.ssl);
printf("revent %llu (%d) on %llu NOT PROCESSED!\n",
(unsigned long long)item->revents, SSL_POLL_EVENT_W,
(unsigned long long)id);
}
}
ret = hassomething;
err:
/* 削除予約されたストリームを実際に削除 */
if (has_ids_to_remove)
remove_marked_ids(h3ssl);
return ret;
}
/*
* ============================================================================
* 【イベントハンドリング・ファイル処理】
* ============================================================================
*/
/*
* handle_events_from_ids() - QUIC内部イベントを処理
*
* SSL_handle_events()を呼び出して、QUICプロトコルの内部処理を行う。
*
* QUICは時間ベースの処理(再送タイマー、アイドルタイムアウト等)が必要。
* SSL_handle_events()は、リスナーや接続に対してこれらの処理を実行する。
*
* SSL_POLL_FLAG_NO_HANDLE_EVENTSを使用している場合、
* 手動でこの関数を呼び出す必要がある。
*/
static void handle_events_from_ids(struct h3ssl *h3ssl)
{
struct ssl_id *ssl_ids = h3ssl->ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
for (i = 0; i < MAXSSL_IDS; i++) {
/* リスナーまたは接続に対してのみ呼び出す */
if (ssl_ids[i].s != NULL &&
(ssl_ids[i].status & ISCONNECTION || ssl_ids[i].status & ISLISTENER)) {
if (SSL_handle_events(ssl_ids[i].s))
ERR_print_errors_fp(stderr);
}
}
}
/*
* get_file_length() - リクエストされたファイルのサイズを取得
*
* h3ssl->urlとh3ssl->fileprefixからファイルパスを構築し、
* stat()でファイルサイズを取得する。
*
* 特別なURL "big" の場合は INT_MAX を返す(テスト用の無限ストリーム)。
*
* 戻り値:
* ファイルサイズ(バイト)
* ファイルが見つからない/通常ファイルでない場合は0
*/
static size_t get_file_length(struct h3ssl *h3ssl)
{
char filename[PATH_MAX];
struct stat st;
/* ファイルパスを構築: fileprefix + url */
memset(filename, 0, PATH_MAX);
if (h3ssl->fileprefix != NULL)
strcat(filename, h3ssl->fileprefix);
strcat(filename, h3ssl->url);
/* "big" は特別なテスト用パス - 無限ストリーム */
if (strcmp(h3ssl->url, "big") == 0) {
printf("big!!!\n");
return (size_t)INT_MAX;
}
/* ファイルのメタデータを取得 */
if (stat(filename, &st) == 0) {
/* Only process regular files */
/* 通常ファイルのみ処理(ディレクトリ等は除外) */
if (S_ISREG(st.st_mode)) {
printf("get_file_length %s %lld\n", filename, (unsigned long long) st.st_size);
return (size_t)st.st_size;
}
}
printf("Can't get_file_length %s\n", filename);
return 0;
}
/*
* get_file_data() - ファイルの内容を読み込む
*
* リクエストされたファイルをメモリに読み込んで返す。
*
* 戻り値:
* ファイル内容を含む動的確保されたバッファ(呼び出し元が解放責任)
* エラー時はNULL
*/
static char *get_file_data(struct h3ssl *h3ssl)
{
char filename[PATH_MAX];
size_t size = get_file_length(h3ssl);
char *res;
int fd;
if (size == 0)
return NULL;
/* ファイルパスを構築 */
memset(filename, 0, PATH_MAX);
if (h3ssl->fileprefix != NULL)
strcat(filename, h3ssl->fileprefix);
strcat(filename, h3ssl->url);
/* バッファを確保してファイル内容を読み込む */
res = malloc(size+1);
res[size] = '\0';
fd = open(filename, O_RDONLY);
if (read(fd, res, size) == -1) {
close(fd);
free(res);
return NULL;
}
close(fd);
printf("read from %s : %zu\n", filename, size);
return res;
}
/*
* step_read_data() - nghttp3からのデータ読み取りコールバック
*
* nghttp3_conn_submit_response()で設定したnghttp3_data_readerの
* read_dataコールバック。nghttp3がレスポンスボディのデータを
* 必要とする時に呼び出される。
*
* 【チャンク送信の仕組み】
* 大きなファイルは4096バイトずつに分割して送信。
* nghttp3_vecにデータへのポインタと長さを設定し、
* nghttp3がそれをHTTP/3フレームにパッケージングする。
*
* パラメータ:
* conn: nghttp3接続
* stream_id: レスポンスストリームID
* vec: データを設定するベクタ配列(出力)
* veccnt: vecの要素数
* pflags: フラグ(NGHTTP3_DATA_FLAG_EOF等)
* user_data: h3ssl構造体
* stream_user_data: ストリーム固有データ
*
* 戻り値:
* 設定したvec要素数(通常は1)
* 0: データ終了
*/
static nghttp3_ssize step_read_data(nghttp3_conn *conn, int64_t stream_id,
nghttp3_vec *vec, size_t veccnt,
uint32_t *pflags, void *user_data,
void *stream_user_data)
{
struct h3ssl *h3ssl = (struct h3ssl *)user_data;
/* 既にデータ送信完了の場合 */
if (h3ssl->datadone) {
*pflags = NGHTTP3_DATA_FLAG_EOF; /* End Of File フラグ */
return 0;
}
/* send the data */
printf("step_read_data for %s %zu\n", h3ssl->url, h3ssl->ldata);
if (h3ssl->ldata <= 4096) {
/*
* 残りデータが4096バイト以下 - 最後のチャンク
*/
vec[0].base = &(h3ssl->ptr_data[h3ssl->offset_data]);
vec[0].len = h3ssl->ldata;
h3ssl->datadone++; /* 送信完了フラグ */
*pflags = NGHTTP3_DATA_FLAG_EOF; /* これが最後のデータ */
} else {
/*
* まだ4096バイト以上残っている - チャンク送信
*/
vec[0].base = &(h3ssl->ptr_data[h3ssl->offset_data]);
vec[0].len = 4096;
if (h3ssl->ldata == INT_MAX) {
/* "big" テスト用: 無限ストリーム(オフセットを進めない) */
printf("big = endless!\n");
} else {
/* 通常のファイル: オフセットを進めて残りを減らす */
h3ssl->offset_data = h3ssl->offset_data + 4096;
h3ssl->ldata = h3ssl->ldata - 4096;
}
}
return 1; /* 1つのvec要素を設定 */
}
/*
* quic_server_write() - QUICストリームにデータを書き込む
*
* nghttp3_conn_writev_stream()で取得したデータを、
* 対応するQUICストリームに書き込む。
*
* パラメータ:
* h3ssl: 状態管理構造体
* streamid: 書き込み先のQUICストリームID
* buff: 書き込むデータ
* len: データ長
* flags: SSL_write_ex2()のフラグ(SSL_WRITE_FLAG_CONCLUDE等)
* written: 実際に書き込んだバイト数(出力)
*
* 戻り値:
* 1: 成功
* 0: エラー(ストリームが見つからない等)
*/
static int quic_server_write(struct h3ssl *h3ssl, uint64_t streamid,
uint8_t *buff, size_t len, uint64_t flags,
size_t *written)
{
struct ssl_id *ssl_ids;
int i;
ssl_ids = h3ssl->ssl_ids;
/* 指定IDのストリームを探す */
for (i = 0; i < MAXSSL_IDS; i++) {
if (ssl_ids[i].id == streamid) {
/*
* SSL_write_ex2() でデータを書き込む
*
* flags引数により、以下のオプションが指定可能:
* SSL_WRITE_FLAG_CONCLUDE: データと同時にFINを送信
* (ストリーム終了を効率的に通知)
*/
if (!SSL_write_ex2(ssl_ids[i].s, buff, len, flags, written) ||
*written != len) {
fprintf(stderr, "couldn't write on connection\n");
ERR_print_errors_fp(stderr);
return 0;
}
printf("written %lld on %lld flags %lld\n", (unsigned long long)len,
(unsigned long long)streamid, (unsigned long long)flags);
return 1;
}
}
/* ストリームが見つからない */
printf("quic_server_write %lld on %lld (NOT FOUND!)\n", (unsigned long long)len,
(unsigned long long)streamid);
return 0;
}
/*
* ============================================================================
* 【SSL_CTX / ソケット初期化】
* ============================================================================
*/
#define OSSL_NELEM(x) (sizeof(x) / sizeof((x)[0]))
/*
* This is a basic demo of QUIC server functionality in which one connection at
* a time is accepted in a blocking loop.
*/
/*
* 【ALPN (Application-Layer Protocol Negotiation) 設定】
*
* HTTP/3では、ALPNを使用してプロトコルを識別する。
* TLSハンドシェイク中にクライアントとサーバーが
* 使用するアプリケーションプロトコルをネゴシエートする。
*
* フォーマット: 長さプレフィックス + プロトコル名
* [5] "h3-29" - HTTP/3 draft-29(開発版)
* [2] "h3" - HTTP/3 正式版(RFC 9114)
*
* クライアントが両方をサポートしていれば "h3" が優先される。
*/
/* ALPN string for TLS handshake. We pretent h3-29 and h3 */
static const unsigned char alpn_ossltest[] = { 5, 'h', '3', '-', '2',
'9', 2, 'h', '3' };
/*
* select_alpn() - ALPNネゴシエーションコールバック
*
* TLSハンドシェイク中に呼び出され、クライアントが提示した
* プロトコルリストからサーバーがサポートするプロトコルを選択する。
*
* パラメータ:
* ssl: SSL接続
* out: 選択されたプロトコル名(出力)
* out_len: 選択されたプロトコル名の長さ(出力)
* in: クライアントが提示したプロトコルリスト
* in_len: リストの長さ
* arg: コールバック引数(未使用)
*
* 戻り値:
* SSL_TLSEXT_ERR_OK: ネゴシエーション成功
* SSL_TLSEXT_ERR_ALERT_FATAL: 一致するプロトコルなし(接続を拒否)
*/
/*
* This callback validates and negotiates the desired ALPN on the server side.
*/
static int select_alpn(SSL *ssl, const unsigned char **out,
unsigned char *out_len, const unsigned char *in,
unsigned int in_len, void *arg)
{
/*
* SSL_select_next_proto(): NPN/ALPNプロトコル選択ヘルパー
*
* サーバーのリスト (alpn_ossltest) とクライアントのリスト (in) を比較し、
* 最初に一致したプロトコルを選択する。
*/
if (SSL_select_next_proto((unsigned char **)out, out_len, alpn_ossltest,
sizeof(alpn_ossltest), in,
in_len) != OPENSSL_NPN_NEGOTIATED)
return SSL_TLSEXT_ERR_ALERT_FATAL; /* 一致なし - 接続拒否 */
return SSL_TLSEXT_ERR_OK; /* ネゴシエーション成功 */
}
/*
* create_ctx() - SSL_CTXを作成・設定
*
* QUICサーバー用のSSL_CTXを作成し、証明書、秘密鍵、
* ALPNコールバックを設定する。
*
* パラメータ:
* cert_path: サーバー証明書ファイルのパス(PEM形式)
* key_path: 秘密鍵ファイルのパス(PEM形式)
*
* 戻り値:
* 成功時: 設定済みのSSL_CTX
* エラー時: NULL
*/
/* Create SSL_CTX. */
static SSL_CTX *create_ctx(const char *cert_path, const char *key_path)
{
SSL_CTX *ctx;
/*
* OSSL_QUIC_server_method(): QUICサーバー用のSSL_METHOD
*
* HTTP/3はQUICの上で動作するため、従来のTLS用メソッド
* (TLS_server_method等)ではなく、QUIC専用のメソッドを使用。
*/
ctx = SSL_CTX_new(OSSL_QUIC_server_method());
if (ctx == NULL)
goto err;
/* Load certificate and corresponding private key. */
/* サーバー証明書チェーンをロード */
if (SSL_CTX_use_certificate_chain_file(ctx, cert_path) <= 0) {
fprintf(stderr, "couldn't load certificate file: %s\n", cert_path);
goto err;
}
/* 秘密鍵をロード */
if (SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM) <= 0) {
fprintf(stderr, "couldn't load key file: %s\n", key_path);
goto err;
}
/* 証明書と秘密鍵が一致するか確認 */
if (!SSL_CTX_check_private_key(ctx)) {
fprintf(stderr, "private key check failed\n");
goto err;
}
/* Setup ALPN negotiation callback. */
/*
* ALPNコールバックを設定
*
* TLSハンドシェイク中にクライアントのALPN拡張を処理し、
* サーバーがサポートするプロトコル(h3, h3-29)を選択する。
*/
SSL_CTX_set_alpn_select_cb(ctx, select_alpn, NULL);
return ctx;
err:
SSL_CTX_free(ctx);
return NULL;
}
/*
* create_socket() - UDPソケットを作成・バインド
*
* QUICはUDP上で動作するため、TCPソケットではなく
* UDPソケットを作成する。
*
* パラメータ:
* port: リスンするポート番号
*
* 戻り値:
* 成功時: ソケットファイルディスクリプタ
* エラー時: -1
*/
/* Create UDP socket using given port. */
static int create_socket(uint16_t port)
{
int fd = -1;
struct sockaddr_in sa = {0};
/* UDPソケットを作成 (SOCK_DGRAM, IPPROTO_UDP) */
if ((fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
fprintf(stderr, "cannot create socket");
goto err;
}
/* IPv4アドレス、全インターフェースでリスン */
sa.sin_family = AF_INET;
sa.sin_port = htons(port); /* ポート番号をネットワークバイトオーダーに変換 */
/* ソケットをアドレスにバインド */
if (bind(fd, (const struct sockaddr *)&sa, sizeof(sa)) < 0) {
fprintf(stderr, "cannot bind to %u\n", port);
goto err;
}
return fd;
err:
if (fd >= 0)
BIO_closesocket(fd); /* OpenSSL提供のクロスプラットフォームclose */
return -1;
}
/*
* ============================================================================
* wait_for_activity() - ソケットアクティビティ待機
* ============================================================================
*
* 【概要】
* select()システムコールを使用して、QUICソケットに対する読み書き可能状態を
* 待機する関数。SSL_poll()を使用したイベントループとは別に、単純なブロッキング
* 待機が必要な場合に使用される。
*
* 【処理フロー】
*
* 1. SSL_get_fd()でソケットFDを取得
* ↓
* 2. SSL_net_write_desired()で書き込み必要性を確認
* → trueなら write_fd に追加
* ↓
* 3. SSL_net_read_desired()で読み取り必要性を確認
* → trueなら read_fd に追加
* ※ 常に read_fd にも追加(新規データ受信のため)
* ↓
* 4. SSL_get_event_timeout()でタイムアウト取得
* → QUICの内部タイマー(ACK遅延、PTO等)に基づく
* ↓
* 5. select()で待機
* → ソケット活動またはタイムアウトで復帰
*
* 【SSL_net_write_desired() / SSL_net_read_desired()】
* これらの関数は、OpenSSL QUICスタックが次に何を必要としているかを示す:
* - write_desired: 送信バッファにデータがあり、ソケットへの書き込みが必要
* - read_desired: ソケットからの読み取りが必要(新規パケット待ち)
*
* 【SSL_get_event_timeout()】
* QUICの内部タイマーに基づく次回処理時刻を返す:
* - ACK遅延タイマー: ACK送信のタイミング
* - PTO (Probe Timeout): パケットロス検出のためのプローブ
* - アイドルタイムアウト: 接続の自動クローズ
* isinfinite=1の場合、タイムアウトなし(無期限待機可能)
*
* 【select()の代替】
* この実装ではselect()を使用しているが、以下の代替も可能:
* - poll(): より多くのFDを扱える
* - epoll() (Linux): 高性能なイベント通知
* - kqueue() (BSD/macOS): epoll相当の機能
*
* 【GUIアプリケーションへの応用】
* コメントに記載の通り、GUIの更新が必要な場合は:
* 1. tvpが100msより大きければ100msに制限
* 2. select()復帰時にタイムアウトか活動かを判定
* 3. GUI更新タイムアウトならGUIを更新してselect()を再開
*
* 【この関数の使用箇所】
* - run_quic_server()内でリスナーの待機
* - ヘッダ受信待ちのループ
* - データ送信完了待ち
*/
/* Copied from demos/guide/quic-server-non-block.c */
/**
* @brief Waits for activity on the SSL socket, either for reading or writing.
*
* This function monitors the underlying file descriptor of the given SSL
* connection to determine when it is ready for reading or writing, or both.
* It uses the select function to wait until the socket is either readable
* or writable, depending on what the SSL connection requires.
*
* @param ssl A pointer to the SSL object representing the connection.
*
* @note This function blocks until there is activity on the socket. In a real
* application, you might want to perform other tasks while waiting, such as
* updating a GUI or handling other connections.
*
* @note This function uses select for simplicity and portability. Depending
* on your application's requirements, you might consider using other
* mechanisms like poll or epoll for handling multiple file descriptors.
*/
static int wait_for_activity(SSL *ssl)
{
int sock, isinfinite;
fd_set read_fd, write_fd;
struct timeval tv;
struct timeval *tvp = NULL;
/* Get hold of the underlying file descriptor for the socket */
if ((sock = SSL_get_fd(ssl)) == -1) {
fprintf(stderr, "Unable to get file descriptor");
return -1;
}
/* Initialize the fd_set structure */
FD_ZERO(&read_fd);
FD_ZERO(&write_fd);
/*
* Determine if we would like to write to the socket, read from it, or both.
*/
if (SSL_net_write_desired(ssl))
FD_SET(sock, &write_fd);
if (SSL_net_read_desired(ssl))
FD_SET(sock, &read_fd);
/* Add the socket file descriptor to the fd_set */
FD_SET(sock, &read_fd);
/*
* Find out when OpenSSL would next like to be called, regardless of
* whether the state of the underlying socket has changed or not.
*/
if (SSL_get_event_timeout(ssl, &tv, &isinfinite) && !isinfinite)
tvp = &tv;
/*
* Wait until the socket is writeable or readable. We use select here
* for the sake of simplicity and portability, but you could equally use
* poll/epoll or similar functions
*
* NOTE: For the purposes of this demonstration code this effectively
* makes this demo block until it has something more useful to do. In a
* real application you probably want to go and do other work here (e.g.
* update a GUI, or service other connections).
*
* Let's say for example that you want to update the progress counter on
* a GUI every 100ms. One way to do that would be to use the timeout in
* the last parameter to "select" below. If the tvp value is greater
* than 100ms then use 100ms instead. Then, when select returns, you
* check if it did so because of activity on the file descriptors or
* because of the timeout. If the 100ms GUI timeout has expired but the
* tvp timeout has not then go and update the GUI and then restart the
* "select" (with updated timeouts).
*/
return (select(sock + 1, &read_fd, &write_fd, NULL, tvp));
}
/*
* ============================================================================
* run_quic_server() - HTTP/3サーバーメインループ
* ============================================================================
*
* 【概要】
* HTTP/3サーバーの中核となるメインループ。QUIC接続の受け入れ、HTTP/3
* リクエストの処理、レスポンス送信を行う。このデモでは1つの接続を
* 順次処理する(マルチスレッドではない)。
*
* 【処理フロー図】
*
* ┌─────────────────────────────────────────────────────────────────┐
* │ 初期化フェーズ │
* │ SSL_new_listener() → SSL_set_fd() → SSL_listen() │
* │ → SSL_set_blocking_mode(0) → nghttp3コールバック設定 │
* └─────────────────────────────────────────────────────────────────┘
* ↓
* ┌─────────────────────────────────────────────────────────────────┐
* │ メインループ (for(;;)) │
* │ │
* │ ┌──────────────────────────────────────────────────────────┐ │
* │ │ 1. 接続待機 (wait_for_activity) │ │
* │ │ → init_ids()で状態リセット │ │
* │ │ → add_ids_listener()でリスナー登録 │ │
* │ └──────────────────────────────────────────────────────────┘ │
* │ ↓ │
* │ ┌──────────────────────────────────────────────────────────┐ │
* │ │ 2. ヘッダ受信ループ (restart:) │ │
* │ │ → read_from_ssl_ids()でイベント処理 │ │
* │ │ → h3ssl.end_headers_received待ち │ │
* │ │ → タイムアウト25回でエラー │ │
* │ └──────────────────────────────────────────────────────────┘ │
* │ ↓ │
* │ ┌──────────────────────────────────────────────────────────┐ │
* │ │ 3. HTTP/3ストリーム作成 (初回のみ) │ │
* │ │ → quic_server_h3streams() │ │
* │ │ → 制御/QPACKエンコーダ/QPACKデコーダストリーム │ │
* │ └──────────────────────────────────────────────────────────┘ │
* │ ↓ │
* │ ┌──────────────────────────────────────────────────────────┐ │
* │ │ 4. レスポンス構築 │ │
* │ │ → get_file_length()でファイル取得試行 │ │
* │ │ → content-type判定(.png, .ico, .htm等) │ │
* │ │ → nghttp3_conn_submit_response() │ │
* │ └──────────────────────────────────────────────────────────┘ │
* │ ↓ │
* │ ┌──────────────────────────────────────────────────────────┐ │
* │ │ 5. データ送信ループ │ │
* │ │ → nghttp3_conn_writev_stream()でデータ取得 │ │
* │ │ → quic_server_write()でQUIC送信 │ │
* │ │ → nghttp3_conn_add_write_offset()で消費報告 │ │
* │ │ → finフラグでストリーム終了 │ │
* │ └──────────────────────────────────────────────────────────┘ │
* │ ↓ │
* │ ┌──────────────────────────────────────────────────────────┐ │
* │ │ 6. クローズ待機 (wait_close:) │ │
* │ │ → クライアントからのクローズ待ち │ │
* │ │ → 新規接続があればnewconn:へジャンプ │ │
* │ │ → 同一接続で新リクエストならrestart:へジャンプ │ │
* │ └──────────────────────────────────────────────────────────┘ │
* │ ↓ │
* │ ┌──────────────────────────────────────────────────────────┐ │
* │ │ 7. クリーンアップ │ │
* │ │ → close_all_ids()で全ストリームクローズ │ │
* │ │ → SSL_free()で接続解放 │ │
* │ │ → 次の接続へループ │ │
* │ └──────────────────────────────────────────────────────────┘ │
* └─────────────────────────────────────────────────────────────────┘
*
* 【重要なラベル】
* newconn: - 新規接続処理開始点(SSL_accept_connection後)
* restart: - 同一接続での新規リクエスト処理開始点
* wait_close: - レスポンス送信後のクローズ待機
*
* 【nghttp3との連携】
* - nghttp3_conn_submit_response(): HTTPレスポンスヘッダをキュー
* - nghttp3_conn_writev_stream(): 送信すべきデータを取得
* - nghttp3_conn_add_write_offset(): 送信済みバイト数を報告
* - step_read_data: データ読み取りコールバック(dr.read_data)
*
* 【ファイル提供機能】
* FILEPREFIX環境変数が設定されている場合、そのディレクトリから
* 静的ファイルを提供する。未設定の場合はデフォルトの20バイト
* ASCII文字列を返す。
*
* Content-Type判定:
* - .png → image/png
* - .ico → image/vnd.microsoft.icon
* - .htm → text/html
* - その他 → application/octet-stream
*
* @param ctx SSL_CTX(証明書・秘密鍵設定済み)
* @param fd UDPソケットファイルディスクリプタ
* @return 成功時1、エラー時0
*/
/* Main loop for server to accept QUIC connections. */
static int run_quic_server(SSL_CTX *ctx, int fd)
{
int ok = 0;
int hassomething = 0; /* 処理すべきイベントがあるかどうか */
SSL *listener = NULL; /* QUICリスナーオブジェクト */
nghttp3_conn *h3conn = NULL; /* nghttp3接続オブジェクト */
struct h3ssl h3ssl; /* HTTP/3接続状態管理 */
SSL *ssl;
char *fileprefix = getenv("FILEPREFIX"); /* ファイル提供用ディレクトリ */
/*
* ========================================
* 初期化フェーズ: QUICリスナーのセットアップ
* ========================================
*/
/* Create a new QUIC listener. */
/* QUICリスナーを作成(TCPのlisten()に相当)*/
if ((listener = SSL_new_listener(ctx, 0)) == NULL)
goto err;
/* Provide the listener with our UDP socket. */
/* UDPソケットをリスナーに関連付け */
if (!SSL_set_fd(listener, fd))
goto err;
/* Begin listening. */
/* リスニング開始 */
if (!SSL_listen(listener))
goto err;
/*
* Listeners, and other QUIC objects, default to operating in blocking mode.
* The configured behaviour is inherited by child objects.
* Make sure we won't block as we use select().
*/
/*
* 非ブロッキングモードを設定。
* デフォルトはブロッキングモードだが、select()を使用するため
* 非ブロッキングに設定する。この設定は子オブジェクト(接続、ストリーム)
* にも継承される。
*/
if (!SSL_set_blocking_mode(listener, 0))
goto err;
/*
* ========================================
* nghttp3コールバックの設定
* ========================================
*
* HTTP/3リクエスト処理のためのコールバック関数を登録。
* これらはnghttp3がHTTP/3フレームを処理する際に呼び出される。
*/
/* Setup callbacks. */
callbacks.recv_header = on_recv_header; /* ヘッダ受信時 */
callbacks.end_headers = on_end_headers; /* ヘッダ終了時 */
callbacks.recv_data = on_recv_data; /* データ受信時 */
callbacks.end_stream = on_end_stream; /* ストリーム終了時 */
/* mem default */
/* nghttp3用メモリアロケータ(標準のmalloc/free使用)*/
mem = nghttp3_mem_default();
memset(&h3ssl, 0, sizeof(h3ssl));
/*
* ========================================
* メインサーバーループ開始
* ========================================
*
* 無限ループで接続を受け入れ、リクエストを処理し、
* レスポンスを送信する。1つの接続を処理完了してから
* 次の接続を受け入れる逐次処理モデル。
*/
for (;;) {
nghttp3_nv resp[10]; /* HTTPレスポンスヘッダ配列 */
size_t num_nv; /* レスポンスヘッダ数 */
nghttp3_data_reader dr; /* データ読み取りコールバック */
int ret;
int numtimeout; /* タイムアウトカウンタ */
char slength[22]; /* Content-Length文字列 */
int hasnothing; /* クローズ待機用フラグ */
/*
* ----------------------------------------
* フェーズ1: 接続待機の初期化
* ----------------------------------------
*/
init_ids(&h3ssl); /* h3ssl状態をリセット */
h3ssl.fileprefix = fileprefix; /* ファイルプレフィックス設定 */
printf("listener: %p\n", (void *)listener);
add_ids_listener(listener, &h3ssl); /* リスナーをssl_id配列に登録 */
/* 前回ループで処理すべきものがなければ、新規イベントを待機 */
if (!hassomething) {
printf("waiting on socket\n");
fflush(stdout);
ret = wait_for_activity(listener);
if (ret == -1) {
fprintf(stderr, "wait_for_activity failed!\n");
goto err;
}
}
/*
* Service the connection. In a real application this would be done
* concurrently. In this demonstration program a single connection is
* accepted and serviced at a time.
*/
/*
* 接続処理開始。
* 実際のアプリケーションでは並行処理するが、このデモでは
* 1つの接続を順次処理する。
*/
/*
* ----------------------------------------
* フェーズ2: newconn - 新規接続処理開始点
* ----------------------------------------
* read_from_ssl_ids()内でSSL_POLL_EVENT_ICを検出し、
* SSL_accept_connection()で新規接続を受け入れた後に
* ここへジャンプしてくる。
*/
newconn:
printf("process_server starting...\n");
fflush(stdout);
/*
* ----------------------------------------
* フェーズ3: restart - ヘッダ受信待機
* ----------------------------------------
* HTTPリクエストヘッダの受信を待つ。
* on_end_headers()コールバックがh3ssl.end_headers_received
* をセットするまでループ。
*
* 同一接続で複数リクエストを処理する場合(HTTP/3の多重化)、
* 新しいリクエストが来るとここへジャンプしてくる。
*/
/* wait until we have received the headers */
restart:
numtimeout = 0;
num_nv = 0;
while (!h3ssl.end_headers_received) {
if (!hassomething) {
if (wait_for_activity(listener) == 0) {
printf("waiting for end_headers_received timeout %d\n", numtimeout);
numtimeout++;
if (numtimeout == 25)
goto err;
}
handle_events_from_ids(&h3ssl);
}
hassomething = read_from_ssl_ids(&h3conn, &h3ssl);
if (hassomething == -1) {
fprintf(stderr, "read_from_ssl_ids hassomething failed\n");
goto err;
} else if (hassomething == 0) {
printf("read_from_ssl_ids hassomething nothing...\n");
} else {
numtimeout = 0;
printf("read_from_ssl_ids hassomething %d...\n", hassomething);
if (h3ssl.close_done) {
/* Other side has closed */
break;
}
h3ssl.restart = 0;
}
}
/* クライアントがリクエストなしで接続を閉じた場合 */
if (h3ssl.close_done) {
printf("Other side close without request\n");
goto wait_close;
}
printf("end_headers_received!!!\n");
/*
* ----------------------------------------
* フェーズ4: HTTP/3必須ストリームの作成
* ----------------------------------------
* 初回のみ、HTTP/3プロトコルに必要な3つの単方向ストリーム
* (制御、QPACKエンコーダ、QPACKデコーダ)を作成する。
* これがないとレスポンスをクライアントに送信できない。
*/
if (!h3ssl.has_uni) {
/* time to create those otherwise we can't push anything to the client */
printf("Create uni\n");
if (quic_server_h3streams(h3conn, &h3ssl) == -1) {
fprintf(stderr, "quic_server_h3streams failed!\n");
goto err;
}
h3ssl.has_uni = 1; /* 作成済みフラグ */
}
/*
* ----------------------------------------
* フェーズ5: HTTPレスポンスの構築
* ----------------------------------------
* リクエストされたファイルを取得し、HTTPレスポンスヘッダを構築。
* - ファイルが見つからない場合: デフォルトのテスト文字列を返す
* - 無限ファイル (INT_MAX): テスト用の無限データ
* - 通常ファイル: Content-Typeを拡張子から判定
*/
/* we have receive the request build the response and send it */
/* XXX add MAKE_NV("connection", "close"), to resp[] and recheck */
make_nv(&resp[num_nv++], ":status", "200"); /* HTTPステータス200 OK */
h3ssl.ldata = get_file_length(&h3ssl);
if (h3ssl.ldata == 0) {
/* We don't find the file: use default test string */
h3ssl.ptr_data = nulldata;
h3ssl.ldata = nulldata_sz;
snprintf(slength, sizeof(slength), "%zu", h3ssl.ldata);
/* content-type: text/html */
make_nv(&resp[num_nv++], "content-type", "text/html");
} else if (h3ssl.ldata == INT_MAX) {
/* endless file for tests */
snprintf(slength, sizeof(slength), "%zu", h3ssl.ldata);
h3ssl.ptr_data = (uint8_t *) malloc(4096);
memset(h3ssl.ptr_data, 'A', 4096);
} else {
/* normal file we have opened */
snprintf(slength, sizeof(slength), "%zu", h3ssl.ldata);
h3ssl.ptr_data = (uint8_t *) get_file_data(&h3ssl);
if (h3ssl.ptr_data == NULL)
abort();
printf("before nghttp3_conn_submit_response on %llu for %s ...\n",
(unsigned long long) h3ssl.id_bidi, h3ssl.url);
if (strstr(h3ssl.url, ".png"))
make_nv(&resp[num_nv++], "content-type", "image/png");
else if (strstr(h3ssl.url, ".ico"))
make_nv(&resp[num_nv++], "content-type", "image/vnd.microsoft.icon");
else if (strstr(h3ssl.url, ".htm"))
make_nv(&resp[num_nv++], "content-type", "text/html");
else
make_nv(&resp[num_nv++], "content-type", "application/octet-stream");
make_nv(&resp[num_nv++], "content-length", slength);
}
/*
* nghttp3にレスポンスを登録。
* dr.read_data (step_read_data) はデータ読み取りコールバック。
* nghttp3がデータを必要とする際に呼び出される。
*/
dr.read_data = step_read_data;
if (nghttp3_conn_submit_response(h3conn, h3ssl.id_bidi, resp, num_nv, &dr)) {
fprintf(stderr, "nghttp3_conn_submit_response failed!\n");
goto err;
}
printf("nghttp3_conn_submit_response on %llu...\n", (unsigned long long) h3ssl.id_bidi);
/*
* ----------------------------------------
* フェーズ6: データ送信ループ
* ----------------------------------------
*
* nghttp3_conn_writev_stream()で送信すべきデータを取得し、
* quic_server_write()でQUICストリームに書き込む。
*
* 【処理の流れ】
* 1. nghttp3_conn_writev_stream(): 送信データをvec配列に取得
* - sveccnt: 取得したベクタ数
* - streamid: 送信先ストリームID
* - fin: ストリーム終了フラグ
*
* 2. quic_server_write(): 各ベクタをQUICストリームに書き込み
* - 最後のベクタでfin=1ならSSL_WRITE_FLAG_CONCLUDE
*
* 3. nghttp3_conn_add_write_offset(): 送信済みバイト数をnghttp3に報告
* - nghttp3の内部状態を更新
*
* 4. sveccnt <= 0 かつ datadone でループ終了
*/
for (;;) {
nghttp3_vec vec[256]; /* 送信データベクタ配列 */
nghttp3_ssize sveccnt; /* 取得したベクタ数 */
int fin, i; /* fin: ストリーム終了フラグ */
int64_t streamid; /* 送信先ストリームID */
/* nghttp3から送信すべきデータを取得 */
sveccnt = nghttp3_conn_writev_stream(h3conn, &streamid, &fin, vec,
nghttp3_arraylen(vec));
/* 送信データがない場合 */
if (sveccnt <= 0) {
printf("nghttp3_conn_writev_stream done: %ld stream: %llu fin %d\n",
(long int)sveccnt,
(unsigned long long)streamid,
fin);
/*
* fin=1 かつ streamid有効の場合:
* データは全て読み取られたが、finフラグの送信が必要。
* 0バイトのwrite_offsetでfinを送信。
*/
if (streamid != -1 && fin) {
printf("Sending end data on %llu fin %d\n",
(unsigned long long) streamid, fin);
nghttp3_conn_add_write_offset(h3conn, streamid, 0);
continue;
}
/* datadone でない場合はエラー、そうでなければ送信完了 */
if (!h3ssl.datadone)
goto err;
else
break; /* Done - 全データ送信完了 */
}
printf("nghttp3_conn_writev_stream: %ld fin: %d\n", (long int)sveccnt, fin);
/* 取得した各ベクタをQUICストリームに書き込み */
for (i = 0; i < sveccnt; i++) {
size_t numbytes = vec[i].len;
int flagwrite = 0;
printf("quic_server_write on %llu for %ld\n",
(unsigned long long)streamid, (unsigned long)vec[i].len);
/*
* 最後のベクタでfin=1の場合、SSL_WRITE_FLAG_CONCLUDE を設定。
* これにより、データ送信と同時にストリームのFINを送信できる。
*/
if (fin && i == sveccnt - 1)
flagwrite = SSL_WRITE_FLAG_CONCLUDE;
if (!quic_server_write(&h3ssl, streamid, vec[i].base,
vec[i].len, flagwrite, &numbytes)) {
fprintf(stderr, "quic_server_write failed!\n");
goto err;
}
}
/*
* 送信済みバイト数をnghttp3に報告。
* これにより nghttp3 は内部バッファを解放し、
* 次のデータを準備できる。
*/
if (nghttp3_conn_add_write_offset(
h3conn, streamid,
(size_t)nghttp3_vec_len(vec, (size_t)sveccnt))) {
fprintf(stderr, "nghttp3_conn_add_write_offset failed!\n");
goto err;
}
}
printf("nghttp3_conn_submit_response DONE!!!\n");
/*
* ----------------------------------------
* フェーズ7: レスポンス送信完了後の処理
* ----------------------------------------
*/
if (h3ssl.datadone) {
/*
* All the data was sent.
* close stream zero
*/
/* 全データ送信完了。ストリームをSERVERCLOSED状態に設定 */
if (!h3ssl.close_done) {
set_id_status(h3ssl.id_bidi, SERVERCLOSED, &h3ssl);
h3ssl.close_wait = 1; /* クローズ待機フラグ */
}
} else {
printf("nghttp3_conn_submit_response still not finished\n");
}
/*
* ----------------------------------------
* フェーズ8: wait_close - クローズ待機
* ----------------------------------------
*
* レスポンス送信後、クライアントからの接続終了を待つ。
* この間に以下のイベントが発生する可能性がある:
* - h3ssl.done: 接続完全終了
* - h3ssl.new_conn: 新規接続受け入れ(→ newconn:へ)
* - h3ssl.restart: 同一接続で新規リクエスト(→ restart:へ)
* - are_all_clientid_closed(): クライアントが全ストリームを閉じた
*/
/* wait until closed */
wait_close:
hasnothing = 0;
for (;;) {
/* イベントがない場合、ソケット活動を待機 */
if (!hasnothing) {
SSL *newssl = get_ids_connection(&h3ssl);
printf("hasnothing nothing WAIT %d!!!\n", h3ssl.close_done);
/* 接続がなければリスナーで待機 */
if (newssl == NULL)
newssl = listener;
ret = wait_for_activity(newssl);
if (ret == -1)
goto err;
if (ret == 0)
printf("hasnothing timeout\n");
/* we have something or a timeout */
/* 何らかのイベントまたはタイムアウト。内部イベント処理を実行 */
handle_events_from_ids(&h3ssl);
}
hasnothing = read_from_ssl_ids(&h3conn, &h3ssl);
if (hasnothing == -1) {
printf("hasnothing failed\n");
break;
/* goto err; well in fact not */
/* エラーでも致命的ではない場合があるのでbreakのみ */
} else if (hasnothing == 0) {
printf("hasnothing nothing\n");
continue; /* イベントなし、再度待機 */
} else {
printf("hasnothing something\n");
/* 何らかのイベントを処理 */
/* 接続完全終了 */
if (h3ssl.done) {
printf("hasnothing something... DONE\n");
/* we might already have the next connection to accept */
/* 次の接続がある可能性があるのでhassomething=1 */
hassomething = 1;
break;
}
/* 新規接続が来た - newconn:へジャンプ */
if (h3ssl.new_conn) {
printf("hasnothing something... NEW CONN\n");
h3ssl.new_conn = 0;
goto newconn;
}
/* 同一接続で新規リクエスト - restart:へジャンプ */
if (h3ssl.restart) {
printf("hasnothing something... RESTART\n");
h3ssl.restart = 0;
goto restart;
}
/* クライアントが全ストリームを閉じた */
if (are_all_clientid_closed(&h3ssl)) {
printf("hasnothing something... DONE other side closed\n");
/* there might 2 or 3 message we will ignore */
/* 残りの2-3メッセージは無視される場合がある */
hassomething = 0;
break;
}
}
}
/*
* ----------------------------------------
* フェーズ9: クリーンアップと次の接続への準備
* ----------------------------------------
*/
/*
* Free the streams, then loop again, accepting another connection.
*/
close_all_ids(&h3ssl); /* 全ストリームをクローズ */
ssl = get_ids_connection(&h3ssl);
if (ssl != NULL) {
SSL_free(ssl); /* QUIC接続を解放 */
replace_ids_connection(&h3ssl, ssl, NULL); /* 参照をクリア */
}
hassomething = 0; /* 次のループで新規接続を待機 */
} /* メインループ終了 */
ok = 1;
err:
if (!ok)
ERR_print_errors_fp(stderr); /* エラー詳細を出力 */
SSL_free(listener); /* リスナーを解放 */
return ok; /* 成功時1、エラー時0 */
}
/*
* ============================================================================
* main() - HTTP/3デモサーバーのエントリポイント
* ============================================================================
*
* 【概要】
* HTTP/3デモサーバーのメイン関数。コマンドライン引数を解析し、
* SSL_CTXとUDPソケットを初期化してサーバーループを開始する。
*
* 【使用方法】
* ./ossl-nghttp3-demo-server <port> <server.crt> <server.key>
*
* 引数:
* <port> - リッスンするポート番号 (1-65535)
* <server.crt> - サーバー証明書ファイルパス(PEM形式)
* <server.key> - 秘密鍵ファイルパス(PEM形式)
*
* 【環境変数】
* FILEPREFIX - 設定すると、そのディレクトリから静的ファイルを提供
* 未設定の場合はデフォルトのテスト文字列を返す
*
* 【処理フロー】
* 1. コマンドライン引数の検証
* 2. SSL_CTXの作成(証明書・秘密鍵のロード)
* 3. ポート番号の解析と検証
* 4. UDPソケットの作成とバインド
* 5. サーバーループの開始 (run_quic_server)
* 6. クリーンアップ
*
* @return 成功時0、エラー時1
*/
/*
* demo server... just return a 20 bytes ascii string as response for any
* request single h3 connection and single threaded.
*/
int main(int argc, char **argv)
{
int rc = 1; /* 戻り値(エラー時1、成功時0)*/
SSL_CTX *ctx = NULL; /* SSL/TLSコンテキスト */
int fd = -1; /* UDPソケットFD */
unsigned long port; /* リスンポート番号 */
/*
* ----------------------------------------
* コマンドライン引数の検証
* ----------------------------------------
*/
if (argc < 4) {
fprintf(stderr, "usage: %s <port> <server.crt> <server.key>\n",
argv[0]);
goto err;
}
/*
* ----------------------------------------
* SSL_CTXの作成
* ----------------------------------------
* QUIC用のSSL_CTXを作成し、証明書と秘密鍵をロード。
* ALPNコールバックも設定される。
*/
/* Create SSL_CTX. */
if ((ctx = create_ctx(argv[2], argv[3])) == NULL)
goto err;
/*
* ----------------------------------------
* ポート番号の解析
* ----------------------------------------
* 有効な範囲 (1-65535) であることを確認。
*/
/* Parse port number from command line arguments. */
port = strtoul(argv[1], NULL, 0);
if (port == 0 || port > UINT16_MAX) {
fprintf(stderr, "invalid port: %lu\n", port);
goto err;
}
/*
* ----------------------------------------
* UDPソケットの作成
* ----------------------------------------
* QUICはUDP上で動作するため、UDPソケットを作成。
*/
/* Create UDP socket. */
if ((fd = create_socket((uint16_t)port)) < 0)
goto err;
/*
* ----------------------------------------
* サーバーループの開始
* ----------------------------------------
* 接続受け入れ、リクエスト処理、レスポンス送信を
* 無限ループで実行。
*/
/* Enter QUIC server connection acceptance loop. */
if (!run_quic_server(ctx, fd))
goto err;
rc = 0; /* 正常終了 */
err:
/*
* ----------------------------------------
* クリーンアップ
* ----------------------------------------
*/
if (rc != 0)
ERR_print_errors_fp(stderr); /* エラー詳細を出力 */
SSL_CTX_free(ctx); /* SSL_CTXを解放(NULLでも安全)*/
if (fd != -1)
BIO_closesocket(fd); /* ソケットをクローズ */
return rc; /* 成功時0、エラー時1 */
}
/*
* Copyright 2023-2024 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* ファイルパス: demos/http3/ossl-nghttp3-demo.c (OpenSSLリポジトリ内)
*
* 注意: このファイルの日本語コメントは Claude Code によって追加されました。
* オリジナルのソースコードは https://github.com/openssl/openssl にあります。
*/
/*
* ============================================================================
* ossl-nghttp3-demo.c - HTTP/3クライアントデモ
* ============================================================================
*
* ※ 日本語コメントは Claude Code によって追加されました。
*
* 【概要】
* OpenSSL QUICとnghttp3ライブラリを使用したHTTP/3クライアントのデモ。
* 指定されたURLに対してGETリクエストを送信し、レスポンスを表示する。
*
* 【関連ファイル】
* - ossl-nghttp3.h / ossl-nghttp3.c: HTTP/3接続のヘルパーライブラリ
* (OSSL_DEMO_H3_CONN構造体と関連関数を提供)
*
* 【ossl-nghttp3-demo-server.cとの違い】
* - このファイル: HTTP/3クライアント(リクエスト送信側)
* - ossl-nghttp3-demo-server.c: HTTP/3サーバー(リクエスト受信側)
*
* 【処理フロー図】
*
* main()
* │
* ├── コマンドライン引数解析 (host:port)
* │
* ├── BIO_parse_hostserv() ... ホスト名とポートを分離
* │
* ├── BIO_lookup_ex() ... DNSルックアップ
* │ ※ IPv4/IPv6両方のアドレスを取得
* │
* ├── SSL_CTX作成
* │ ├── OSSL_QUIC_client_method()
* │ ├── SSL_VERIFY_PEER ... サーバー証明書を検証
* │ └── デフォルト検証パス設定
* │
* ├── nghttp3コールバック設定
* │ ├── recv_header → on_recv_header()
* │ ├── end_headers → on_end_headers()
* │ ├── recv_data → on_recv_data()
* │ └── end_stream → on_end_stream()
* │
* └── for (各DNSアドレス) {
* │
* ├── OSSL_DEMO_H3_CONN_new_for_addr() ... HTTP/3接続作成
* │
* └── try_conn()
* │
* ├── HTTPリクエストヘッダ構築
* │ (:method, :scheme, :authority, :path, user-agent)
* │
* ├── OSSL_DEMO_H3_CONN_submit_request() ... リクエスト送信
* │
* └── while (!done)
* └── OSSL_DEMO_H3_CONN_handle_events()
* │
* └── nghttp3コールバックが呼ばれる
* ├── on_recv_header() → ヘッダをstderrに出力
* ├── on_end_headers() → 改行出力
* ├── on_recv_data() → ボディをstdoutに出力
* └── on_end_stream() → done=1
* }
*
* 【使用方法】
* ./ossl-nghttp3-demo <host:port>
*
* 例:
* ./ossl-nghttp3-demo www.example.com:443
*
* 【DNSフォールバック】
* UDPではconnect(2)がハンドシェイクを行わないため、QUICハンドシェイクが
* 失敗して初めてサーバーに到達できないことが判明する。そのため、DNS
* ルックアップで複数のアドレスが返された場合、順番に試行する。
*
* 【出力】
* - stderr: HTTPレスポンスヘッダ
* - stdout: HTTPレスポンスボディ
*/
#include "ossl-nghttp3.h"
#include <openssl/err.h>
#include <sys/socket.h>
#include <netinet/in.h>
/*
* 【グローバル変数】
* done: HTTPトランザクション完了フラグ
* on_end_stream()コールバックで1に設定され、メインループを終了させる。
*/
static int done;
/*
* make_nv() - nghttp3_nv構造体の初期化ヘルパー
*
* HTTP/3ヘッダ(名前-値ペア)を表すnghttp3_nv構造体を初期化する。
*
* 【nghttp3_nv構造体】
* HTTP/2やHTTP/3では、ヘッダは名前-値のペアとして表現される。
* nghttp3_nvは以下のフィールドを持つ:
* - name/value: ヘッダ名/値へのポインタ
* - namelen/valuelen: それぞれの長さ
* - flags: ヘッダ処理のフラグ
*
* 【HTTP/3の擬似ヘッダ】
* HTTP/3では以下の擬似ヘッダ(コロンで始まる)が使用される:
* - :method ... HTTPメソッド (GET, POST等)
* - :scheme ... スキーム (https)
* - :authority ... ホスト名 (Host相当)
* - :path ... リクエストパス
*
* @param nv 初期化するnghttp3_nv構造体へのポインタ
* @param name ヘッダ名(静的文字列)
* @param value ヘッダ値(静的文字列)
*/
static void make_nv(nghttp3_nv *nv, const char *name, const char *value)
{
nv->name = (uint8_t *)name;
nv->value = (uint8_t *)value;
nv->namelen = strlen(name);
nv->valuelen = strlen(value);
nv->flags = NGHTTP3_NV_FLAG_NONE; /* 特別なフラグなし */
}
/*
* ============================================================================
* nghttp3コールバック関数群
* ============================================================================
*
* nghttp3ライブラリがHTTP/3フレームを処理する際に呼び出される
* コールバック関数。サーバーからのレスポンスを受信した際に呼ばれる。
*
* 【呼び出し順序】
* 1. on_recv_header() ... 各ヘッダごとに呼ばれる(複数回)
* 2. on_end_headers() ... 全ヘッダ受信完了時
* 3. on_recv_data() ... ボディデータ受信時(複数回の可能性)
* 4. on_end_stream() ... ストリーム終了時
*/
/*
* on_recv_header() - HTTPレスポンスヘッダ受信コールバック
*
* サーバーから1つのHTTPヘッダを受信するたびに呼び出される。
* ヘッダ名と値をstderrに出力する。
*
* 【nghttp3_rcbuf】
* 参照カウント付きバッファ。nghttp3_rcbuf_get_buf()で
* 実際のデータ(nghttp3_vec: base + len)を取得できる。
*
* @param h3conn nghttp3接続オブジェクト
* @param stream_id ストリームID
* @param token QPACKトークン(ヘッダの種類を識別)
* @param name ヘッダ名(参照カウント付きバッファ)
* @param value ヘッダ値(参照カウント付きバッファ)
* @param flags フラグ
* @param conn_user_data 接続のユーザーデータ
* @param stream_user_data ストリームのユーザーデータ
* @return 0: 成功, 非0: エラー
*/
static int on_recv_header(nghttp3_conn *h3conn, int64_t stream_id,
int32_t token,
nghttp3_rcbuf *name, nghttp3_rcbuf *value,
uint8_t flags,
void *conn_user_data,
void *stream_user_data)
{
nghttp3_vec vname, vvalue;
/* Received a single HTTP header. */
/* 参照カウント付きバッファから実際のデータを取得 */
vname = nghttp3_rcbuf_get_buf(name);
vvalue = nghttp3_rcbuf_get_buf(value);
/* ヘッダをstderrに出力: "name: value\n" */
fwrite(vname.base, vname.len, 1, stderr);
fprintf(stderr, ": ");
fwrite(vvalue.base, vvalue.len, 1, stderr);
fprintf(stderr, "\n");
return 0;
}
/*
* on_end_headers() - ヘッダ終了コールバック
*
* 全てのHTTPレスポンスヘッダを受信した後に呼び出される。
* ヘッダとボディの区切りとして空行を出力。
*
* @param fin 1の場合、このストリームは終了(ボディなし)
*/
static int on_end_headers(nghttp3_conn *h3conn, int64_t stream_id,
int fin,
void *conn_user_data, void *stream_user_data)
{
fprintf(stderr, "\n"); /* ヘッダとボディの間に空行 */
return 0;
}
/*
* on_recv_data() - HTTPレスポンスボディ受信コールバック
*
* サーバーからレスポンスボディのデータを受信するたびに呼び出される。
* 受信したデータをそのままstdoutに出力する。
*
* 【注意】
* 大きなレスポンスの場合、このコールバックは複数回呼び出される。
* 各呼び出しでdatalenバイトのデータが渡される。
*
* @param data 受信したデータへのポインタ
* @param datalen 受信したデータのバイト数
* @return 0: 成功, 非0: エラー(接続中断)
*/
static int on_recv_data(nghttp3_conn *h3conn, int64_t stream_id,
const uint8_t *data, size_t datalen,
void *conn_user_data, void *stream_user_data)
{
size_t wr;
/* HTTP response body data - write it to stdout. */
/* レスポンスボディをstdoutに出力 */
while (datalen > 0) {
wr = fwrite(data, 1, datalen, stdout);
if (ferror(stdout))
return 1; /* 書き込みエラー */
data += wr;
datalen -= wr;
}
return 0;
}
/*
* on_end_stream() - ストリーム終了コールバック
*
* HTTPトランザクションが完了した際に呼び出される。
* サーバーがレスポンスの送信を完了し、ストリームを閉じたことを示す。
*
* doneフラグを1に設定してメインループを終了させる。
*/
static int on_end_stream(nghttp3_conn *h3conn, int64_t stream_id,
void *conn_user_data, void *stream_user_data)
{
/* HTTP transaction is done - set done flag so that we stop looping. */
/* HTTPトランザクション完了。doneフラグでメインループを終了させる */
done = 1;
return 0;
}
/*
* try_conn() - HTTP/3リクエストの送信と完了待機
*
* 確立されたHTTP/3接続を使用して、ルートパス ("/") に対する
* GETリクエストを送信し、レスポンスが完了するまで待機する。
*
* 【処理フロー】
* 1. HTTPリクエストヘッダを構築
* - :method = "GET"
* - :scheme = "https"
* - :authority = ホスト名
* - :path = "/"
* - user-agent = "OpenSSL-Demo/nghttp3"
*
* 2. OSSL_DEMO_H3_CONN_submit_request()でリクエスト送信
*
* 3. レスポンス完了待機ループ
* - OSSL_DEMO_H3_CONN_handle_events()でイベント処理
* - on_end_stream()がdone=1を設定するまでループ
*
* 【OSSL_DEMO_H3_CONN_submit_request()】
* ossl-nghttp3.cで定義されているヘルパー関数。
* nghttp3_conn_submit_request()をラップし、QUICストリームの
* 作成とリクエスト送信を行う。
*
* 【OSSL_DEMO_H3_CONN_handle_events()】
* QUICとnghttp3のイベントを処理するヘルパー関数。
* ソケットの読み書き、SSL_handle_events()の呼び出し、
* nghttp3コールバックの実行を行う。
*
* @param conn HTTP/3接続オブジェクト
* @param bare_hostname ホスト名(:authority用)
* @return 1: 成功, 0: エラー
*/
static int try_conn(OSSL_DEMO_H3_CONN *conn, const char *bare_hostname)
{
nghttp3_nv nva[16]; /* HTTPヘッダ配列 */
size_t num_nv = 0; /* ヘッダ数 */
/*
* Build HTTP headers.
* HTTP/3リクエストヘッダを構築。
* 擬似ヘッダ(コロンで始まる)は最初に配置する必要がある。
*/
make_nv(&nva[num_nv++], ":method", "GET"); /* HTTPメソッド */
make_nv(&nva[num_nv++], ":scheme", "https"); /* スキーム(QUICは常にhttps)*/
make_nv(&nva[num_nv++], ":authority", bare_hostname); /* ホスト名 */
make_nv(&nva[num_nv++], ":path", "/"); /* リクエストパス */
make_nv(&nva[num_nv++], "user-agent", "OpenSSL-Demo/nghttp3"); /* UA */
/* Submit request. */
/* HTTP/3リクエストを送信 */
if (!OSSL_DEMO_H3_CONN_submit_request(conn, nva, num_nv, NULL, NULL)) {
ERR_raise_data(ERR_LIB_USER, ERR_R_OPERATION_FAIL,
"cannot submit HTTP/3 request");
return 0;
}
/*
* Wait for request to complete.
* レスポンス完了待機ループ。
* on_end_stream()がdone=1を設定するまで、
* イベント処理を繰り返す。
*/
done = 0;
while (!done)
if (!OSSL_DEMO_H3_CONN_handle_events(conn)) {
ERR_raise_data(ERR_LIB_USER, ERR_R_OPERATION_FAIL,
"cannot handle events");
return 0;
}
return 1; /* 成功 */
}
/*
* ============================================================================
* main() - HTTP/3クライアントのエントリポイント
* ============================================================================
*
* 【使用方法】
* ./ossl-nghttp3-demo <host:port>
*
* 例:
* ./ossl-nghttp3-demo www.example.com:443
* ./ossl-nghttp3-demo localhost:4433
*
* 【処理フロー】
* 1. コマンドライン引数の解析
* 2. ホスト名とポートの分離 (BIO_parse_hostserv)
* 3. DNSルックアップ (BIO_lookup_ex)
* 4. SSL_CTX作成と証明書検証設定
* 5. nghttp3コールバック設定
* 6. 各DNSアドレスに対して接続試行
* 7. クリーンアップ
*
* @return 0: 成功, 1: エラー
*/
int main(int argc, char **argv)
{
int ret = 1; /* 戻り値(デフォルトはエラー)*/
int ok;
SSL_CTX *ctx = NULL; /* SSL/TLSコンテキスト */
OSSL_DEMO_H3_CONN *conn = NULL; /* HTTP/3接続オブジェクト */
nghttp3_callbacks callbacks = {0}; /* nghttp3コールバック構造体 */
const char *addr; /* コマンドライン引数 (host:port) */
char *hostname, *service; /* 分離されたホスト名とポート */
BIO_ADDRINFO *bai = NULL; /* DNSルックアップ結果 */
const BIO_ADDRINFO *bai_walk; /* 結果リストのイテレータ */
/*
* ----------------------------------------
* コマンドライン引数の検証
* ----------------------------------------
*/
/* Check arguments. */
if (argc < 2) {
fprintf(stderr, "usage: %s <host:port>\n", argv[0]);
goto err;
}
addr = argv[1];
/*
* ----------------------------------------
* ホスト名とポートの分離
* ----------------------------------------
* BIO_parse_hostserv()は "host:port" 形式の文字列を
* ホスト名とサービス(ポート)に分離する。
*/
hostname = NULL;
service = NULL;
if (BIO_parse_hostserv(addr, &hostname, &service, 0) != 1) {
fprintf(stderr, "usage: %s <host:port>\n", argv[0]);
goto err;
}
if (hostname == NULL || service == NULL) {
fprintf(stderr, "usage: %s <host:port>\n", argv[0]);
goto err;
}
/*
* ----------------------------------------
* DNSルックアップ
* ----------------------------------------
* BIO_lookup_ex()でホスト名をIPアドレスに解決。
* デュアルスタック(IPv4/IPv6両対応)環境では
* 複数のアドレスが返される可能性がある。
*
* BIO_LOOKUP_CLIENT: クライアント用のアドレスを取得
* SOCK_DGRAM: UDPソケット用
* IPPROTO_UDP: UDPプロトコル
*/
/*
* Remember DNS may return more IP addresses (and it typically does these
* dual-stack days).
*/
ok = BIO_lookup_ex(hostname, service, BIO_LOOKUP_CLIENT,
0, SOCK_DGRAM, IPPROTO_UDP, &bai);
if (ok == 0) {
fprintf(stderr, "host %s not found\n", hostname);
goto err;
}
/*
* ----------------------------------------
* SSL_CTXの作成
* ----------------------------------------
* QUICクライアント用のSSL_CTXを作成し、
* サーバー証明書の検証を有効化。
*/
/* Setup SSL_CTX. */
if ((ctx = SSL_CTX_new(OSSL_QUIC_client_method())) == NULL)
goto err;
/* サーバー証明書の検証を有効化(HTTPS必須)*/
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
/* システムデフォルトのCA証明書パスを使用 */
if (SSL_CTX_set_default_verify_paths(ctx) == 0)
goto err;
/*
* ----------------------------------------
* nghttp3コールバックの設定
* ----------------------------------------
* HTTP/3レスポンスを処理するためのコールバック関数を登録。
*/
/* Setup callbacks. */
callbacks.recv_header = on_recv_header; /* ヘッダ受信 */
callbacks.end_headers = on_end_headers; /* ヘッダ終了 */
callbacks.recv_data = on_recv_data; /* データ受信 */
callbacks.end_stream = on_end_stream; /* ストリーム終了 */
/*
* ----------------------------------------
* DNSアドレスリストへの接続試行
* ----------------------------------------
*
* 【なぜ複数アドレスを試行するのか】
* TCPと異なり、UDPではconnect(2)がハンドシェイクを行わない。
* UDPのconnect(2)は単にローカルソケットにリモートアドレスを
* 関連付けるだけで、相手に到達可能かどうかは確認しない。
*
* QUICでは、TLSハンドシェイクが実行されて初めて、
* サーバーに到達できないことが判明する。そのため、
* DNSが複数アドレスを返した場合は、順番に試行する必要がある。
*
* 典型的なケース:
* - IPv6アドレスが返されたが、IPv6接続ができない
* - 複数のIPv4アドレスのうち、一部がダウンしている
*/
/*
* Unlike TCP there is no handshake on UDP protocol.
* The BIO subsystem uses connect(2) to establish
* connection. However connect(2) for UDP does not
* perform handshake, so BIO just assumes the remote
* service is reachable on the first address returned
* by DNS. It's the SSL-handshake itself when QUIC stack
* realizes the service is not reachable, so the application
* needs to initiate a QUIC connection on the next address
* returned by DNS.
*/
for (bai_walk = bai; bai_walk != NULL;
bai_walk = BIO_ADDRINFO_next(bai_walk)) {
/* 各アドレスに対してHTTP/3接続を試行 */
conn = OSSL_DEMO_H3_CONN_new_for_addr(ctx, bai_walk, hostname,
&callbacks, NULL, NULL);
if (conn != NULL) {
if (try_conn(conn, addr) == 0) {
/*
* Failure, try the next address.
*/
/* 失敗。次のアドレスを試行 */
OSSL_DEMO_H3_CONN_free(conn);
conn = NULL;
ret = 1;
} else {
/*
* Success, done.
*/
/* 成功。ループを終了 */
ret = 0;
break;
}
}
}
/*
* ----------------------------------------
* クリーンアップ
* ----------------------------------------
*/
err:
if (ret != 0)
ERR_print_errors_fp(stderr); /* エラー詳細を出力 */
OSSL_DEMO_H3_CONN_free(conn); /* HTTP/3接続を解放 */
SSL_CTX_free(ctx); /* SSL_CTXを解放 */
BIO_ADDRINFO_free(bai); /* DNSルックアップ結果を解放 */
return ret; /* 成功時0、エラー時1 */
}
/*
* Copyright 2023-2024 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* ファイルパス: demos/http3/ossl-nghttp3.c (OpenSSLリポジトリ内)
*
* 注意: このファイルの日本語コメントは Claude Code によって追加されました。
* オリジナルのソースコードは https://github.com/openssl/openssl にあります。
*/
/*
* ============================================================================
* OpenSSL QUIC と nghttp3 のアダプテーションレイヤー
* ============================================================================
*
* ※ 日本語コメントは Claude Code によって追加されました。
*
* 【概要】
* このファイルは、OpenSSL の QUIC 実装と nghttp3 HTTP/3 ライブラリを
* 統合するためのアダプテーションレイヤーを提供します。
*
* 【HTTP/3 の構造】
*
* アプリケーション (ossl-nghttp3-demo.c)
* ↓
* HTTP/3 レイヤー (nghttp3)
* ↓
* アダプテーションレイヤー (このファイル)
* ↓
* QUIC レイヤー (OpenSSL QUIC)
* ↓
* UDP ソケット
*
* 【HTTP/3 に必要なストリーム】
* HTTP/3 では以下の単方向ストリームが必要:
*
* クライアント → サーバー:
* - Control Stream: HTTP/3 制御情報
* - QPACK Encoder Stream: ヘッダー圧縮エンコーダ
* - QPACK Decoder Stream: ヘッダー圧縮デコーダ
*
* サーバー → クライアント:
* - 同様の3つのストリーム(サーバーが作成)
*
* リクエスト/レスポンス:
* - 双方向ストリーム(リクエストごとに1つ)
*
* 【主要なデータ構造】
* - OSSL_DEMO_H3_CONN: HTTP/3 接続を表す(QUIC 接続 + nghttp3 状態)
* - OSSL_DEMO_H3_STREAM: 個々のストリームを表す
*
* 【データフロー】
* 送信: nghttp3 → writev_stream → SSL_write_ex2 → ネットワーク
* 受信: ネットワーク → SSL_read_ex → nghttp3_conn_read_stream → nghttp3
* ============================================================================
*/
#include "ossl-nghttp3.h"
#include <openssl/err.h>
#include <assert.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define ARRAY_LEN(x) (sizeof(x)/sizeof((x)[0]))
/*
* ストリームタイプの定義
* HTTP/3 では複数種類のストリームを使用する
*/
enum {
OSSL_DEMO_H3_STREAM_TYPE_CTRL_SEND, /* 制御ストリーム(単方向、送信用) */
OSSL_DEMO_H3_STREAM_TYPE_QPACK_ENC_SEND, /* QPACK エンコーダストリーム */
OSSL_DEMO_H3_STREAM_TYPE_QPACK_DEC_SEND, /* QPACK デコーダストリーム */
OSSL_DEMO_H3_STREAM_TYPE_REQ, /* リクエストストリーム(双方向) */
};
#define BUF_SIZE 4096
/*
* ============================================================================
* OSSL_DEMO_H3_STREAM - 個々の QUIC ストリームを表す構造体
* ============================================================================
*
* HTTP/3 では各ストリームが独立したデータチャネルとして機能する。
* この構造体は QUIC ストリームと HTTP/3 ストリームの対応を管理する。
*/
struct ossl_demo_h3_stream_st {
uint64_t id; /* QUIC stream ID */
SSL *s; /* QUIC stream SSL object (QSSO) */
int done_recv_fin; /* Received FIN: ストリーム終了フラグ */
void *user_data; /* アプリケーション用データ */
/* 受信バッファ: QUIC から読み取ったデータを一時保存 */
uint8_t buf[BUF_SIZE];
size_t buf_cur, buf_total; /* バッファの現在位置と合計サイズ */
};
/*
* ストリーム ID をキーとするハッシュテーブルの定義
* OpenSSL の LHASH マクロを使用
*/
DEFINE_LHASH_OF_EX(OSSL_DEMO_H3_STREAM);
/* ストリームオブジェクトを解放 */
static void h3_stream_free(OSSL_DEMO_H3_STREAM *s)
{
if (s == NULL)
return;
SSL_free(s->s);
OPENSSL_free(s);
}
/* ハッシュテーブル用: ストリーム ID からハッシュ値を計算 */
static unsigned long h3_stream_hash(const OSSL_DEMO_H3_STREAM *s)
{
return (unsigned long)s->id;
}
/* ハッシュテーブル用: ストリーム ID を比較 */
static int h3_stream_eq(const OSSL_DEMO_H3_STREAM *a, const OSSL_DEMO_H3_STREAM *b)
{
if (a->id < b->id) return -1;
if (a->id > b->id) return 1;
return 0;
}
/* ストリームに関連付けられたユーザーデータを取得 */
void *OSSL_DEMO_H3_STREAM_get_user_data(const OSSL_DEMO_H3_STREAM *s)
{
return s->user_data;
}
/*
* ============================================================================
* OSSL_DEMO_H3_CONN - HTTP/3 接続を表す構造体
* ============================================================================
*
* QUIC 接続と nghttp3 の状態を統合し、HTTP/3 通信を管理する。
* ストリームのマップを保持し、イベント処理を行う。
*/
struct ossl_demo_h3_conn_st {
/* QUIC connection SSL object (QCSO) */
SSL *qconn;
/* BIO wrapping QCSO */
BIO *qconn_bio;
/* HTTP/3 connection object (nghttp3 の状態) */
nghttp3_conn *h3conn;
/* map of stream IDs to OSSL_DEMO_H3_STREAMs */
/* ストリーム ID → OSSL_DEMO_H3_STREAM のハッシュマップ */
LHASH_OF(OSSL_DEMO_H3_STREAM) *streams;
/* opaque user data pointer */
void *user_data;
int pump_res; /* pump 処理の結果 */
size_t consumed_app_data; /* 消費したアプリデータ量 */
/*
* Forwarding callbacks
* nghttp3 のコールバックをラップして内部処理後にアプリに転送
*/
nghttp3_recv_data recv_data_cb; /* データ受信 */
nghttp3_stream_close stream_close_cb; /* ストリームクローズ */
nghttp3_stop_sending stop_sending_cb; /* 送信停止要求 */
nghttp3_reset_stream reset_stream_cb; /* ストリームリセット */
nghttp3_deferred_consume deferred_consume_cb; /* 遅延消費 */
};
/*
* HTTP/3 接続オブジェクトを解放
* 全ストリーム、nghttp3 状態、BIO を解放する
*/
void OSSL_DEMO_H3_CONN_free(OSSL_DEMO_H3_CONN *conn)
{
if (conn == NULL)
return;
/* 全ストリームを解放 */
lh_OSSL_DEMO_H3_STREAM_doall(conn->streams, h3_stream_free);
nghttp3_conn_del(conn->h3conn);
BIO_free_all(conn->qconn_bio);
lh_OSSL_DEMO_H3_STREAM_free(conn->streams);
OPENSSL_free(conn);
}
/*
* 新しい QUIC ストリームを作成
*
* type:
* OSSL_DEMO_H3_STREAM_TYPE_REQ: 双方向ストリーム(リクエスト用)
* その他: 単方向ストリーム(制御、QPACK 用)
*/
static OSSL_DEMO_H3_STREAM *h3_conn_create_stream(OSSL_DEMO_H3_CONN *conn, int type)
{
OSSL_DEMO_H3_STREAM *s;
uint64_t flags = SSL_STREAM_FLAG_ADVANCE;
if ((s = OPENSSL_zalloc(sizeof(OSSL_DEMO_H3_STREAM))) == NULL)
return NULL;
/* リクエスト以外は単方向ストリーム */
if (type != OSSL_DEMO_H3_STREAM_TYPE_REQ)
flags |= SSL_STREAM_FLAG_UNI;
/* QUIC ストリームを作成 */
if ((s->s = SSL_new_stream(conn->qconn, flags)) == NULL) {
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"could not create QUIC stream object");
goto err;
}
/* ストリーム ID を取得してハッシュマップに登録 */
s->id = SSL_get_stream_id(s->s);
lh_OSSL_DEMO_H3_STREAM_insert(conn->streams, s);
return s;
err:
OPENSSL_free(s);
return NULL;
}
/*
* サーバーから受け入れたストリームをラップ
* サーバーが開始した単方向ストリーム(制御、QPACK)を管理に追加
*/
static OSSL_DEMO_H3_STREAM *h3_conn_accept_stream(OSSL_DEMO_H3_CONN *conn, SSL *qstream)
{
OSSL_DEMO_H3_STREAM *s;
if ((s = OPENSSL_zalloc(sizeof(OSSL_DEMO_H3_STREAM))) == NULL)
return NULL;
s->id = SSL_get_stream_id(qstream);
s->s = qstream;
lh_OSSL_DEMO_H3_STREAM_insert(conn->streams, s);
return s;
}
/* ストリームを管理から削除して解放 */
static void h3_conn_remove_stream(OSSL_DEMO_H3_CONN *conn, OSSL_DEMO_H3_STREAM *s)
{
if (s == NULL)
return;
lh_OSSL_DEMO_H3_STREAM_delete(conn->streams, s);
h3_stream_free(s);
}
/*
* ============================================================================
* nghttp3 コールバック関数群
* ============================================================================
*
* nghttp3 からのイベントを受け取り、内部処理を行った後、
* アプリケーションのコールバックに転送する。
*/
/*
* データ受信コールバック
* HTTP/3 のボディデータを受信した時に呼ばれる
*/
static int h3_conn_recv_data(nghttp3_conn *h3conn, int64_t stream_id,
const uint8_t *data, size_t datalen,
void *conn_user_data, void *stream_user_data)
{
OSSL_DEMO_H3_CONN *conn = conn_user_data;
/* 消費したアプリデータ量を記録 */
conn->consumed_app_data += datalen;
if (conn->recv_data_cb == NULL)
return 0;
/* アプリケーションのコールバックに転送 */
return conn->recv_data_cb(h3conn, stream_id, data, datalen,
conn_user_data, stream_user_data);
}
/*
* ストリームクローズコールバック
* HTTP/3 ストリームが閉じられた時に呼ばれる
*/
static int h3_conn_stream_close(nghttp3_conn *h3conn, int64_t stream_id,
uint64_t app_error_code,
void *conn_user_data, void *stream_user_data)
{
int ret = 0;
OSSL_DEMO_H3_CONN *conn = conn_user_data;
OSSL_DEMO_H3_STREAM *stream = stream_user_data;
if (conn->stream_close_cb != NULL)
ret = conn->stream_close_cb(h3conn, stream_id, app_error_code,
conn_user_data, stream_user_data);
/* 内部管理からストリームを削除 */
h3_conn_remove_stream(conn, stream);
return ret;
}
/*
* 送信停止コールバック
* ピアが STOP_SENDING フレームを送信した時に呼ばれる
*/
static int h3_conn_stop_sending(nghttp3_conn *h3conn, int64_t stream_id,
uint64_t app_error_code,
void *conn_user_data, void *stream_user_data)
{
int ret = 0;
OSSL_DEMO_H3_CONN *conn = conn_user_data;
OSSL_DEMO_H3_STREAM *stream = stream_user_data;
if (conn->stop_sending_cb != NULL)
ret = conn->stop_sending_cb(h3conn, stream_id, app_error_code,
conn_user_data, stream_user_data);
/* ストリームを解放(以降このストリームは使用しない) */
SSL_free(stream->s);
stream->s = NULL;
return ret;
}
/*
* ストリームリセットコールバック
* nghttp3 がストリームのリセットを要求した時に呼ばれる
*/
static int h3_conn_reset_stream(nghttp3_conn *h3conn, int64_t stream_id,
uint64_t app_error_code,
void *conn_user_data, void *stream_user_data)
{
int ret = 0;
OSSL_DEMO_H3_CONN *conn = conn_user_data;
OSSL_DEMO_H3_STREAM *stream = stream_user_data;
SSL_STREAM_RESET_ARGS args = {0};
if (conn->reset_stream_cb != NULL)
ret = conn->reset_stream_cb(h3conn, stream_id, app_error_code,
conn_user_data, stream_user_data);
if (stream->s != NULL) {
args.quic_error_code = app_error_code;
/* QUIC ストリームをリセット */
if (!SSL_stream_reset(stream->s, &args, sizeof(args)))
return 1;
}
return ret;
}
/*
* 遅延消費コールバック
* nghttp3 がデータの消費を遅延して報告する時に呼ばれる
*/
static int h3_conn_deferred_consume(nghttp3_conn *h3conn, int64_t stream_id,
size_t consumed,
void *conn_user_data, void *stream_user_data)
{
int ret = 0;
OSSL_DEMO_H3_CONN *conn = conn_user_data;
if (conn->deferred_consume_cb != NULL)
ret = conn->deferred_consume_cb(h3conn, stream_id, consumed,
conn_user_data, stream_user_data);
conn->consumed_app_data += consumed;
return ret;
}
/*
* ============================================================================
* OSSL_DEMO_H3_CONN_new_for_conn - 既存の QUIC 接続から HTTP/3 接続を作成
* ============================================================================
*
* 【機能】
* 既に確立された QUIC 接続 (SSL オブジェクト) を使用して、
* HTTP/3 接続オブジェクトを作成する。
*
* 【処理内容】
* 1. ALPN を "h3" に設定
* 2. デフォルトストリームモードを無効化
* 3. 制御ストリーム、QPACK ストリームを作成
* 4. nghttp3 のクライアント状態を初期化
* 5. ストリーム ID を nghttp3 にバインド
*
* 【引数】
* - qconn: QUIC 接続 SSL オブジェクト
* - callbacks: nghttp3 コールバック(NULL 可)
* - settings: nghttp3 設定(NULL でデフォルト)
* - user_data: アプリケーション用データ
*
* 【戻り値】
* - 成功: HTTP/3 接続オブジェクト
* - 失敗: NULL
*/
OSSL_DEMO_H3_CONN *OSSL_DEMO_H3_CONN_new_for_conn(SSL *qconn,
const nghttp3_callbacks *callbacks,
const nghttp3_settings *settings,
void *user_data)
{
int ec;
OSSL_DEMO_H3_CONN *conn;
OSSL_DEMO_H3_STREAM *s_ctl_send = NULL; /* 制御ストリーム */
OSSL_DEMO_H3_STREAM *s_qpenc_send = NULL; /* QPACK エンコーダ */
OSSL_DEMO_H3_STREAM *s_qpdec_send = NULL; /* QPACK デコーダ */
nghttp3_settings dsettings = {0};
nghttp3_callbacks intl_callbacks = {0};
/* HTTP/3 の ALPN: 長さプレフィックス付き "h3" */
static const unsigned char alpn[] = {2, 'h', '3'};
if (qconn == NULL) {
ERR_raise_data(ERR_LIB_USER, ERR_R_PASSED_NULL_PARAMETER,
"QUIC connection BIO must be provided");
return NULL;
}
if ((conn = OPENSSL_zalloc(sizeof(OSSL_DEMO_H3_CONN))) == NULL)
return NULL;
conn->qconn = qconn;
conn->user_data = user_data;
/* Create the map of stream IDs to OSSL_DEMO_H3_STREAM structures. */
/* ストリーム ID → ストリームオブジェクトのハッシュマップを作成 */
if ((conn->streams = lh_OSSL_DEMO_H3_STREAM_new(h3_stream_hash, h3_stream_eq)) == NULL)
goto err;
/*
* HTTP/3 の ALPN "h3" を設定
* 注意: SSL_set_alpn_protos は失敗時に 1 を返す(成功時は 0)
*/
if (SSL_set_alpn_protos(conn->qconn, alpn, sizeof(alpn))) {
/* SSL_set_alpn_protos returns 1 on failure */
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"failed to configure ALPN");
goto err;
}
/*
* Disable default stream mode and create all streams explicitly. Each QUIC
* stream will be represented by its own QUIC stream SSL object (QSSO). This
* also automatically enables us to accept incoming streams (see
* SSL_set_incoming_stream_policy(3)).
*/
/*
* デフォルトストリームモードを無効化
* HTTP/3 では複数のストリームを明示的に管理する必要があるため
* SSL_DEFAULT_STREAM_MODE_NONE を設定
*/
if (!SSL_set_default_stream_mode(conn->qconn, SSL_DEFAULT_STREAM_MODE_NONE)) {
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"failed to configure default stream mode");
goto err;
}
/*
* HTTP/3 requires a couple of unidirectional management streams: a control
* stream and some QPACK state management streams for each side of a
* connection. These are the instances on our side (with us sending); the
* server will also create its own equivalent unidirectional streams on its
* side, which we handle subsequently as they come in (see SSL_accept_stream
* in the event handling code below).
*/
/*
* HTTP/3 に必要な単方向管理ストリームを作成:
* - 制御ストリーム: HTTP/3 の設定やフレームを送信
* - QPACK エンコーダ: ヘッダー圧縮の動的テーブル更新
* - QPACK デコーダ: ヘッダー解凍の確認応答
* サーバー側も同様のストリームを作成し、SSL_accept_stream で受信する
*/
if ((s_ctl_send
= h3_conn_create_stream(conn, OSSL_DEMO_H3_STREAM_TYPE_CTRL_SEND)) == NULL)
goto err;
if ((s_qpenc_send
= h3_conn_create_stream(conn, OSSL_DEMO_H3_STREAM_TYPE_QPACK_ENC_SEND)) == NULL)
goto err;
if ((s_qpdec_send
= h3_conn_create_stream(conn, OSSL_DEMO_H3_STREAM_TYPE_QPACK_DEC_SEND)) == NULL)
goto err;
/* デフォルト設定を使用 */
if (settings == NULL) {
nghttp3_settings_default(&dsettings);
settings = &dsettings;
}
if (callbacks != NULL)
intl_callbacks = *callbacks;
/*
* We need to do some of our own processing when many of these events occur,
* so we note the original callback functions and forward appropriately.
*/
/*
* コールバックのラッピング
* 内部処理を行った後にアプリケーションのコールバックを呼び出す
*/
conn->recv_data_cb = intl_callbacks.recv_data;
conn->stream_close_cb = intl_callbacks.stream_close;
conn->stop_sending_cb = intl_callbacks.stop_sending;
conn->reset_stream_cb = intl_callbacks.reset_stream;
conn->deferred_consume_cb = intl_callbacks.deferred_consume;
intl_callbacks.recv_data = h3_conn_recv_data;
intl_callbacks.stream_close = h3_conn_stream_close;
intl_callbacks.stop_sending = h3_conn_stop_sending;
intl_callbacks.reset_stream = h3_conn_reset_stream;
intl_callbacks.deferred_consume = h3_conn_deferred_consume;
/* Create the HTTP/3 client state. */
/* nghttp3 クライアント状態を作成 */
ec = nghttp3_conn_client_new(&conn->h3conn, &intl_callbacks, settings,
NULL, conn);
if (ec < 0) {
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"cannot create nghttp3 connection: %s (%d)",
nghttp3_strerror(ec), ec);
goto err;
}
/*
* Tell the HTTP/3 stack which stream IDs are used for our outgoing control
* and QPACK streams. Note that we don't have to tell the HTTP/3 stack what
* IDs are used for incoming streams as this is inferred automatically from
* the stream type byte which starts every incoming unidirectional stream,
* so it will autodetect the correct stream IDs for the incoming control and
* QPACK streams initiated by the server.
*/
/*
* 送信用ストリーム ID を nghttp3 にバインド
* 受信側のストリーム ID はストリームタイプバイトから自動検出される
*/
ec = nghttp3_conn_bind_control_stream(conn->h3conn, s_ctl_send->id);
if (ec < 0) {
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"cannot bind nghttp3 control stream: %s (%d)",
nghttp3_strerror(ec), ec);
goto err;
}
ec = nghttp3_conn_bind_qpack_streams(conn->h3conn,
s_qpenc_send->id,
s_qpdec_send->id);
if (ec < 0) {
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"cannot bind nghttp3 QPACK streams: %s (%d)",
nghttp3_strerror(ec), ec);
goto err;
}
return conn;
err:
nghttp3_conn_del(conn->h3conn);
h3_stream_free(s_ctl_send);
h3_stream_free(s_qpenc_send);
h3_stream_free(s_qpdec_send);
lh_OSSL_DEMO_H3_STREAM_free(conn->streams);
OPENSSL_free(conn);
return NULL;
}
/*
* ============================================================================
* create_socket_bio - UDP ソケット BIO を作成
* ============================================================================
*
* 【機能】
* 指定されたアドレスに接続された UDP ソケットを作成し、
* BIO でラップして返す。QUIC は UDP 上で動作する。
*/
/*
* Create BIO with UDP connected socket for one of the address we got from
* resolver.
*/
static BIO *
create_socket_bio(const BIO_ADDRINFO *bai)
{
int sock;
BIO *bio;
/* UDP ソケットを作成(QUIC 用) */
sock = BIO_socket(BIO_ADDRINFO_family(bai), SOCK_DGRAM, 0, 0);
if (sock == -1)
return NULL;
/* サーバーに接続(UDP でも connect() でピアアドレスを設定可能) */
if (!BIO_connect(sock, BIO_ADDRINFO_address(bai), 0))
goto err;
/* 非ブロッキングモードに設定 */
if (!BIO_socket_nbio(sock, 1))
goto err;
/* データグラム BIO を作成(UDP 用) */
if ((bio = BIO_new(BIO_s_datagram())) == NULL)
goto err;
BIO_set_fd(bio, sock, BIO_CLOSE);
return bio;
err:
BIO_closesocket(sock);
return NULL;
}
/*
* ============================================================================
* OSSL_DEMO_H3_CONN_new_for_addr - アドレスから HTTP/3 接続を作成
* ============================================================================
*
* 【機能】
* 指定されたアドレスに接続する HTTP/3 クライアント接続を作成する。
* QUIC 接続の確立から HTTP/3 の初期化までを一括で行う。
*
* 【処理内容】
* 1. UDP ソケット BIO を作成
* 2. QUIC SSL オブジェクトを作成
* 3. ホスト名検証と SNI を設定
* 4. HTTP/3 接続を初期化
*/
OSSL_DEMO_H3_CONN *OSSL_DEMO_H3_CONN_new_for_addr(SSL_CTX *ctx, const BIO_ADDRINFO *bai,
const char *bare_hostname,
const nghttp3_callbacks *callbacks,
const nghttp3_settings *settings,
void *user_data)
{
BIO *qconn_bio = NULL;
SSL *qconn = NULL;
OSSL_DEMO_H3_CONN *conn = NULL;
/* UDP ソケット BIO を作成 */
if ((qconn_bio = create_socket_bio(bai)) == NULL)
return NULL;
/* QUIC クライアント SSL オブジェクトを作成 */
qconn = SSL_new(ctx);
if (qconn == NULL)
goto err;
/* BIO を SSL に関連付け */
SSL_set_bio(qconn, qconn_bio, qconn_bio);
qconn_bio = NULL;
/* Set the hostname we will validate the X.509 certificate against. */
/* 証明書検証用のホスト名を設定 */
if (SSL_set1_host(qconn, bare_hostname) <= 0)
goto err;
/* Configure SNI */
/* SNI (Server Name Indication) を設定 */
if (!SSL_set_tlsext_host_name(qconn, bare_hostname))
goto err;
/* HTTP/3 接続を初期化 */
conn = OSSL_DEMO_H3_CONN_new_for_conn(qconn, callbacks,
settings, user_data);
if (conn == NULL)
goto err;
return conn;
err:
SSL_free(qconn);
BIO_free_all(qconn_bio);
return NULL;
}
/* 接続に関連付けられたユーザーデータを取得 */
void *OSSL_DEMO_H3_CONN_get_user_data(const OSSL_DEMO_H3_CONN *conn)
{
return conn->user_data;
}
/* 基盤となる QUIC 接続 SSL オブジェクトを取得 */
SSL *OSSL_DEMO_H3_CONN_get0_connection(const OSSL_DEMO_H3_CONN *conn)
{
return conn->qconn;
}
/*
* ============================================================================
* h3_conn_pump_stream - 単一ストリームのデータを HTTP/3 スタックに転送
* ============================================================================
*
* 【機能】
* QUIC ストリームから受信したデータを nghttp3 に渡す。
* SSL_read_ex → nghttp3_conn_read_stream の流れでデータを転送。
*/
/* Pumps received data to the HTTP/3 stack for a single stream. */
static void h3_conn_pump_stream(OSSL_DEMO_H3_STREAM *s, void *conn_)
{
int ec;
OSSL_DEMO_H3_CONN *conn = conn_;
size_t num_bytes, consumed;
uint64_t aec;
if (!conn->pump_res)
/*
* Handling of a previous stream in the iteration over all streams
* failed, so just do nothing.
*/
return;
for (;;) {
if (s->s == NULL /* If we already did STOP_SENDING, ignore this stream. */
/* If this is a write-only stream, there is no read data to check. */
|| SSL_get_stream_read_state(s->s) == SSL_STREAM_STATE_WRONG_DIR
/*
* If we already got a FIN for this stream, there is nothing more to
* do for it.
*/
|| s->done_recv_fin)
break;
/*
* Pump data from OpenSSL QUIC to the HTTP/3 stack by calling SSL_peek
* to get received data and passing it to nghttp3 using
* nghttp3_conn_read_stream. Note that this function is confusingly
* named and inputs data to the HTTP/3 stack.
*/
if (s->buf_cur == s->buf_total) {
/* Need more data. */
ec = SSL_read_ex(s->s, s->buf, sizeof(s->buf), &num_bytes);
if (ec <= 0) {
num_bytes = 0;
if (SSL_get_error(s->s, ec) == SSL_ERROR_ZERO_RETURN) {
/* Stream concluded normally. Pass FIN to HTTP/3 stack. */
ec = nghttp3_conn_read_stream(conn->h3conn, s->id, NULL, 0,
/*fin=*/1);
if (ec < 0) {
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"cannot pass FIN to nghttp3: %s (%d)",
nghttp3_strerror(ec), ec);
goto err;
}
s->done_recv_fin = 1;
} else if (SSL_get_stream_read_state(s->s)
== SSL_STREAM_STATE_RESET_REMOTE) {
/* Stream was reset by peer. */
if (!SSL_get_stream_read_error_code(s->s, &aec))
goto err;
ec = nghttp3_conn_close_stream(conn->h3conn, s->id, aec);
if (ec < 0) {
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"cannot mark stream as reset: %s (%d)",
nghttp3_strerror(ec), ec);
goto err;
}
s->done_recv_fin = 1;
} else {
/* Other error. */
goto err;
}
}
s->buf_cur = 0;
s->buf_total = num_bytes;
}
if (s->buf_cur == s->buf_total)
break;
/*
* This function is confusingly named as it is is named from nghttp3's
* 'perspective'; it is used to pass data *into* the HTTP/3 stack which
* has been received from the network.
*/
assert(conn->consumed_app_data == 0);
ec = nghttp3_conn_read_stream(conn->h3conn, s->id, s->buf + s->buf_cur,
s->buf_total - s->buf_cur, /*fin=*/0);
if (ec < 0) {
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"nghttp3 failed to process incoming data: %s (%d)",
nghttp3_strerror(ec), ec);
goto err;
}
/*
* read_stream reports the data it consumes from us in two different
* ways; the non-application data is returned as a number of bytes 'ec'
* above, but the number of bytes of application data has to be recorded
* by our callback. We sum the two to determine the total number of
* bytes which nghttp3 consumed.
*/
consumed = ec + conn->consumed_app_data;
assert(consumed <= s->buf_total - s->buf_cur);
s->buf_cur += consumed;
conn->consumed_app_data = 0;
}
return;
err:
conn->pump_res = 0;
}
/*
* ============================================================================
* OSSL_DEMO_H3_CONN_handle_events - HTTP/3 イベント処理のメインループ
* ============================================================================
*
* 【機能】
* HTTP/3 接続のイベントを処理する。以下の3つの処理を行う:
*
* 1. 新しい受信ストリームの処理 (SSL_accept_stream)
* - サーバーが開始した単方向ストリームを受け入れる
*
* 2. 送信データの転送 (nghttp3 → QUIC)
* - nghttp3_conn_writev_stream でデータを取得
* - SSL_write_ex2 でネットワークに送信
*
* 3. 受信データの転送 (QUIC → nghttp3)
* - SSL_read_ex でデータを受信
* - nghttp3_conn_read_stream で nghttp3 に渡す
*
* 【戻り値】
* - 1: 成功
* - 0: エラー
*/
int OSSL_DEMO_H3_CONN_handle_events(OSSL_DEMO_H3_CONN *conn)
{
int ec, fin;
size_t i, num_vecs, written, total_written, total_len;
int64_t stream_id;
uint64_t flags;
nghttp3_vec vecs[8] = {0};
OSSL_DEMO_H3_STREAM key, *s;
SSL *snew;
if (conn == NULL)
return 0;
/*
* We handle events by doing three things:
*
* 1. Handle new incoming streams
* 2. Pump outgoing data from the HTTP/3 stack to the QUIC engine
* 3. Pump incoming data from the QUIC engine to the HTTP/3 stack
*/
/* 1. Check for new incoming streams */
/*
* ステップ 1: 新しい受信ストリームをチェック
* サーバーが開始した制御/QPACK ストリームを受け入れる
*/
for (;;) {
if ((snew = SSL_accept_stream(conn->qconn, SSL_ACCEPT_STREAM_NO_BLOCK)) == NULL)
break;
/*
* Each new incoming stream gets wrapped into an OSSL_DEMO_H3_STREAM object and
* added into our stream ID map.
*/
/* 新しいストリームをラップしてマップに追加 */
if (h3_conn_accept_stream(conn, snew) == NULL) {
SSL_free(snew);
return 0;
}
}
/* 2. Pump outgoing data from HTTP/3 engine to QUIC. */
/*
* ステップ 2: nghttp3 から QUIC へ送信データを転送
* nghttp3_conn_writev_stream → SSL_write_ex2
*/
for (;;) {
/*
* Get a number of send vectors from the HTTP/3 engine.
*
* Note that this function is confusingly named as it is named from
* nghttp3's 'perspective': this outputs pointers to data which nghttp3
* wants to *write* to the network.
*/
/*
* nghttp3 から送信データを取得
* 注意: 関数名は nghttp3 の視点から命名されており、
* nghttp3 が「書き込みたい」データを出力する
*/
ec = nghttp3_conn_writev_stream(conn->h3conn, &stream_id, &fin,
vecs, ARRAY_LEN(vecs));
if (ec < 0)
return 0;
if (ec == 0)
break;
/*
* we let SSL_write_ex2(3) to conclude the stream for us (send FIN)
* after all data are written.
*/
flags = (fin == 0) ? 0 : SSL_WRITE_FLAG_CONCLUDE;
/* For each of the vectors returned, pass it to OpenSSL QUIC. */
key.id = stream_id;
if ((s = lh_OSSL_DEMO_H3_STREAM_retrieve(conn->streams, &key)) == NULL) {
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"no stream for ID %zd", stream_id);
return 0;
}
num_vecs = ec;
total_len = nghttp3_vec_len(vecs, num_vecs);
total_written = 0;
for (i = 0; i < num_vecs; ++i) {
if (vecs[i].len == 0)
continue;
if (s->s == NULL) {
/* Already did STOP_SENDING and threw away stream, ignore */
written = vecs[i].len;
} else if (!SSL_write_ex2(s->s, vecs[i].base, vecs[i].len, flags, &written)) {
if (SSL_get_error(s->s, 0) == SSL_ERROR_WANT_WRITE) {
/*
* We have filled our send buffer so tell nghttp3 to stop
* generating more data; we have to do this explicitly.
*/
written = 0;
nghttp3_conn_block_stream(conn->h3conn, stream_id);
} else {
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"writing HTTP/3 data to network failed");
return 0;
}
} else {
/*
* Tell nghttp3 it can resume generating more data in case we
* previously called block_stream.
*/
nghttp3_conn_unblock_stream(conn->h3conn, stream_id);
}
total_written += written;
if (written > 0) {
/*
* Tell nghttp3 we have consumed the data it output when we
* called writev_stream, otherwise subsequent calls to
* writev_stream will output the same data.
*/
ec = nghttp3_conn_add_write_offset(conn->h3conn, stream_id, written);
if (ec < 0)
return 0;
/*
* Tell nghttp3 it can free the buffered data because we will
* not need it again. In our case we can always do this right
* away because we copy the data into our QUIC send buffers
* rather than simply storing a reference to it.
*/
ec = nghttp3_conn_add_ack_offset(conn->h3conn, stream_id, written);
if (ec < 0)
return 0;
}
}
if (fin && total_written == total_len) {
if (total_len == 0) {
/*
* As a special case, if nghttp3 requested to write a
* zero-length stream with a FIN, we have to tell it we did this
* by calling add_write_offset(0).
*/
ec = nghttp3_conn_add_write_offset(conn->h3conn, stream_id, 0);
if (ec < 0)
return 0;
}
}
}
/* 3. Pump incoming data from QUIC to HTTP/3 engine. */
/*
* ステップ 3: QUIC から nghttp3 へ受信データを転送
* 全ストリームに対して h3_conn_pump_stream を実行
*/
conn->pump_res = 1; /* cleared in below call if an error occurs */
lh_OSSL_DEMO_H3_STREAM_doall_arg(conn->streams, h3_conn_pump_stream, conn);
if (!conn->pump_res)
return 0;
return 1;
}
/*
* ============================================================================
* OSSL_DEMO_H3_CONN_submit_request - HTTP/3 リクエストを送信
* ============================================================================
*
* 【機能】
* HTTP/3 リクエストを送信する。新しい双方向ストリームを作成し、
* リクエストヘッダーとオプションでボディを送信する。
*
* 【引数】
* - conn: HTTP/3 接続
* - nva: ヘッダーの名前-値ペア配列(:method, :path, :scheme など)
* - nvlen: ヘッダー数
* - dr: リクエストボディのデータリーダー(NULL 可)
* - user_data: リクエストに関連付けるユーザーデータ
*
* 【戻り値】
* - 1: 成功
* - 0: エラー
*/
int OSSL_DEMO_H3_CONN_submit_request(OSSL_DEMO_H3_CONN *conn,
const nghttp3_nv *nva, size_t nvlen,
const nghttp3_data_reader *dr,
void *user_data)
{
int ec;
OSSL_DEMO_H3_STREAM *s_req = NULL;
if (conn == NULL) {
ERR_raise_data(ERR_LIB_USER, ERR_R_PASSED_NULL_PARAMETER,
"connection must be specified");
return 0;
}
/* Each HTTP/3 request is represented by a stream. */
/* HTTP/3 では各リクエストが1つの双方向ストリームで表現される */
if ((s_req = h3_conn_create_stream(conn, OSSL_DEMO_H3_STREAM_TYPE_REQ)) == NULL)
goto err;
s_req->user_data = user_data;
/* nghttp3 にリクエストを送信 */
ec = nghttp3_conn_submit_request(conn->h3conn, s_req->id, nva, nvlen,
dr, s_req);
if (ec < 0) {
ERR_raise_data(ERR_LIB_USER, ERR_R_INTERNAL_ERROR,
"cannot submit HTTP/3 request: %s (%d)",
nghttp3_strerror(ec), ec);
goto err;
}
return 1;
err:
h3_conn_remove_stream(conn, s_req);
return 0;
}
/* POSIX機能を有効にする(clock_gettime, localtime_r用) */
#ifndef _WIN32
# define _POSIX_C_SOURCE 199309L
#endif
/*
* Copyright 2023-2025 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* ファイルパス: demos/guide/quic-client-block-with-debug.c (OpenSSLリポジトリ内)
*
* 注意: このファイルの日本語コメントは Claude Code によって追加されました。
* オリジナルのソースコードは https://github.com/openssl/openssl にあります。
*/
/*
* ============================================================================
* quic-client-block-with-debug.c - デバッグ出力付きブロッキングモードQUICクライアント
* ============================================================================
*
* ※ 日本語コメントは Claude Code によって追加されました。
*
* quic-client-block.c に最大限のデバッグ出力を追加したバージョン。
* 全てのAPI呼び出し、データ内容、エラー詳細を出力する。
*/
#include <string.h>
#include <time.h>
#include <stdarg.h>
/* Include the appropriate header file for SOCK_DGRAM */
#ifdef _WIN32 /* Windows */
# include <winsock2.h>
# include <ws2tcpip.h>
#else /* Linux/Unix */
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
#endif
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
/* ============================================================================
* デバッグユーティリティ
* ============================================================================ */
/*
* DEBUG_LOG() - タイムスタンプ付きデバッグログ出力マクロ
*
* フォーマット: [HH:MM:SS.mmm] [関数名] メッセージ
*/
#define DEBUG_LOG(fmt, ...) do { \
struct timespec ts; \
struct tm tm_info; \
clock_gettime(CLOCK_REALTIME, &ts); \
localtime_r(&ts.tv_sec, &tm_info); \
fprintf(stderr, "[%02d:%02d:%02d.%03ld] [%s] " fmt "\n", \
tm_info.tm_hour, tm_info.tm_min, tm_info.tm_sec, \
ts.tv_nsec / 1000000, __func__, ##__VA_ARGS__); \
fflush(stderr); \
} while(0)
/*
* DEBUG_LOG_ENTER() - 関数開始ログ(関数名を表示)
*/
#define DEBUG_LOG_ENTER() DEBUG_LOG(">>> %s() 開始", __func__)
/*
* DEBUG_LOG_EXIT() - 関数終了ログ(関数名を表示)
*/
#define DEBUG_LOG_EXIT() DEBUG_LOG("<<< %s() 終了", __func__)
/*
* DEBUG_LOG_EXIT_WITH_RESULT() - 結果付き関数終了ログ(関数名を表示)
*/
#define DEBUG_LOG_EXIT_WITH_RESULT(result) DEBUG_LOG("<<< %s() 終了 (result=%d)", __func__, (int)(result))
/*
* debug_hexdump() - データの16進ダンプを出力
*
* @param prefix 出力行の先頭に付けるプレフィックス
* @param data ダンプするデータ
* @param len データの長さ
*/
static void debug_hexdump(const char *prefix, const unsigned char *data, size_t len)
{
size_t i, j;
fprintf(stderr, "%s (%zu bytes):\n", prefix, len);
for (i = 0; i < len; i += 16) {
/* オフセット */
fprintf(stderr, " %04zx: ", i);
/* 16進表示 */
for (j = 0; j < 16; j++) {
if (i + j < len)
fprintf(stderr, "%02x ", data[i + j]);
else
fprintf(stderr, " ");
if (j == 7)
fprintf(stderr, " ");
}
/* ASCII表示 */
fprintf(stderr, " |");
for (j = 0; j < 16 && i + j < len; j++) {
unsigned char c = data[i + j];
fprintf(stderr, "%c", (c >= 32 && c < 127) ? c : '.');
}
fprintf(stderr, "|\n");
}
fflush(stderr);
}
/*
* debug_print_ssl_error() - OpenSSL エラースタックの詳細を出力
*
* @param context エラーが発生したコンテキストの説明
*/
static void debug_print_ssl_error(const char *context)
{
unsigned long err;
char buf[256];
DEBUG_LOG("=== SSL エラー詳細 [%s] ===", context);
while ((err = ERR_get_error()) != 0) {
ERR_error_string_n(err, buf, sizeof(buf));
fprintf(stderr, " エラーコード: 0x%lx\n", err);
fprintf(stderr, " ライブラリ: %s\n", ERR_lib_error_string(err));
fprintf(stderr, " 理由: %s\n", ERR_reason_error_string(err));
fprintf(stderr, " 詳細: %s\n", buf);
fprintf(stderr, " ---\n");
}
fflush(stderr);
}
/*
* debug_print_ssl_state() - SSL接続の状態を出力
*
* @param ssl SSLオブジェクト
*/
static void debug_print_ssl_state(const SSL *ssl)
{
if (ssl == NULL) {
DEBUG_LOG("SSL オブジェクト: NULL");
return;
}
DEBUG_LOG("=== SSL 状態 ===");
fprintf(stderr, " 状態文字列: %s\n", SSL_state_string_long(ssl));
fprintf(stderr, " バージョン: %s\n", SSL_get_version(ssl));
/* ALPN */
const unsigned char *alpn_data;
unsigned int alpn_len;
SSL_get0_alpn_selected(ssl, &alpn_data, &alpn_len);
if (alpn_data != NULL && alpn_len > 0) {
fprintf(stderr, " 選択されたALPN: %.*s\n", alpn_len, alpn_data);
} else {
fprintf(stderr, " 選択されたALPN: (なし)\n");
}
/* 暗号スイート */
const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl);
if (cipher != NULL) {
fprintf(stderr, " 暗号スイート: %s\n", SSL_CIPHER_get_name(cipher));
fprintf(stderr, " 暗号強度: %d bits\n", SSL_CIPHER_get_bits(cipher, NULL));
}
/* 証明書検証結果 */
long verify_result = SSL_get_verify_result(ssl);
fprintf(stderr, " 証明書検証結果: %ld (%s)\n", verify_result,
X509_verify_cert_error_string(verify_result));
fflush(stderr);
}
/*
* debug_print_peer_certificate() - ピア証明書の情報を出力
*
* @param ssl SSLオブジェクト
*/
static void debug_print_peer_certificate(const SSL *ssl)
{
X509 *cert;
char *subject, *issuer;
DEBUG_LOG("=== ピア証明書情報 ===");
cert = SSL_get_peer_certificate(ssl);
if (cert == NULL) {
fprintf(stderr, " ピア証明書: (なし)\n");
fflush(stderr);
return;
}
subject = X509_NAME_oneline(X509_get_subject_name(cert), NULL, 0);
issuer = X509_NAME_oneline(X509_get_issuer_name(cert), NULL, 0);
fprintf(stderr, " Subject: %s\n", subject ? subject : "(取得失敗)");
fprintf(stderr, " Issuer: %s\n", issuer ? issuer : "(取得失敗)");
OPENSSL_free(subject);
OPENSSL_free(issuer);
X509_free(cert);
fflush(stderr);
}
/*
* debug_print_bio_addr() - BIO_ADDRの内容を出力
*
* @param prefix 出力の先頭に付けるプレフィックス
* @param addr BIO_ADDRオブジェクト
*/
static void debug_print_bio_addr(const char *prefix, const BIO_ADDR *addr)
{
char *host = NULL;
char *port = NULL;
if (addr == NULL) {
DEBUG_LOG("%s: (NULL)", prefix);
return;
}
/* BIO_ADDR_hostname_string/service_string は動的に確保した文字列を返す */
host = BIO_ADDR_hostname_string(addr, 1); /* 1 = numeric */
port = BIO_ADDR_service_string(addr, 1); /* 1 = numeric */
if (host != NULL && port != NULL) {
DEBUG_LOG("%s: %s:%s", prefix, host, port);
} else {
DEBUG_LOG("%s: (アドレス取得失敗)", prefix);
}
OPENSSL_free(host);
OPENSSL_free(port);
}
/*
* ============================================================================
* create_socket_bio() - サーバーに接続するBIOを作成(デバッグ出力付き)
* ============================================================================
*/
static BIO *create_socket_bio(const char *hostname, const char *port,
int family, BIO_ADDR **peer_addr)
{
int sock = -1;
BIO_ADDRINFO *res;
const BIO_ADDRINFO *ai = NULL;
BIO *bio;
int attempt = 0;
DEBUG_LOG_ENTER();
DEBUG_LOG("ホスト名: %s", hostname);
DEBUG_LOG("ポート: %s", port);
DEBUG_LOG("アドレスファミリ: %s", family == AF_INET6 ? "AF_INET6" : "AF_INET");
/* DNSルックアップ */
DEBUG_LOG("BIO_lookup_ex() を呼び出し中...");
if (!BIO_lookup_ex(hostname, port, BIO_LOOKUP_CLIENT, family, SOCK_DGRAM, 0,
&res)) {
DEBUG_LOG("BIO_lookup_ex() 失敗!");
debug_print_ssl_error("BIO_lookup_ex");
DEBUG_LOG_EXIT_WITH_RESULT(0);
return NULL;
}
DEBUG_LOG("BIO_lookup_ex() 成功");
/* 各アドレスに対してループ */
for (ai = res; ai != NULL; ai = BIO_ADDRINFO_next(ai)) {
attempt++;
DEBUG_LOG("接続試行 #%d", attempt);
/* UDPソケット作成 */
DEBUG_LOG("BIO_socket() を呼び出し中...");
sock = BIO_socket(BIO_ADDRINFO_family(ai), SOCK_DGRAM, 0, 0);
DEBUG_LOG("BIO_socket() 戻り値: %d", sock);
if (sock == -1) {
DEBUG_LOG("ソケット作成失敗、次のアドレスを試行");
continue;
}
/* ソケット接続 */
DEBUG_LOG("BIO_connect() を呼び出し中...");
if (!BIO_connect(sock, BIO_ADDRINFO_address(ai), 0)) {
DEBUG_LOG("BIO_connect() 失敗");
debug_print_ssl_error("BIO_connect");
BIO_closesocket(sock);
sock = -1;
continue;
}
DEBUG_LOG("BIO_connect() 成功");
/* 非ブロッキングモード設定 */
DEBUG_LOG("BIO_socket_nbio() を呼び出し中...");
if (!BIO_socket_nbio(sock, 1)) {
DEBUG_LOG("BIO_socket_nbio() 失敗");
debug_print_ssl_error("BIO_socket_nbio");
BIO_closesocket(sock);
sock = -1;
continue;
}
DEBUG_LOG("BIO_socket_nbio() 成功");
break; /* 成功 */
}
/* ピアアドレスを複製 */
if (sock != -1) {
DEBUG_LOG("BIO_ADDR_dup() を呼び出し中...");
*peer_addr = BIO_ADDR_dup(BIO_ADDRINFO_address(ai));
if (*peer_addr == NULL) {
DEBUG_LOG("BIO_ADDR_dup() 失敗!");
BIO_closesocket(sock);
BIO_ADDRINFO_free(res);
DEBUG_LOG_EXIT_WITH_RESULT(0);
return NULL;
}
debug_print_bio_addr("ピアアドレス", *peer_addr);
}
/* DNSルックアップ結果を解放 */
DEBUG_LOG("BIO_ADDRINFO_free() を呼び出し中...");
BIO_ADDRINFO_free(res);
if (sock == -1) {
DEBUG_LOG("全てのアドレスで接続失敗");
DEBUG_LOG_EXIT_WITH_RESULT(0);
return NULL;
}
/* データグラムBIOを作成 */
DEBUG_LOG("BIO_new(BIO_s_datagram()) を呼び出し中...");
bio = BIO_new(BIO_s_datagram());
if (bio == NULL) {
DEBUG_LOG("BIO_new() 失敗!");
debug_print_ssl_error("BIO_new");
BIO_closesocket(sock);
DEBUG_LOG_EXIT_WITH_RESULT(0);
return NULL;
}
DEBUG_LOG("BIO_new() 成功: bio=%p", (void *)bio);
/* BIOにソケットを関連付け */
DEBUG_LOG("BIO_set_fd(bio, %d, BIO_CLOSE) を呼び出し中...", sock);
BIO_set_fd(bio, sock, BIO_CLOSE);
DEBUG_LOG("BIO_set_fd() 完了");
DEBUG_LOG_EXIT();
return bio;
}
/*
* ============================================================================
* main() - QUICクライアントのエントリポイント(デバッグ出力付き)
* ============================================================================
*/
int main(int argc, char *argv[])
{
SSL_CTX *ctx = NULL;
SSL *ssl = NULL;
BIO *bio = NULL;
int res = EXIT_FAILURE;
int ret;
int i;
unsigned char alpn[] = { 8, 'h', 't', 't', 'p', '/', '1', '.', '0' };
const char *request_start = "GET / HTTP/1.0\r\nConnection: close\r\nHost: ";
const char *request_end = "\r\n\r\n";
size_t written, readbytes;
char buf[160];
BIO_ADDR *peer_addr = NULL;
char *hostname, *port;
int argnext = 1;
int ipv6 = 0;
unsigned long read_count = 0;
DEBUG_LOG("========================================");
DEBUG_LOG("QUIC クライアント (デバッグモード) 起動");
DEBUG_LOG("========================================");
DEBUG_LOG("OpenSSL バージョン: %s", OpenSSL_version(OPENSSL_VERSION));
DEBUG_LOG("引数の数: %d", argc);
for (i = 0; i < argc; i++) {
DEBUG_LOG(" argv[%d] = \"%s\"", i, argv[i]);
}
/* コマンドライン引数解析 */
if (argc < 3) {
printf("Usage: quic-client-block [-6] hostname port\n");
goto end;
}
if (!strcmp(argv[argnext], "-6")) {
if (argc < 4) {
printf("Usage: quic-client-block [-6] hostname port\n");
goto end;
}
ipv6 = 1;
argnext++;
DEBUG_LOG("IPv6モード有効");
}
hostname = argv[argnext++];
port = argv[argnext];
DEBUG_LOG("----------------------------------------");
DEBUG_LOG("接続先:");
DEBUG_LOG(" ホスト名: %s", hostname);
DEBUG_LOG(" ポート: %s", port);
DEBUG_LOG(" IPバージョン: %s", ipv6 ? "IPv6" : "IPv4");
DEBUG_LOG("----------------------------------------");
/*
* SSL_CTX作成
*/
DEBUG_LOG("SSL_CTX_new(OSSL_QUIC_client_method()) を呼び出し中...");
ctx = SSL_CTX_new(OSSL_QUIC_client_method());
if (ctx == NULL) {
DEBUG_LOG("SSL_CTX_new() 失敗!");
debug_print_ssl_error("SSL_CTX_new");
printf("Failed to create the SSL_CTX\n");
goto end;
}
DEBUG_LOG("SSL_CTX_new() 成功: ctx=%p", (void *)ctx);
/* サーバー証明書検証設定 */
DEBUG_LOG("SSL_CTX_set_verify(SSL_VERIFY_PEER) を呼び出し中...");
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
DEBUG_LOG("証明書検証モード: SSL_VERIFY_PEER");
/* デフォルト検証パス設定 */
DEBUG_LOG("SSL_CTX_set_default_verify_paths() を呼び出し中...");
if (!SSL_CTX_set_default_verify_paths(ctx)) {
DEBUG_LOG("SSL_CTX_set_default_verify_paths() 失敗!");
debug_print_ssl_error("SSL_CTX_set_default_verify_paths");
printf("Failed to set the default trusted certificate store\n");
goto end;
}
DEBUG_LOG("SSL_CTX_set_default_verify_paths() 成功");
/*
* SSLオブジェクト作成
*/
DEBUG_LOG("SSL_new(ctx) を呼び出し中...");
ssl = SSL_new(ctx);
if (ssl == NULL) {
DEBUG_LOG("SSL_new() 失敗!");
debug_print_ssl_error("SSL_new");
printf("Failed to create the SSL object\n");
goto end;
}
DEBUG_LOG("SSL_new() 成功: ssl=%p", (void *)ssl);
/*
* ソケットBIO作成
*/
DEBUG_LOG("create_socket_bio() を呼び出し中...");
bio = create_socket_bio(hostname, port, ipv6 ? AF_INET6 : AF_INET, &peer_addr);
if (bio == NULL) {
DEBUG_LOG("create_socket_bio() 失敗!");
printf("Failed to crete the BIO\n");
goto end;
}
DEBUG_LOG("create_socket_bio() 成功: bio=%p", (void *)bio);
/* BIOをSSLに関連付け */
DEBUG_LOG("SSL_set_bio(ssl, bio, bio) を呼び出し中...");
SSL_set_bio(ssl, bio, bio);
DEBUG_LOG("SSL_set_bio() 完了");
/*
* SNI設定
*/
DEBUG_LOG("SSL_set_tlsext_host_name(ssl, \"%s\") を呼び出し中...", hostname);
if (!SSL_set_tlsext_host_name(ssl, hostname)) {
DEBUG_LOG("SSL_set_tlsext_host_name() 失敗!");
debug_print_ssl_error("SSL_set_tlsext_host_name");
printf("Failed to set the SNI hostname\n");
goto end;
}
DEBUG_LOG("SSL_set_tlsext_host_name() 成功");
/*
* 証明書検証用ホスト名設定
*/
DEBUG_LOG("SSL_set1_host(ssl, \"%s\") を呼び出し中...", hostname);
if (!SSL_set1_host(ssl, hostname)) {
DEBUG_LOG("SSL_set1_host() 失敗!");
debug_print_ssl_error("SSL_set1_host");
printf("Failed to set the certificate verification hostname");
goto end;
}
DEBUG_LOG("SSL_set1_host() 成功");
/*
* ALPN設定
*/
DEBUG_LOG("SSL_set_alpn_protos() を呼び出し中...");
debug_hexdump("ALPNプロトコル", alpn, sizeof(alpn));
ret = SSL_set_alpn_protos(ssl, alpn, sizeof(alpn));
DEBUG_LOG("SSL_set_alpn_protos() 戻り値: %d (0=成功)", ret);
if (ret != 0) {
DEBUG_LOG("SSL_set_alpn_protos() 失敗!");
debug_print_ssl_error("SSL_set_alpn_protos");
printf("Failed to set the ALPN for the connection\n");
goto end;
}
/*
* ピアアドレス設定
*/
DEBUG_LOG("SSL_set1_initial_peer_addr() を呼び出し中...");
debug_print_bio_addr("初期ピアアドレス", peer_addr);
if (!SSL_set1_initial_peer_addr(ssl, peer_addr)) {
DEBUG_LOG("SSL_set1_initial_peer_addr() 失敗!");
debug_print_ssl_error("SSL_set1_initial_peer_addr");
printf("Failed to set the initial peer address\n");
goto end;
}
DEBUG_LOG("SSL_set1_initial_peer_addr() 成功");
/*
* QUICハンドシェイク
*/
DEBUG_LOG("========================================");
DEBUG_LOG("QUICハンドシェイク開始");
DEBUG_LOG("========================================");
DEBUG_LOG("SSL_connect(ssl) を呼び出し中...");
ret = SSL_connect(ssl);
DEBUG_LOG("SSL_connect() 戻り値: %d", ret);
if (ret < 1) {
DEBUG_LOG("SSL_connect() 失敗!");
debug_print_ssl_error("SSL_connect");
printf("Failed to connect to the server\n");
if (SSL_get_verify_result(ssl) != X509_V_OK) {
DEBUG_LOG("証明書検証エラー: %s",
X509_verify_cert_error_string(SSL_get_verify_result(ssl)));
printf("Verify error: %s\n",
X509_verify_cert_error_string(SSL_get_verify_result(ssl)));
}
goto end;
}
DEBUG_LOG("SSL_connect() 成功!");
DEBUG_LOG("========================================");
DEBUG_LOG("QUICハンドシェイク完了");
DEBUG_LOG("========================================");
/* 接続情報を出力 */
debug_print_ssl_state(ssl);
debug_print_peer_certificate(ssl);
/*
* HTTPリクエスト送信
*/
DEBUG_LOG("========================================");
DEBUG_LOG("HTTPリクエスト送信開始");
DEBUG_LOG("========================================");
/* リクエスト開始部分 */
DEBUG_LOG("[Write #1] SSL_write_ex() を呼び出し中...");
debug_hexdump("送信データ", (const unsigned char *)request_start, strlen(request_start));
if (!SSL_write_ex(ssl, request_start, strlen(request_start), &written)) {
DEBUG_LOG("SSL_write_ex() 失敗!");
debug_print_ssl_error("SSL_write_ex");
printf("Failed to write start of HTTP request\n");
goto end;
}
DEBUG_LOG("[Write #1] SSL_write_ex() 成功: %zu バイト書き込み", written);
/* ホスト名 */
DEBUG_LOG("[Write #2] SSL_write_ex() を呼び出し中...");
debug_hexdump("送信データ", (const unsigned char *)hostname, strlen(hostname));
if (!SSL_write_ex(ssl, hostname, strlen(hostname), &written)) {
DEBUG_LOG("SSL_write_ex() 失敗!");
debug_print_ssl_error("SSL_write_ex");
printf("Failed to write hostname in HTTP request\n");
goto end;
}
DEBUG_LOG("[Write #2] SSL_write_ex() 成功: %zu バイト書き込み", written);
/* リクエスト終了部分(SSL_WRITE_FLAG_CONCLUDE付き) */
DEBUG_LOG("[Write #3] SSL_write_ex2(SSL_WRITE_FLAG_CONCLUDE) を呼び出し中...");
debug_hexdump("送信データ", (const unsigned char *)request_end, strlen(request_end));
if (!SSL_write_ex2(ssl, request_end, strlen(request_end),
SSL_WRITE_FLAG_CONCLUDE, &written)) {
DEBUG_LOG("SSL_write_ex2() 失敗!");
debug_print_ssl_error("SSL_write_ex2");
printf("Failed to write end of HTTP request\n");
goto end;
}
DEBUG_LOG("[Write #3] SSL_write_ex2() 成功: %zu バイト書き込み (ストリームFIN送信)", written);
DEBUG_LOG("========================================");
DEBUG_LOG("HTTPリクエスト送信完了");
DEBUG_LOG("========================================");
/*
* HTTPレスポンス受信
*/
DEBUG_LOG("========================================");
DEBUG_LOG("HTTPレスポンス受信開始");
DEBUG_LOG("========================================");
while (SSL_read_ex(ssl, buf, sizeof(buf), &readbytes)) {
read_count++;
DEBUG_LOG("[Read #%lu] SSL_read_ex() 成功: %zu バイト受信", read_count, readbytes);
debug_hexdump("受信データ", (const unsigned char *)buf, readbytes);
fwrite(buf, 1, readbytes, stdout);
}
printf("\n");
DEBUG_LOG("SSL_read_ex() ループ終了 (合計 %lu 回読み取り)", read_count);
/*
* 読み取り終了の原因確認
*/
DEBUG_LOG("SSL_get_error(ssl, 0) を呼び出し中...");
ret = SSL_get_error(ssl, 0);
DEBUG_LOG("SSL_get_error() 戻り値: %d", ret);
switch (ret) {
case SSL_ERROR_ZERO_RETURN:
DEBUG_LOG("SSL_ERROR_ZERO_RETURN: 正常終了(ピアがFINを送信)");
break;
case SSL_ERROR_SSL:
DEBUG_LOG("SSL_ERROR_SSL: ストリームエラー発生");
DEBUG_LOG("SSL_get_stream_read_state() を呼び出し中...");
ret = SSL_get_stream_read_state(ssl);
DEBUG_LOG("SSL_get_stream_read_state() 戻り値: %d", ret);
switch (ret) {
case SSL_STREAM_STATE_RESET_REMOTE:
DEBUG_LOG("SSL_STREAM_STATE_RESET_REMOTE: リモートがストリームをリセット");
printf("Stream reset occurred\n");
break;
case SSL_STREAM_STATE_CONN_CLOSED:
DEBUG_LOG("SSL_STREAM_STATE_CONN_CLOSED: 接続が閉じられた");
printf("Connection closed\n");
goto end;
default:
DEBUG_LOG("不明なストリーム状態: %d", ret);
printf("Unknown stream failure\n");
break;
}
break;
default:
DEBUG_LOG("予期しないエラー: %d", ret);
debug_print_ssl_error("SSL_read_ex");
printf("Failed reading remaining data\n");
break;
}
/*
* 接続シャットダウン
*/
DEBUG_LOG("========================================");
DEBUG_LOG("接続シャットダウン開始");
DEBUG_LOG("========================================");
DEBUG_LOG("SSL_shutdown() を呼び出し中...");
do {
ret = SSL_shutdown(ssl);
DEBUG_LOG("SSL_shutdown() 戻り値: %d", ret);
if (ret < 0) {
DEBUG_LOG("SSL_shutdown() エラー!");
debug_print_ssl_error("SSL_shutdown");
printf("Error shutting down: %d\n", ret);
goto end;
}
} while (ret != 1);
DEBUG_LOG("SSL_shutdown() 完了");
DEBUG_LOG("========================================");
DEBUG_LOG("接続シャットダウン完了");
DEBUG_LOG("========================================");
/* Success! */
res = EXIT_SUCCESS;
end:
DEBUG_LOG("========================================");
DEBUG_LOG("クリーンアップ開始");
DEBUG_LOG("========================================");
if (res == EXIT_FAILURE) {
DEBUG_LOG("エラー発生、OpenSSLエラースタックを出力");
ERR_print_errors_fp(stderr);
}
DEBUG_LOG("SSL_free(ssl) を呼び出し中...");
SSL_free(ssl);
DEBUG_LOG("SSL_CTX_free(ctx) を呼び出し中...");
SSL_CTX_free(ctx);
DEBUG_LOG("BIO_ADDR_free(peer_addr) を呼び出し中...");
BIO_ADDR_free(peer_addr);
DEBUG_LOG("========================================");
DEBUG_LOG("プログラム終了 (exit code: %d)", res);
DEBUG_LOG("========================================");
return res;
}
/*
* Copyright 2023-2025 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* ファイルパス: demos/guide/quic-client-block.c (OpenSSLリポジトリ内)
*
* 注意: このファイルの日本語コメントは Claude Code によって追加されました。
* オリジナルのソースコードは https://github.com/openssl/openssl にあります。
*/
/*
* ============================================================================
* quic-client-block.c - ブロッキングモードQUICクライアントデモ
* ============================================================================
*
* ※ 日本語コメントは Claude Code によって追加されました。
*
* 【概要】
* OpenSSL QUICを使用したシンプルなHTTPクライアントのデモ。
* ブロッキングモードで動作し、指定されたサーバーに接続して
* HTTP/1.0 GETリクエストを送信し、レスポンスを表示する。
*
* 【注意】
* HTTP/1.0 over QUICは非標準であり、実際のサーバーでは
* サポートされていない。これはデモ目的のみ。
* 実用的なHTTP/3クライアントにはnghttp3等のライブラリが必要。
*
* 【他のデモとの比較】
* - quic-client-block.c (このファイル): ブロッキングモード、最もシンプル
* - quic-client-non-block.c: 非ブロッキングモード、select()使用
* - quic-multi-stream.c: 複数ストリームの並行処理
* - ossl-nghttp3-demo.c: HTTP/3プロトコル対応
*
* 【処理フロー図】
*
* main()
* │
* ├── コマンドライン引数解析 (-6オプション、hostname、port)
* │
* ├── SSL_CTX作成
* │ ├── OSSL_QUIC_client_method() ... QUICクライアント用メソッド
* │ ├── SSL_VERIFY_PEER ... サーバー証明書検証
* │ └── デフォルト検証パス設定
* │
* ├── SSL作成
* │
* ├── create_socket_bio()
* │ ├── BIO_lookup_ex() ... DNSルックアップ
* │ ├── BIO_socket() ... UDPソケット作成
* │ ├── BIO_connect() ... ソケット接続
* │ ├── BIO_socket_nbio() ... 非ブロッキング設定
* │ └── BIO_new(BIO_s_datagram()) ... データグラムBIO作成
* │
* ├── SSL_set_bio() ... BIOをSSLに関連付け
* ├── SSL_set_tlsext_host_name() ... SNI設定
* ├── SSL_set1_host() ... 証明書検証用ホスト名
* ├── SSL_set_alpn_protos() ... ALPN設定 (http/1.0)
* ├── SSL_set1_initial_peer_addr() ... ピアアドレス設定
* │
* ├── SSL_connect() ... QUICハンドシェイク
* │
* ├── SSL_write_ex() x3 ... HTTPリクエスト送信
* │ └── SSL_WRITE_FLAG_CONCLUDE ... ストリーム終了フラグ
* │
* ├── SSL_read_ex() ループ ... レスポンス受信
* │
* ├── SSL_get_error() ... エラー確認
* │ ├── SSL_ERROR_ZERO_RETURN ... 正常終了(FIN受信)
* │ ├── SSL_ERROR_SSL ... エラー
* │ │ ├── SSL_STREAM_STATE_RESET_REMOTE ... リセット
* │ │ └── SSL_STREAM_STATE_CONN_CLOSED ... 接続クローズ
* │ └── その他のエラー
* │
* └── SSL_shutdown() ループ ... 接続終了
*
* 【使用方法】
* ./quic-client-block [-6] hostname port
*
* オプション:
* -6 IPv6を使用
*
* 例:
* ./quic-client-block localhost 4433
* ./quic-client-block -6 localhost 4433
*
* 【ブロッキングモードの特徴】
* - SSL_connect(), SSL_read_ex(), SSL_write_ex() が完了まで待機
* - 単純で理解しやすいコード
* - 複数の接続やストリームの同時処理には不向き
* - GUIアプリケーションではUIがフリーズする可能性
*
* 【関連ドキュメント】
* doc/man7/ossl-guide-quic-client-block.pod
*/
/*
* NB: Changes to this file should also be reflected in
* doc/man7/ossl-guide-quic-client-block.pod
*/
#include <string.h>
/* Include the appropriate header file for SOCK_DGRAM */
#ifdef _WIN32 /* Windows */
# include <winsock2.h>
#else /* Linux/Unix */
# include <sys/socket.h>
#endif
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
/*
* create_socket_bio() - サーバーに接続するBIOを作成
*
* DNSルックアップを行い、UDPソケットを作成してサーバーに接続し、
* データグラムBIOでラップして返す。
*
* 【処理フロー】
* 1. BIO_lookup_ex(): DNSルックアップ
* - 複数のアドレスが返される可能性がある(IPv4/IPv6)
*
* 2. 各アドレスに対してループ:
* a. BIO_socket(): UDPソケット作成
* b. BIO_connect(): ソケットをサーバーアドレスに接続
* ※ UDPのconnect()は実際には通信しない(アドレスを関連付けるだけ)
* c. BIO_socket_nbio(): 非ブロッキングモードに設定
* ※ QUICスタックが内部でI/Oを管理するため必要
*
* 3. BIO_new(BIO_s_datagram()): データグラムBIO作成
* - UDP通信用のBIOタイプ
*
* 4. BIO_set_fd(): ソケットをBIOに関連付け
* - BIO_CLOSE: BIO解放時にソケットも自動クローズ
*
* 【なぜOpenSSLのソケット関数を使うのか】
* socket(), connect()などの標準関数も使用可能だが、OpenSSLの関数を
* 使用すると:
* - クロスプラットフォーム対応(Windows/Unix)
* - エラーがOpenSSLエラースタックに記録される
*
* 【UDPのconnect()について】
* TCPと異なり、UDPのconnect()はハンドシェイクを行わない。
* ローカルソケットにリモートアドレスを関連付けるだけ。
* これにより:
* - send()/recv()でアドレス指定が不要になる
* - 他のアドレスからのパケットが自動的に拒否される
*
* @param hostname 接続先ホスト名
* @param port 接続先ポート
* @param family アドレスファミリ (AF_INET または AF_INET6)
* @param peer_addr [出力] サーバーのアドレス(QUICで使用)
* @return 成功時はBIO、失敗時はNULL
*/
/* Helper function to create a BIO connected to the server */
static BIO *create_socket_bio(const char *hostname, const char *port,
int family, BIO_ADDR **peer_addr)
{
int sock = -1;
BIO_ADDRINFO *res;
const BIO_ADDRINFO *ai = NULL;
BIO *bio;
/*
* Lookup IP address info for the server.
*/
/*
* DNSルックアップ。
* BIO_LOOKUP_CLIENT: クライアント用のアドレスを取得
* SOCK_DGRAM: UDPソケット用(QUICはUDP上で動作)
*/
if (!BIO_lookup_ex(hostname, port, BIO_LOOKUP_CLIENT, family, SOCK_DGRAM, 0,
&res))
return NULL;
/*
* Loop through all the possible addresses for the server and find one
* we can connect to.
*/
/*
* DNSが返した全アドレスをループして、接続可能なものを探す。
* デュアルスタック環境では複数のアドレス(IPv4/IPv6)が返される。
*/
for (ai = res; ai != NULL; ai = BIO_ADDRINFO_next(ai)) {
/*
* Create a UDP socket. We could equally use non-OpenSSL calls such
* as "socket" here for this and the subsequent connect and close
* functions. But for portability reasons and also so that we get
* errors on the OpenSSL stack in the event of a failure we use
* OpenSSL's versions of these functions.
*/
/* UDPソケットを作成 */
sock = BIO_socket(BIO_ADDRINFO_family(ai), SOCK_DGRAM, 0, 0);
if (sock == -1)
continue;
/* Connect the socket to the server's address */
/* ソケットをサーバーアドレスに接続(UDPなのでアドレス関連付けのみ)*/
if (!BIO_connect(sock, BIO_ADDRINFO_address(ai), 0)) {
BIO_closesocket(sock);
sock = -1;
continue;
}
/* Set to nonblocking mode */
/*
* 非ブロッキングモードに設定。
* QUICスタックが内部でI/Oタイミングを管理するため、
* ソケット自体は非ブロッキングである必要がある。
* ただし、SSLオブジェクトはデフォルトでブロッキングモード
* として動作する(内部でポーリングを行う)。
*/
if (!BIO_socket_nbio(sock, 1)) {
BIO_closesocket(sock);
sock = -1;
continue;
}
break; /* 成功 */
}
/* 接続成功時、ピアアドレスを複製して保存 */
if (sock != -1) {
*peer_addr = BIO_ADDR_dup(BIO_ADDRINFO_address(ai));
if (*peer_addr == NULL) {
BIO_closesocket(sock);
return NULL;
}
}
/* Free the address information resources we allocated earlier */
/* DNSルックアップ結果を解放 */
BIO_ADDRINFO_free(res);
/* If sock is -1 then we've been unable to connect to the server */
/* 全てのアドレスで失敗した場合 */
if (sock == -1)
return NULL;
/* Create a BIO to wrap the socket */
/*
* データグラムBIOを作成。
* BIO_s_datagram()はUDP通信用のBIOタイプ。
* TCP用のBIO_s_socket()とは異なり、データグラム境界を保持する。
*/
bio = BIO_new(BIO_s_datagram());
if (bio == NULL) {
BIO_closesocket(sock);
return NULL;
}
/*
* Associate the newly created BIO with the underlying socket. By
* passing BIO_CLOSE here the socket will be automatically closed when
* the BIO is freed. Alternatively you can use BIO_NOCLOSE, in which
* case you must close the socket explicitly when it is no longer
* needed.
*/
/*
* BIOにソケットを関連付け。
* BIO_CLOSE: BIO_free()時にソケットも自動的にクローズ
* BIO_NOCLOSE: ソケットは手動でクローズする必要がある
*/
BIO_set_fd(bio, sock, BIO_CLOSE);
return bio;
}
/*
* ============================================================================
* main() - QUICクライアントのエントリポイント
* ============================================================================
*
* HTTP/1.0 GETリクエストをQUIC経由で送信し、レスポンスを表示する。
* ブロッキングモードで動作するため、各操作は完了まで待機する。
*
* 【注意】
* HTTP/1.0 over QUICは非標準。実際のサーバーでは動作しない。
* デモ目的のみ。
*/
/*
* Simple application to send a basic HTTP/1.0 request to a server and
* print the response on the screen. Note that HTTP/1.0 over QUIC is
* non-standard and will not typically be supported by real world servers. This
* is for demonstration purposes only.
*/
int main(int argc, char *argv[])
{
SSL_CTX *ctx = NULL; /* SSL/TLSコンテキスト */
SSL *ssl = NULL; /* QUIC接続オブジェクト */
BIO *bio = NULL; /* データグラムBIO(UDPソケット)*/
int res = EXIT_FAILURE; /* 戻り値(デフォルトはエラー)*/
int ret;
/*
* ALPN (Application-Layer Protocol Negotiation)
* QUICではALPNが必須。ここでは "http/1.0" を指定。
* フォーマット: 長さ(1バイト) + 文字列
*/
unsigned char alpn[] = { 8, 'h', 't', 't', 'p', '/', '1', '.', '0' };
/* HTTP/1.0リクエストの構成要素 */
const char *request_start = "GET / HTTP/1.0\r\nConnection: close\r\nHost: ";
const char *request_end = "\r\n\r\n";
size_t written, readbytes;
char buf[160]; /* レスポンス読み取りバッファ */
BIO_ADDR *peer_addr = NULL; /* サーバーのアドレス */
char *hostname, *port;
int argnext = 1;
int ipv6 = 0; /* IPv6使用フラグ */
/*
* ----------------------------------------
* コマンドライン引数の解析
* ----------------------------------------
*/
if (argc < 3) {
printf("Usage: quic-client-block [-6] hostname port\n");
goto end;
}
/* -6 オプション: IPv6を使用 */
if (!strcmp(argv[argnext], "-6")) {
if (argc < 4) {
printf("Usage: quic-client-block [-6] hostname port\n");
goto end;
}
ipv6 = 1;
argnext++;
}
hostname = argv[argnext++];
port = argv[argnext];
/*
* ----------------------------------------
* SSL_CTXの作成
* ----------------------------------------
*/
/*
* Create an SSL_CTX which we can use to create SSL objects from. We
* want an SSL_CTX for creating clients so we use
* OSSL_QUIC_client_method() here.
*/
/*
* QUICクライアント用のSSL_CTXを作成。
* OSSL_QUIC_client_method()はQUICクライアント専用のメソッド。
* TCPのSSL_CTX_new(TLS_client_method())に相当。
*/
ctx = SSL_CTX_new(OSSL_QUIC_client_method());
if (ctx == NULL) {
printf("Failed to create the SSL_CTX\n");
goto end;
}
/*
* Configure the client to abort the handshake if certificate
* verification fails. Virtually all clients should do this unless you
* really know what you are doing.
*/
/*
* サーバー証明書の検証を有効化。
* 検証失敗時にハンドシェイクを中断する。
* 本番環境では必須の設定。
*/
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
/* Use the default trusted certificate store */
/*
* システムデフォルトのCA証明書パスを使用。
* /etc/ssl/certs 等のシステム証明書ストアを参照。
*/
if (!SSL_CTX_set_default_verify_paths(ctx)) {
printf("Failed to set the default trusted certificate store\n");
goto end;
}
/*
* ----------------------------------------
* SSLオブジェクトの作成
* ----------------------------------------
*/
/* Create an SSL object to represent the TLS connection */
/* QUIC接続を表すSSLオブジェクトを作成 */
ssl = SSL_new(ctx);
if (ssl == NULL) {
printf("Failed to create the SSL object\n");
goto end;
}
/*
* ----------------------------------------
* ソケットBIOの作成と関連付け
* ----------------------------------------
*/
/*
* Create the underlying transport socket/BIO and associate it with the
* connection.
*/
/* UDPソケットを作成してBIOでラップ */
bio = create_socket_bio(hostname, port, ipv6 ? AF_INET6 : AF_INET, &peer_addr);
if (bio == NULL) {
printf("Failed to crete the BIO\n");
goto end;
}
/*
* BIOをSSLオブジェクトに関連付け。
* 両方の引数に同じBIOを渡す(読み書き両方に使用)。
* この時点でBIOの所有権はSSLに移る。
*/
SSL_set_bio(ssl, bio, bio);
/*
* ----------------------------------------
* SNI (Server Name Indication) の設定
* ----------------------------------------
*/
/*
* Tell the server during the handshake which hostname we are attempting
* to connect to in case the server supports multiple hosts.
*/
/*
* SNIを設定。ハンドシェイク時にサーバーにホスト名を通知。
* 1つのIPアドレスで複数のドメインをホストしている場合に必要。
*/
if (!SSL_set_tlsext_host_name(ssl, hostname)) {
printf("Failed to set the SNI hostname\n");
goto end;
}
/*
* ----------------------------------------
* 証明書検証用ホスト名の設定
* ----------------------------------------
*/
/*
* Ensure we check during certificate verification that the server has
* supplied a certificate for the hostname that we were expecting.
* Virtually all clients should do this unless you really know what you
* are doing.
*/
/*
* 証明書のホスト名検証を有効化。
* サーバー証明書のCN/SANが指定したホスト名と一致することを確認。
* SNIとは別の設定(SNIは通知、これは検証)。
*/
if (!SSL_set1_host(ssl, hostname)) {
printf("Failed to set the certificate verification hostname");
goto end;
}
/*
* ----------------------------------------
* ALPNの設定
* ----------------------------------------
*/
/* SSL_set_alpn_protos returns 0 for success! */
/*
* ALPN (Application-Layer Protocol Negotiation) を設定。
* QUICではALPNが必須。サーバーとアプリケーションプロトコルをネゴシエート。
* 注意: この関数は成功時に0を返す(他のSSL関数と逆)。
*/
if (SSL_set_alpn_protos(ssl, alpn, sizeof(alpn)) != 0) {
printf("Failed to set the ALPN for the connection\n");
goto end;
}
/*
* ----------------------------------------
* ピアアドレスの設定
* ----------------------------------------
*/
/* Set the IP address of the remote peer */
/*
* QUICの初期ピアアドレスを設定。
* QUICはコネクションマイグレーションをサポートするため、
* アドレスが変わる可能性がある。これは初期アドレス。
*/
if (!SSL_set1_initial_peer_addr(ssl, peer_addr)) {
printf("Failed to set the initial peer address\n");
goto end;
}
/*
* ----------------------------------------
* QUICハンドシェイクの実行
* ----------------------------------------
*/
/* Do the handshake with the server */
/*
* QUICハンドシェイクを実行。
* ブロッキングモードなので、ハンドシェイク完了まで待機。
* 内部的にはQUIC Initial、Handshake、1-RTTパケットの交換が行われる。
*/
if (SSL_connect(ssl) < 1) {
printf("Failed to connect to the server\n");
/*
* If the failure is due to a verification error we can get more
* information about it from SSL_get_verify_result().
*/
/* 証明書検証エラーの場合、詳細を表示 */
if (SSL_get_verify_result(ssl) != X509_V_OK)
printf("Verify error: %s\n",
X509_verify_cert_error_string(SSL_get_verify_result(ssl)));
goto end;
}
/*
* ----------------------------------------
* HTTPリクエストの送信
* ----------------------------------------
*/
/* Write an HTTP GET request to the peer */
/*
* HTTP/1.0 GETリクエストを送信。
* 3回のSSL_write_ex()で送信:
* 1. "GET / HTTP/1.0\r\nConnection: close\r\nHost: "
* 2. hostname
* 3. "\r\n\r\n"
*/
if (!SSL_write_ex(ssl, request_start, strlen(request_start), &written)) {
printf("Failed to write start of HTTP request\n");
goto end;
}
if (!SSL_write_ex(ssl, hostname, strlen(hostname), &written)) {
printf("Failed to write hostname in HTTP request\n");
goto end;
}
/*
* SSL_write_ex2()でSSL_WRITE_FLAG_CONCLUDEを指定。
* これにより、データ送信と同時にストリームのFINを送信。
* サーバーに「これ以上データを送らない」ことを通知。
*/
if (!SSL_write_ex2(ssl, request_end, strlen(request_end),
SSL_WRITE_FLAG_CONCLUDE, &written)) {
printf("Failed to write end of HTTP request\n");
goto end;
}
/*
* ----------------------------------------
* HTTPレスポンスの受信
* ----------------------------------------
*/
/*
* Get up to sizeof(buf) bytes of the response. We keep reading until the
* server closes the connection.
*/
/*
* レスポンスを読み取り、stdoutに出力。
* サーバーがストリームを閉じる(FINを送信する)まで読み続ける。
*/
while (SSL_read_ex(ssl, buf, sizeof(buf), &readbytes)) {
/*
* OpenSSL does not guarantee that the returned data is a string or
* that it is NUL terminated so we use fwrite() to write the exact
* number of bytes that we read. The data could be non-printable or
* have NUL characters in the middle of it. For this simple example
* we're going to print it to stdout anyway.
*/
/*
* OpenSSLはNUL終端を保証しないので、fwrite()で正確なバイト数を出力。
* バイナリデータやNUL文字を含む可能性がある。
*/
fwrite(buf, 1, readbytes, stdout);
}
/* In case the response didn't finish with a newline we add one now */
/* レスポンスが改行で終わらない場合のための改行 */
printf("\n");
/*
* ----------------------------------------
* 読み取り終了の原因を確認
* ----------------------------------------
*/
/*
* Check whether we finished the while loop above normally or as the
* result of an error. The 0 argument to SSL_get_error() is the return
* code we received from the SSL_read_ex() call. It must be 0 in order
* to get here. Normal completion is indicated by SSL_ERROR_ZERO_RETURN. In
* QUIC terms this means that the peer has sent FIN on the stream to
* indicate that no further data will be sent.
*/
/*
* SSL_read_ex()が失敗した理由を確認。
* SSL_get_error()に渡す0は、SSL_read_ex()の戻り値(失敗時は0)。
*
* 【エラーコードの意味】
* SSL_ERROR_ZERO_RETURN: 正常終了。サーバーがFINを送信。
* SSL_ERROR_SSL: 何らかのエラー発生。詳細は別途確認が必要。
*/
switch (SSL_get_error(ssl, 0)) {
case SSL_ERROR_ZERO_RETURN:
/* Normal completion of the stream */
/* 正常終了: サーバーがストリームを閉じた */
break;
case SSL_ERROR_SSL:
/*
* Some stream fatal error occurred. This could be because of a stream
* reset - or some failure occurred on the underlying connection.
*/
/*
* ストリームレベルのエラー。
* SSL_get_stream_read_state()で詳細を確認。
*/
switch (SSL_get_stream_read_state(ssl)) {
case SSL_STREAM_STATE_RESET_REMOTE:
printf("Stream reset occurred\n");
/* The stream has been reset but the connection is still healthy. */
/*
* リモートがストリームをリセット(RESET_STREAM)。
* ストリームは終了だが、接続自体は健全な可能性がある。
*/
break;
case SSL_STREAM_STATE_CONN_CLOSED:
printf("Connection closed\n");
/* Connection is already closed. Skip SSL_shutdown() */
/*
* 接続自体が閉じられた。
* SSL_shutdown()は不要なのでスキップ。
*/
goto end;
default:
printf("Unknown stream failure\n");
break;
}
break;
default:
/* Some other unexpected error occurred */
/* 予期しないエラー */
printf ("Failed reading remaining data\n");
break;
}
/*
* ----------------------------------------
* 接続のシャットダウン
* ----------------------------------------
*/
/*
* Repeatedly call SSL_shutdown() until the connection is fully
* closed.
*/
/*
* QUIC接続をクリーンに終了。
* SSL_shutdown()は複数回呼び出す必要がある場合がある:
* - 戻り値0: シャットダウン進行中
* - 戻り値1: シャットダウン完了
* - 戻り値<0: エラー
*
* QUICでは、CONNECTION_CLOSEフレームの送受信が行われる。
*/
do {
ret = SSL_shutdown(ssl);
if (ret < 0) {
printf("Error shutting down: %d\n", ret);
goto end;
}
} while (ret != 1);
/* Success! */
res = EXIT_SUCCESS;
/*
* ----------------------------------------
* クリーンアップ
* ----------------------------------------
*/
end:
/*
* If something bad happened then we will dump the contents of the
* OpenSSL error stack to stderr. There might be some useful diagnostic
* information there.
*/
/* エラー発生時、OpenSSLエラースタックの内容を出力 */
if (res == EXIT_FAILURE)
ERR_print_errors_fp(stderr);
/*
* Free the resources we allocated. We do not free the BIO object here
* because ownership of it was immediately transferred to the SSL object
* via SSL_set_bio(). The BIO will be freed when we free the SSL object.
*/
/*
* リソースを解放。
* BIOはSSL_set_bio()でSSLに所有権が移ったため、
* SSL_free()時に自動的に解放される。
* 明示的にBIO_free()を呼ぶ必要はない。
*/
SSL_free(ssl); /* SSLオブジェクトを解放(BIOも同時に解放)*/
SSL_CTX_free(ctx); /* SSL_CTXを解放 */
BIO_ADDR_free(peer_addr); /* ピアアドレスを解放 */
return res; /* 成功時EXIT_SUCCESS(0)、失敗時EXIT_FAILURE(1) */
}
/*
* Copyright 2023-2025 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* ファイルパス: demos/guide/quic-client-non-block.c (OpenSSLリポジトリ内)
*
* 注意: このファイルの日本語コメントは Claude Code によって追加されました。
* オリジナルのソースコードは https://github.com/openssl/openssl にあります。
*/
/*
* ============================================================================
* quic-client-non-block.c - 非ブロッキングモードQUICクライアントデモ
* ============================================================================
*
* ※ 日本語コメントは Claude Code によって追加されました。
*
* 【概要】
* OpenSSL QUICを使用した非ブロッキングHTTPクライアントのデモ。
* select()を使用してソケット活動を待機し、I/O操作が一時的に
* 失敗した場合はリトライする。
*
* 【quic-client-block.cとの違い】
* - quic-client-block.c: ブロッキングモード。操作完了まで待機。
* - このファイル: 非ブロッキングモード。select()で明示的に待機。
*
* 【非ブロッキングモードの利点】
* - 複数の接続を1つのスレッドで同時に処理可能
* - GUIアプリケーションでUIがフリーズしない
* - タイムアウトや他のイベントとの統合が容易
* - より細かいI/O制御が可能
*
* 【処理フロー図】
*
* main()
* │
* ├── SSL_CTX作成、SSL作成、BIO作成(quic-client-block.cと同様)
* │
* ├── SSL_set_blocking_mode(ssl, 0) ... 非ブロッキングモード設定
* │
* ├── SSL_connect() ループ
* │ │
* │ └── 失敗時 → handle_io_failure()
* │ ├── SSL_ERROR_WANT_READ/WRITE → wait_for_activity() → リトライ
* │ ├── SSL_ERROR_ZERO_RETURN → EOF
* │ └── その他 → エラー
* │
* ├── SSL_write_ex() ループ(リトライあり)
* │
* ├── SSL_read_ex() ループ(リトライあり)
* │
* └── SSL_shutdown() ループ(リトライあり)
*
* 【wait_for_activity()関数】
* select()を使用してソケットの読み書き可能状態を待機する。
* QUICスタックが必要とするI/O方向(読み取り/書き込み)と
* タイムアウトを考慮。
*
* SSL_net_read_desired() → 読み取りが必要か
* SSL_net_write_desired() → 書き込みが必要か
* SSL_get_event_timeout() → 次のタイムアウト
*
* 【handle_io_failure()関数】
* SSL操作が失敗した際のエラーハンドリング:
* SSL_ERROR_WANT_READ/WRITE: 一時的な失敗、リトライ可能
* SSL_ERROR_ZERO_RETURN: EOF(正常終了)
* SSL_ERROR_SYSCALL: システムエラー
* SSL_ERROR_SSL: SSLレベルのエラー
*
* 【使用方法】
* ./quic-client-non-block [-6] hostname port
*
* 【関連ドキュメント】
* doc/man7/ossl-guide-quic-client-non-block.pod
*/
/*
* NB: Changes to this file should also be reflected in
* doc/man7/ossl-guide-quic-client-non-block.pod
*/
#include <string.h>
/* Include the appropriate header file for SOCK_DGRAM */
#ifdef _WIN32 /* Windows */
# include <winsock2.h>
#else /* Linux/Unix */
# include <sys/socket.h>
# include <sys/select.h>
#endif
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
/*
* create_socket_bio() - サーバーに接続するBIOを作成
*
* quic-client-block.cのcreate_socket_bio()と同一。
* DNSルックアップを行い、UDPソケットを作成してサーバーに接続し、
* データグラムBIOでラップして返す。
*
* @param hostname 接続先ホスト名
* @param port 接続先ポート
* @param family アドレスファミリ (AF_INET または AF_INET6)
* @param peer_addr [出力] サーバーのアドレス(QUICで使用)
* @return 成功時はBIO、失敗時はNULL
*/
/* Helper function to create a BIO connected to the server */
static BIO *create_socket_bio(const char *hostname, const char *port,
int family, BIO_ADDR **peer_addr)
{
int sock = -1;
BIO_ADDRINFO *res;
const BIO_ADDRINFO *ai = NULL;
BIO *bio;
/*
* Lookup IP address info for the server.
*/
/* DNSルックアップ */
if (!BIO_lookup_ex(hostname, port, BIO_LOOKUP_CLIENT, family, SOCK_DGRAM, 0,
&res))
return NULL;
/*
* Loop through all the possible addresses for the server and find one
* we can connect to.
*/
/* 全てのアドレスを試行 */
for (ai = res; ai != NULL; ai = BIO_ADDRINFO_next(ai)) {
/*
* Create a UDP socket. We could equally use non-OpenSSL calls such
* as "socket" here for this and the subsequent connect and close
* functions. But for portability reasons and also so that we get
* errors on the OpenSSL stack in the event of a failure we use
* OpenSSL's versions of these functions.
*/
/* UDPソケット作成 */
sock = BIO_socket(BIO_ADDRINFO_family(ai), SOCK_DGRAM, 0, 0);
if (sock == -1)
continue;
/* Connect the socket to the server's address */
/* ソケットをサーバーアドレスに接続(UDPなのでアドレス関連付けのみ)*/
if (!BIO_connect(sock, BIO_ADDRINFO_address(ai), 0)) {
BIO_closesocket(sock);
sock = -1;
continue;
}
/* Set to nonblocking mode */
/* 非ブロッキングモードに設定(QUICスタックが必要とする)*/
if (!BIO_socket_nbio(sock, 1)) {
BIO_closesocket(sock);
sock = -1;
continue;
}
break; /* 成功 */
}
/* 接続成功時、ピアアドレスを複製 */
if (sock != -1) {
*peer_addr = BIO_ADDR_dup(BIO_ADDRINFO_address(ai));
if (*peer_addr == NULL) {
BIO_closesocket(sock);
return NULL;
}
}
/* Free the address information resources we allocated earlier */
/* DNSルックアップ結果を解放 */
BIO_ADDRINFO_free(res);
/* If sock is -1 then we've been unable to connect to the server */
if (sock == -1)
return NULL;
/* Create a BIO to wrap the socket */
/* データグラムBIOを作成 */
bio = BIO_new(BIO_s_datagram());
if (bio == NULL) {
BIO_closesocket(sock);
return NULL;
}
/*
* Associate the newly created BIO with the underlying socket. By
* passing BIO_CLOSE here the socket will be automatically closed when
* the BIO is freed. Alternatively you can use BIO_NOCLOSE, in which
* case you must close the socket explicitly when it is no longer
* needed.
*/
/* BIOにソケットを関連付け(BIO_CLOSE: BIO解放時にソケットも閉じる)*/
BIO_set_fd(bio, sock, BIO_CLOSE);
return bio;
}
/*
* wait_for_activity() - ソケット活動を待機
*
* 非ブロッキングモードで重要な関数。select()を使用して、
* ソケットが読み書き可能になるまで、またはタイムアウトまで待機する。
*
* 【処理フロー】
* 1. SSL_get_fd()でソケットFDを取得
* 2. SSL_net_write_desired()で書き込み必要性を確認
* 3. SSL_net_read_desired()で読み取り必要性を確認
* 4. SSL_get_event_timeout()でタイムアウトを取得
* 5. select()で待機
*
* 【SSL_net_write_desired() / SSL_net_read_desired()】
* QUICスタックが次にどの方向のI/Oを必要としているかを返す。
* 非ブロッキングモードでは、これらを確認してselect()に
* 適切なFDを設定する必要がある。
*
* 【SSL_get_event_timeout()】
* QUICの内部タイマー(ACK遅延、PTO等)に基づくタイムアウトを返す。
* isinfinite=1の場合、タイムアウトなし。
*
* 【GUIアプリケーションへの応用】
* コメントに記載の通り、100msごとにGUIを更新したい場合は
* tvpを100msに制限し、タイムアウト時にGUI更新を行う。
*
* @param ssl SSLオブジェクト
*/
static void wait_for_activity(SSL *ssl)
{
fd_set wfds, rfds; /* 書き込み/読み取り用fd_set */
int width, sock, isinfinite;
struct timeval tv;
struct timeval *tvp = NULL; /* タイムアウト(NULLなら無期限)*/
/* Get hold of the underlying file descriptor for the socket */
/* ソケットFDを取得 */
sock = SSL_get_fd(ssl);
FD_ZERO(&wfds);
FD_ZERO(&rfds);
/*
* Find out if we would like to write to the socket, or read from it (or
* both)
*/
/*
* QUICスタックが必要とするI/O方向を確認。
* write_desired: 送信バッファにデータがあり、送信が必要
* read_desired: 受信が必要(新規パケット待ち)
*/
if (SSL_net_write_desired(ssl))
FD_SET(sock, &wfds);
if (SSL_net_read_desired(ssl))
FD_SET(sock, &rfds);
width = sock + 1; /* select()の第1引数(最大FD+1)*/
/*
* Find out when OpenSSL would next like to be called, regardless of
* whether the state of the underlying socket has changed or not.
*/
/*
* QUICタイマーによる次のタイムアウトを取得。
* ACK遅延、PTO(パケットロス検出)、アイドルタイムアウト等。
*/
if (SSL_get_event_timeout(ssl, &tv, &isinfinite) && !isinfinite)
tvp = &tv;
/*
* Wait until the socket is writeable or readable. We use select here
* for the sake of simplicity and portability, but you could equally use
* poll/epoll or similar functions
*
* NOTE: For the purposes of this demonstration code this effectively
* makes this demo block until it has something more useful to do. In a
* real application you probably want to go and do other work here (e.g.
* update a GUI, or service other connections).
*
* Let's say for example that you want to update the progress counter on
* a GUI every 100ms. One way to do that would be to use the timeout in
* the last parameter to "select" below. If the tvp value is greater
* than 100ms then use 100ms instead. Then, when select returns, you
* check if it did so because of activity on the file descriptors or
* because of the timeout. If the 100ms GUI timeout has expired but the
* tvp timeout has not then go and update the GUI and then restart the
* "select" (with updated timeouts).
*/
/*
* select()でソケット活動またはタイムアウトを待機。
* poll()やepoll()も使用可能。
*
* 【注意】
* このデモではselect()でブロックするが、実際のアプリケーションでは
* この間に他の作業(GUI更新、他の接続処理等)を行うことが多い。
*/
select(width, &rfds, &wfds, NULL, tvp);
}
/*
* handle_io_failure() - I/O失敗時のエラーハンドリング
*
* 非ブロッキングモードでSSL操作が失敗した際に呼び出される。
* エラーの種類に応じて、リトライ可能かどうかを判断する。
*
* 【戻り値】
* 1: リトライ可能(wait_for_activity()後に再試行)
* 0: EOF(正常終了)
* -1: 致命的エラー(リトライ不可)
*
* 【エラーコードの意味】
* SSL_ERROR_WANT_READ: データ受信が必要。後でリトライ。
* SSL_ERROR_WANT_WRITE: データ送信が必要。後でリトライ。
* SSL_ERROR_ZERO_RETURN: EOF。サーバーがFINを送信。
* SSL_ERROR_SYSCALL: システムコールエラー。
* SSL_ERROR_SSL: SSLレベルのエラー。
*
* 【非ブロッキングモードでのパターン】
*
* while (!SSL_xxx(ssl, ...)) {
* if (handle_io_failure(ssl, ret) == 1)
* continue; // リトライ
* // エラー処理
* }
*
* @param ssl SSLオブジェクト
* @param res SSL操作の戻り値
* @return 1:リトライ, 0:EOF, -1:エラー
*/
static int handle_io_failure(SSL *ssl, int res)
{
switch (SSL_get_error(ssl, res)) {
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
/* Temporary failure. Wait until we can read/write and try again */
/*
* 一時的な失敗。ソケットが読み書き可能になるまで待機して
* リトライする。非ブロッキングモードでは頻繁に発生する。
*/
wait_for_activity(ssl);
return 1; /* リトライ可能 */
case SSL_ERROR_ZERO_RETURN:
/* EOF */
/* EOF: サーバーがストリームを閉じた(FIN送信)*/
return 0;
case SSL_ERROR_SYSCALL:
/* システムコールエラー */
return -1;
case SSL_ERROR_SSL:
/*
* Some stream fatal error occurred. This could be because of a
* stream reset - or some failure occurred on the underlying
* connection.
*/
/*
* SSLレベルのエラー。ストリームリセットや接続エラーの可能性。
* SSL_get_stream_read_state()で詳細を確認。
*/
switch (SSL_get_stream_read_state(ssl)) {
case SSL_STREAM_STATE_RESET_REMOTE:
printf("Stream reset occurred\n");
/*
* The stream has been reset but the connection is still
* healthy.
*/
/*
* リモートがストリームをリセット(RESET_STREAM)。
* ストリームは終了だが、接続自体は健全な可能性がある。
*/
break;
case SSL_STREAM_STATE_CONN_CLOSED:
printf("Connection closed\n");
/* Connection is already closed. */
/* 接続が既に閉じられている */
break;
default:
printf("Unknown stream failure\n");
break;
}
/*
* If the failure is due to a verification error we can get more
* information about it from SSL_get_verify_result().
*/
/* 証明書検証エラーの場合、詳細を表示 */
if (SSL_get_verify_result(ssl) != X509_V_OK)
printf("Verify error: %s\n",
X509_verify_cert_error_string(SSL_get_verify_result(ssl)));
return -1;
default:
/* その他の予期しないエラー */
return -1;
}
}
/*
* ============================================================================
* main() - 非ブロッキングQUICクライアントのエントリポイント
* ============================================================================
*
* quic-client-block.cとの主な違い:
* 1. SSL_set_blocking_mode(ssl, 0) で非ブロッキングモードを設定
* 2. 各SSL操作をwhileループでリトライ
* 3. handle_io_failure()でエラー種別を判定
*
* 【注意】
* HTTP/1.0 over QUICは非標準。デモ目的のみ。
*/
/*
* Simple application to send a basic HTTP/1.0 request to a server and
* print the response on the screen. Note that HTTP/1.0 over QUIC is
* non-standard and will not typically be supported by real world servers. This
* is for demonstration purposes only.
*/
int main(int argc, char *argv[])
{
SSL_CTX *ctx = NULL;
SSL *ssl = NULL;
BIO *bio = NULL;
int res = EXIT_FAILURE;
int ret;
unsigned char alpn[] = { 8, 'h', 't', 't', 'p', '/', '1', '.', '0' }; /* ALPN: http/1.0 */
const char *request_start = "GET / HTTP/1.0\r\nConnection: close\r\nHost: ";
const char *request_end = "\r\n\r\n";
size_t written, readbytes = 0;
char buf[160];
BIO_ADDR *peer_addr = NULL;
int eof = 0; /* EOF検出フラグ */
char *hostname, *port;
int ipv6 = 0;
int argnext = 1;
if (argc < 3) {
printf("Usage: quic-client-non-block [-6] hostname port\n");
goto end;
}
if (!strcmp(argv[argnext], "-6")) {
if (argc < 4) {
printf("Usage: quic-client-non-block [-6] hostname port\n");
goto end;
}
ipv6 = 1;
argnext++;
}
hostname = argv[argnext++];
port = argv[argnext];
/*
* =========================================================================
* 【SSL_CTX作成】
* =========================================================================
*
* SSL_CTXはSSLオブジェクトのファクトリ。複数のSSL接続で共有可能。
* OSSL_QUIC_client_method(): QUICクライアント用メソッド
*
* 注意: TLSクライアントでは TLS_client_method() を使用するが、
* QUICでは専用のメソッドが必要。
*/
/*
* Create an SSL_CTX which we can use to create SSL objects from. We
* want an SSL_CTX for creating clients so we use
* OSSL_QUIC_client_method() here.
*/
ctx = SSL_CTX_new(OSSL_QUIC_client_method());
if (ctx == NULL) {
printf("Failed to create the SSL_CTX\n");
goto end;
}
/*
* -------------------------------------------------------------------------
* 【証明書検証設定】
* -------------------------------------------------------------------------
*
* SSL_VERIFY_PEER: サーバー証明書を検証する
* - 検証失敗時はハンドシェイクを中止
* - 本番環境では必須の設定
*
* SSL_CTX_set_default_verify_paths():
* - システムのデフォルトCA証明書ストアを使用
* - Linux: /etc/ssl/certs/ など
*/
/*
* Configure the client to abort the handshake if certificate
* verification fails. Virtually all clients should do this unless you
* really know what you are doing.
*/
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
/* Use the default trusted certificate store */
/* システムのデフォルトCA証明書ストアを使用 */
if (!SSL_CTX_set_default_verify_paths(ctx)) {
printf("Failed to set the default trusted certificate store\n");
goto end;
}
/*
* -------------------------------------------------------------------------
* 【SSLオブジェクト作成】
* -------------------------------------------------------------------------
*
* SSL_new(): SSL_CTXから個別の接続を表すSSLオブジェクトを作成
* 1つの接続につき1つのSSLオブジェクトが必要。
*/
/* Create an SSL object to represent the TLS connection */
ssl = SSL_new(ctx);
if (ssl == NULL) {
printf("Failed to create the SSL object\n");
goto end;
}
/*
* -------------------------------------------------------------------------
* 【BIO作成と関連付け】
* -------------------------------------------------------------------------
*
* create_socket_bio(): UDPソケットを作成し、データグラムBIOでラップ
* SSL_set_bio(): BIOをSSLオブジェクトに関連付け
*
* 注意: SSL_set_bio()後、BIOの所有権はSSLに移転。
* SSL_free()時に自動的にBIOも解放される。
*/
/*
* Create the underlying transport socket/BIO and associate it with the
* connection.
*/
bio = create_socket_bio(hostname, port, ipv6 ? AF_INET6 : AF_INET,
&peer_addr);
if (bio == NULL) {
printf("Failed to crete the BIO\n");
goto end;
}
SSL_set_bio(ssl, bio, bio); /* 読み書き両方に同じBIOを使用 */
/*
* -------------------------------------------------------------------------
* 【SNI (Server Name Indication) 設定】
* -------------------------------------------------------------------------
*
* SSL_set_tlsext_host_name(): TLS拡張でホスト名を送信
* - 1つのIPアドレスで複数のドメインをホストするサーバー向け
* - サーバーが適切な証明書を選択できるようにする
*/
/*
* Tell the server during the handshake which hostname we are attempting
* to connect to in case the server supports multiple hosts.
*/
if (!SSL_set_tlsext_host_name(ssl, hostname)) {
printf("Failed to set the SNI hostname\n");
goto end;
}
/*
* -------------------------------------------------------------------------
* 【ホスト名検証設定】
* -------------------------------------------------------------------------
*
* SSL_set1_host(): 証明書のCN/SANとホスト名を照合
* - サーバー証明書が期待するホスト名と一致することを確認
* - 中間者攻撃の防止に必須
*/
/*
* Ensure we check during certificate verification that the server has
* supplied a certificate for the hostname that we were expecting.
* Virtually all clients should do this unless you really know what you
* are doing.
*/
if (!SSL_set1_host(ssl, hostname)) {
printf("Failed to set the certificate verification hostname");
goto end;
}
/*
* -------------------------------------------------------------------------
* 【ALPN (Application-Layer Protocol Negotiation) 設定】
* -------------------------------------------------------------------------
*
* ALPNはTLSハンドシェイク中にアプリケーションプロトコルを交渉する。
* QUICでは必須。
*
* alpn配列のフォーマット: { 長さ, プロトコル名... }
* 例: { 8, 'h', 't', 't', 'p', '/', '1', '.', '0' } = "http/1.0"
*
* 注意: SSL_set_alpn_protos()は成功時に0を返す(他のAPIと逆)
*/
/* SSL_set_alpn_protos returns 0 for success! */
if (SSL_set_alpn_protos(ssl, alpn, sizeof(alpn)) != 0) {
printf("Failed to set the ALPN for the connection\n");
goto end;
}
/*
* -------------------------------------------------------------------------
* 【ピアアドレス設定】QUIC固有
* -------------------------------------------------------------------------
*
* SSL_set1_initial_peer_addr(): QUICパケットの送信先アドレスを設定
* - UDPはコネクションレスなので、送信先を明示的に指定
* - TLS over TCPでは不要(TCPが接続を管理)
*/
/* Set the IP address of the remote peer */
if (!SSL_set1_initial_peer_addr(ssl, peer_addr)) {
printf("Failed to set the initial peer address\n");
goto end;
}
/*
* =========================================================================
* 【重要】非ブロッキングモードの設定
* =========================================================================
*
* QUICでは内部的にソケットは常に非ブロッキングだが、SSLオブジェクトの
* デフォルト動作はブロッキング(操作完了まで待機)。
*
* SSL_set_blocking_mode(ssl, 0) を呼ぶことで:
* - SSL操作が即座にリターン(待機しない)
* - SSL_ERROR_WANT_READ/WRITE が返る可能性
* - アプリケーションが自分でリトライループを実装する必要
*
* これが quic-client-block.c との最大の違い!
* ブロッキング版ではこの呼び出しがなく、各操作が完了まで待機する。
*/
/*
* The underlying socket is always nonblocking with QUIC, but the default
* behaviour of the SSL object is still to block. We set it for nonblocking
* mode in this demo.
*/
if (!SSL_set_blocking_mode(ssl, 0)) {
printf("Failed to turn off blocking mode\n");
goto end;
}
/*
* =========================================================================
* 【非ブロッキングハンドシェイク】
* =========================================================================
*
* ブロッキング版: ret = SSL_connect(ssl); if (ret != 1) error;
* 非ブロッキング版: whileループでリトライ
*
* 非ブロッキングモードでは SSL_connect() が即座にリターンするため:
* - 成功(1)するまでwhileループで繰り返し呼び出す
* - handle_io_failure()が1を返す限りリトライ継続
* - 0(EOF)や-1(エラー)が返ったら終了
*/
/* Do the handshake with the server */
while ((ret = SSL_connect(ssl)) != 1) {
if (handle_io_failure(ssl, ret) == 1)
continue; /* Retry リトライ可能 */
printf("Failed to connect to server\n");
goto end; /* Cannot retry: error リトライ不可のエラー */
}
/*
* =========================================================================
* 【非ブロッキング書き込み】HTTPリクエストの送信
* =========================================================================
*
* HTTPリクエストを3つの部分に分けて送信:
* 1. request_start: "GET / HTTP/1.0\r\nConnection: close\r\nHost: "
* 2. hostname: 接続先ホスト名
* 3. request_end: "\r\n\r\n" (SSL_WRITE_FLAG_CONCLUDE付き)
*
* 各SSL_write_ex()をwhileループでラップ:
* - 非ブロッキングでは書き込みが即座に完了しない場合がある
* - handle_io_failure()が1を返す限りリトライ
*
* 最後のSSL_write_ex2()でSSL_WRITE_FLAG_CONCLUDEを指定:
* - QUICストリームにFIN(終了)フラグを送信
* - これ以上データを送らないことをサーバーに通知
*/
/* Write an HTTP GET request to the peer */
while (!SSL_write_ex(ssl, request_start, strlen(request_start), &written)) {
if (handle_io_failure(ssl, 0) == 1)
continue; /* Retry リトライ可能 */
printf("Failed to write start of HTTP request\n");
goto end; /* Cannot retry: error リトライ不可のエラー */
}
while (!SSL_write_ex(ssl, hostname, strlen(hostname), &written)) {
if (handle_io_failure(ssl, 0) == 1)
continue; /* Retry リトライ可能 */
printf("Failed to write hostname in HTTP request\n");
goto end; /* Cannot retry: error リトライ不可のエラー */
}
/* SSL_WRITE_FLAG_CONCLUDE: ストリーム終了(FIN送信) */
while (!SSL_write_ex2(ssl, request_end, strlen(request_end),
SSL_WRITE_FLAG_CONCLUDE, &written)) {
if (handle_io_failure(ssl, 0) == 1)
continue; /* Retry リトライ可能 */
printf("Failed to write end of HTTP request\n");
goto end; /* Cannot retry: error リトライ不可のエラー */
}
/*
* =========================================================================
* 【非ブロッキング読み込み】HTTPレスポンスの受信
* =========================================================================
*
* 外側のdo-whileループ: EOFまでデータを読み続ける
* 内側のwhileループ: SSL_read_ex()をリトライ
*
* handle_io_failure()の戻り値による分岐:
* case 1: リトライ可能 → continue(内側ループ継続)
* case 0: EOF検出 → eof=1をセットして終了
* case -1: エラー → 処理終了
*
* 非ブロッキングでは複数回のリトライで1つのレスポンスを読む。
* ブロッキング版ではSSL_read_ex()が完了するまで内部で待機する。
*/
do {
/*
* Get up to sizeof(buf) bytes of the response. We keep reading until
* the server closes the connection.
*/
while (!eof && !SSL_read_ex(ssl, buf, sizeof(buf), &readbytes)) {
switch (handle_io_failure(ssl, 0)) {
case 1:
continue; /* Retry リトライ可能 */
case 0:
eof = 1; /* EOF検出: サーバーが接続を閉じた */
continue;
case -1:
default:
printf("Failed reading remaining data\n");
goto end; /* Cannot retry: error リトライ不可のエラー */
}
}
/*
* OpenSSL does not guarantee that the returned data is a string or
* that it is NUL terminated so we use fwrite() to write the exact
* number of bytes that we read. The data could be non-printable or
* have NUL characters in the middle of it. For this simple example
* we're going to print it to stdout anyway.
*/
/* 読み取ったバイト数だけ出力(NUL終端は保証されない) */
if (!eof)
fwrite(buf, 1, readbytes, stdout);
} while (!eof);
/* In case the response didn't finish with a newline we add one now */
printf("\n");
/*
* =========================================================================
* 【非ブロッキングシャットダウン】
* =========================================================================
*
* SSL_shutdown()の戻り値:
* - 1: 完全にシャットダウン完了(双方向close完了)
* - 0: 送信側のみ完了(受信側close待ち)
* - <0: エラーまたはリトライ必要
*
* 非ブロッキングでは完了(1)するまでループでリトライ。
* ret < 0 の場合のみ handle_io_failure() でリトライ判定。
* ret == 0 の場合はそのまま次のSSL_shutdown()を呼ぶ。
*/
/*
* Repeatedly call SSL_shutdown() until the connection is fully
* closed.
*/
while ((ret = SSL_shutdown(ssl)) != 1) {
if (ret < 0 && handle_io_failure(ssl, ret) == 1)
continue; /* Retry リトライ可能 */
}
/* Success! 成功 */
res = EXIT_SUCCESS;
end:
/*
* =========================================================================
* 【クリーンアップ】
* =========================================================================
*/
/*
* If something bad happened then we will dump the contents of the
* OpenSSL error stack to stderr. There might be some useful diagnostic
* information there.
*/
/* エラー時はOpenSSLエラースタックを出力(デバッグ用) */
if (res == EXIT_FAILURE)
ERR_print_errors_fp(stderr);
/*
* Free the resources we allocated. We do not free the BIO object here
* because ownership of it was immediately transferred to the SSL object
* via SSL_set_bio(). The BIO will be freed when we free the SSL object.
*/
/*
* リソース解放
* 注意: BIOはSSL_set_bio()でSSLに所有権移転済み。
* SSL_free()時に自動的に解放されるため、個別解放不要。
*/
SSL_free(ssl);
SSL_CTX_free(ctx);
BIO_ADDR_free(peer_addr);
return res;
}
/*
* Copyright 2023-2024 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* ファイルパス: demos/guide/quic-multi-stream.c (OpenSSLリポジトリ内)
*
* 注意: このファイルの日本語コメントは Claude Code によって追加されました。
* オリジナルのソースコードは https://github.com/openssl/openssl にあります。
*/
/*
* ============================================================================
* quic-multi-stream.c - QUICマルチストリームクライアントデモ
* ============================================================================
*
* ※ 日本語コメントは Claude Code によって追加されました。
*
* 【概要】
* OpenSSL QUICのマルチストリーム機能を示すデモプログラム。
* 1つのQUIC接続上で複数の独立したストリームを使用してデータを送受信する。
*
* 【QUICストリームの種類】
*
* 1. 双方向ストリーム (Bidirectional Stream)
* - クライアント/サーバー両方がデータを送受信可能
* - SSL_new_stream(ssl, 0) で作成
*
* 2. 単方向ストリーム (Unidirectional Stream)
* - 作成側のみがデータを送信可能
* - SSL_new_stream(ssl, SSL_STREAM_FLAG_UNI) で作成
*
* 【ストリームの開始者】
*
* - クライアント開始: SSL_new_stream() で作成
* - サーバー開始: SSL_accept_stream() で受け入れ
*
* 【処理フロー図】
*
* main()
* │
* ├── SSL_CTX作成、SSL作成
* │
* ├── SSL_set_default_stream_mode(SSL_DEFAULT_STREAM_MODE_NONE)
* │ └── デフォルトストリームを無効化(マルチストリーム使用時推奨)
* │
* ├── SSL_connect() - QUICハンドシェイク
* │
* ├── SSL_new_stream(ssl, 0) ────────────── stream1 (双方向)
* ├── SSL_new_stream(ssl, SSL_STREAM_FLAG_UNI) ── stream2 (単方向)
* │
* ├── write_a_request(stream1, ...) ─────── リクエスト送信
* ├── write_a_request(stream2, ...) ─────── リクエスト送信
* │
* ├── SSL_read_ex(stream1, ...) ─────────── stream1からレスポンス読み取り
* │
* ├── SSL_accept_stream(ssl, 0) ─────────── stream3 (サーバー開始)
* │
* ├── SSL_read_ex(stream3, ...) ─────────── stream3からレスポンス読み取り
* │
* └── SSL_shutdown(ssl) - 接続終了
*
* 【マルチストリームの利点】
*
* - Head-of-Line Blocking の回避
* TCPでは1つのパケットロスが全データをブロック
* QUICでは影響を受けるストリームのみがブロック
*
* - 並列処理
* 複数のリクエスト/レスポンスを同時に処理可能
*
* - 優先度制御
* ストリームごとに優先度を設定可能
*
* 【使用方法】
* ./quic-multi-stream [-6] hostname port
*
* 【関連ドキュメント】
* doc/man7/ossl-guide-quic-multi-stream.pod
*
* 【注意】
* HTTP/1.0 over QUICは非標準。デモ目的のみ。
* 実際のHTTP/3では異なるプロトコル(nghttp3等)を使用する。
*/
/*
* NB: Changes to this file should also be reflected in
* doc/man7/ossl-guide-quic-multi-stream.pod
*/
#include <string.h>
/* Include the appropriate header file for SOCK_DGRAM */
#ifdef _WIN32 /* Windows */
# include <winsock2.h>
#else /* Linux/Unix */
# include <sys/socket.h>
#endif
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
/*
* create_socket_bio() - サーバーに接続するBIOを作成
*
* 他のQUICデモ(quic-client-block.c等)と同一の実装。
* DNSルックアップを行い、UDPソケットを作成してサーバーに接続し、
* データグラムBIOでラップして返す。
*
* @param hostname 接続先ホスト名
* @param port 接続先ポート
* @param family アドレスファミリ (AF_INET または AF_INET6)
* @param peer_addr [出力] サーバーのアドレス(QUICで使用)
* @return 成功時はBIO、失敗時はNULL
*/
/* Helper function to create a BIO connected to the server */
static BIO *create_socket_bio(const char *hostname, const char *port,
int family, BIO_ADDR **peer_addr)
{
int sock = -1;
BIO_ADDRINFO *res;
const BIO_ADDRINFO *ai = NULL;
BIO *bio;
/*
* Lookup IP address info for the server.
*/
/* DNSルックアップ(SOCK_DGRAM: UDP用) */
if (!BIO_lookup_ex(hostname, port, BIO_LOOKUP_CLIENT, family, SOCK_DGRAM, 0,
&res))
return NULL;
/*
* Loop through all the possible addresses for the server and find one
* we can connect to.
*/
/* 全てのアドレスを試行 */
for (ai = res; ai != NULL; ai = BIO_ADDRINFO_next(ai)) {
/*
* Create a UDP socket. We could equally use non-OpenSSL calls such
* as "socket" here for this and the subsequent connect and close
* functions. But for portability reasons and also so that we get
* errors on the OpenSSL stack in the event of a failure we use
* OpenSSL's versions of these functions.
*/
/* UDPソケット作成 */
sock = BIO_socket(BIO_ADDRINFO_family(ai), SOCK_DGRAM, 0, 0);
if (sock == -1)
continue;
/* Connect the socket to the server's address */
/* ソケットをサーバーアドレスに接続(UDPなのでアドレス関連付けのみ)*/
if (!BIO_connect(sock, BIO_ADDRINFO_address(ai), 0)) {
BIO_closesocket(sock);
sock = -1;
continue;
}
/* Set to nonblocking mode */
/* 非ブロッキングモードに設定(QUICスタックが必要とする) */
if (!BIO_socket_nbio(sock, 1)) {
BIO_closesocket(sock);
sock = -1;
continue;
}
break; /* 成功 */
}
/* 接続成功時、ピアアドレスを複製 */
if (sock != -1) {
*peer_addr = BIO_ADDR_dup(BIO_ADDRINFO_address(ai));
if (*peer_addr == NULL) {
BIO_closesocket(sock);
return NULL;
}
}
/* Free the address information resources we allocated earlier */
/* DNSルックアップ結果を解放 */
BIO_ADDRINFO_free(res);
/* If sock is -1 then we've been unable to connect to the server */
if (sock == -1)
return NULL;
/* Create a BIO to wrap the socket */
/* データグラムBIOを作成 */
bio = BIO_new(BIO_s_datagram());
if (bio == NULL) {
BIO_closesocket(sock);
return NULL;
}
/*
* Associate the newly created BIO with the underlying socket. By
* passing BIO_CLOSE here the socket will be automatically closed when
* the BIO is freed. Alternatively you can use BIO_NOCLOSE, in which
* case you must close the socket explicitly when it is no longer
* needed.
*/
/* BIOにソケットを関連付け(BIO_CLOSE: BIO解放時にソケットも閉じる) */
BIO_set_fd(bio, sock, BIO_CLOSE);
return bio;
}
/*
* write_a_request() - ストリームにHTTPリクエストを書き込む
*
* 指定されたストリームにHTTP/1.0 GETリクエストを送信する。
* マルチストリームでは、各ストリームに対して個別にデータを送受信できる。
*
* @param stream データを送信するストリーム(SSLオブジェクト)
* @param request_start リクエストの開始部分("GET /path HTTP/1.0\r\n...")
* @param hostname Hostヘッダに含めるホスト名
* @return 成功時1、失敗時0
*/
static int write_a_request(SSL *stream, const char *request_start,
const char *hostname)
{
const char *request_end = "\r\n\r\n";
size_t written;
/* リクエスト開始部分を送信 */
if (!SSL_write_ex(stream, request_start, strlen(request_start),
&written))
return 0;
/* ホスト名を送信 */
if (!SSL_write_ex(stream, hostname, strlen(hostname), &written))
return 0;
/* リクエスト終了(空行)を送信 */
if (!SSL_write_ex(stream, request_end, strlen(request_end), &written))
return 0;
return 1;
}
/*
* ============================================================================
* main() - マルチストリームQUICクライアントのエントリポイント
* ============================================================================
*
* このデモでは3つのストリームを使用:
* stream1: クライアント開始、双方向(リクエスト送信&レスポンス受信)
* stream2: クライアント開始、単方向(リクエスト送信のみ)
* stream3: サーバー開始(SSL_accept_streamで受け入れ)
*
* 【注意】
* HTTP/1.0 over QUICは非標準。デモ目的のみ。
*/
/*
* Simple application to send basic HTTP/1.0 requests to a server and print the
* response on the screen. Note that HTTP/1.0 over QUIC is not a real protocol
* and will not be supported by real world servers. This is for demonstration
* purposes only.
*/
int main(int argc, char *argv[])
{
SSL_CTX *ctx = NULL;
SSL *ssl = NULL; /* QUIC接続オブジェクト */
SSL *stream1 = NULL, *stream2 = NULL, *stream3 = NULL; /* 個別ストリーム */
BIO *bio = NULL;
int res = EXIT_FAILURE;
int ret;
unsigned char alpn[] = { 8, 'h', 't', 't', 'p', '/', '1', '.', '0' }; /* ALPN: http/1.0 */
const char *request1_start =
"GET /request1.html HTTP/1.0\r\nConnection: close\r\nHost: ";
const char *request2_start =
"GET /request2.html HTTP/1.0\r\nConnection: close\r\nHost: ";
size_t readbytes;
char buf[160];
BIO_ADDR *peer_addr = NULL;
char *hostname, *port;
int argnext = 1;
int ipv6 = 0;
if (argc < 3) {
printf("Usage: quic-client-non-block [-6] hostname port\n");
goto end;
}
if (!strcmp(argv[argnext], "-6")) {
if (argc < 4) {
printf("Usage: quic-client-non-block [-6] hostname port\n");
goto end;
}
ipv6 = 1;
argnext++;
}
hostname = argv[argnext++];
port = argv[argnext];
/*
* =========================================================================
* 【SSL_CTX / SSL オブジェクト作成】
* =========================================================================
*/
/*
* Create an SSL_CTX which we can use to create SSL objects from. We
* want an SSL_CTX for creating clients so we use
* OSSL_QUIC_client_method() here.
*/
/* QUICクライアント用SSL_CTXを作成 */
ctx = SSL_CTX_new(OSSL_QUIC_client_method());
if (ctx == NULL) {
printf("Failed to create the SSL_CTX\n");
goto end;
}
/*
* Configure the client to abort the handshake if certificate
* verification fails. Virtually all clients should do this unless you
* really know what you are doing.
*/
/* サーバー証明書の検証を有効化 */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
/* Use the default trusted certificate store */
/* システムのデフォルトCA証明書ストアを使用 */
if (!SSL_CTX_set_default_verify_paths(ctx)) {
printf("Failed to set the default trusted certificate store\n");
goto end;
}
/* Create an SSL object to represent the TLS connection */
/* QUIC接続を表すSSLオブジェクトを作成 */
ssl = SSL_new(ctx);
if (ssl == NULL) {
printf("Failed to create the SSL object\n");
goto end;
}
/*
* =========================================================================
* 【重要】デフォルトストリームモードの無効化
* =========================================================================
*
* SSL_DEFAULT_STREAM_MODE_NONE を設定すると:
* - SSLオブジェクト自体でのread/writeが無効化される
* - 明示的にストリームを作成する必要がある
* - マルチストリーム使用時に推奨される設定
*
* デフォルトストリームモードが有効な場合:
* - SSL_read/write は暗黙的にデフォルトストリームを使用
* - シングルストリームの場合は便利だが、マルチストリームでは混乱の元
*/
/*
* We will use multiple streams so we will disable the default stream mode.
* This is not a requirement for using multiple streams but is recommended.
*/
if (!SSL_set_default_stream_mode(ssl, SSL_DEFAULT_STREAM_MODE_NONE)) {
printf("Failed to disable the default stream mode\n");
goto end;
}
/*
* -------------------------------------------------------------------------
* 【BIO作成・SNI・ALPN設定】(他のQUICデモと同様)
* -------------------------------------------------------------------------
*/
/*
* Create the underlying transport socket/BIO and associate it with the
* connection.
*/
/* UDPソケットを作成しBIOでラップ */
bio = create_socket_bio(hostname, port, ipv6 ? AF_INET6 : AF_INET, &peer_addr);
if (bio == NULL) {
printf("Failed to crete the BIO\n");
goto end;
}
SSL_set_bio(ssl, bio, bio);
/*
* Tell the server during the handshake which hostname we are attempting
* to connect to in case the server supports multiple hosts.
*/
/* SNI (Server Name Indication) を設定 */
if (!SSL_set_tlsext_host_name(ssl, hostname)) {
printf("Failed to set the SNI hostname\n");
goto end;
}
/*
* Ensure we check during certificate verification that the server has
* supplied a certificate for the hostname that we were expecting.
* Virtually all clients should do this unless you really know what you
* are doing.
*/
/* 証明書のホスト名検証を設定 */
if (!SSL_set1_host(ssl, hostname)) {
printf("Failed to set the certificate verification hostname");
goto end;
}
/* SSL_set_alpn_protos returns 0 for success! */
/* ALPN設定(注意: 成功時に0を返す) */
if (SSL_set_alpn_protos(ssl, alpn, sizeof(alpn)) != 0) {
printf("Failed to set the ALPN for the connection\n");
goto end;
}
/* Set the IP address of the remote peer */
/* QUICパケットの送信先アドレスを設定 */
if (!SSL_set1_initial_peer_addr(ssl, peer_addr)) {
printf("Failed to set the initial peer address\n");
goto end;
}
/*
* =========================================================================
* 【QUICハンドシェイク】
* =========================================================================
*/
/* Do the handshake with the server */
if (SSL_connect(ssl) < 1) {
printf("Failed to connect to the server\n");
/*
* If the failure is due to a verification error we can get more
* information about it from SSL_get_verify_result().
*/
if (SSL_get_verify_result(ssl) != X509_V_OK)
printf("Verify error: %s\n",
X509_verify_cert_error_string(SSL_get_verify_result(ssl)));
goto end;
}
/*
* =========================================================================
* 【クライアント開始ストリームの作成】
* =========================================================================
*
* SSL_new_stream() でクライアント側からストリームを作成:
*
* stream1 = SSL_new_stream(ssl, 0)
* → 双方向ストリーム(bidirectional)
* → クライアント・サーバー両方がデータを送受信可能
*
* stream2 = SSL_new_stream(ssl, SSL_STREAM_FLAG_UNI)
* → 単方向ストリーム(unidirectional)
* → クライアントのみが送信可能、サーバーは受信のみ
*
* 各ストリームは独立したSSLオブジェクトとして扱われ、
* 個別にSSL_read_ex/SSL_write_exでデータを送受信できる。
*/
/*
* We create two new client initiated streams. The first will be
* bi-directional, and the second will be uni-directional.
*/
stream1 = SSL_new_stream(ssl, 0); /* 双方向ストリーム */
stream2 = SSL_new_stream(ssl, SSL_STREAM_FLAG_UNI); /* 単方向ストリーム */
if (stream1 == NULL || stream2 == NULL) {
printf("Failed to create streams\n");
goto end;
}
/*
* -------------------------------------------------------------------------
* 【各ストリームにリクエストを送信】
* -------------------------------------------------------------------------
*/
/* Write an HTTP GET request on each of our streams to the peer */
/* stream1 (双方向) にリクエスト1を送信 */
if (!write_a_request(stream1, request1_start, hostname)) {
printf("Failed to write HTTP request on stream 1\n");
goto end;
}
/* stream2 (単方向) にリクエスト2を送信 */
if (!write_a_request(stream2, request2_start, hostname)) {
printf("Failed to write HTTP request on stream 2\n");
goto end;
}
/*
* =========================================================================
* 【stream1 (双方向) からレスポンスを読み取り】
* =========================================================================
*
* このデモでは簡単のため、1つのストリームを読み終えてから次のストリームを読む。
* 実際には、複数ストリームのI/Oをインターリーブしたり、
* 別スレッドで個別に管理することも可能。
*/
/*
* In this demo we read all the data from one stream before reading all the
* data from the next stream for simplicity. In practice there is no need to
* do this. We can interleave IO on the different streams if we wish, or
* manage the streams entirely separately on different threads.
*/
printf("Stream 1 data:\n");
/*
* Get up to sizeof(buf) bytes of the response from stream 1 (which is a
* bidirectional stream). We keep reading until the server closes the
* connection.
*/
/* stream1からデータを読み取り(サーバーがFIN送信まで) */
while (SSL_read_ex(stream1, buf, sizeof(buf), &readbytes)) {
/*
* OpenSSL does not guarantee that the returned data is a string or
* that it is NUL terminated so we use fwrite() to write the exact
* number of bytes that we read. The data could be non-printable or
* have NUL characters in the middle of it. For this simple example
* we're going to print it to stdout anyway.
*/
fwrite(buf, 1, readbytes, stdout);
}
/* In case the response didn't finish with a newline we add one now */
printf("\n");
/*
* -------------------------------------------------------------------------
* 【ストリーム終了状態の確認】
* -------------------------------------------------------------------------
*
* SSL_ERROR_ZERO_RETURN: 正常終了(サーバーがFIN送信)
* SSL_ERROR_SSL: ストリームエラー
* - SSL_STREAM_STATE_RESET_REMOTE: サーバーがストリームをリセット
* - SSL_STREAM_STATE_CONN_CLOSED: 接続が閉じられた
*/
/*
* Check whether we finished the while loop above normally or as the
* result of an error. The 0 argument to SSL_get_error() is the return
* code we received from the SSL_read_ex() call. It must be 0 in order
* to get here. Normal completion is indicated by SSL_ERROR_ZERO_RETURN. In
* QUIC terms this means that the peer has sent FIN on the stream to
* indicate that no further data will be sent.
*/
switch (SSL_get_error(stream1, 0)) {
case SSL_ERROR_ZERO_RETURN:
/* Normal completion of the stream */
/* 正常終了: サーバーがストリームにFINを送信 */
break;
case SSL_ERROR_SSL:
/*
* Some stream fatal error occurred. This could be because of a stream
* reset - or some failure occurred on the underlying connection.
*/
/* ストリームエラー: 詳細を確認 */
switch (SSL_get_stream_read_state(stream1)) {
case SSL_STREAM_STATE_RESET_REMOTE:
printf("Stream reset occurred\n");
/* The stream has been reset but the connection is still healthy. */
/* ストリームはリセットされたが、接続は健全 */
break;
case SSL_STREAM_STATE_CONN_CLOSED:
printf("Connection closed\n");
/* Connection is already closed. Skip SSL_shutdown() */
/* 接続が閉じられた。SSL_shutdown()をスキップ */
goto end;
default:
printf("Unknown stream failure\n");
break;
}
break;
default:
/* Some other unexpected error occurred */
printf ("Failed reading remaining data\n");
break;
}
/*
* =========================================================================
* 【サーバー開始ストリームの受け入れ】
* =========================================================================
*
* SSL_accept_stream(): サーバーが開始したストリームを受け入れる
*
* このデモでは、stream2(単方向)で送ったリクエストに対して、
* サーバーが新しいストリームを開始してレスポンスを返すと仮定。
* (これは実際のプロトコルとしては不自然だが、デモ目的)
*
* フラグ:
* 0: ブロッキングモード(ストリームが来るまで待機)
* SSL_ACCEPT_STREAM_NO_BLOCK: 非ブロッキングモード
*/
/*
* In our hypothetical HTTP/1.0 over QUIC protocol that we are using we
* assume that the server will respond with a server initiated stream
* containing the data requested in our uni-directional stream. This doesn't
* really make sense to do in a real protocol, but its just for
* demonstration purposes.
*
* We're using blocking mode so this will block until a stream becomes
* available. We could override this behaviour if we wanted to by setting
* the SSL_ACCEPT_STREAM_NO_BLOCK flag in the second argument below.
*/
stream3 = SSL_accept_stream(ssl, 0); /* サーバー開始ストリームを受け入れ */
if (stream3 == NULL) {
printf("Failed to accept a new stream\n");
goto end;
}
/*
* -------------------------------------------------------------------------
* 【stream3からレスポンスを読み取り】
* -------------------------------------------------------------------------
*
* 注意: stream2は単方向(クライアント→サーバー)なので、
* stream2からの読み取りは不可。
*/
printf("Stream 3 data:\n");
/*
* Read the data from stream 3 like we did for stream 1 above. Note that
* stream 2 was uni-directional so there is no data to be read from that
* one.
*/
/* stream3からデータを読み取り */
while (SSL_read_ex(stream3, buf, sizeof(buf), &readbytes))
fwrite(buf, 1, readbytes, stdout);
printf("\n");
/* Check for errors on the stream */
/* stream3の終了状態を確認(stream1と同様) */
switch (SSL_get_error(stream3, 0)) {
case SSL_ERROR_ZERO_RETURN:
/* Normal completion of the stream */
/* 正常終了 */
break;
case SSL_ERROR_SSL:
switch (SSL_get_stream_read_state(stream3)) {
case SSL_STREAM_STATE_RESET_REMOTE:
printf("Stream reset occurred\n");
break;
case SSL_STREAM_STATE_CONN_CLOSED:
printf("Connection closed\n");
goto end;
default:
printf("Unknown stream failure\n");
break;
}
break;
default:
printf ("Failed reading remaining data\n");
break;
}
/*
* =========================================================================
* 【接続のシャットダウン】
* =========================================================================
*
* SSL_shutdown()は接続全体をシャットダウンする。
* 個別のストリームは自動的に閉じられる。
*
* 注意: SSL_shutdown()は接続オブジェクト(ssl)に対して呼ぶ。
* 個別のストリーム(stream1, stream2, stream3)には呼ばない。
*/
/*
* Repeatedly call SSL_shutdown() until the connection is fully
* closed.
*/
do {
ret = SSL_shutdown(ssl);
if (ret < 0) {
printf("Error shutting down: %d\n", ret);
goto end;
}
} while (ret != 1);
/* Success! 成功 */
res = EXIT_SUCCESS;
end:
/*
* =========================================================================
* 【クリーンアップ】
* =========================================================================
*
* 各ストリーム(stream1, stream2, stream3)も個別にSSL_free()が必要。
* 接続オブジェクト(ssl)のSSL_free()とは別。
*
* 注意: BIOはSSL_set_bio()でsslに所有権移転済みなので、
* SSL_free(ssl)時に自動的に解放される。
*/
/*
* If something bad happened then we will dump the contents of the
* OpenSSL error stack to stderr. There might be some useful diagnostic
* information there.
*/
/* エラー時はOpenSSLエラースタックを出力 */
if (res == EXIT_FAILURE)
ERR_print_errors_fp(stderr);
/*
* Free the resources we allocated. We do not free the BIO object here
* because ownership of it was immediately transferred to the SSL object
* via SSL_set_bio(). The BIO will be freed when we free the SSL object.
*/
SSL_free(ssl); /* 接続オブジェクト(BIOも自動解放) */
SSL_free(stream1); /* 双方向ストリーム */
SSL_free(stream2); /* 単方向ストリーム(クライアント開始) */
SSL_free(stream3); /* サーバー開始ストリーム */
SSL_CTX_free(ctx);
BIO_ADDR_free(peer_addr);
return res;
}
/* POSIX機能を有効にする(clock_gettime, localtime_r用) */
#ifndef _WIN32
# define _POSIX_C_SOURCE 199309L
#endif
/*
* Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* ファイルパス: demos/guide/quic-server-block-with-debug.c (OpenSSLリポジトリ内)
*
* 注意: このファイルの日本語コメントは Claude Code によって追加されました。
* オリジナルのソースコードは https://github.com/openssl/openssl にあります。
*/
/*
* ============================================================================
* quic-server-block-with-debug.c - デバッグ出力付きブロッキングモードQUICサーバー
* ============================================================================
*
* ※ 日本語コメントは Claude Code によって追加されました。
*
* quic-server-block.c に最大限のデバッグ出力を追加したバージョン。
* 全てのAPI呼び出し、データ内容、エラー詳細を出力する。
*/
#include <string.h>
#include <time.h>
#include <stdarg.h>
/* Include the appropriate header file for SOCK_STREAM */
#ifdef _WIN32 /* Windows */
# include <winsock2.h>
# include <ws2tcpip.h>
#else /* Linux/Unix */
# include <err.h>
# include <sys/socket.h>
# include <sys/select.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <unistd.h>
#endif
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/quic.h>
/* ============================================================================
* デバッグユーティリティ
* ============================================================================ */
/*
* DEBUG_LOG() - タイムスタンプ付きデバッグログ出力マクロ
*
* フォーマット: [HH:MM:SS.mmm] [関数名] メッセージ
*/
#define DEBUG_LOG(fmt, ...) do { \
struct timespec ts; \
struct tm tm_info; \
clock_gettime(CLOCK_REALTIME, &ts); \
localtime_r(&ts.tv_sec, &tm_info); \
fprintf(stderr, "[%02d:%02d:%02d.%03ld] [%s] " fmt "\n", \
tm_info.tm_hour, tm_info.tm_min, tm_info.tm_sec, \
ts.tv_nsec / 1000000, __func__, ##__VA_ARGS__); \
fflush(stderr); \
} while(0)
/*
* DEBUG_LOG_ENTER() - 関数開始ログ(関数名を表示)
*/
#define DEBUG_LOG_ENTER() DEBUG_LOG(">>> %s() 開始", __func__)
/*
* DEBUG_LOG_EXIT() - 関数終了ログ(関数名を表示)
*/
#define DEBUG_LOG_EXIT() DEBUG_LOG("<<< %s() 終了", __func__)
/*
* DEBUG_LOG_EXIT_WITH_RESULT() - 結果付き関数終了ログ(関数名を表示)
*/
#define DEBUG_LOG_EXIT_WITH_RESULT(result) DEBUG_LOG("<<< %s() 終了 (result=%d)", __func__, (int)(result))
/*
* debug_hexdump() - データの16進ダンプを出力
*
* @param prefix 出力行の先頭に付けるプレフィックス
* @param data ダンプするデータ
* @param len データの長さ
*/
static void debug_hexdump(const char *prefix, const unsigned char *data, size_t len)
{
size_t i, j;
fprintf(stderr, "%s (%zu bytes):\n", prefix, len);
for (i = 0; i < len; i += 16) {
/* オフセット */
fprintf(stderr, " %04zx: ", i);
/* 16進表示 */
for (j = 0; j < 16; j++) {
if (i + j < len)
fprintf(stderr, "%02x ", data[i + j]);
else
fprintf(stderr, " ");
if (j == 7)
fprintf(stderr, " ");
}
/* ASCII表示 */
fprintf(stderr, " |");
for (j = 0; j < 16 && i + j < len; j++) {
unsigned char c = data[i + j];
fprintf(stderr, "%c", (c >= 32 && c < 127) ? c : '.');
}
fprintf(stderr, "|\n");
}
fflush(stderr);
}
/*
* debug_print_ssl_error() - OpenSSL エラースタックの詳細を出力
*
* @param context エラーが発生したコンテキストの説明
*/
static void debug_print_ssl_error(const char *context)
{
unsigned long err;
char buf[256];
DEBUG_LOG("=== SSL エラー詳細 [%s] ===", context);
while ((err = ERR_get_error()) != 0) {
ERR_error_string_n(err, buf, sizeof(buf));
fprintf(stderr, " エラーコード: 0x%lx\n", err);
fprintf(stderr, " ライブラリ: %s\n", ERR_lib_error_string(err));
fprintf(stderr, " 関数: %s\n", ERR_func_error_string(err));
fprintf(stderr, " 理由: %s\n", ERR_reason_error_string(err));
fprintf(stderr, " 詳細: %s\n", buf);
fprintf(stderr, " ---\n");
}
fflush(stderr);
}
/*
* debug_print_ssl_state() - SSL接続の状態を出力
*
* @param ssl SSLオブジェクト
*/
static void debug_print_ssl_state(const SSL *ssl)
{
if (ssl == NULL) {
DEBUG_LOG("SSL オブジェクト: NULL");
return;
}
DEBUG_LOG("=== SSL 状態 ===");
fprintf(stderr, " 状態文字列: %s\n", SSL_state_string_long(ssl));
fprintf(stderr, " バージョン: %s\n", SSL_get_version(ssl));
/* ALPN */
const unsigned char *alpn_data;
unsigned int alpn_len;
SSL_get0_alpn_selected(ssl, &alpn_data, &alpn_len);
if (alpn_data != NULL && alpn_len > 0) {
fprintf(stderr, " 選択されたALPN: %.*s\n", alpn_len, alpn_data);
} else {
fprintf(stderr, " 選択されたALPN: (なし)\n");
}
/* 暗号スイート */
const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl);
if (cipher != NULL) {
fprintf(stderr, " 暗号スイート: %s\n", SSL_CIPHER_get_name(cipher));
fprintf(stderr, " 暗号強度: %d bits\n", SSL_CIPHER_get_bits(cipher, NULL));
}
fflush(stderr);
}
/*
* debug_print_connection_info() - 接続情報を出力
*
* @param ssl 接続のSSLオブジェクト
*/
static void debug_print_connection_info(const SSL *ssl)
{
BIO *bio;
int fd;
struct sockaddr_storage peer_addr;
socklen_t peer_len = sizeof(peer_addr);
char addr_str[INET6_ADDRSTRLEN];
uint16_t port = 0;
DEBUG_LOG("=== 接続情報 ===");
if (ssl == NULL) {
fprintf(stderr, " SSL: NULL\n");
fflush(stderr);
return;
}
/* BIOからFDを取得してピアアドレスを取得 */
bio = SSL_get_rbio(ssl);
if (bio != NULL && BIO_get_fd(bio, &fd) > 0) {
if (getpeername(fd, (struct sockaddr *)&peer_addr, &peer_len) == 0) {
if (peer_addr.ss_family == AF_INET) {
struct sockaddr_in *s = (struct sockaddr_in *)&peer_addr;
inet_ntop(AF_INET, &s->sin_addr, addr_str, sizeof(addr_str));
port = ntohs(s->sin_port);
} else if (peer_addr.ss_family == AF_INET6) {
struct sockaddr_in6 *s = (struct sockaddr_in6 *)&peer_addr;
inet_ntop(AF_INET6, &s->sin6_addr, addr_str, sizeof(addr_str));
port = ntohs(s->sin6_port);
}
fprintf(stderr, " ピアアドレス: %s:%u\n", addr_str, port);
} else {
fprintf(stderr, " ピアアドレス: (取得失敗: %s)\n", strerror(errno));
}
fprintf(stderr, " ファイルディスクリプタ: %d\n", fd);
} else {
fprintf(stderr, " BIO/FD: (取得失敗)\n");
}
fflush(stderr);
}
#ifdef _WIN32
static const char *progname;
static void vwarnx(const char *fmt, va_list ap)
{
if (progname != NULL)
fprintf(stderr, "%s: ", progname);
vfprintf(stderr, fmt, ap);
putc('\n', stderr);
}
static void errx(int status, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vwarnx(fmt, ap);
va_end(ap);
exit(status);
}
static void warnx(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vwarnx(fmt, ap);
va_end(ap);
}
#endif
/*
* ============================================================================
* ALPN (Application-Layer Protocol Negotiation) 設定
* ============================================================================
*/
static const unsigned char alpn_ossltest[] = {
8, 'h', 't', 't', 'p', '/', '1', '.', '0', /* http/1.0 */
10, 'h', 'q', '-', 'i', 'n', 't', 'e', 'r', 'o', 'p', /* hq-interop */
};
/*
* select_alpn() - ALPN選択コールバック(デバッグ出力付き)
*/
static int select_alpn(SSL *ssl, const unsigned char **out,
unsigned char *out_len, const unsigned char *in,
unsigned int in_len, void *arg)
{
int result;
DEBUG_LOG_ENTER();
DEBUG_LOG("SSLオブジェクト: %p", (void *)ssl);
/* クライアントが提示したALPNリストをダンプ */
debug_hexdump("クライアントALPNリスト", in, in_len);
/* サーバーがサポートするALPNリストをダンプ */
debug_hexdump("サーバーALPNリスト", alpn_ossltest, sizeof(alpn_ossltest));
/* ALPN交渉実行 */
DEBUG_LOG("SSL_select_next_proto() を呼び出し中...");
result = SSL_select_next_proto((unsigned char **)out, out_len, alpn_ossltest,
sizeof(alpn_ossltest), in, in_len);
DEBUG_LOG("SSL_select_next_proto() 戻り値: %d", result);
if (result == OPENSSL_NPN_NEGOTIATED) {
DEBUG_LOG("ALPN交渉成功!");
DEBUG_LOG("選択されたプロトコル: %.*s (長さ: %u)", *out_len, *out, *out_len);
DEBUG_LOG_EXIT_WITH_RESULT(SSL_TLSEXT_ERR_OK);
return SSL_TLSEXT_ERR_OK;
}
DEBUG_LOG("ALPN交渉失敗: 共通のプロトコルが見つかりません");
debug_print_ssl_error("ALPN交渉");
DEBUG_LOG_EXIT_WITH_RESULT(SSL_TLSEXT_ERR_ALERT_FATAL);
return SSL_TLSEXT_ERR_ALERT_FATAL;
}
/*
* ============================================================================
* create_ctx() - SSL_CTXの作成(デバッグ出力付き)
* ============================================================================
*/
static SSL_CTX *create_ctx(const char *cert_path, const char *key_path)
{
SSL_CTX *ctx;
int ret;
DEBUG_LOG_ENTER();
DEBUG_LOG("証明書パス: %s", cert_path);
DEBUG_LOG("秘密鍵パス: %s", key_path);
/* QUICサーバー用SSL_CTXを作成 */
DEBUG_LOG("SSL_CTX_new(OSSL_QUIC_server_method()) を呼び出し中...");
ctx = SSL_CTX_new(OSSL_QUIC_server_method());
if (ctx == NULL) {
DEBUG_LOG("SSL_CTX_new() 失敗!");
debug_print_ssl_error("SSL_CTX_new");
goto err;
}
DEBUG_LOG("SSL_CTX_new() 成功: ctx=%p", (void *)ctx);
/* 証明書チェーンのロード */
DEBUG_LOG("SSL_CTX_use_certificate_chain_file(%s) を呼び出し中...", cert_path);
ret = SSL_CTX_use_certificate_chain_file(ctx, cert_path);
DEBUG_LOG("SSL_CTX_use_certificate_chain_file() 戻り値: %d", ret);
if (ret <= 0) {
DEBUG_LOG("証明書ロード失敗!");
debug_print_ssl_error("SSL_CTX_use_certificate_chain_file");
fprintf(stderr, "couldn't load certificate file: %s\n", cert_path);
goto err;
}
DEBUG_LOG("証明書ロード成功");
/* 秘密鍵のロード */
DEBUG_LOG("SSL_CTX_use_PrivateKey_file(%s, SSL_FILETYPE_PEM) を呼び出し中...", key_path);
ret = SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM);
DEBUG_LOG("SSL_CTX_use_PrivateKey_file() 戻り値: %d", ret);
if (ret <= 0) {
DEBUG_LOG("秘密鍵ロード失敗!");
debug_print_ssl_error("SSL_CTX_use_PrivateKey_file");
fprintf(stderr, "couldn't load key file: %s\n", key_path);
goto err;
}
DEBUG_LOG("秘密鍵ロード成功");
/* 証明書と秘密鍵の整合性チェック */
DEBUG_LOG("SSL_CTX_check_private_key() を呼び出し中...");
ret = SSL_CTX_check_private_key(ctx);
DEBUG_LOG("SSL_CTX_check_private_key() 戻り値: %d", ret);
if (ret != 1) {
DEBUG_LOG("証明書と秘密鍵が一致しません!");
debug_print_ssl_error("SSL_CTX_check_private_key");
} else {
DEBUG_LOG("証明書と秘密鍵の整合性OK");
}
/* クライアント証明書検証の設定 */
DEBUG_LOG("SSL_CTX_set_verify(SSL_VERIFY_NONE) を呼び出し中...");
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
DEBUG_LOG("クライアント証明書検証: 無効 (SSL_VERIFY_NONE)");
/* ALPNコールバック設定 */
DEBUG_LOG("SSL_CTX_set_alpn_select_cb() を呼び出し中...");
SSL_CTX_set_alpn_select_cb(ctx, select_alpn, NULL);
DEBUG_LOG("ALPNコールバック設定完了");
DEBUG_LOG_EXIT();
DEBUG_LOG("SSL_CTX作成成功: %p", (void *)ctx);
return ctx;
err:
DEBUG_LOG("エラー発生、SSL_CTX_free() を呼び出し中...");
SSL_CTX_free(ctx);
DEBUG_LOG_EXIT_WITH_RESULT(0);
return NULL;
}
/*
* ============================================================================
* create_socket() - UDPソケットの作成(デバッグ出力付き)
* ============================================================================
*/
static int create_socket(uint16_t port)
{
int fd;
struct sockaddr_in sa = {0};
int ret;
char addr_str[INET_ADDRSTRLEN];
DEBUG_LOG_ENTER();
DEBUG_LOG("ポート: %u", port);
/* UDPソケットを作成 */
DEBUG_LOG("socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) を呼び出し中...");
fd = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
DEBUG_LOG("socket() 戻り値: %d", fd);
if (fd < 0) {
DEBUG_LOG("ソケット作成失敗: %s", strerror(errno));
fprintf(stderr, "cannot create socket");
goto err;
}
DEBUG_LOG("ソケット作成成功: fd=%d", fd);
/* アドレス設定 */
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
sa.sin_addr.s_addr = INADDR_ANY;
inet_ntop(AF_INET, &sa.sin_addr, addr_str, sizeof(addr_str));
DEBUG_LOG("バインドアドレス: %s:%u", addr_str, port);
/* ソケットをポートにバインド */
DEBUG_LOG("bind() を呼び出し中...");
ret = bind(fd, (const struct sockaddr *)&sa, sizeof(sa));
DEBUG_LOG("bind() 戻り値: %d", ret);
if (ret < 0) {
DEBUG_LOG("bind失敗: %s", strerror(errno));
fprintf(stderr, "cannot bind to %u\n", port);
BIO_closesocket(fd);
goto err;
}
DEBUG_LOG("bind成功");
DEBUG_LOG_EXIT_WITH_RESULT(fd);
return fd;
err:
DEBUG_LOG("エラー発生");
BIO_closesocket(fd);
DEBUG_LOG_EXIT_WITH_RESULT(-1);
return -1;
}
/*
* ============================================================================
* run_quic_server() - QUICサーバーのメインループ(デバッグ出力付き)
* ============================================================================
*/
static int run_quic_server(SSL_CTX *ctx, int fd)
{
int ok = 0;
SSL *listener, *conn;
unsigned char buf[8192];
size_t nread;
size_t nwritten;
int ret;
unsigned long conn_count = 0;
unsigned long read_count, write_count;
DEBUG_LOG_ENTER();
DEBUG_LOG("SSL_CTX: %p", (void *)ctx);
DEBUG_LOG("ソケットFD: %d", fd);
/* リスナーの作成 */
DEBUG_LOG("SSL_new_listener(ctx, 0) を呼び出し中...");
listener = SSL_new_listener(ctx, 0);
DEBUG_LOG("SSL_new_listener() 戻り値: %p", (void *)listener);
if (listener == NULL) {
DEBUG_LOG("リスナー作成失敗!");
debug_print_ssl_error("SSL_new_listener");
goto err;
}
DEBUG_LOG("リスナー作成成功");
/* リスナーにUDPソケットを関連付け */
DEBUG_LOG("SSL_set_fd(listener, %d) を呼び出し中...", fd);
ret = SSL_set_fd(listener, fd);
DEBUG_LOG("SSL_set_fd() 戻り値: %d", ret);
if (!ret) {
DEBUG_LOG("SSL_set_fd失敗!");
debug_print_ssl_error("SSL_set_fd");
goto err;
}
DEBUG_LOG("SSL_set_fd成功");
/* リッスン開始 */
DEBUG_LOG("SSL_listen(listener) を呼び出し中...");
ret = SSL_listen(listener);
DEBUG_LOG("SSL_listen() 戻り値: %d", ret);
if (!ret) {
DEBUG_LOG("SSL_listen失敗!");
debug_print_ssl_error("SSL_listen");
goto err;
}
DEBUG_LOG("リッスン開始成功");
DEBUG_LOG("========================================");
DEBUG_LOG("接続受け入れループ開始");
DEBUG_LOG("========================================");
/* 接続受け入れループ */
for (;;) {
conn_count++;
read_count = 0;
write_count = 0;
DEBUG_LOG("----------------------------------------");
DEBUG_LOG("接続 #%lu を待機中...", conn_count);
DEBUG_LOG("----------------------------------------");
/* エラースタックをクリア */
DEBUG_LOG("ERR_clear_error() を呼び出し中...");
ERR_clear_error();
/* 接続受け入れ */
printf("Waiting for connection\n");
DEBUG_LOG("SSL_accept_connection(listener, 0) を呼び出し中...");
conn = SSL_accept_connection(listener, 0);
DEBUG_LOG("SSL_accept_connection() 戻り値: %p", (void *)conn);
if (conn == NULL) {
DEBUG_LOG("接続受け入れ失敗!");
debug_print_ssl_error("SSL_accept_connection");
fprintf(stderr, "error while accepting connection\n");
goto err;
}
printf("Accepted new connection\n");
DEBUG_LOG("新しい接続を受け入れました: conn=%p", (void *)conn);
/* 接続情報を出力 */
debug_print_ssl_state(conn);
debug_print_connection_info(conn);
/* エコー処理ループ */
DEBUG_LOG("エコー処理ループ開始");
while (1) {
read_count++;
DEBUG_LOG("[Read #%lu] SSL_read_ex() を呼び出し中... (バッファサイズ: %zu)",
read_count, sizeof(buf));
ret = SSL_read_ex(conn, buf, sizeof(buf), &nread);
DEBUG_LOG("[Read #%lu] SSL_read_ex() 戻り値: %d, 読み取りバイト数: %zu",
read_count, ret, nread);
if (ret <= 0) {
int ssl_error = SSL_get_error(conn, ret);
DEBUG_LOG("[Read #%lu] SSL_get_error() = %d", read_count, ssl_error);
switch (ssl_error) {
case SSL_ERROR_ZERO_RETURN:
DEBUG_LOG(" -> SSL_ERROR_ZERO_RETURN: ピアがストリームを閉じました");
break;
case SSL_ERROR_WANT_READ:
DEBUG_LOG(" -> SSL_ERROR_WANT_READ: 読み取り待ち");
break;
case SSL_ERROR_WANT_WRITE:
DEBUG_LOG(" -> SSL_ERROR_WANT_WRITE: 書き込み待ち");
break;
case SSL_ERROR_SYSCALL:
DEBUG_LOG(" -> SSL_ERROR_SYSCALL: システムコールエラー (errno=%d: %s)",
errno, strerror(errno));
break;
case SSL_ERROR_SSL:
DEBUG_LOG(" -> SSL_ERROR_SSL: SSLプロトコルエラー");
debug_print_ssl_error("SSL_read_ex");
break;
default:
DEBUG_LOG(" -> 不明なエラー: %d", ssl_error);
}
break;
}
DEBUG_LOG("[Read #%lu] データ受信成功: %zu バイト", read_count, nread);
debug_hexdump("受信データ", buf, nread);
/* エコー(書き戻し) */
write_count++;
DEBUG_LOG("[Write #%lu] SSL_write_ex() を呼び出し中... (書き込みサイズ: %zu)",
write_count, nread);
ret = SSL_write_ex(conn, buf, nread, &nwritten);
DEBUG_LOG("[Write #%lu] SSL_write_ex() 戻り値: %d, 書き込みバイト数: %zu",
write_count, ret, nwritten);
if (ret <= 0 || nwritten != nread) {
int ssl_error = SSL_get_error(conn, ret);
DEBUG_LOG("[Write #%lu] 書き込みエラー!", write_count);
DEBUG_LOG(" SSL_get_error() = %d", ssl_error);
debug_print_ssl_error("SSL_write_ex");
fprintf(stderr, "Error echoing client input");
break;
}
DEBUG_LOG("[Write #%lu] エコー成功: %zu バイト", write_count, nwritten);
}
DEBUG_LOG("エコー処理ループ終了 (読み取り: %lu回, 書き込み: %lu回)",
read_count, write_count);
/* ストリーム終了 */
DEBUG_LOG("SSL_stream_conclude(conn, 0) を呼び出し中...");
ret = SSL_stream_conclude(conn, 0);
DEBUG_LOG("SSL_stream_conclude() 戻り値: %d", ret);
if (ret != 1) {
DEBUG_LOG("SSL_stream_conclude失敗!");
debug_print_ssl_error("SSL_stream_conclude");
fprintf(stderr, "Unable to conclude stream\n");
SSL_free(conn);
goto err;
}
DEBUG_LOG("ストリーム終了成功");
/* 接続シャットダウン */
DEBUG_LOG("SSL_shutdown() を呼び出し中...");
while (SSL_shutdown(conn) != 1)
continue;
DEBUG_LOG("SSL_shutdown() 完了");
/* 接続解放 */
DEBUG_LOG("SSL_free(conn) を呼び出し中...");
SSL_free(conn);
DEBUG_LOG("接続解放完了");
DEBUG_LOG("========================================");
DEBUG_LOG("接続 #%lu 処理完了", conn_count);
DEBUG_LOG("========================================\n");
}
err:
DEBUG_LOG("エラー発生、クリーンアップ中...");
DEBUG_LOG("SSL_free(listener) を呼び出し中...");
SSL_free(listener);
DEBUG_LOG_EXIT_WITH_RESULT(ok);
return ok;
}
/*
* ============================================================================
* main() - QUICサーバーのエントリポイント(デバッグ出力付き)
* ============================================================================
*/
int main(int argc, char *argv[])
{
int res = EXIT_FAILURE;
SSL_CTX *ctx = NULL;
int fd;
unsigned long port;
int i;
#ifdef _WIN32
static const char *progname;
progname = argv[0];
#endif
DEBUG_LOG("========================================");
DEBUG_LOG("QUIC サーバー (デバッグモード) 起動");
DEBUG_LOG("========================================");
DEBUG_LOG("OpenSSL バージョン: %s", OpenSSL_version(OPENSSL_VERSION));
DEBUG_LOG("引数の数: %d", argc);
for (i = 0; i < argc; i++) {
DEBUG_LOG(" argv[%d] = \"%s\"", i, argv[i]);
}
/* 引数チェック */
if (argc != 4) {
DEBUG_LOG("引数エラー: 4個の引数が必要です (現在: %d)", argc);
errx(res, "usage: %s <port> <server.crt> <server.key>", argv[0]);
}
DEBUG_LOG("----------------------------------------");
DEBUG_LOG("設定:");
DEBUG_LOG(" ポート: %s", argv[1]);
DEBUG_LOG(" 証明書: %s", argv[2]);
DEBUG_LOG(" 秘密鍵: %s", argv[3]);
DEBUG_LOG("----------------------------------------");
/* SSL_CTX作成 */
DEBUG_LOG("SSL_CTX を作成中...");
ctx = create_ctx(argv[2], argv[3]);
if (ctx == NULL) {
DEBUG_LOG("SSL_CTX 作成失敗!");
ERR_print_errors_fp(stderr);
errx(res, "Failed to create context");
}
DEBUG_LOG("SSL_CTX 作成成功: %p", (void *)ctx);
/* ポート番号解析 */
DEBUG_LOG("ポート番号を解析中: \"%s\"", argv[1]);
port = strtoul(argv[1], NULL, 0);
DEBUG_LOG("解析結果: %lu", port);
if (port == 0 || port > UINT16_MAX) {
DEBUG_LOG("ポート番号エラー: 0 < port <= %u である必要があります", UINT16_MAX);
SSL_CTX_free(ctx);
errx(res, "Failed to parse port number");
}
DEBUG_LOG("ポート番号OK: %lu", port);
/* UDPソケット作成 */
DEBUG_LOG("UDPソケットを作成中...");
fd = create_socket((uint16_t)port);
if (fd < 0) {
DEBUG_LOG("ソケット作成失敗!");
SSL_CTX_free(ctx);
ERR_print_errors_fp(stderr);
errx(res, "Failed to create socket");
}
DEBUG_LOG("ソケット作成成功: fd=%d", fd);
DEBUG_LOG("========================================");
DEBUG_LOG("サーバー起動準備完了");
DEBUG_LOG(" リッスンポート: %lu", port);
DEBUG_LOG(" ソケットFD: %d", fd);
DEBUG_LOG("========================================\n");
/* QUICサーバーのメインループを実行 */
DEBUG_LOG("run_quic_server() を呼び出し中...");
if (!run_quic_server(ctx, fd)) {
DEBUG_LOG("run_quic_server() がエラーで終了");
SSL_CTX_free(ctx);
BIO_closesocket(fd);
ERR_print_errors_fp(stderr);
errx(res, "Error in QUIC server loop");
}
/* クリーンアップ(通常ここには到達しない) */
DEBUG_LOG("クリーンアップ中...");
DEBUG_LOG("SSL_CTX_free() を呼び出し中...");
SSL_CTX_free(ctx);
DEBUG_LOG("BIO_closesocket() を呼び出し中...");
BIO_closesocket(fd);
DEBUG_LOG("クリーンアップ完了");
res = EXIT_SUCCESS;
DEBUG_LOG("プログラム終了 (exit code: %d)", res);
return res;
}
/*
* Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* ファイルパス: demos/guide/quic-server-block.c (OpenSSLリポジトリ内)
*
* 注意: このファイルの日本語コメントは Claude Code によって追加されました。
* オリジナルのソースコードは https://github.com/openssl/openssl にあります。
*/
/*
* ============================================================================
* quic-server-block.c - ブロッキングモードQUICサーバーデモ
* ============================================================================
*
* ※ 日本語コメントは Claude Code によって追加されました。
*
* 【概要】
* OpenSSL QUICを使用したシンプルなエコーサーバーのデモ。
* クライアントから受信したデータをそのまま返す(エコー)。
* ブロッキングモードで動作し、各操作は完了するまで待機する。
*
* 【アーキテクチャ図】
*
* ┌─────────────────────────────────────────────────────────────┐
* │ QUICサーバー │
* │ │
* │ ┌─────────────┐ │
* │ │ SSL_CTX │ サーバー設定(証明書、秘密鍵、ALPN) │
* │ └──────┬──────┘ │
* │ │ │
* │ ▼ │
* │ ┌─────────────┐ │
* │ │ Listener │ SSL_new_listener() で作成 │
* │ │ (SSL) │ 接続要求を待ち受ける │
* │ └──────┬──────┘ │
* │ │ SSL_accept_connection() │
* │ ▼ │
* │ ┌─────────────┐ │
* │ │ Connection │ 個別のQUIC接続 │
* │ │ (SSL) │ SSL_read_ex/SSL_write_ex でデータ送受信 │
* │ └─────────────┘ │
* │ │
* └─────────────────────────────────────────────────────────────┘
* │
* │ UDP
* ▼
* ┌───────────┐
* │ Client │
* └───────────┘
*
* 【処理フロー】
*
* main()
* │
* ├── create_ctx() ─── SSL_CTX作成(証明書・秘密鍵ロード)
* │
* ├── create_socket() ─── UDPソケット作成・bind
* │
* └── run_quic_server()
* │
* ├── SSL_new_listener() ─── リスナー作成
* ├── SSL_set_fd() ─── ソケット関連付け
* ├── SSL_listen() ─── リッスン開始
* │
* └── for(;;) ─── 接続受け入れループ
* │
* ├── SSL_accept_connection() ─── 新規接続受け入れ
* │
* ├── SSL_read_ex() / SSL_write_ex() ─── エコー処理
* │
* ├── SSL_stream_conclude() ─── ストリーム終了
* │
* ├── SSL_shutdown() ─── 接続シャットダウン
* │
* └── SSL_free() ─── 接続解放
*
* 【QUICサーバーの主要API】
*
* - SSL_new_listener(): QUICリスナーを作成
* - SSL_listen(): リッスン開始
* - SSL_accept_connection(): 新規接続を受け入れ
* - SSL_stream_conclude(): ストリーム送信終了(FIN送信)
*
* 【使用方法】
* ./quic-server-block <port> <server.crt> <server.key>
*
* 【関連ドキュメント】
* doc/man7/ossl-guide-quic-server-block.pod
*/
/*
* NB: Changes to this file should also be reflected in
* doc/man7/ossl-guide-quic-server-block.pod
*/
#include <string.h>
/* Include the appropriate header file for SOCK_STREAM */
#ifdef _WIN32 /* Windows */
# include <stdarg.h>
# include <winsock2.h>
#else /* Linux/Unix */
# include <err.h>
# include <sys/socket.h>
# include <sys/select.h>
# include <netinet/in.h>
# include <unistd.h>
#endif
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/quic.h>
#ifdef _WIN32
static const char *progname;
static void vwarnx(const char *fmt, va_list ap)
{
if (progname != NULL)
fprintf(stderr, "%s: ", progname);
vfprintf(stderr, fmt, ap);
putc('\n', stderr);
}
static void errx(int status, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vwarnx(fmt, ap);
va_end(ap);
exit(status);
}
static void warnx(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vwarnx(fmt, ap);
va_end(ap);
}
#endif
/*
* ============================================================================
* ALPN (Application-Layer Protocol Negotiation) 設定
* ============================================================================
*
* サーバーがサポートするALPNプロトコルのリスト。
* フォーマット: { 長さ, プロトコル名... } の繰り返し
*
* このサーバーは以下のプロトコルを受け入れる:
* - "http/1.0" (8バイト): HTTP/1.0 over QUIC(デモ用)
* - "hq-interop" (10バイト): QUIC相互運用性テスト用プロトコル
*/
/*
* ALPN strings for TLS handshake. Only 'http/1.0' and 'hq-interop'
* are accepted.
*/
static const unsigned char alpn_ossltest[] = {
8, 'h', 't', 't', 'p', '/', '1', '.', '0', /* http/1.0 */
10, 'h', 'q', '-', 'i', 'n', 't', 'e', 'r', 'o', 'p', /* hq-interop */
};
/*
* select_alpn() - ALPN選択コールバック
*
* TLSハンドシェイク中に呼び出され、クライアントが提示したALPNリストと
* サーバーがサポートするプロトコルを照合する。
*
* @param ssl SSLオブジェクト
* @param out [出力] 選択されたプロトコルへのポインタ
* @param out_len [出力] 選択されたプロトコルの長さ
* @param in クライアントが提示したALPNリスト
* @param in_len クライアントALPNリストの長さ
* @param arg ユーザーデータ(未使用)
* @return SSL_TLSEXT_ERR_OK: 成功, SSL_TLSEXT_ERR_ALERT_FATAL: 失敗
*/
/*
* This callback validates and negotiates the desired ALPN on the server side.
*/
static int select_alpn(SSL *ssl, const unsigned char **out,
unsigned char *out_len, const unsigned char *in,
unsigned int in_len, void *arg)
{
/*
* SSL_select_next_proto(): サーバーとクライアントのALPNリストを照合
* OPENSSL_NPN_NEGOTIATED: 共通のプロトコルが見つかった
*/
if (SSL_select_next_proto((unsigned char **)out, out_len, alpn_ossltest,
sizeof(alpn_ossltest), in,
in_len) == OPENSSL_NPN_NEGOTIATED)
return SSL_TLSEXT_ERR_OK; /* 交渉成功 */
return SSL_TLSEXT_ERR_ALERT_FATAL; /* 交渉失敗: ハンドシェイク中止 */
}
/*
* ============================================================================
* create_ctx() - SSL_CTXの作成
* ============================================================================
*
* QUICサーバー用のSSL_CTXを作成し、証明書と秘密鍵をロードする。
*
* 【クライアントとの違い】
* - OSSL_QUIC_server_method() を使用(クライアントはOSSL_QUIC_client_method)
* - サーバー証明書と秘密鍵のロードが必要
* - クライアント証明書の検証は無効(SSL_VERIFY_NONE)
* - ALPNコールバックを設定してプロトコル交渉を処理
*
* @param cert_path サーバー証明書チェーンファイルのパス(PEM形式)
* @param key_path サーバー秘密鍵ファイルのパス(PEM形式)
* @return 成功時はSSL_CTX、失敗時はNULL
*/
/* Create SSL_CTX. */
static SSL_CTX *create_ctx(const char *cert_path, const char *key_path)
{
SSL_CTX *ctx;
/*
* An SSL_CTX holds shared configuration information for multiple
* subsequent per-client connections. We specifically load a QUIC
* server method here.
*/
/* QUICサーバー用SSL_CTXを作成 */
ctx = SSL_CTX_new(OSSL_QUIC_server_method());
if (ctx == NULL)
goto err;
/*
* -------------------------------------------------------------------------
* 【サーバー証明書チェーンのロード】
* -------------------------------------------------------------------------
*
* 証明書チェーンファイルには以下が含まれる(PEM形式):
* 1. リーフ証明書(サーバー証明書)- 必ず最初
* 2. 中間CA証明書(あれば)
*
* 注意: 複数の公開鍵アルゴリズム(RSA、ECDSAなど)に対応する場合は、
* 各アルゴリズムごとに証明書と秘密鍵をロードできる。
*/
/*
* Load the server's certificate *chain* file (PEM format), which includes
* not only the leaf (end-entity) server certificate, but also any
* intermediate issuer-CA certificates. The leaf certificate must be the
* first certificate in the file.
*
* In advanced use-cases this can be called multiple times, once per public
* key algorithm for which the server has a corresponding certificate.
* However, the corresponding private key (see below) must be loaded first,
* *before* moving on to the next chain file.
*
* The requisite files "chain.pem" and "pkey.pem" can be generated by running
* "make chain" in this directory. If the server will be executed from some
* other directory, move or copy the files there.
*/
if (SSL_CTX_use_certificate_chain_file(ctx, cert_path) <= 0) {
fprintf(stderr, "couldn't load certificate file: %s\n", cert_path);
goto err;
}
/*
* -------------------------------------------------------------------------
* 【秘密鍵のロード】
* -------------------------------------------------------------------------
*
* 証明書に対応する秘密鍵をロード。
* 秘密鍵と証明書の対応関係も自動的に検証される。
*/
/*
* Load the corresponding private key, this also checks that the private
* key matches the just loaded end-entity certificate. It does not check
* whether the certificate chain is valid, the certificates could be
* expired, or may otherwise fail to form a chain that a client can validate.
*/
if (SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM) <= 0) {
fprintf(stderr, "couldn't load key file: %s\n", key_path);
goto err;
}
/*
* -------------------------------------------------------------------------
* 【クライアント証明書検証の設定】
* -------------------------------------------------------------------------
*
* SSL_VERIFY_NONE: クライアント証明書を要求しない
* - 一般的なWebサーバーではクライアント証明書は不要
* - 相互TLS認証が必要な場合はSSL_VERIFY_PEERを使用
*/
/*
* Clients rarely employ certificate-based authentication, and so we don't
* require "mutual" TLS authentication (indeed there's no way to know
* whether or how the client authenticated the server, so the term "mutual"
* is potentially misleading).
*
* Since we're not soliciting or processing client certificates, we don't
* need to configure a trusted-certificate store, so no call to
* SSL_CTX_set_default_verify_paths() is needed. The server's own
* certificate chain is assumed valid.
*/
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
/* Setup ALPN negotiation callback to decide which ALPN is accepted. */
/* ALPN選択コールバックを設定 */
SSL_CTX_set_alpn_select_cb(ctx, select_alpn, NULL);
return ctx;
err:
SSL_CTX_free(ctx);
return NULL;
}
/*
* ============================================================================
* create_socket() - UDPソケットの作成
* ============================================================================
*
* 指定されたポートでリッスンするUDPソケットを作成する。
*
* 【クライアントとの違い】
* - サーバー: bind()でポートにバインド、connect()は不要
* - クライアント: connect()でサーバーに接続
*
* @param port リッスンするポート番号
* @return 成功時はソケットFD、失敗時は-1
*/
/* Create UDP socket on the given port. */
static int create_socket(uint16_t port)
{
int fd;
struct sockaddr_in sa = {0};
/* Retrieve the file descriptor for a new UDP socket */
/* UDPソケットを作成 */
if ((fd = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
fprintf(stderr, "cannot create socket");
goto err;
}
/* IPv4、指定ポート、全インターフェースでリッスン */
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
/* sa.sin_addr は 0 (INADDR_ANY) のまま = 全インターフェース */
/* Bind to the new UDP socket on localhost */
/* ソケットをポートにバインド */
if (bind(fd, (const struct sockaddr *)&sa, sizeof(sa)) < 0) {
fprintf(stderr, "cannot bind to %u\n", port);
BIO_closesocket(fd);
goto err;
}
return fd;
err:
BIO_closesocket(fd);
return -1;
}
/*
* ============================================================================
* run_quic_server() - QUICサーバーのメインループ
* ============================================================================
*
* QUICリスナーを作成し、接続を受け入れてエコー処理を行う。
*
* 【QUICサーバーの階層構造】
*
* SSL_CTX (設定共有)
* │
* ▼
* Listener (SSL_new_listener)
* │ 接続要求を待ち受け
* │
* ▼ SSL_accept_connection()
* Connection (SSL)
* │ 個別のQUIC接続
* │ SSL_read_ex/SSL_write_ex でデータ送受信
* ▼
*
* @param ctx SSL_CTX(サーバー設定)
* @param fd UDPソケットのファイルディスクリプタ
* @return 成功時は1(実際には無限ループなので到達しない)、失敗時は0
*/
/*
* Main loop for server to accept QUIC connections.
* Echo every request back to the client.
*/
static int run_quic_server(SSL_CTX *ctx, int fd)
{
int ok = 0;
SSL *listener, *conn; /* listener: リスナー, conn: 個別接続 */
unsigned char buf[8192];
size_t nread;
size_t nwritten;
/*
* =========================================================================
* 【リスナーの作成と初期化】
* =========================================================================
*
* SSL_new_listener(): QUICリスナーを作成
* - 複数のクライアント接続を受け入れる基盤
* - デフォルトでブロッキングモード
* - 子オブジェクト(接続)は親の設定を継承
*/
/*
* Create a new QUIC listener. Listeners, and other QUIC objects, default
* to operating in blocking mode. The configured behaviour is inherited by
* child objects.
*/
if ((listener = SSL_new_listener(ctx, 0)) == NULL)
goto err;
/* Provide the listener with our UDP socket. */
/* リスナーにUDPソケットを関連付け */
if (!SSL_set_fd(listener, fd))
goto err;
/* Begin listening. */
/* リッスン開始 */
if (!SSL_listen(listener))
goto err;
/*
* =========================================================================
* 【接続受け入れループ】
* =========================================================================
*
* 無限ループで接続を受け入れ、各接続に対してエコー処理を行う。
* エラーが発生した場合のみループを抜ける。
*/
/*
* Begin an infinite loop of listening for connections. We will only
* exit this loop if we encounter an error.
*/
for (;;) {
/* Pristine error stack for each new connection */
/* 新しい接続ごとにエラースタックをクリア */
ERR_clear_error();
/*
* ---------------------------------------------------------------------
* 【接続の受け入れ】
* ---------------------------------------------------------------------
*
* SSL_accept_connection(): 新しいQUIC接続を受け入れ
* - ブロッキングモードでは接続が来るまで待機
* - 返されるSSLオブジェクトは個別の接続を表す
*/
/* Block while waiting for a client connection */
printf("Waiting for connection\n");
conn = SSL_accept_connection(listener, 0);
if (conn == NULL) {
fprintf(stderr, "error while accepting connection\n");
goto err;
}
printf("Accepted new connection\n");
/*
* ---------------------------------------------------------------------
* 【エコー処理】
* ---------------------------------------------------------------------
*
* クライアントからデータを読み取り、そのまま送り返す。
* クライアントがストリームを閉じるまで(FIN受信まで)継続。
*/
/* Echo client input */
while (SSL_read_ex(conn, buf, sizeof(buf), &nread) > 0) {
if (SSL_write_ex(conn, buf, nread, &nwritten) > 0
&& nwritten == nread)
continue;
fprintf(stderr, "Error echoing client input");
break;
}
/*
* ---------------------------------------------------------------------
* 【ストリーム終了とシャットダウン】
* ---------------------------------------------------------------------
*
* SSL_stream_conclude(): 送信側ストリームを終了(FIN送信)
* - これ以上データを送らないことをクライアントに通知
*
* SSL_shutdown(): QUIC接続全体をシャットダウン
* - 完了するまで複数回呼び出す必要がある場合がある
*/
/* Signal the end of the stream. */
if (SSL_stream_conclude(conn, 0) != 1) {
fprintf(stderr, "Unable to conclude stream\n");
SSL_free(conn);
goto err;
}
/*
* Shut down the connection. We may need to call this multiple times
* to ensure the connection is shutdown completely.
*/
/* 接続のシャットダウン(完了まで繰り返し呼び出し) */
while (SSL_shutdown(conn) != 1)
continue;
/* 接続オブジェクトを解放して次の接続を待つ */
SSL_free(conn);
}
err:
SSL_free(listener);
return ok;
}
/*
* ============================================================================
* main() - QUICサーバーのエントリポイント
* ============================================================================
*
* コマンドライン引数を解析し、SSL_CTXとソケットを作成してサーバーを起動する。
*
* 【使用方法】
* ./quic-server-block <port> <server.crt> <server.key>
*
* 【引数】
* port - リッスンするポート番号
* server.crt - サーバー証明書(PEM形式)
* server.key - サーバー秘密鍵(PEM形式)
*/
/* Minimal QUIC HTTP/1.0 server. */
int main(int argc, char *argv[])
{
int res = EXIT_FAILURE;
SSL_CTX *ctx = NULL;
int fd;
unsigned long port;
#ifdef _WIN32
static const char *progname;
progname = argv[0];
#endif
/* 引数チェック */
if (argc != 4)
errx(res, "usage: %s <port> <server.crt> <server.key>", argv[0]);
/*
* -------------------------------------------------------------------------
* 【初期化】
* -------------------------------------------------------------------------
*/
/* Create SSL_CTX that supports QUIC. */
/* QUIC対応SSL_CTXを作成(証明書・秘密鍵ロード) */
if ((ctx = create_ctx(argv[2], argv[3])) == NULL) {
ERR_print_errors_fp(stderr);
errx(res, "Failed to create context");
}
/* Parse port number from command line arguments. */
/* ポート番号を解析 */
port = strtoul(argv[1], NULL, 0);
if (port == 0 || port > UINT16_MAX) {
SSL_CTX_free(ctx);
errx(res, "Failed to parse port number");
}
/* Create and bind a UDP socket. */
/* UDPソケットを作成しポートにバインド */
if ((fd = create_socket((uint16_t)port)) < 0) {
SSL_CTX_free(ctx);
ERR_print_errors_fp(stderr);
errx(res, "Failed to create socket");
}
/*
* -------------------------------------------------------------------------
* 【サーバー起動】
* -------------------------------------------------------------------------
*/
/* QUIC server connection acceptance loop. */
/* QUICサーバーのメインループを実行(無限ループ) */
if (!run_quic_server(ctx, fd)) {
SSL_CTX_free(ctx);
BIO_closesocket(fd);
ERR_print_errors_fp(stderr);
errx(res, "Error in QUIC server loop");
}
/*
* -------------------------------------------------------------------------
* 【クリーンアップ】
* -------------------------------------------------------------------------
* 注意: run_quic_server()は通常無限ループなので、ここには到達しない。
* エラー時のみここに来る。
*/
/* Free resources. */
SSL_CTX_free(ctx);
BIO_closesocket(fd);
res = EXIT_SUCCESS;
return res;
}
/*
* Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the Apache License 2.0 (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
/*
* ファイルパス: demos/guide/quic-server-non-block.c (OpenSSLリポジトリ内)
*
* 注意: このファイルの日本語コメントは Claude Code によって追加されました。
* オリジナルのソースコードは https://github.com/openssl/openssl にあります。
*/
/*
* ============================================================================
* quic-server-non-block.c - 非ブロッキングモードQUICサーバーデモ
* ============================================================================
*
* ※ 日本語コメントは Claude Code によって追加されました。
*
* 【概要】
* OpenSSL QUICを使用した非ブロッキングエコーサーバーのデモ。
* select()を使用してソケット活動を待機し、I/O操作が一時的に
* 失敗した場合はリトライする。
*
* 【quic-server-block.cとの違い】
* - quic-server-block.c: ブロッキングモード。操作完了まで待機。
* - このファイル: 非ブロッキングモード。select()で明示的に待機。
*
* 【非ブロッキングモードの利点】
* - 複数の接続を1つのスレッドで同時に処理可能
* - GUIアプリケーションでUIがフリーズしない
* - タイムアウトや他のイベントとの統合が容易
* - より細かいI/O制御が可能
*
* 【処理フロー図】
*
* main()
* │
* ├── create_ctx() ─── SSL_CTX作成(証明書・秘密鍵ロード)
* │
* ├── create_socket() ─── UDPソケット作成・bind・非ブロッキング設定
* │
* └── run_quic_server()
* │
* ├── SSL_new_listener() ─── リスナー作成
* ├── SSL_set_fd() ─── ソケット関連付け
* ├── SSL_set_blocking_mode(0) ─── 非ブロッキングモード設定 ★
* ├── SSL_listen() ─── リッスン開始
* │
* └── for(;;) ─── 接続受け入れループ
* │
* ├── SSL_accept_connection() ループ
* │ └── 失敗時 → wait_for_activity() → リトライ
* │
* ├── SSL_read_ex() ループ(全データ読み取りまで)
* │ └── 失敗時 → handle_io_failure() → リトライ
* │
* ├── SSL_write_ex2() ループ(SSL_WRITE_FLAG_CONCLUDE付き)
* │ └── 失敗時 → handle_io_failure() → リトライ
* │
* ├── SSL_shutdown() ループ
* │ └── 失敗時 → handle_io_failure() → リトライ
* │
* └── SSL_free()
*
* 【非ブロッキングサーバーの主要パターン】
*
* 1. SSL_set_blocking_mode(listener, 0) でリスナーを非ブロッキングに
* → 子オブジェクト(接続)も非ブロッキングを継承
*
* 2. 各SSL操作をwhileループでラップ
* → handle_io_failure()でリトライ判定
*
* 3. wait_for_activity()でソケット活動を待機
* → SSL_net_read_desired(), SSL_net_write_desired()で方向確認
* → SSL_get_event_timeout()でタイムアウト取得
*
* 【使用方法】
* ./quic-server-non-block <port> <server.crt> <server.key>
*
* 【関連ドキュメント】
* doc/man7/ossl-guide-quic-server-non-block.pod
*/
/*
* NB: Changes to this file should also be reflected in
* doc/man7/ossl-guide-quic-server-non-block.pod
*/
#include <string.h>
/* Include the appropriate header file for SOCK_STREAM */
#ifdef _WIN32 /* Windows */
# include <stdarg.h>
# include <winsock2.h>
#else /* Linux/Unix */
# include <err.h>
# include <sys/socket.h>
# include <sys/select.h>
# include <netinet/in.h>
# include <unistd.h>
#endif
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/quic.h>
#ifdef _WIN32
static const char *progname;
static void vwarnx(const char *fmt, va_list ap)
{
if (progname != NULL)
fprintf(stderr, "%s: ", progname);
vfprintf(stderr, fmt, ap);
putc('\n', stderr);
}
static void errx(int status, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vwarnx(fmt, ap);
va_end(ap);
exit(status);
}
static void warnx(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vwarnx(fmt, ap);
va_end(ap);
}
#endif
/*
* ============================================================================
* ALPN (Application-Layer Protocol Negotiation) 設定
* ============================================================================
*
* サーバーがサポートするALPNプロトコルのリスト。
* quic-server-block.cと同一。
*/
/*
* ALPN strings for TLS handshake. Only 'http/1.0' and 'hq-interop'
* are accepted.
*/
static const unsigned char alpn_ossltest[] = {
8, 'h', 't', 't', 'p', '/', '1', '.', '0', /* http/1.0 */
10, 'h', 'q', '-', 'i', 'n', 't', 'e', 'r', 'o', 'p', /* hq-interop */
};
/*
* select_alpn() - ALPN選択コールバック
*
* quic-server-block.cと同一の実装。
*/
/*
* This callback validates and negotiates the desired ALPN on the server side.
*/
static int select_alpn(SSL *ssl, const unsigned char **out,
unsigned char *out_len, const unsigned char *in,
unsigned int in_len, void *arg)
{
if (SSL_select_next_proto((unsigned char **)out, out_len, alpn_ossltest,
sizeof(alpn_ossltest), in,
in_len) == OPENSSL_NPN_NEGOTIATED)
return SSL_TLSEXT_ERR_OK; /* 交渉成功 */
return SSL_TLSEXT_ERR_ALERT_FATAL; /* 交渉失敗 */
}
/*
* ============================================================================
* create_ctx() - SSL_CTXの作成
* ============================================================================
*
* quic-server-block.cと同一の実装。
* 証明書・秘密鍵のロード、ALPN設定を行う。
*/
/* Create SSL_CTX. */
static SSL_CTX *create_ctx(const char *cert_path, const char *key_path)
{
SSL_CTX *ctx;
/*
* An SSL_CTX holds shared configuration information for multiple
* subsequent per-client connections. We specifically load a QUIC
* server method here.
*/
/* QUICサーバー用SSL_CTXを作成 */
ctx = SSL_CTX_new(OSSL_QUIC_server_method());
if (ctx == NULL)
goto err;
/*
* Load the server's certificate *chain* file (PEM format), which includes
* not only the leaf (end-entity) server certificate, but also any
* intermediate issuer-CA certificates. The leaf certificate must be the
* first certificate in the file.
*
* In advanced use-cases this can be called multiple times, once per public
* key algorithm for which the server has a corresponding certificate.
* However, the corresponding private key (see below) must be loaded first,
* *before* moving on to the next chain file.
*
* The requisite files "chain.pem" and "pkey.pem" can be generated by running
* "make chain" in this directory. If the server will be executed from some
* other directory, move or copy the files there.
*/
/* サーバー証明書チェーンをロード */
if (SSL_CTX_use_certificate_chain_file(ctx, cert_path) <= 0) {
fprintf(stderr, "couldn't load certificate file: %s\n", cert_path);
goto err;
}
/*
* Load the corresponding private key, this also checks that the private
* key matches the just loaded end-entity certificate. It does not check
* whether the certificate chain is valid, the certificates could be
* expired, or may otherwise fail to form a chain that a client can validate.
*/
/* 秘密鍵をロード */
if (SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM) <= 0) {
fprintf(stderr, "couldn't load key file: %s\n", key_path);
goto err;
}
/*
* Clients rarely employ certificate-based authentication, and so we don't
* require "mutual" TLS authentication (indeed there's no way to know
* whether or how the client authenticated the server, so the term "mutual"
* is potentially misleading).
*
* Since we're not soliciting or processing client certificates, we don't
* need to configure a trusted-certificate store, so no call to
* SSL_CTX_set_default_verify_paths() is needed. The server's own
* certificate chain is assumed valid.
*/
/* クライアント証明書検証は無効 */
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
/* Setup ALPN negotiation callback to decide which ALPN is accepted. */
/* ALPN選択コールバックを設定 */
SSL_CTX_set_alpn_select_cb(ctx, select_alpn, NULL);
return ctx;
err:
SSL_CTX_free(ctx);
return NULL;
}
/*
* ============================================================================
* create_socket() - UDPソケットの作成(非ブロッキング版)
* ============================================================================
*
* 【quic-server-block.cとの違い】
* - BIO_socket_nbio()でソケットを非ブロッキングモードに設定
*
* @param port リッスンするポート番号
* @return 成功時はソケットFD、失敗時は-1
*/
/* Create UDP socket on the given port. */
static int create_socket(uint16_t port)
{
int fd;
struct sockaddr_in sa = {0};
/* Retrieve the file descriptor for a new UDP socket */
/* UDPソケットを作成 */
if ((fd = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
fprintf(stderr, "cannot create socket");
return -1;
}
/* IPv4、指定ポート、全インターフェースでリッスン */
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
/* Bind to the new UDP socket on localhost */
/* ソケットをポートにバインド */
if (bind(fd, (const struct sockaddr *)&sa, sizeof(sa)) < 0) {
fprintf(stderr, "cannot bind to %u\n", port);
BIO_closesocket(fd);
return -1;
}
/*
* 【重要】ソケットを非ブロッキングモードに設定
* これがquic-server-block.cとの違い。
* 非ブロッキングソケットでは、I/O操作が即座に完了しない場合に
* エラーを返す(ブロックしない)。
*/
/* Set port to nonblocking mode */
if (BIO_socket_nbio(fd, 1) <= 0) {
fprintf(stderr, "Unable to set port to nonblocking mode");
BIO_closesocket(fd);
return -1;
}
return fd;
}
/*
* ============================================================================
* wait_for_activity() - ソケット活動を待機
* ============================================================================
*
* 非ブロッキングモードで重要な関数。select()を使用して、
* ソケットが読み書き可能になるまで、またはタイムアウトまで待機する。
*
* 【処理フロー】
* 1. SSL_get_fd()でソケットFDを取得
* 2. SSL_net_write_desired()で書き込み必要性を確認
* 3. SSL_net_read_desired()で読み取り必要性を確認
* 4. SSL_get_event_timeout()でQUICタイムアウトを取得
* 5. select()で待機
*
* quic-client-non-block.cのwait_for_activity()と同様の実装。
*
* @param ssl SSLオブジェクト(リスナーまたは接続)
*/
/**
* @brief Waits for activity on the SSL socket, either for reading or writing.
*
* This function monitors the underlying file descriptor of the given SSL
* connection to determine when it is ready for reading or writing, or both.
* It uses the select function to wait until the socket is either readable
* or writable, depending on what the SSL connection requires.
*
* @param ssl A pointer to the SSL object representing the connection.
*
* @note This function blocks until there is activity on the socket. In a real
* application, you might want to perform other tasks while waiting, such as
* updating a GUI or handling other connections.
*
* @note This function uses select for simplicity and portability. Depending
* on your application's requirements, you might consider using other
* mechanisms like poll or epoll for handling multiple file descriptors.
*/
static void wait_for_activity(SSL *ssl)
{
int sock, isinfinite;
fd_set read_fd, write_fd;
struct timeval tv;
struct timeval *tvp = NULL; /* タイムアウト(NULLなら無期限) */
/* Get hold of the underlying file descriptor for the socket */
/* ソケットFDを取得 */
if ((sock = SSL_get_fd(ssl)) == -1) {
fprintf(stderr, "Unable to get file descriptor");
return;
}
/* Initialize the fd_set structure */
FD_ZERO(&read_fd);
FD_ZERO(&write_fd);
/*
* Determine if we would like to write to the socket, read from it, or both.
*/
/*
* QUICスタックが必要とするI/O方向を確認。
* SSL_net_write_desired(): 送信が必要か
* SSL_net_read_desired(): 受信が必要か
*/
if (SSL_net_write_desired(ssl))
FD_SET(sock, &write_fd);
if (SSL_net_read_desired(ssl))
FD_SET(sock, &read_fd);
/*
* Find out when OpenSSL would next like to be called, regardless of
* whether the state of the underlying socket has changed or not.
*/
/*
* QUICタイマーによる次のタイムアウトを取得。
* ACK遅延、PTO(パケットロス検出)、アイドルタイムアウト等。
*/
if (SSL_get_event_timeout(ssl, &tv, &isinfinite) && !isinfinite)
tvp = &tv;
/*
* Wait until the socket is writeable or readable. We use select here
* for the sake of simplicity and portability, but you could equally use
* poll/epoll or similar functions
*
* NOTE: For the purposes of this demonstration code this effectively
* makes this demo block until it has something more useful to do. In a
* real application you probably want to go and do other work here (e.g.
* update a GUI, or service other connections).
*
* Let's say for example that you want to update the progress counter on
* a GUI every 100ms. One way to do that would be to use the timeout in
* the last parameter to "select" below. If the tvp value is greater
* than 100ms then use 100ms instead. Then, when select returns, you
* check if it did so because of activity on the file descriptors or
* because of the timeout. If the 100ms GUI timeout has expired but the
* tvp timeout has not then go and update the GUI and then restart the
* "select" (with updated timeouts).
*/
/* select()でソケット活動またはタイムアウトを待機 */
select(sock + 1, &read_fd, &write_fd, NULL, tvp);
}
/*
* ============================================================================
* handle_io_failure() - I/O失敗時のエラーハンドリング
* ============================================================================
*
* 非ブロッキングモードでSSL操作が失敗した際に呼び出される。
* エラーの種類に応じて、リトライ可能かどうかを判断する。
*
* 【戻り値】
* 1: リトライ可能(wait_for_activity()後に再試行)
* 0: EOF(正常終了)
* -1: 致命的エラー(リトライ不可)
*
* 【エラーコードの意味】
* SSL_ERROR_WANT_READ/WRITE: 一時的な失敗、リトライ可能
* SSL_ERROR_ZERO_RETURN: EOF(正常終了)
* SSL_ERROR_NONE: エラーなし(EOFとして扱う)
* SSL_ERROR_SYSCALL: システムコールエラー
* SSL_ERROR_SSL: SSLレベルのエラー
*
* quic-client-non-block.cのhandle_io_failure()と類似の実装。
*/
/**
* @brief Handles I/O failures on an SSL connection based on the result code.
*
* This function processes the result of an SSL I/O operation and handles
* different types of errors that may occur during the operation. It takes
* appropriate actions such as retrying the operation, reporting errors, or
* returning specific status codes based on the error type.
*
* @param ssl A pointer to the SSL object representing the connection.
* @param res The result code from the SSL I/O operation.
* @return An integer indicating the outcome:
* - 1: Temporary failure, the operation should be retried.
* - 0: EOF, indicating the connection has been closed.
* - -1: A fatal error occurred or the connection has been reset.
*
* @note This function may block if a temporary failure occurs and
* wait_for_activity() is called.
*
* @note If the failure is due to an SSL verification error, additional
* information will be logged to stderr.
*/
static int handle_io_failure(SSL *ssl, int res)
{
switch (SSL_get_error(ssl, res)) {
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
/* Temporary failure. Wait until we can read/write and try again */
/*
* 一時的な失敗。ソケットが読み書き可能になるまで待機して
* リトライする。非ブロッキングモードでは頻繁に発生する。
*/
wait_for_activity(ssl);
return 1; /* リトライ可能 */
case SSL_ERROR_ZERO_RETURN:
case SSL_ERROR_NONE:
/* EOF */
/* EOF: 相手がストリームを閉じた */
return 0;
case SSL_ERROR_SYSCALL:
/* システムコールエラー */
return -1;
case SSL_ERROR_SSL:
/*
* Some stream fatal error occurred. This could be because of a
* stream reset - or some failure occurred on the underlying
* connection.
*/
/* SSLレベルのエラー。詳細を確認 */
switch (SSL_get_stream_read_state(ssl)) {
case SSL_STREAM_STATE_RESET_REMOTE:
printf("Stream reset occurred\n");
/*
* The stream has been reset but the connection is still
* healthy.
*/
/* ストリームはリセットされたが、接続は健全 */
break;
case SSL_STREAM_STATE_CONN_CLOSED:
printf("Connection closed\n");
/* Connection is already closed. */
/* 接続が既に閉じられている */
break;
default:
printf("Unknown stream failure\n");
break;
}
/*
* If the failure is due to a verification error we can get more
* information about it from SSL_get_verify_result().
*/
/* 証明書検証エラーの場合、詳細を表示 */
if (SSL_get_verify_result(ssl) != X509_V_OK)
printf("Verify error: %s\n",
X509_verify_cert_error_string(SSL_get_verify_result(ssl)));
return -1;
default:
/* その他の予期しないエラー */
return -1;
}
}
/*
* ============================================================================
* run_quic_server() - 非ブロッキングQUICサーバーのメインループ
* ============================================================================
*
* 【quic-server-block.cとの主な違い】
* 1. SSL_set_blocking_mode(listener, 0) で非ブロッキングモード設定
* 2. SSL_accept_connection() をwhileループでリトライ
* 3. SSL_read_ex() / SSL_write_ex2() をループでリトライ
* 4. SSL_shutdown() をループでリトライ
* 5. handle_io_failure() でエラー種別を判定
*
* @param ctx SSL_CTX(サーバー設定)
* @param fd 非ブロッキングUDPソケットのFD
* @return 成功時はEXIT_SUCCESS、失敗時は-1
*/
/*
* Main loop for server to accept QUIC connections.
* Echo every request back to the client.
*/
static int run_quic_server(SSL_CTX *ctx, int fd)
{
int ok = -1;
int ret, eof;
SSL *listener, *conn = NULL; /* listener: リスナー, conn: 個別接続 */
unsigned char buf[8192];
size_t nread, total_read, total_written;
/*
* =========================================================================
* 【リスナーの作成と非ブロッキング設定】
* =========================================================================
*/
/* Create a new QUIC listener */
/* QUICリスナーを作成 */
if ((listener = SSL_new_listener(ctx, 0)) == NULL)
goto err;
/* Provide the listener with our UDP socket. */
/* リスナーにUDPソケットを関連付け */
if (!SSL_set_fd(listener, fd))
goto err;
/*
* 【重要】リスナーを非ブロッキングモードに設定
* これがquic-server-block.cとの最大の違い!
* 子オブジェクト(接続)も非ブロッキングモードを継承する。
*/
/*
* Set the listener mode to non-blocking, which is inherited by
* child objects.
*/
if (!SSL_set_blocking_mode(listener, 0))
goto err;
/*
* Begin listening. Note that is not usually needed as SSL_accept_connection
* will implicitly start listening. It is only needed if a server wishes to
* ensure it has started to accept incoming connections but does not wish to
* actually call SSL_accept_connection yet.
*/
/* リッスン開始(SSL_accept_connectionが暗黙的に行うため通常は不要) */
if (!SSL_listen(listener))
goto err;
/*
* =========================================================================
* 【接続受け入れループ】
* =========================================================================
*/
/*
* Begin an infinite loop of listening for connections. We will only
* exit this loop if we encounter an error.
*/
for (;;) {
eof = 0;
total_read = 0;
total_written = 0;
/* Pristine error stack for each new connection */
/* 新しい接続ごとにエラースタックをクリア */
ERR_clear_error();
/*
* ---------------------------------------------------------------------
* 【非ブロッキング接続受け入れ】
* ---------------------------------------------------------------------
*
* SSL_accept_connection()は非ブロッキングモードでは即座にリターン。
* NULLが返る場合はwait_for_activity()で待機してリトライ。
*/
/* Block while waiting for a client connection */
printf("Waiting for connection\n");
while ((conn = SSL_accept_connection(listener, 0)) == NULL)
wait_for_activity(listener); /* リスナーで待機 */
printf("Accepted new connection\n");
/*
* ---------------------------------------------------------------------
* 【非ブロッキング読み取り】
* ---------------------------------------------------------------------
*
* クライアントがストリームを閉じる(FIN送信)まで全データを読み取る。
* handle_io_failure()が1を返す限りリトライ。
*/
/* Read from client until the client sends a end of stream packet */
while (!eof) {
ret = SSL_read_ex(conn, buf + total_read, sizeof(buf) - total_read,
&nread);
total_read += nread;
if (total_read >= 8192) {
fprintf(stderr, "Could not fit all data into buffer\n");
goto err;
}
switch (handle_io_failure(conn, ret)) {
case 1:
continue; /* Retry リトライ可能 */
case 0:
/* Reached end of stream */
/* EOF: SSL_has_pending()で未処理データを確認 */
if (!SSL_has_pending(conn))
eof = 1;
break;
default:
fprintf(stderr, "Failed reading remaining data\n");
goto err;
}
}
/*
* ---------------------------------------------------------------------
* 【非ブロッキング書き込み(エコー)】
* ---------------------------------------------------------------------
*
* SSL_WRITE_FLAG_CONCLUDE: 書き込みと同時にストリーム終了(FIN送信)
*/
/* Echo client input */
while (!SSL_write_ex2(conn, buf,
total_read,
SSL_WRITE_FLAG_CONCLUDE, &total_written)) {
if (handle_io_failure(conn, 0) == 1)
continue; /* リトライ可能 */
fprintf(stderr, "Failed to write data\n");
goto err;
}
if (total_read != total_written)
fprintf(stderr, "Failed to echo data [read: %zu, written: %zu]\n",
total_read, total_written);
/*
* ---------------------------------------------------------------------
* 【非ブロッキングシャットダウン】
* ---------------------------------------------------------------------
*/
/*
* Shut down the connection. We may need to call this multiple times
* to ensure the connection is shutdown completely.
*/
while ((ret = SSL_shutdown(conn)) != 1) {
if (ret < 0 && handle_io_failure(conn, ret) == 1)
continue; /* Retry リトライ可能 */
}
/* 接続オブジェクトを解放して次の接続を待つ */
SSL_free(conn);
}
ok = EXIT_SUCCESS;
err:
SSL_free(listener);
return ok;
}
/*
* ============================================================================
* main() - 非ブロッキングQUICサーバーのエントリポイント
* ============================================================================
*
* quic-server-block.cと同様だが、create_socket()で非ブロッキングソケットを作成。
*
* 【使用方法】
* ./quic-server-non-block <port> <server.crt> <server.key>
*/
/* Minimal QUIC HTTP/1.0 server. */
int main(int argc, char *argv[])
{
int res = EXIT_FAILURE;
SSL_CTX *ctx = NULL;
int fd;
unsigned long port;
#ifdef _WIN32
progname = argv[0];
#endif
/* 引数チェック */
if (argc != 4)
errx(res, "usage: %s <port> <server.crt> <server.key>", argv[0]);
/* Create SSL_CTX that supports QUIC. */
/* QUIC対応SSL_CTXを作成 */
if ((ctx = create_ctx(argv[2], argv[3])) == NULL) {
ERR_print_errors_fp(stderr);
errx(res, "Failed to create context");
}
/* Parse port number from command line arguments. */
/* ポート番号を解析 */
port = strtoul(argv[1], NULL, 0);
if (port == 0 || port > UINT16_MAX) {
SSL_CTX_free(ctx);
errx(res, "Failed to parse port number");
}
/* Create and bind a UDP socket. */
/* 非ブロッキングUDPソケットを作成しポートにバインド */
if ((fd = create_socket((uint16_t)port)) < 0) {
SSL_CTX_free(ctx);
ERR_print_errors_fp(stderr);
errx(res, "Failed to create socket");
}
/* QUIC server connection acceptance loop. */
/* 非ブロッキングQUICサーバーのメインループを実行 */
if (run_quic_server(ctx, fd) < 0) {
SSL_CTX_free(ctx);
BIO_closesocket(fd);
ERR_print_errors_fp(stderr);
errx(res, "Error in QUIC server loop");
}
/* Free resources. */
/* リソース解放(通常は無限ループなのでここには到達しない) */
SSL_CTX_free(ctx);
BIO_closesocket(fd);
res = EXIT_SUCCESS;
return res;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment