/* http_h2.c - HTTP/2 support functions
*
* Copyright (c) 1994-2018 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <config.h>
#include <sysexits.h>
#include "httpd.h"
#include "md5.h"
#include "util.h"
int (*alpn_select_cb)(SSL *ssl,
const unsigned char **out, unsigned char *outlen,
const unsigned char *in, unsigned int inlen,
void *arg) = NULL;
#ifdef HAVE_NGHTTP2
#include <errno.h>
#include <syslog.h>
#include <sasl/saslutil.h>
#include "http_h2.h"
#include "http_ws.h"
#include "prometheus.h"
#include "retry.h"
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#define HTTP2_MAX_HEADERS 100
/* HTTP/2 session context */
struct http2_context {
nghttp2_session *session; /* HTTP/2 session */
nghttp2_option *options; /* Config options for HTTP/2 session */
unsigned ws_count; /* Count of WebSocket streams */
};
/* HTTP/2 stream context */
struct http2_stream {
int32_t id; /* Stream ID */
size_t num_resp_hdrs; /* Number of response headers */
nghttp2_nv resp_hdrs[HTTP2_MAX_HEADERS]; /* Array of response headers */
};
static nghttp2_session_callbacks *http2_callbacks = NULL;
static int _alpn_select_cb(SSL *ssl __attribute__((unused)),
const unsigned char **out, unsigned char *outlen,
const unsigned char *in, unsigned int inlen,
void *arg)
{
struct http_connection *http_conn = (struct http_connection *) arg;
int r = nghttp2_select_next_protocol((u_char **) out, outlen, in, inlen);
switch (r) {
case 0: /* http/1.1 */
break;
case 1: /* h2 */
if (http2_start_session(NULL, http_conn) == 0) break;
/* Fall through as unsupported */
GCC_FALLTHROUGH
default:
return SSL_TLSEXT_ERR_NOACK;
}
return SSL_TLSEXT_ERR_OK;
}
static ssize_t send_cb(nghttp2_session *session __attribute__((unused)),
const uint8_t *data, size_t length,
int flags __attribute__((unused)),
void *user_data)
{
struct http_connection *conn = (struct http_connection *) user_data;
struct protstream *pout = conn->pout;
int r;
r = prot_write(pout, (const char *) data, length);
syslog(LOG_DEBUG, "http2_send_cb(%zu): %d", length, r);
if (r) return NGHTTP2_ERR_CALLBACK_FAILURE;
return length;
}
static ssize_t recv_cb(nghttp2_session *session __attribute__((unused)),
uint8_t *buf, size_t length,
int flags __attribute__((unused)),
void *user_data)
{
struct http_connection *conn = (struct http_connection *) user_data;
struct protstream *pin = conn->pin;
ssize_t n;
n = prot_read(pin, (char *) buf, length);
if (n) {
/* We received some data - don't block next time
Note: This callback gets called multiple times until it
would block. We don't actually want to block and prevent
output from being submitted */
prot_NONBLOCK(pin);
}
else if (!((struct http2_context *) conn->sess_ctx)->ws_count) {
/* No data (and no WebSockets) - block next time (for client timeout) */
prot_BLOCK(pin);
if (pin->eof) n = NGHTTP2_ERR_EOF;
else if (pin->error) n = NGHTTP2_ERR_CALLBACK_FAILURE;
else n = NGHTTP2_ERR_WOULDBLOCK;
}
syslog(LOG_DEBUG,
"http2_recv_cb(%zu): n = %zd, eof = %d, err = '%s', errno = %m",
length, n, pin->eof, pin->error ? pin->error : "");
return n;
}
static ssize_t data_source_read_cb(nghttp2_session *sess __attribute__((unused)),
int32_t stream_id,
uint8_t *buf, size_t length,
uint32_t *data_flags,
nghttp2_data_source *source,
void *user_data __attribute__((unused)))
{
struct protstream *s = source->ptr;
size_t n = prot_read(s, (char *) buf, length);
syslog(LOG_DEBUG,
"http2_data_source_read_cb(id=%d, len=%zu): n=%zu, eof=%d",
stream_id, length, n, !s->cnt);
if (!s->cnt) {
*data_flags |= NGHTTP2_DATA_FLAG_EOF;
prot_free(s); /* Done with the protstream */
}
return n;
}
static int begin_headers_cb(nghttp2_session *session,
const nghttp2_frame *frame, void *user_data)
{
if (frame->hd.type != NGHTTP2_HEADERS ||
frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
return 0;
}
syslog(LOG_DEBUG, "http2_begin_headers_cb(id=%d, type=%d)",
frame->hd.stream_id, frame->hd.type);
struct transaction_t *txn = xzmalloc(sizeof(struct transaction_t));
txn->conn = (struct http_connection *) user_data;
txn->meth = METH_UNKNOWN;
txn->flags.ver = VER_2;
txn->flags.vary = VARY_AE;
txn->req_line.ver = HTTP2_VERSION;
if (config_getswitch(IMAPOPT_HTTPALLOWCOMPRESS)) {
txn->zstrm = zlib_init();
txn->brotli = brotli_init();
txn->zstd = zstd_init();
}
struct http2_stream *strm = xzmalloc(sizeof(struct http2_stream));
strm->id = frame->hd.stream_id;
txn->strm_ctx = strm;
/* Create header cache */
if (!(txn->req_hdrs = spool_new_hdrcache())) {
syslog(LOG_ERR, "Unable to create header cache");
return NGHTTP2_ERR_CALLBACK_FAILURE;
}
nghttp2_session_set_stream_user_data(session, frame->hd.stream_id, txn);
return 0;
}
static int header_cb(nghttp2_session *session,
const nghttp2_frame *frame,
const uint8_t *name, size_t namelen,
const uint8_t *value, size_t valuelen,
uint8_t flags __attribute__((unused)),
void *user_data __attribute__((unused)))
{
if (frame->hd.type != NGHTTP2_HEADERS ||
frame->headers.cat != NGHTTP2_HCAT_REQUEST) {
return 0;
}
char *my_name, *my_value;
struct transaction_t *txn =
nghttp2_session_get_stream_user_data(session, frame->hd.stream_id);
if (!txn) return 0;
my_name = xstrndup((const char *) name, namelen);
my_value = xstrndup((const char *) value, valuelen);
syslog(LOG_DEBUG, "http2_header_cb(%s: %s)", my_name, my_value);
if (my_name[0] == ':') {
switch (my_name[1]) {
case 'm': /* :method */
if (!strcmp("ethod", my_name+2)) txn->req_line.meth = my_value;
break;
case 's': /* :scheme */
break;
case 'a': /* :authority */
break;
case 'p': /* :path, :protocol */
if (!strcmp("ath", my_name+2)) txn->req_line.uri = my_value;
else if (!strcmp("rotocol", my_name+2) &&
!strcmp(my_value, WS_TOKEN)) {
txn->flags.upgrade |= UPGRADE_WS;
}
break;
}
}
spool_cache_header(my_name, my_value, txn->req_hdrs);
return 0;
}
static int data_chunk_recv_cb(nghttp2_session *session,
uint8_t flags __attribute__((unused)),
int32_t stream_id,
const uint8_t *data, size_t len,
void *user_data __attribute__((unused)))
{
struct transaction_t *txn =
nghttp2_session_get_stream_user_data(session, stream_id);
if (!txn) return 0;
syslog(LOG_DEBUG, "http2_data_chunk_recv_cb(id=%d, len=%zu, txnflags=%#x)",
stream_id, len, txn->req_body.flags);
if (txn->req_body.flags & BODY_DISCARD) return 0;
if (len) {
txn->req_body.framing = FRAMING_HTTP2;
txn->req_body.len += len;
buf_appendmap(&txn->req_body.payload, (const char *) data, len);
}
return 0;
}
static int frame_recv_cb(nghttp2_session *session,
const nghttp2_frame *frame,
void *user_data __attribute__((unused)))
{
int ret = 0;
struct transaction_t *txn =
nghttp2_session_get_stream_user_data(session, frame->hd.stream_id);
struct http2_context *ctx;
struct buf *logbuf = NULL;
if (!txn) return 0;
ctx = (struct http2_context *) txn->conn->sess_ctx;
syslog(LOG_DEBUG, "http2_frame_recv_cb(id=%d, type=%d, flags=%#x)",
frame->hd.stream_id, frame->hd.type, frame->hd.flags);
if ((txn->conn->logfd != -1) && (frame->hd.type <= NGHTTP2_HEADERS)) {
/* telemetry log */
logbuf = &txn->conn->logbuf;
buf_reset(logbuf);
buf_printf(logbuf, "<" TIME_T_FMT "<", time(NULL)); /* timestamp */
write(txn->conn->logfd, buf_base(logbuf), buf_len(logbuf));
}
switch (frame->hd.type) {
case NGHTTP2_HEADERS:
if (frame->headers.cat == NGHTTP2_HCAT_REQUEST) {
if (txn->conn->logfd != -1) {
/* telemetry log */
buf_reset(logbuf);
buf_printf(logbuf, "%s %s %s\r\n", /* request-line*/
txn->req_line.meth, txn->req_line.uri, HTTP2_VERSION);
spool_enum_hdrcache(txn->req_hdrs, /* header fields */
&log_cachehdr, logbuf);
buf_appendcstr(logbuf, "\r\n"); /* CRLF */
write(txn->conn->logfd, buf_base(logbuf), buf_len(logbuf));
}
/* Examine request */
ret = examine_request(txn, NULL);
if (ret) {
txn->req_body.flags |= BODY_DISCARD;
error_response(ret, txn);
break;
}
if (txn->req_body.flags & BODY_CONTINUE) {
txn->req_body.flags &= ~BODY_CONTINUE;
response_header(HTTP_CONTINUE, txn);
break;
}
}
GCC_FALLTHROUGH
case NGHTTP2_DATA:
if (txn->ws_ctx) {
/* WebSocket over HTTP/2 input */
ws_input(txn);
if (txn->flags.conn & CONN_CLOSE) {
/* Issue RST_STREAM so that stream does not hang around. */
syslog(LOG_DEBUG, "nghttp2_submit_rst stream()");
nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE,
frame->hd.stream_id,
NGHTTP2_NO_ERROR);
}
break;
}
if (txn->conn->logfd != -1) {
/* telemetry log */
write(txn->conn->logfd, buf_base(&txn->req_body.payload),
buf_len(&txn->req_body.payload));
}
if (txn->meth != METH_CONNECT) {
/* Check that the client request has finished */
if (!(frame->hd.flags & NGHTTP2_FLAG_END_STREAM)) break;
/* Check that we still want to process the request */
if (txn->req_body.flags & BODY_DISCARD) break;
}
/* Process the requested method */
ret = process_request(txn);
/* Handle errors (success responses handled by method functions) */
if (ret) error_response(ret, txn);
if (txn->flags.conn & CONN_CLOSE) {
int32_t stream_id =
nghttp2_session_get_last_proc_stream_id(ctx->session);
syslog(LOG_DEBUG, "nghttp2_submit_goaway()");
nghttp2_submit_goaway(session, NGHTTP2_FLAG_NONE, stream_id,
NGHTTP2_NO_ERROR, NULL, 0);
}
else if (txn->ws_ctx) {
/* Increment WebSocket stream count */
ctx->ws_count++;
}
break;
}
return 0;
}
static int stream_close_cb(nghttp2_session *session, int32_t stream_id,
uint32_t error_code,
void *user_data __attribute__((unused)))
{
struct transaction_t *txn =
nghttp2_session_get_stream_user_data(session, stream_id);
syslog(LOG_DEBUG, "http2_stream_close_cb(id=%d): '%s'",
stream_id, nghttp2_http2_strerror(error_code));
if (txn) {
if (txn->ws_ctx) {
/* Decrement WebSocket stream count */
struct http2_context *http2_ctx =
(struct http2_context *) txn->conn->sess_ctx;
if (--http2_ctx->ws_count == 0) {
/* No WebSockets - block next time (for client timeout) */
prot_BLOCK(txn->conn->pin);
}
}
/* Memory cleanup */
transaction_free(txn);
free(txn);
}
return 0;
}
static int frame_not_send_cb(nghttp2_session *session,
const nghttp2_frame *frame,
int lib_error_code,
void *user_data __attribute__((unused)))
{
syslog(LOG_DEBUG, "http2_frame_not_send_cb(id=%d, type=%d, flags=%#x)",
frame->hd.stream_id, frame->hd.type, frame->hd.flags);
/* Issue RST_STREAM so that stream does not hang around. */
nghttp2_submit_rst_stream(session, NGHTTP2_FLAG_NONE,
frame->hd.stream_id, lib_error_code);
return 0;
}
static int frame_send_cb(nghttp2_session *session __attribute__((unused)),
const nghttp2_frame *frame,
void *user_data __attribute__((unused)))
{
syslog(LOG_DEBUG, "http2_frame_send_cb(id=%d, type=%d, flags=%#x)",
frame->hd.stream_id, frame->hd.type, frame->hd.flags);
return 0;
}
static int begin_frame_cb(nghttp2_session *session __attribute__((unused)),
const nghttp2_frame_hd *hd,
void *user_data __attribute__((unused)))
{
syslog(LOG_DEBUG, "http2_begin_frame_cb(id=%d, type=%d, flags=%#x)",
hd->stream_id, hd->type, hd->flags);
return 0;
}
HIDDEN void http2_init(struct buf *serverinfo)
{
int r;
buf_printf(serverinfo, " Nghttp2/%s", NGHTTP2_VERSION);
/* Setup HTTP/2 callbacks */
if ((r = nghttp2_session_callbacks_new(&http2_callbacks))) {
syslog(LOG_WARNING,
"nghttp2_session_callbacks_new: %s", nghttp2_strerror(r));
return;
}
nghttp2_session_callbacks_set_send_callback(http2_callbacks,
&send_cb);
nghttp2_session_callbacks_set_recv_callback(http2_callbacks,
&recv_cb);
nghttp2_session_callbacks_set_on_begin_headers_callback(http2_callbacks,
&begin_headers_cb);
nghttp2_session_callbacks_set_on_header_callback(http2_callbacks,
&header_cb);
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(http2_callbacks,
&data_chunk_recv_cb);
nghttp2_session_callbacks_set_on_frame_recv_callback(http2_callbacks,
&frame_recv_cb);
nghttp2_session_callbacks_set_on_stream_close_callback(http2_callbacks,
&stream_close_cb);
nghttp2_session_callbacks_set_on_frame_not_send_callback(http2_callbacks,
&frame_not_send_cb);
if (config_getswitch(IMAPOPT_DEBUG)) {
nghttp2_session_callbacks_set_on_begin_frame_callback(http2_callbacks,
&begin_frame_cb);
nghttp2_session_callbacks_set_on_frame_send_callback(http2_callbacks,
&frame_send_cb);
}
/* Setup for ALPN */
alpn_select_cb = &_alpn_select_cb;
}
HIDDEN int http2_enabled()
{
return (http2_callbacks != NULL);
}
HIDDEN void http2_done()
{
nghttp2_session_callbacks_del(http2_callbacks);
}
HIDDEN int http2_preface(struct http_connection *conn)
{
if (http2_enabled()) {
/* Check initial client input for HTTP/2 preface */
int c;
if (prot_lookahead(conn->pin,
NGHTTP2_CLIENT_MAGIC, NGHTTP2_CLIENT_MAGIC_LEN, &c)) {
syslog(LOG_DEBUG, "HTTP/2 client connection preface");
return 1;
}
}
return 0;
}
#if HAVE_DECL_NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL
static int nghttp2_extended_connect = 1;
#else
#define NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL 0x08
static int nghttp2_extended_connect = 0;
#endif
HIDDEN int http2_start_session(struct transaction_t *txn,
struct http_connection *conn)
{
nghttp2_settings_entry iv[] = {
{ NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100 },
{ NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL, 1 } /* MUST be last */
};
size_t niv = (sizeof(iv) / sizeof(iv[0])) - !ws_enabled();
struct http2_context *ctx;
int r;
if (!conn) conn = txn->conn;
if (conn->sess_ctx) return 0;
ctx = xzmalloc(sizeof(struct http2_context));
r = nghttp2_option_new(&ctx->options);
if (r) {
syslog(LOG_WARNING,
"nghttp2_option_new: %s", nghttp2_strerror(r));
free(ctx);
return HTTP_SERVER_ERROR;
}
if (ws_enabled() && !nghttp2_extended_connect) {
/* Need to forcefully allow :scheme, :path, :protocol pseudo-headers
(this version of libnghttp2 doesn't understand extended CONNECT) */
nghttp2_option_set_no_http_messaging(ctx->options, 1);
}
r = nghttp2_session_server_new2(&ctx->session,
http2_callbacks, conn, ctx->options);
if (r) {
syslog(LOG_WARNING,
"nghttp2_session_server_new2: %s", nghttp2_strerror(r));
free(ctx);
return HTTP_SERVER_ERROR;
}
conn->sess_ctx = ctx;
if (txn && (txn->flags.conn & CONN_UPGRADE)) {
struct http2_stream *strm;
struct buf *buf = &txn->buf;
unsigned outlen;
const char **hdr = spool_getheader(txn->req_hdrs, "HTTP2-Settings");
if (!hdr || hdr[1]) return 0;
/* base64url decode the settings.
Use the SASL base64 decoder after replacing the encoded values
for chars 62 and 63 and adding appropriate padding. */
buf_setcstr(buf, hdr[0]);
buf_replace_char(buf, '-', '+');
buf_replace_char(buf, '_', '/');
buf_appendmap(buf, "==", (4 - (buf_len(buf) % 4)) % 4);
r = sasl_decode64(buf_base(buf), buf_len(buf),
(char *) buf_base(buf), buf_len(buf), &outlen);
if (r != SASL_OK) {
syslog(LOG_WARNING, "sasl_decode64 failed: %s",
sasl_errstring(r, NULL, NULL));
}
else {
r = nghttp2_session_upgrade2(ctx->session,
(const uint8_t *) buf_base(buf),
outlen, txn->meth == METH_HEAD, NULL);
if (r) {
syslog(LOG_WARNING, "nghttp2_session_upgrade: %s",
nghttp2_strerror(r));
}
}
buf_reset(buf);
if (r) return HTTP_BAD_REQUEST;
/* tell client to start h2c upgrade (RFC 7540) */
response_header(HTTP_SWITCH_PROT, txn);
strm = xzmalloc(sizeof(struct http2_stream));
strm->id = nghttp2_session_get_last_proc_stream_id(ctx->session);
txn->strm_ctx = strm;
txn->flags.ver = VER_2;
}
/* Don't do telemetry logging in prot layer */
prot_setlog(conn->pin, PROT_NO_FD);
prot_setlog(conn->pout, PROT_NO_FD);
tcp_disable_nagle(1); /* output fd */
r = nghttp2_submit_settings(ctx->session, NGHTTP2_FLAG_NONE, iv, niv);
if (r) {
syslog(LOG_ERR, "nghttp2_submit_settings: %s", nghttp2_strerror(r));
return HTTP_SERVER_ERROR;
}
return 0;
}
HIDDEN void http2_end_session(void *http2_ctx)
{
struct http2_context *ctx = (struct http2_context *) http2_ctx;
if (!ctx) return;
nghttp2_option_del(ctx->options);
nghttp2_session_del(ctx->session);
free(ctx);
}
HIDDEN void http2_output(struct transaction_t *txn)
{
struct http2_context *ctx = (struct http2_context *) txn->conn->sess_ctx;
if (nghttp2_session_want_write(ctx->session)) {
/* Send queued frame(s) */
int r = nghttp2_session_send(ctx->session);
if (r) {
syslog(LOG_ERR, "nghttp2_session_send: %s", nghttp2_strerror(r));
txn->flags.conn = CONN_CLOSE;
}
}
else if (!nghttp2_session_want_read(ctx->session)) {
/* We're done */
syslog(LOG_DEBUG, "closing connection");
txn->flags.conn = CONN_CLOSE;
return;
}
}
HIDDEN void http2_input(struct transaction_t *txn)
{
struct http2_context *ctx = (struct http2_context *) txn->conn->sess_ctx;
int want_read = nghttp2_session_want_read(ctx->session);
int goaway = txn->flags.conn & CONN_CLOSE;
nghttp2_error_code err = goaway ? NGHTTP2_REFUSED_STREAM : NGHTTP2_NO_ERROR;
struct protstream *pin = txn->conn->pin;
syslog(LOG_DEBUG, "http2_input() goaway: %d, eof: %d, want read: %d",
goaway, prot_IS_EOF(pin), want_read);
if (want_read && !goaway) {
/* Read frame(s) */
int r = nghttp2_session_recv(ctx->session);
if (!r) {
/* Successfully received frames */
syslog(LOG_DEBUG, "nghttp2_session_recv: success");
}
else if (r == NGHTTP2_ERR_EOF) {
/* Client closed connection */
syslog(LOG_DEBUG, "client closed connection");
txn->flags.conn = CONN_CLOSE;
}
else {
/* Failure */
syslog(LOG_DEBUG, "nghttp2_session_recv: %s (%s)",
nghttp2_strerror(r), prot_error(pin));
goaway = 1;
if (r == NGHTTP2_ERR_CALLBACK_FAILURE) {
/* Client timeout */
txn->error.desc = prot_error(pin);
err = NGHTTP2_REFUSED_STREAM;
}
else {
txn->error.desc = nghttp2_strerror(r);
if (r == NGHTTP2_ERR_NOMEM)
err = NGHTTP2_INTERNAL_ERROR;
else if (r == NGHTTP2_ERR_BAD_CLIENT_MAGIC)
err = NGHTTP2_PROTOCOL_ERROR;
else if (r == NGHTTP2_ERR_FLOODED)
err = NGHTTP2_ENHANCE_YOUR_CALM;
}
}
}
if (goaway) {
/* Tell client we are closing session */
int32_t stream_id =
nghttp2_session_get_last_proc_stream_id(ctx->session);
syslog(LOG_WARNING, "%s, closing connection", txn->error.desc);
syslog(LOG_DEBUG, "nghttp2_submit_goaway()");
int r = nghttp2_submit_goaway(ctx->session,
NGHTTP2_FLAG_NONE, stream_id, err,
(const uint8_t *) txn->error.desc,
strlen(txn->error.desc));
if (r) {
syslog(LOG_ERR, "nghttp2_submit_goaway: %s", nghttp2_strerror(r));
}
txn->flags.conn = CONN_CLOSE;
}
/* Write frame(s) */
http2_output(txn);
return;
}
HIDDEN void http2_begin_headers(struct transaction_t *txn)
{
struct http2_stream *strm = (struct http2_stream *) txn->strm_ctx;
strm->num_resp_hdrs = 0;
if (txn->conn->logfd != -1) {
/* telemetry log */
struct buf *logbuf = &txn->conn->logbuf;
buf_reset(logbuf);
buf_printf(logbuf, ">" TIME_T_FMT ">", time(NULL)); /* timestamp */
write(txn->conn->logfd, buf_base(logbuf), buf_len(logbuf));
}
}
HIDDEN void http2_add_header(struct transaction_t *txn,
const char *name, struct buf *value)
{
struct http2_stream *strm = (struct http2_stream *) txn->strm_ctx;
if (strm->num_resp_hdrs >= HTTP2_MAX_HEADERS) {
buf_free(value);
return;
}
else {
nghttp2_nv *nv = &strm->resp_hdrs[strm->num_resp_hdrs];
free(nv->value);
nv->namelen = strlen(name);
nv->name = (uint8_t *) name;
nv->valuelen = buf_len(value);
nv->value = (uint8_t *) buf_release(value);
nv->flags = NGHTTP2_NV_FLAG_NO_COPY_VALUE;
strm->num_resp_hdrs++;
if (txn->conn->logfd != -1) {
/* telemetry log */
struct iovec iov[4];
int niov = 0;
if (name[0] == ':') {
/* :status */
WRITEV_ADD_TO_IOVEC(iov, niov, "HTTP/2 ", 7);
}
else {
WRITEV_ADD_TO_IOVEC(iov, niov, nv->name, nv->namelen);
WRITEV_ADD_TO_IOVEC(iov, niov, ": ", 2);
}
WRITEV_ADD_TO_IOVEC(iov, niov, nv->value, nv->valuelen);
WRITEV_ADD_TO_IOVEC(iov, niov, "\r\n", 2);
writev(txn->conn->logfd, iov, niov);
}
}
}
HIDDEN int http2_end_headers(struct transaction_t *txn, long code)
{
struct http2_context *ctx = (struct http2_context *) txn->conn->sess_ctx;
struct http2_stream *strm = (struct http2_stream *) txn->strm_ctx;
uint8_t flags = NGHTTP2_FLAG_NONE;
int r;
syslog(LOG_DEBUG,
"end_resp_headers(code = %ld, len = %ld, flags.te = %#x)",
code, txn->resp_body.len, txn->flags.te);
if (txn->conn->logfd != -1) {
/* telemetry log */
write(txn->conn->logfd, "\r\n", 2);
}
switch (code) {
case 0:
/* Trailer */
flags = NGHTTP2_FLAG_END_STREAM;
break;
case HTTP_CONTINUE:
case HTTP_PROCESSING:
/* Provisional response */
break;
case HTTP_NO_CONTENT:
case HTTP_NOT_MODIFIED:
/* MUST NOT include a body */
flags = NGHTTP2_FLAG_END_STREAM;
break;
default:
if (txn->meth == METH_HEAD) {
/* MUST NOT include a body */
flags = NGHTTP2_FLAG_END_STREAM;
}
else if (!(txn->resp_body.len || (txn->flags.te & TE_CHUNKED))) {
/* Empty body */
flags = NGHTTP2_FLAG_END_STREAM;
}
break;
}
syslog(LOG_DEBUG, "%s(id=%d, flags=%#x)",
code ? "nghttp2_submit headers" : "nghttp2_submit_trailers",
strm->id, flags);
if (code) {
r = nghttp2_submit_headers(ctx->session, flags, strm->id, NULL,
strm->resp_hdrs, strm->num_resp_hdrs, NULL);
}
else {
r = nghttp2_submit_trailer(ctx->session, strm->id,
strm->resp_hdrs, strm->num_resp_hdrs);
}
if (r) {
syslog(LOG_ERR, "%s: %s",
code ? "nghttp2_submit headers" : "nghttp2_submit_trailers",
nghttp2_strerror(r));
}
return r;
}
HIDDEN int http2_data_chunk(struct transaction_t *txn,
const char *data, unsigned datalen,
int last_chunk, MD5_CTX *md5ctx)
{
static unsigned char md5[MD5_DIGEST_LENGTH];
struct http2_context *ctx = (struct http2_context *) txn->conn->sess_ctx;
struct http2_stream *strm = (struct http2_stream *) txn->strm_ctx;
uint8_t flags = NGHTTP2_FLAG_END_STREAM;
nghttp2_data_provider prd;
int r;
syslog(LOG_DEBUG, "http2_data_chunk(datalen=%u, last=%d)",
datalen, last_chunk);
if (datalen && (txn->conn->logfd != -1)) {
/* telemetry log */
struct buf *logbuf = &txn->conn->logbuf;
struct iovec iov[2];
int niov = 0;
buf_reset(logbuf);
buf_printf(logbuf, ">" TIME_T_FMT ">", time(NULL)); /* timestamp */
WRITEV_ADD_TO_IOVEC(iov, niov,
buf_base(logbuf), buf_len(logbuf));
WRITEV_ADD_TO_IOVEC(iov, niov, data, datalen);
writev(txn->conn->logfd, iov, niov);
}
/* NOTE: The protstream that we use as the data source MUST remain
available until the data source read callback has retrieved all data.
Also note that we need to make a copy of the data because data frames
may not be sent prior to the original pointer becoming invalid.
*/
struct protstream *s = prot_readmap(xmemdup(data, datalen), datalen);
s->buf = s->ptr;
s->buf_size = datalen;
prd.source.ptr = s;
prd.read_callback = data_source_read_cb;
if (txn->flags.te) {
if (!last_chunk) {
flags = NGHTTP2_FLAG_NONE;
if (datalen && (txn->flags.trailer & TRAILER_CMD5)) {
MD5Update(md5ctx, data, datalen);
}
}
else if (txn->flags.trailer) {
flags = NGHTTP2_FLAG_NONE;
if (txn->flags.trailer & TRAILER_CMD5) MD5Final(md5, md5ctx);
}
}
syslog(LOG_DEBUG, "nghttp2_submit_data(id=%d, datalen=%d, flags=%#x)",
strm->id, datalen, flags);
r = nghttp2_submit_data(ctx->session, flags, strm->id, &prd);
if (r) {
syslog(LOG_ERR, "nghttp2_submit_data: %s", nghttp2_strerror(r));
return HTTP_SERVER_ERROR;
}
else {
http2_output(txn);
if (last_chunk && (txn->flags.trailer & ~TRAILER_PROXY)) {
begin_resp_headers(txn, 0);
if (txn->flags.trailer & TRAILER_CMD5) content_md5_hdr(txn, md5);
if ((txn->flags.trailer & TRAILER_CTAG) && txn->resp_body.ctag) {
simple_hdr(txn, "CTag", "%s", txn->resp_body.ctag);
}
end_resp_headers(txn, 0);
}
}
return 0;
}
HIDDEN int32_t http2_get_streamid(void *http2_strm)
{
struct http2_stream *strm = (struct http2_stream *) http2_strm;
return strm ? strm->id : 0;
}
HIDDEN void http2_end_stream(void *http2_strm)
{
struct http2_stream *strm = (struct http2_stream *) http2_strm;
int i;
if (!strm) return;
for (i = 0; i < HTTP2_MAX_HEADERS; i++) {
free(strm->resp_hdrs[i].value);
}
free(strm);
}
#else /* !HAVE_NGHTTP2 */
HIDDEN void http2_init(struct buf *serverinfo __attribute__((unused))) {}
HIDDEN int http2_enabled()
{
return 0;
}
HIDDEN void http2_done() {}
HIDDEN int http2_preface(struct http_connection *conn __attribute__((unused)))
{
return 0;
}
HIDDEN int http2_start_session(struct transaction_t *txn __attribute__((unused)),
struct http_connection *c __attribute__((unused)))
{
fatal("http2_start() called, but no Nghttp2", EX_SOFTWARE);
}
HIDDEN void http2_end_session(void *http2_ctx __attribute__((unused)))
{
}
HIDDEN void http2_output(struct transaction_t *txn __attribute__((unused)))
{
fatal("http2_output() called, but no Nghttp2", EX_SOFTWARE);
}
HIDDEN void http2_input(struct transaction_t *txn __attribute__((unused)))
{
fatal("http2_input() called, but no Nghttp2", EX_SOFTWARE);
}
HIDDEN int http2_begin_headers(struct transaction_t *txn __attribute__((unused)))
{
fatal("http2_begin_headers() called, but no Nghttp2", EX_SOFTWARE);
}
HIDDEN void http2_add_header(struct transaction_t *txn __attribute__((unused)),
const char *name __attribute__((unused)),
struct buf *value __attribute__((unused)))
{
fatal("http2_add_header() called, but no Nghttp2", EX_SOFTWARE);
}
HIDDEN int http2_end_headers(struct transaction_t *txn __attribute__((unused)),
long code __attribute__((unused)))
{
fatal("http2_end_headers() called, but no Nghttp2", EX_SOFTWARE);
}
HIDDEN int http2_data_chunk(struct transaction_t *txn __attribute__((unused)),
const char *data __attribute__((unused)),
unsigned datalen __attribute__((unused)),
int last_chunk __attribute__((unused)),
MD5_CTX *md5ctx __attribute__((unused)))
{
fatal("http2_data_chunk() called, but no Nghttp2", EX_SOFTWARE);
}
HIDDEN int32_t http2_get_streamid(void *http2_strm __attribute__((unused)))
{
return 0;
}
HIDDEN void http2_end_stream(void *http2_strm __attribute__((unused))) {}
#endif /* HAVE_NGHTTP2 */