HTTP3之QUIC协议early data
Tags: http3, quic, earlydata
Published: 2024-05-29 07:24:57

## 背景
上回实现了QUIC协议的Connection Migration，本章实现early data。首先要浓情以这个概念，early data跟0-RTT不是一个概念，前者基于后者，在首次发送初始frame时候就将请求内容一起发送过去，这里涉及到PSK和session ticket概念，这个可以通过过QUIC协议整明白，网上也有文章说明，后面有机会整理。
https://datatracker.ietf.org/doc/html/rfc9000
https://datatracker.ietf.org/doc/html/rfc9001#name-0-rtt

## 实现
本文主要使用ngtcp2和nghttp3实现early data请求，下面主要描述主要代码步骤，如果不明白，可以参考全部[文件](https://github.com/Shouhua/aioquic/blob/note/note/ngtcp2/http3_client.c)。

1. OpenSSL session管理
TLS1.3目前已经抛弃前面版本使用的[Session IDs](https://datatracker.ietf.org/doc/html/rfc8446#section-2.2), 转而使用PSK(Pre Shared Key)。OpenSSL库还是使用Session的概念管理TLS1.3的Session Resumption。

2. OpenSSL可以配置使用外部pem文件保存session数据
```c
/* 在成功新建SSL Context后配置callback保存PSK数据 */
if (c->session_file)
{
     // session stored externally by hand in callback function
     SSL_CTX_set_session_cache_mode(c->ssl_ctx, SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL);
     SSL_CTX_sess_set_new_cb(c->ssl_ctx, new_session_cb);
}
```
3. Session callback中先使用max_early_data参数判断当前连接是否支持early data, 然后保存session数据
```c
uint32_t max_early_data;
if ((max_early_data = SSL_SESSION_get_max_early_data(session)) != UINT32_MAX)
{
     fprintf(stderr, "max_early_data_size is not 0xffffffff: %#x\n", max_early_data);
}
BIO *f = BIO_new_file(c->session_file, "w");
if (f == NULL)
{
     fprintf(stderr, "Could not write TLS session in %s\n", c->session_file);
     return 0;
}

if (!PEM_write_bio_SSL_SESSION(f, session))
{
     fprintf(stderr, "Unable to write TLS session to file\n");
}

BIO_free(f);
```
4. 新建SSL对象后, 加载session数据
```c
BIO *f = BIO_new_file(c->session_file, "r");
if (f == NULL) /* open BIO file failed */
{
     fprintf(stderr, "BIO_new_file: Could not read TLS session file %s\n", c->session_file);
}
else
{
     SSL_SESSION *session = PEM_read_bio_SSL_SESSION(f, NULL, 0, NULL);
     BIO_free(f);
     if (session == NULL)
     {
          fprintf(stderr, "PEM_read_bio_SSL_SESSION: Could not read TLS session file %s\n", c->session_file);
     }
     else
     {
          if (!SSL_set_session(c->ssl, session))
          {
               fprintf(stderr, "SSL_set_session: Could not set session\n");
          }
          else if (!c->disable_early_data && SSL_SESSION_get_max_early_data(session))
          {
               c->early_data_enabled = 1;
               SSL_set_quic_early_data_enabled(c->ssl, 1);
          }
          SSL_SESSION_free(session);
     }
}
```
5. 保存QUIC协议中的Transport Prameters为pem格式文件，用于下次early data时发送，可以在ngtcp2的handshake_completed的callback中保存
```c
/* save quic transport parameters */
if (c->tp_file)
{
     uint8_t data[256];
     ngtcp2_ssize datalen = ngtcp2_conn_encode_0rtt_transport_params(c->conn, data, 256);
     if (datalen < 0)
     {
          fprintf(stderr, "Could not encode 0-RTT transport parameters: %s\n", ngtcp2_strerror(datalen));
          return -1;
     }
     else if (write_transport_params(c->tp_file, data, datalen) != 0)
     {
          fprintf(stderr, "Could not write transport parameters in %s\n", c->tp_file);
     }
}
```
6. 在应用代码侧, 如果可以使用early data功能, 传递上次保存的Quic Transport Parameters, 
```c
/* load quic transport parameters */
if (c->early_data_enabled && c->tp_file)
{
     char *data;
     long datalen;
     if ((data = read_pem(c->tp_file, "transport parameters", "QUIC TRANSPORT PARAMETERS", &datalen)) == NULL)
     {
          fprintf(stderr, "client quic init early data read pem failed\n");
          c->early_data_enabled = 0;
     }
     else
     {
          rv = ngtcp2_conn_decode_and_set_0rtt_transport_params(c->conn, (uint8_t *)data, (size_t)datalen);
          if (rv != 0)
          {
               fprintf(stderr, "ngtcp2_conn_decode_and_set_0rtt_transport_params failed: %s\n", ngtcp2_strerror(rv));
               c->early_data_enabled = 0;
          }
          else if (make_stream_early(c) != 0) // setup nghttp3 connection and populate http3 request
          {
               free(data); // free memory which allocated in read_pem function
               return -1;
          }
     }
     free(data); // free memory which allocated in read_pem function
}
```
## 后续
下次继续QUIC特性key update。