| From 22b2efacfe1fdca8834ebf89b44135c2d01e482f Mon Sep 17 00:00:00 2001 |
| From: Alessandro Ghedini <alessandro@cloudflare.com> |
| Date: Fri, 14 Jul 2023 09:57:31 +0100 |
| Subject: [PATCH] Initial QUIC and HTTP/3 implementation using quiche |
| |
| --- |
| auto/lib/conf | 4 + |
| auto/lib/make | 4 + |
| auto/lib/openssl/make | 12 +- |
| auto/lib/quiche/conf | 23 + |
| auto/lib/quiche/make | 23 + |
| auto/make | 3 +- |
| auto/modules | 44 + |
| auto/options | 9 + |
| auto/unix | 46 + |
| src/core/ngx_connection.c | 82 + |
| src/core/ngx_connection.h | 11 + |
| src/core/ngx_core.h | 3 + |
| src/event/ngx_event.c | 44 +- |
| src/event/ngx_event.h | 4 - |
| src/event/ngx_event_posted.c | 1 + |
| src/event/ngx_event_posted.h | 3 + |
| src/event/ngx_event_quic.c | 902 +++++++++ |
| src/event/ngx_event_quic.h | 63 + |
| src/event/ngx_event_udp.c | 39 +- |
| src/http/modules/ngx_http_ssl_module.c | 13 +- |
| src/http/ngx_http.c | 33 +- |
| src/http/ngx_http.h | 4 + |
| src/http/ngx_http_core_module.c | 7 + |
| src/http/ngx_http_core_module.h | 3 + |
| src/http/ngx_http_request.c | 140 +- |
| src/http/ngx_http_request.h | 3 + |
| src/http/ngx_http_request_body.c | 33 + |
| src/http/ngx_http_upstream.c | 13 + |
| src/http/v3/ngx_http_v3.c | 2273 +++++++++++++++++++++++ |
| src/http/v3/ngx_http_v3.h | 80 + |
| src/http/v3/ngx_http_v3_filter_module.c | 74 + |
| src/http/v3/ngx_http_v3_module.c | 321 ++++ |
| src/http/v3/ngx_http_v3_module.h | 36 + |
| src/os/unix/ngx_linux_config.h | 10 + |
| src/os/unix/ngx_udp_sendmsg_chain.c | 117 +- |
| 35 files changed, 4451 insertions(+), 29 deletions(-) |
| create mode 100644 auto/lib/quiche/conf |
| create mode 100644 auto/lib/quiche/make |
| create mode 100644 src/event/ngx_event_quic.c |
| create mode 100644 src/event/ngx_event_quic.h |
| create mode 100644 src/http/v3/ngx_http_v3.c |
| create mode 100644 src/http/v3/ngx_http_v3.h |
| create mode 100644 src/http/v3/ngx_http_v3_filter_module.c |
| create mode 100644 src/http/v3/ngx_http_v3_module.c |
| create mode 100644 src/http/v3/ngx_http_v3_module.h |
| |
| diff --git a/auto/lib/conf b/auto/lib/conf |
| index 2c7af1040..abf920bae 100644 |
| --- a/auto/lib/conf |
| +++ b/auto/lib/conf |
| @@ -25,6 +25,10 @@ if [ $USE_OPENSSL = YES ]; then |
| . auto/lib/openssl/conf |
| fi |
| |
| +if [ $USE_QUICHE = YES ]; then |
| + . auto/lib/quiche/conf |
| +fi |
| + |
| if [ $USE_ZLIB = YES ]; then |
| . auto/lib/zlib/conf |
| fi |
| diff --git a/auto/lib/make b/auto/lib/make |
| index b64e32908..c8f34ae2e 100644 |
| --- a/auto/lib/make |
| +++ b/auto/lib/make |
| @@ -11,6 +11,10 @@ if [ $OPENSSL != NONE -a $OPENSSL != NO -a $OPENSSL != YES ]; then |
| . auto/lib/openssl/make |
| fi |
| |
| +if [ $QUICHE != NONE -a $QUICHE != NO -a $QUICHE != YES ]; then |
| + . auto/lib/quiche/make |
| +fi |
| + |
| if [ $ZLIB != NONE -a $ZLIB != NO -a $ZLIB != YES ]; then |
| . auto/lib/zlib/make |
| fi |
| diff --git a/auto/lib/openssl/make b/auto/lib/openssl/make |
| index 126a23875..139008207 100644 |
| --- a/auto/lib/openssl/make |
| +++ b/auto/lib/openssl/make |
| @@ -49,11 +49,13 @@ END |
| cat << END >> $NGX_MAKEFILE |
| |
| $OPENSSL/.openssl/include/openssl/ssl.h: $NGX_MAKEFILE |
| - cd $OPENSSL \\ |
| - && if [ -f Makefile ]; then \$(MAKE) clean; fi \\ |
| - && ./config --prefix=$ngx_prefix no-shared no-threads $OPENSSL_OPT \\ |
| - && \$(MAKE) \\ |
| - && \$(MAKE) install_sw LIBDIR=lib |
| + mkdir -p $OPENSSL/build $OPENSSL/.openssl/lib $OPENSSL/.openssl/include/openssl \\ |
| + && cd $OPENSSL/build \\ |
| + && cmake -DCMAKE_C_FLAGS="$OPENSSL_OPT" -DCMAKE_CXX_FLAGS="$OPENSSL_OPT" .. \\ |
| + && \$(MAKE) VERBOSE=1 \\ |
| + && cd .. \\ |
| + && cp -r src/include/openssl/*.h .openssl/include/openssl \\ |
| + && cp build/libssl.a build/libcrypto.a .openssl/lib |
| |
| END |
| |
| diff --git a/auto/lib/quiche/conf b/auto/lib/quiche/conf |
| new file mode 100644 |
| index 000000000..b853c1f18 |
| --- /dev/null |
| +++ b/auto/lib/quiche/conf |
| @@ -0,0 +1,23 @@ |
| + |
| +# Copyright (C) Cloudflare, Inc. |
| + |
| + |
| +if [ $QUICHE != NONE ]; then |
| + |
| + have=NGX_QUIC . auto/have |
| + |
| + QUICHE_BUILD_TARGET="release" |
| + |
| + if [ $NGX_DEBUG = YES ]; then |
| + QUICHE_BUILD_TARGET="debug" |
| + fi |
| + |
| + CORE_INCS="$CORE_INCS $QUICHE/quiche/include" |
| + CORE_DEPS="$CORE_DEPS $QUICHE/target/$QUICHE_BUILD_TARGET/libquiche.a" |
| + CORE_LIBS="$CORE_LIBS $QUICHE/target/$QUICHE_BUILD_TARGET/libquiche.a $NGX_LIBPTHREAD -lm" |
| + |
| + if [ "$NGX_SYSTEM" = "Darwin" ]; then |
| + CORE_LIBS+=" -framework Security" |
| + fi |
| + |
| +fi |
| diff --git a/auto/lib/quiche/make b/auto/lib/quiche/make |
| new file mode 100644 |
| index 000000000..65b50e1d1 |
| --- /dev/null |
| +++ b/auto/lib/quiche/make |
| @@ -0,0 +1,23 @@ |
| + |
| +# Copyright (C) Cloudflare, Inc. |
| + |
| +QUICHE_COMMON_FLAGS="--package quiche --verbose --no-default-features --features ffi" |
| + |
| +# Default is release build |
| +QUICHE_BUILD_FLAGS="$QUICHE_COMMON_FLAGS --release" |
| +QUICHE_BUILD_TARGET="release" |
| + |
| +if [ $NGX_DEBUG = YES ]; then |
| + QUICHE_BUILD_FLAGS="$QUICHE_COMMON_FLAGS" |
| + QUICHE_BUILD_TARGET="debug" |
| +fi |
| + |
| + |
| +cat << END >> $NGX_MAKEFILE |
| + |
| +$QUICHE/target/$QUICHE_BUILD_TARGET/libquiche.a: \\ |
| + $OPENSSL/.openssl/include/openssl/ssl.h \\ |
| + $NGX_MAKEFILE |
| + cd $QUICHE && cargo build $QUICHE_BUILD_FLAGS $QUICHE_OPT |
| + |
| +END |
| diff --git a/auto/make b/auto/make |
| index 34c40cdd5..136c0a64e 100644 |
| --- a/auto/make |
| +++ b/auto/make |
| @@ -7,7 +7,8 @@ echo "creating $NGX_MAKEFILE" |
| |
| mkdir -p $NGX_OBJS/src/core $NGX_OBJS/src/event $NGX_OBJS/src/event/modules \ |
| $NGX_OBJS/src/os/unix $NGX_OBJS/src/os/win32 \ |
| - $NGX_OBJS/src/http $NGX_OBJS/src/http/v2 $NGX_OBJS/src/http/modules \ |
| + $NGX_OBJS/src/http $NGX_OBJS/src/http/v2 $NGX_OBJS/src/http/v3 \ |
| + $NGX_OBJS/src/http/modules \ |
| $NGX_OBJS/src/http/modules/perl \ |
| $NGX_OBJS/src/mail \ |
| $NGX_OBJS/src/stream \ |
| diff --git a/auto/modules b/auto/modules |
| index 09bfcb08d..2b2e6a889 100644 |
| --- a/auto/modules |
| +++ b/auto/modules |
| @@ -134,6 +134,7 @@ if [ $HTTP = YES ]; then |
| # ngx_http_header_filter |
| # ngx_http_chunked_filter |
| # ngx_http_v2_filter |
| + # ngx_http_v3_filter |
| # ngx_http_range_header_filter |
| # ngx_http_gzip_filter |
| # ngx_http_postpone_filter |
| @@ -166,6 +167,7 @@ if [ $HTTP = YES ]; then |
| ngx_http_header_filter_module \ |
| ngx_http_chunked_filter_module \ |
| ngx_http_v2_filter_module \ |
| + ngx_http_v3_filter_module \ |
| ngx_http_range_header_filter_module \ |
| ngx_http_gzip_filter_module \ |
| ngx_http_postpone_filter_module \ |
| @@ -227,6 +229,17 @@ if [ $HTTP = YES ]; then |
| . auto/module |
| fi |
| |
| + if [ $HTTP_V3 = YES ]; then |
| + ngx_module_name=ngx_http_v3_filter_module |
| + ngx_module_incs= |
| + ngx_module_deps= |
| + ngx_module_srcs=src/http/v3/ngx_http_v3_filter_module.c |
| + ngx_module_libs= |
| + ngx_module_link=$HTTP_V3 |
| + |
| + . auto/module |
| + fi |
| + |
| if :; then |
| ngx_module_name=ngx_http_range_header_filter_module |
| ngx_module_incs= |
| @@ -438,6 +451,24 @@ if [ $HTTP = YES ]; then |
| . auto/module |
| fi |
| |
| + if [ $HTTP_V3 = YES ]; then |
| + USE_QUICHE=YES |
| + USE_OPENSSL=YES |
| + have=NGX_HTTP_V3 . auto/have |
| + have=NGX_HTTP_HEADERS . auto/have |
| + |
| + ngx_module_name=ngx_http_v3_module |
| + ngx_module_incs=src/http/v3 |
| + ngx_module_deps="src/http/v3/ngx_http_v3.h \ |
| + src/http/v3/ngx_http_v3_module.h" |
| + ngx_module_srcs="src/http/v3/ngx_http_v3.c \ |
| + src/http/v3/ngx_http_v3_module.c" |
| + ngx_module_libs= |
| + ngx_module_link=$HTTP_V3 |
| + |
| + . auto/module |
| + fi |
| + |
| if :; then |
| ngx_module_name=ngx_http_static_module |
| ngx_module_incs= |
| @@ -1268,6 +1299,19 @@ if [ $USE_OPENSSL = YES ]; then |
| fi |
| |
| |
| +if [ $USE_QUICHE = YES ]; then |
| + ngx_module_type=CORE |
| + ngx_module_name=ngx_quic_module |
| + ngx_module_incs= |
| + ngx_module_deps=src/event/ngx_event_quic.h |
| + ngx_module_srcs=src/event/ngx_event_quic.c |
| + ngx_module_libs= |
| + ngx_module_link=YES |
| + |
| + . auto/module |
| +fi |
| + |
| + |
| if [ $USE_PCRE = YES ]; then |
| ngx_module_type=CORE |
| ngx_module_name=ngx_regex_module |
| diff --git a/auto/options b/auto/options |
| index d8b421b0f..6b443f048 100644 |
| --- a/auto/options |
| +++ b/auto/options |
| @@ -59,6 +59,7 @@ HTTP_CHARSET=YES |
| HTTP_GZIP=YES |
| HTTP_SSL=NO |
| HTTP_V2=NO |
| +HTTP_V3=NO |
| HTTP_SSI=YES |
| HTTP_POSTPONE=NO |
| HTTP_REALIP=NO |
| @@ -148,6 +149,9 @@ PCRE_JIT=NO |
| USE_OPENSSL=NO |
| OPENSSL=NONE |
| |
| +USE_QUICHE=NO |
| +QUICHE=NONE |
| + |
| USE_ZLIB=NO |
| ZLIB=NONE |
| ZLIB_OPT= |
| @@ -225,6 +229,7 @@ $0: warning: the \"--with-ipv6\" option is deprecated" |
| |
| --with-http_ssl_module) HTTP_SSL=YES ;; |
| --with-http_v2_module) HTTP_V2=YES ;; |
| + --with-http_v3_module) HTTP_V3=YES ;; |
| --with-http_realip_module) HTTP_REALIP=YES ;; |
| --with-http_addition_module) HTTP_ADDITION=YES ;; |
| --with-http_xslt_module) HTTP_XSLT=YES ;; |
| @@ -358,6 +363,9 @@ use the \"--with-mail_ssl_module\" option instead" |
| --with-openssl=*) OPENSSL="$value" ;; |
| --with-openssl-opt=*) OPENSSL_OPT="$value" ;; |
| |
| + --with-quiche=*) QUICHE="$value" ;; |
| + --with-quiche-opt=*) QUICHE_OPT="$value" ;; |
| + |
| --with-md5=*) |
| NGX_POST_CONF_MSG="$NGX_POST_CONF_MSG |
| $0: warning: the \"--with-md5\" option is deprecated" |
| @@ -440,6 +448,7 @@ cat << END |
| |
| --with-http_ssl_module enable ngx_http_ssl_module |
| --with-http_v2_module enable ngx_http_v2_module |
| + --with-http_v3_module enable ngx_http_v3_module |
| --with-http_realip_module enable ngx_http_realip_module |
| --with-http_addition_module enable ngx_http_addition_module |
| --with-http_xslt_module enable ngx_http_xslt_module |
| diff --git a/auto/unix b/auto/unix |
| index 43d3b25a5..35e0d6bcf 100644 |
| --- a/auto/unix |
| +++ b/auto/unix |
| @@ -434,6 +434,30 @@ ngx_feature_test="struct in_pktinfo pkt; |
| setsockopt(0, IPPROTO_IP, IP_PKTINFO, NULL, 0)" |
| . auto/feature |
| |
| +# Linux UDP segmentation offloading |
| + |
| +ngx_feature="UDP_SEGMENT" |
| +ngx_feature_name="NGX_HAVE_UDP_SEGMENT" |
| +ngx_feature_run=no |
| +ngx_feature_incs="#include <sys/socket.h> |
| + /* Can use <netinet/udp.h> only with newer glibc */ |
| + #include <linux/udp.h>" |
| +ngx_feature_path= |
| +ngx_feature_libs= |
| +ngx_feature_test="setsockopt(0, /* SOL_UDP */ 17, UDP_SEGMENT, NULL, 0)" |
| +. auto/feature |
| + |
| +# Linux time based packet transmission |
| + |
| +ngx_feature="SO_TXTIME" |
| +ngx_feature_name="NGX_HAVE_SO_TXTIME" |
| +ngx_feature_run=no |
| +ngx_feature_incs="#include <sys/socket.h> |
| + #include <linux/net_tstamp.h>" |
| +ngx_feature_path= |
| +ngx_feature_libs= |
| +ngx_feature_test="setsockopt(0, SOL_SOCKET, SO_TXTIME, NULL, 0)" |
| +. auto/feature |
| |
| # RFC 3542 way to get IPv6 datagram destination address |
| |
| @@ -448,6 +472,28 @@ ngx_feature_test="setsockopt(0, IPPROTO_IPV6, IPV6_RECVPKTINFO, NULL, 0)" |
| . auto/feature |
| |
| |
| +ngx_feature="IP_MTU_DISCOVER" |
| +ngx_feature_name="NGX_HAVE_IP_MTU_DISCOVER" |
| +ngx_feature_run=no |
| +ngx_feature_incs="#include <sys/socket.h> |
| + #include <netinet/in.h>" |
| +ngx_feature_path= |
| +ngx_feature_libs= |
| +ngx_feature_test="setsockopt(0, IPPROTO_IP, IP_MTU_DISCOVER, NULL, 0)" |
| +. auto/feature |
| + |
| + |
| +ngx_feature="IPV6_MTU_DISCOVER" |
| +ngx_feature_name="NGX_HAVE_IPV6_MTU_DISCOVER" |
| +ngx_feature_run=no |
| +ngx_feature_incs="#include <sys/socket.h> |
| + #include <netinet/in.h>" |
| +ngx_feature_path= |
| +ngx_feature_libs= |
| +ngx_feature_test="setsockopt(0, IPPROTO_IPV6, IPV6_MTU_DISCOVER, NULL, 0)" |
| +. auto/feature |
| + |
| + |
| ngx_feature="TCP_DEFER_ACCEPT" |
| ngx_feature_name="NGX_HAVE_DEFERRED_ACCEPT" |
| ngx_feature_run=no |
| diff --git a/src/core/ngx_connection.c b/src/core/ngx_connection.c |
| index 33682532a..87e73783a 100644 |
| --- a/src/core/ngx_connection.c |
| +++ b/src/core/ngx_connection.c |
| @@ -1010,6 +1010,72 @@ ngx_configure_listening_sockets(ngx_cycle_t *cycle) |
| } |
| } |
| |
| +#endif |
| + |
| +#if (NGX_QUIC) |
| + |
| +#if (NGX_HAVE_SO_TXTIME) |
| + |
| + /* Set SO_TXTIME socket option for pacing the QUIC outgoing |
| + * packets using FQ qdisc. */ |
| + if (ls[i].quic) { |
| + struct sock_txtime sk_txtime; |
| + |
| + sk_txtime.clockid = CLOCK_MONOTONIC; |
| + sk_txtime.flags = 0; |
| + |
| + if (setsockopt(ls[i].fd, SOL_SOCKET, SO_TXTIME, &sk_txtime, |
| + sizeof(sk_txtime)) == -1 ) |
| + { |
| + ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno, |
| + "setsockopt(SO_TXTIME) " |
| + "for %V failed, ignored.", |
| + &ls[i].addr_text); |
| + } else { |
| + ls[i].quic_so_txtime = 1; |
| + } |
| + } |
| + |
| +#endif |
| + |
| +#if (NGX_HAVE_IP_MTU_DISCOVER) |
| + |
| + /* Disable path MTU discovery and enable DF flag for QUIC. */ |
| + if (ls[i].quic && ls[i].sockaddr->sa_family == AF_INET) { |
| + int pmtud = IP_PMTUDISC_PROBE; |
| + |
| + if (setsockopt(ls[i].fd, IPPROTO_IP, IP_MTU_DISCOVER, &pmtud, |
| + sizeof(pmtud)) == -1 ) |
| + |
| + { |
| + ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno, |
| + "setsockopt(IP_MTU_DISCOVER) " |
| + "for %V failed, ignored.", |
| + &ls[i].addr_text); |
| + } |
| + } |
| + |
| +#endif |
| + |
| +#if (NGX_HAVE_IPV6_MTU_DISCOVER) |
| + |
| + /* Disable path MTU discovery and enable DF flag for QUIC. */ |
| + if (ls[i].quic && ls[i].sockaddr->sa_family == AF_INET6) { |
| + int pmtud = IPV6_PMTUDISC_PROBE; |
| + |
| + if (setsockopt(ls[i].fd, IPPROTO_IPV6, IPV6_MTU_DISCOVER, &pmtud, |
| + sizeof(pmtud)) == -1 ) |
| + |
| + { |
| + ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno, |
| + "setsockopt(IPV6_MTU_DISCOVER) " |
| + "for %V failed, ignored.", |
| + &ls[i].addr_text); |
| + } |
| + } |
| + |
| +#endif |
| + |
| #endif |
| } |
| |
| @@ -1053,6 +1119,22 @@ ngx_close_listening_sockets(ngx_cycle_t *cycle) |
| } |
| } |
| |
| + if (c->write->active) { |
| + if (ngx_event_flags & NGX_USE_EPOLL_EVENT) { |
| + |
| + /* |
| + * it seems that Linux-2.6.x OpenVZ sends events |
| + * for closed shared listening sockets unless |
| + * the events was explicitly deleted |
| + */ |
| + |
| + ngx_del_event(c->write, NGX_WRITE_EVENT, 0); |
| + |
| + } else { |
| + ngx_del_event(c->write, NGX_WRITE_EVENT, NGX_CLOSE_EVENT); |
| + } |
| + } |
| + |
| ngx_free_connection(c); |
| |
| c->fd = (ngx_socket_t) -1; |
| diff --git a/src/core/ngx_connection.h b/src/core/ngx_connection.h |
| index 54059629e..db39965c0 100644 |
| --- a/src/core/ngx_connection.h |
| +++ b/src/core/ngx_connection.h |
| @@ -79,6 +79,13 @@ struct ngx_listening_s { |
| unsigned deferred_accept:1; |
| unsigned delete_deferred:1; |
| unsigned add_deferred:1; |
| +#if (NGX_QUIC) |
| + unsigned quic:1; |
| + ngx_queue_t quic_blocked_events; |
| +#if (NGX_HAVE_SO_TXTIME) |
| + unsigned quic_so_txtime:1; |
| +#endif |
| +#endif |
| #if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER) |
| char *accept_filter; |
| #endif |
| @@ -156,6 +163,10 @@ struct ngx_connection_s { |
| |
| ngx_udp_connection_t *udp; |
| |
| +#if (NGX_QUIC) |
| + ngx_quic_connection_t *quic; |
| +#endif |
| + |
| struct sockaddr *local_sockaddr; |
| socklen_t local_socklen; |
| |
| diff --git a/src/core/ngx_core.h b/src/core/ngx_core.h |
| index 93ca9174d..d0441f034 100644 |
| --- a/src/core/ngx_core.h |
| +++ b/src/core/ngx_core.h |
| @@ -82,6 +82,9 @@ typedef void (*ngx_connection_handler_pt)(ngx_connection_t *c); |
| #if (NGX_OPENSSL) |
| #include <ngx_event_openssl.h> |
| #endif |
| +#if (NGX_QUIC) |
| +#include <ngx_event_quic.h> |
| +#endif |
| #include <ngx_process_cycle.h> |
| #include <ngx_conf_file.h> |
| #include <ngx_module.h> |
| diff --git a/src/event/ngx_event.c b/src/event/ngx_event.c |
| index 69c55d7a0..7c6bae343 100644 |
| --- a/src/event/ngx_event.c |
| +++ b/src/event/ngx_event.c |
| @@ -196,6 +196,9 @@ ngx_process_events_and_timers(ngx_cycle_t *cycle) |
| ngx_uint_t flags; |
| ngx_msec_t timer, delta; |
| |
| + ngx_queue_t *q; |
| + ngx_event_t *ev; |
| + |
| if (ngx_timer_resolution) { |
| timer = NGX_TIMER_INFINITE; |
| flags = 0; |
| @@ -215,6 +218,13 @@ ngx_process_events_and_timers(ngx_cycle_t *cycle) |
| #endif |
| } |
| |
| + if (!ngx_queue_empty(&ngx_posted_delayed_events)) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, |
| + "posted delayed event queue not empty" |
| + " making poll timeout 0"); |
| + timer = 0; |
| + } |
| + |
| if (ngx_use_accept_mutex) { |
| if (ngx_accept_disabled > 0) { |
| ngx_accept_disabled--; |
| @@ -252,11 +262,38 @@ ngx_process_events_and_timers(ngx_cycle_t *cycle) |
| ngx_shmtx_unlock(&ngx_accept_mutex); |
| } |
| |
| - if (delta) { |
| - ngx_event_expire_timers(); |
| - } |
| + ngx_event_expire_timers(); |
| |
| ngx_event_process_posted(cycle, &ngx_posted_events); |
| + |
| + while (!ngx_queue_empty(&ngx_posted_delayed_events)) { |
| + q = ngx_queue_head(&ngx_posted_delayed_events); |
| + |
| + ev = ngx_queue_data(q, ngx_event_t, queue); |
| + if (ev->delayed) { |
| + /* start of newly inserted nodes */ |
| + for (/* void */; |
| + q != ngx_queue_sentinel(&ngx_posted_delayed_events); |
| + q = ngx_queue_next(q)) |
| + { |
| + ev = ngx_queue_data(q, ngx_event_t, queue); |
| + ev->delayed = 0; |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, |
| + "skipping delayed posted event %p," |
| + " till next iteration", ev); |
| + } |
| + |
| + break; |
| + } |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, |
| + "delayed posted event %p", ev); |
| + |
| + ngx_delete_posted_event(ev); |
| + |
| + ev->handler(ev); |
| + } |
| } |
| |
| |
| @@ -640,6 +677,7 @@ ngx_event_process_init(ngx_cycle_t *cycle) |
| |
| ngx_queue_init(&ngx_posted_accept_events); |
| ngx_queue_init(&ngx_posted_events); |
| + ngx_queue_init(&ngx_posted_delayed_events); |
| |
| if (ngx_event_timer_init(cycle->log) == NGX_ERROR) { |
| return NGX_ERROR; |
| diff --git a/src/event/ngx_event.h b/src/event/ngx_event.h |
| index bb77c4ae6..8101a7061 100644 |
| --- a/src/event/ngx_event.h |
| +++ b/src/event/ngx_event.h |
| @@ -101,11 +101,7 @@ struct ngx_event_s { |
| * accept: 1 if accept many, 0 otherwise |
| */ |
| |
| -#if (NGX_HAVE_KQUEUE) || (NGX_HAVE_IOCP) |
| int available; |
| -#else |
| - unsigned available:1; |
| -#endif |
| |
| ngx_event_handler_pt handler; |
| |
| diff --git a/src/event/ngx_event_posted.c b/src/event/ngx_event_posted.c |
| index d851f3d14..b6cea009e 100644 |
| --- a/src/event/ngx_event_posted.c |
| +++ b/src/event/ngx_event_posted.c |
| @@ -12,6 +12,7 @@ |
| |
| ngx_queue_t ngx_posted_accept_events; |
| ngx_queue_t ngx_posted_events; |
| +ngx_queue_t ngx_posted_delayed_events; |
| |
| |
| void |
| diff --git a/src/event/ngx_event_posted.h b/src/event/ngx_event_posted.h |
| index 145d30fea..6c3885537 100644 |
| --- a/src/event/ngx_event_posted.h |
| +++ b/src/event/ngx_event_posted.h |
| @@ -43,6 +43,9 @@ void ngx_event_process_posted(ngx_cycle_t *cycle, ngx_queue_t *posted); |
| |
| extern ngx_queue_t ngx_posted_accept_events; |
| extern ngx_queue_t ngx_posted_events; |
| +extern ngx_queue_t ngx_posted_delayed_events; |
| + |
| +#define HAVE_POSTED_DELAYED_EVENTS_PATCH |
| |
| |
| #endif /* _NGX_EVENT_POSTED_H_INCLUDED_ */ |
| diff --git a/src/event/ngx_event_quic.c b/src/event/ngx_event_quic.c |
| new file mode 100644 |
| index 000000000..bc73cffc9 |
| --- /dev/null |
| +++ b/src/event/ngx_event_quic.c |
| @@ -0,0 +1,902 @@ |
| + |
| +/* |
| + * Copyright (C) Cloudflare, Inc. |
| + */ |
| + |
| +#include <ngx_config.h> |
| +#include <ngx_core.h> |
| +#include <ngx_event.h> |
| + |
| + |
| +/* Max send burst limit */ |
| +#define MAX_SEND_BURST_LIMIT 65507 |
| + |
| +/* errors */ |
| +#define NGX_QUIC_NO_ERROR 0x0 |
| +#define NGX_QUIC_INTERNAL_ERROR 0x1 |
| + |
| +/* From https://github.com/torvalds/linux/blob/24be4d0b46bb0c3c1dc7bacd30957d6144a70dfc/include/linux/udp.h#L98 */ |
| +#define UDP_MAX_SEGMENTS (1 << 6UL) |
| + |
| + |
| +static void ngx_quic_read_handler(ngx_event_t *ev); |
| +static void ngx_quic_write_handler(ngx_event_t *ev); |
| +static void ngx_quic_listener_write_event_handler(ngx_event_t *ev); |
| + |
| +static void ngx_quic_set_timer(ngx_connection_t *c); |
| + |
| +static void ngx_quic_handshake_completed(ngx_connection_t *c); |
| + |
| +static void ngx_quic_shutdown_handler(ngx_event_t *ev); |
| + |
| +static void ngx_quic_finalize_connection(ngx_connection_t *c, ngx_uint_t status); |
| +static void ngx_quic_close_connection(ngx_connection_t *c); |
| + |
| +static void ngx_quic_clear_sending_state(ngx_quic_connection_t *c); |
| +static ngx_int_t ngx_quic_try_send(ngx_connection_t *c); |
| +static ngx_int_t ngx_quic_send_udp_packet(ngx_connection_t *c, uint8_t *buf, |
| + size_t len, uint16_t segment_size, bool retry); |
| + |
| + |
| +static ngx_command_t ngx_quic_commands[] = { |
| + |
| + ngx_null_command |
| +}; |
| + |
| + |
| +static ngx_core_module_t ngx_quic_module_ctx = { |
| + ngx_string("quic"), |
| + NULL, |
| + NULL |
| +}; |
| + |
| + |
| +ngx_module_t ngx_quic_module = { |
| + NGX_MODULE_V1, |
| + &ngx_quic_module_ctx, /* module context */ |
| + ngx_quic_commands, /* module directives */ |
| + NGX_CORE_MODULE, /* module type */ |
| + NULL, /* init master */ |
| + NULL, /* init module */ |
| + NULL, /* init process */ |
| + NULL, /* init thread */ |
| + NULL, /* exit thread */ |
| + NULL, /* exit process */ |
| + NULL, /* exit master */ |
| + NGX_MODULE_V1_PADDING |
| +}; |
| + |
| + |
| +ngx_int_t |
| +ngx_quic_create_conf(ngx_quic_t *quic) |
| +{ |
| + quic->config = quiche_config_new(QUICHE_PROTOCOL_VERSION); |
| + if (quic->config == NULL) { |
| + ngx_log_error(NGX_LOG_EMERG, quic->log, 0, "failed to create quic config"); |
| + return NGX_ERROR; |
| + } |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +ngx_int_t |
| +ngx_quic_validate_initial(ngx_event_t *ev, u_char *buf, ssize_t buf_len) |
| +{ |
| + /* Check incoming packet type, if it's not Initial we shouldn't be here. */ |
| + if (((buf[0] & 0x30) >> 4) != 0) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, |
| + "packet is not quic client initial"); |
| + return NGX_ERROR; |
| + } |
| + |
| + /* Client Initial packets must be at least 1200 bytes. */ |
| + if (buf_len < QUICHE_MIN_CLIENT_INITIAL_LEN) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, |
| + "quic initial packet is too short"); |
| + return NGX_ERROR; |
| + } |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +ngx_int_t |
| +ngx_quic_create_connection(ngx_quic_t *quic, ngx_connection_t *c) |
| +{ |
| + int rc; |
| + u_char *buf; |
| + size_t buf_len; |
| + quiche_conn *conn; |
| + static uint8_t out[MAX_DATAGRAM_SIZE]; |
| + |
| + uint8_t pkt_type; |
| + uint32_t pkt_version; |
| + |
| + uint8_t scid[QUICHE_MAX_CONN_ID_LEN]; |
| + size_t scid_len = sizeof(scid); |
| + |
| + uint8_t dcid[QUICHE_MAX_CONN_ID_LEN]; |
| + size_t dcid_len = sizeof(dcid); |
| + |
| + uint8_t token[1]; |
| + size_t token_len = sizeof(token); |
| + |
| + ngx_quic_connection_t *qc; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic init connection"); |
| + |
| + /* Extract some fields from the client's Initial packet, which was saved |
| + * into c->buffer by ngx_event_recvmsg(). */ |
| + buf = c->buffer->pos; |
| + buf_len = ngx_buf_size(c->buffer); |
| + |
| + rc = quiche_header_info(buf, buf_len, QUICHE_MAX_CONN_ID_LEN, |
| + &pkt_version, &pkt_type, |
| + scid, &scid_len, dcid, &dcid_len, |
| + token, &token_len); |
| + if (rc < 0) { |
| + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "failed to parse quic header: %d", rc); |
| + return NGX_ERROR; |
| + } |
| + |
| + /* Version mismatch, do version negotiation. */ |
| + if (!quiche_version_is_supported(pkt_version)) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "quic version negotiation"); |
| + |
| + ssize_t written = quiche_negotiate_version(scid, scid_len, |
| + dcid, dcid_len, |
| + out, sizeof(out)); |
| + |
| + if (written < 0) { |
| + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "failed to create quic vneg packet: %d", written); |
| + return NGX_ERROR; |
| + } |
| + |
| + if (ngx_quic_send_udp_packet(c, out, written, 0, false) == NGX_ERROR) { |
| + return NGX_ERROR; |
| + } |
| + |
| + return NGX_DONE; |
| + } |
| + |
| + /* Initialize source connection ID with some random bytes. */ |
| + RAND_bytes(scid, sizeof(scid)); |
| + |
| +#if (NGX_DEBUG) |
| + { |
| + uint8_t dcid_hex[QUICHE_MAX_CONN_ID_LEN * 2], |
| + scid_hex[QUICHE_MAX_CONN_ID_LEN * 2]; |
| + |
| + ngx_log_debug4(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "new quic connection dcid:%*.s new_scid:%*.s", |
| + ngx_hex_dump(dcid_hex, dcid, dcid_len) - dcid_hex, dcid_hex, |
| + ngx_hex_dump(scid_hex, scid, sizeof(scid)) - scid_hex, scid_hex); |
| + } |
| +#endif |
| + |
| + conn = quiche_conn_new_with_tls(dcid, sizeof(dcid), NULL, 0, |
| + c->local_sockaddr, c->local_socklen, |
| + c->sockaddr, c->socklen, quic->config, |
| + c->ssl->connection, true); |
| + if (conn == NULL) { |
| + ngx_log_error(NGX_LOG_ERR, c->log, 0, "failed to create quic connection"); |
| + return NGX_ERROR; |
| + } |
| + |
| + qc = ngx_pcalloc(c->pool, sizeof(ngx_quic_connection_t)); |
| + if (qc == NULL) { |
| + quiche_conn_free(conn); |
| + return NGX_ERROR; |
| + } |
| + |
| + qc->handler = NULL; |
| + |
| + qc->conn = conn; |
| + |
| + qc->pacing = quic->pacing; |
| + |
| + qc->send_buf = ngx_palloc(c->pool, MAX_SEND_BURST_LIMIT); |
| + if (qc->send_buf == NULL) { |
| + quiche_conn_free(conn); |
| + return NGX_ERROR; |
| + } |
| + |
| + ngx_quic_clear_sending_state(qc); |
| + |
| + c->quic = qc; |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +ngx_int_t |
| +ngx_quic_handshake(ngx_connection_t *c) |
| +{ |
| + u_char *buf; |
| + size_t buf_len; |
| + ssize_t done; |
| + |
| + quiche_recv_info recv_info = { |
| + c->sockaddr, |
| + c->socklen, |
| + c->local_sockaddr, |
| + c->local_socklen, |
| + }; |
| + |
| + /* Process the client's Initial packet, which was saved into c->buffer by |
| + * ngx_event_recvmsg(). */ |
| + buf = c->buffer->pos; |
| + buf_len = ngx_buf_size(c->buffer); |
| + |
| + done = quiche_conn_recv(c->quic->conn, buf, buf_len, &recv_info); |
| + |
| + if ((done < 0) && (done != QUICHE_ERR_DONE)) { |
| + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "failed to process quic packet: %d", done); |
| + return NGX_ERROR; |
| + } |
| + |
| + c->read->handler = ngx_quic_read_handler; |
| + c->write->handler = ngx_quic_write_handler; |
| + |
| + ngx_post_event(c->write, &ngx_posted_events); |
| + |
| + return NGX_AGAIN; |
| +} |
| + |
| + |
| +static void |
| +ngx_quic_read_handler(ngx_event_t *rev) |
| +{ |
| + int n; |
| + static uint8_t buf[65535]; |
| + ngx_connection_t *c; |
| + uint8_t *b; |
| + |
| + c = rev->data; |
| + b = buf; |
| + |
| + c->log->action = "reading QUIC packets"; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic read handler"); |
| + |
| + if (rev->timedout) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "quic connection timed out"); |
| + |
| + if (c->quic->handler != NULL) { |
| + c->quic->handler(c); |
| + } |
| + |
| + return; |
| + } |
| + |
| + for (;;) { |
| + n = c->recv(c, buf, sizeof(buf)); |
| + if (n == NGX_AGAIN) { |
| + break; |
| + } |
| + |
| + if (n == NGX_ERROR) { |
| + ngx_quic_finalize_connection(c, NGX_QUIC_INTERNAL_ERROR); |
| + return; |
| + } |
| + |
| + quiche_recv_info recv_info = { |
| + c->sockaddr, |
| + c->socklen, |
| + c->local_sockaddr, |
| + c->local_socklen, |
| + }; |
| + |
| + ssize_t done = quiche_conn_recv(c->quic->conn, b, n, &recv_info); |
| + |
| + if (done == QUICHE_ERR_DONE) { |
| + break; |
| + } |
| + |
| + if (done < 0) { |
| + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "failed to process quic packet: %d", done); |
| + |
| + ngx_quic_finalize_connection(c, NGX_QUIC_INTERNAL_ERROR); |
| + return; |
| + } |
| + } |
| + |
| + if (quiche_conn_is_in_early_data(c->quic->conn) || |
| + quiche_conn_is_established(c->quic->conn)) { |
| + if (!c->ssl->handshaked) { |
| + ngx_quic_handshake_completed(c); |
| + } |
| + |
| + if ((c->quic == NULL) || (c->quic->handler == NULL)) { |
| + return; |
| + } |
| + |
| + /* Notify application layer that there might be stream data to read. */ |
| + c->quic->handler(c); |
| + } |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic done reading"); |
| + |
| + ngx_post_event(c->write, &ngx_posted_events); |
| +} |
| + |
| +static void |
| +ngx_quic_listener_write_event_handler(ngx_event_t *ev) |
| +{ |
| + ngx_queue_t *q; |
| + ngx_event_t *wev; |
| + ngx_listening_t *ls; |
| + ngx_connection_t *c; |
| + |
| + c = ev->data; |
| + ls = c->listening; |
| + |
| + /* Unblock the quic events waiting on socket buffer. */ |
| + while (!ngx_queue_empty(&ls->quic_blocked_events)) { |
| + |
| + q = ngx_queue_head(&ls->quic_blocked_events); |
| + wev = ngx_queue_data(q, ngx_event_t, queue); |
| + |
| + ngx_delete_posted_event(wev); |
| + |
| + ngx_post_event(wev, &ngx_posted_events); |
| + } |
| + |
| + /* Unregister from spurious wake ups */ |
| + if (ls->connection->write->active) { |
| + ngx_del_event(ls->connection->write, NGX_WRITE_EVENT, 0); |
| + } |
| +} |
| + |
| +static void |
| +ngx_quic_clear_sending_state(ngx_quic_connection_t *qc) |
| +{ |
| + qc->segment_size = 0; |
| + qc->last_segment_size = 0; |
| + qc->send_buf_size = 0; |
| + qc->send_buf_offset = 0; |
| + qc->blocked = 0; |
| +} |
| + |
| +static void |
| +ngx_quic_write_handler(ngx_event_t *wev) |
| +{ |
| + bool done; |
| + size_t total_written; |
| + size_t total_segments; |
| + ssize_t prev_written; |
| + uint16_t last_segment_size; |
| + uint16_t segment_size; |
| + ngx_connection_t *c; |
| + quiche_send_info send_info; |
| + ngx_connection_t *ls_c; |
| + ngx_listening_t *ls; |
| + ngx_event_t *ls_wev; |
| + ngx_quic_connection_t *qc; |
| + uint8_t *out; |
| + |
| + c = wev->data; |
| + ls = c->listening; |
| + ls_c = ls->connection; |
| + ls_wev = ls_c->write; |
| + qc = c->quic; |
| + |
| + c->log->action = "writing QUIC packets"; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic write handler"); |
| + |
| + if (wev->timedout) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic alarm fired"); |
| + |
| + quiche_conn_on_timeout(c->quic->conn); |
| + } |
| + |
| + if (quiche_conn_is_closed(c->quic->conn)) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "quic connection is closed"); |
| + |
| + ngx_quic_finalize_connection(c, NGX_QUIC_NO_ERROR); |
| + return; |
| + } |
| + |
| + /* Socket is not ready, add to blocked queue. */ |
| + if (!ls_wev->ready) { |
| + ngx_post_event(wev, &ls->quic_blocked_events); |
| + return; |
| + } |
| + |
| + /* Try sending blocked packets first. */ |
| + if (qc->blocked) { |
| + |
| + if (ngx_quic_try_send(c) == NGX_OK) { |
| + ngx_post_event(wev, &ngx_posted_events); |
| + } |
| + |
| + return; |
| + } |
| + |
| + /* Round down by MAX_DATAGRAM_SIZE. */ |
| + const size_t max_send_burst = |
| + ngx_min(quiche_conn_send_quantum(c->quic->conn), |
| + MAX_SEND_BURST_LIMIT) |
| + / MAX_DATAGRAM_SIZE * MAX_DATAGRAM_SIZE; |
| + |
| + prev_written = 0; |
| + total_written = 0; |
| + total_segments = 0; |
| + segment_size = 0; |
| + last_segment_size = 0; |
| + done = 0; |
| + |
| + out = qc->send_buf; |
| + |
| + for (;;) { |
| + /* Make sure we don't exceed the max send burst limit. */ |
| + size_t max_len = ngx_min(max_send_burst - total_written, |
| + MAX_DATAGRAM_SIZE); |
| + |
| + ssize_t written = quiche_conn_send(c->quic->conn, out + total_written, |
| + max_len, &send_info); |
| + |
| + /* Flush and exit if there are no more packets to send. */ |
| + if (written == QUICHE_ERR_DONE) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic done writing"); |
| + |
| + /* Nothing more to send. */ |
| + done = 1; |
| + |
| + break; |
| + } |
| + |
| + if (written < 0) { |
| + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "failed to create quic packet: %d", written); |
| + |
| + ngx_quic_finalize_connection(c, NGX_QUIC_INTERNAL_ERROR); |
| + return; |
| + } |
| + |
| + total_written += written; |
| + |
| + total_segments += 1; |
| + |
| + /* |
| + * segment_size is the first packet size in the loop, if |
| + * there are 2 or more packets. |
| + */ |
| + if (!segment_size) { |
| + segment_size = written; |
| + |
| + /* 1st packet timestamp is used when sending in a batch */ |
| + ngx_memcpy(&c->quic->send_info, &send_info, sizeof(quiche_send_info)); |
| + } |
| + |
| + last_segment_size = written; |
| + |
| + /* Flush but keep sending if the max burst limit is reached. */ |
| + if (total_written >= max_send_burst) { |
| + break; |
| + } |
| + |
| + /* Flush but keep sending if the number of segments reached the GSO |
| + * limit. */ |
| + if (total_segments >= UDP_MAX_SEGMENTS) { |
| + break; |
| + } |
| + |
| + /* Flush but keep sending if the packet size changes but the max burst |
| + * limit isn't reached yet. */ |
| + if (prev_written && written != prev_written) { |
| + break; |
| + } |
| + |
| + prev_written = written; |
| + } |
| + |
| + /* Send packets to the network. */ |
| + if (total_written > 0) { |
| + if (last_segment_size > segment_size) { |
| + qc->last_segment_size = last_segment_size; |
| + } |
| + |
| + qc->send_buf_size = total_written; |
| + qc->segment_size = segment_size; |
| + |
| + if (ngx_quic_try_send(c) == NGX_ERROR) { |
| + return; |
| + } |
| + |
| + /* Still need to send more. Schedule to next worker cycle. */ |
| + if (!done) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic pause writing"); |
| + |
| + wev->delayed = 1; |
| + ngx_post_event(wev, &ngx_posted_delayed_events); |
| + } |
| + } |
| + |
| + ngx_quic_set_timer(c); |
| +} |
| + |
| +static ngx_int_t |
| +ngx_quic_try_send(ngx_connection_t *c) |
| +{ |
| + int rc; |
| + size_t send_size; |
| + size_t off; |
| + uint16_t segment_size; |
| + uint16_t last_segment_size; |
| + uint8_t *out; |
| + ngx_quic_connection_t *qc; |
| + |
| + qc = c->quic; |
| + |
| + out = qc->send_buf; |
| + off = qc->send_buf_offset; |
| + |
| + segment_size = qc->segment_size; |
| + last_segment_size = qc->last_segment_size; |
| + |
| + send_size = qc->send_buf_size - last_segment_size; |
| + |
| + rc = NGX_OK; |
| + |
| + /* |
| + * Resume sending only the last packet which is bigger than |
| + * the segment size of the rest of the packets in the batch. |
| + */ |
| + if (send_size == off) { |
| + goto last_segment; |
| + } |
| + |
| +#if (NGX_HAVE_UDP_SEGMENT) |
| + rc = ngx_quic_send_udp_packet(c, out, send_size, segment_size, true); |
| +#else |
| + while (off < send_size) { |
| + size_t len = (send_size - off) > segment_size |
| + ? segment_size : (send_size - off); |
| + |
| + rc = ngx_quic_send_udp_packet(c, out + off, len, 0, true); |
| + |
| + if (rc != NGX_OK) { |
| + break; |
| + } |
| + |
| + off += len; |
| + } |
| +#endif |
| + |
| + if (rc == NGX_AGAIN) { |
| + /* Update where to resume sending once unblocked. */ |
| + qc->send_buf_offset = off; |
| + qc->blocked = 1; |
| + |
| + return NGX_OK; |
| + } |
| + |
| + if (rc == NGX_ERROR) { |
| + ngx_quic_finalize_connection(c, NGX_QUIC_INTERNAL_ERROR); |
| + return rc; |
| + } |
| + |
| +last_segment: |
| + /* |
| + * If the last segment size is bigger than the segment size, |
| + * send packets with the same size using GSO (if available), |
| + * and then send the last one separately at the end. |
| + */ |
| + if (rc == NGX_OK && last_segment_size > 0) { |
| + |
| + c->quic->segment_size = 0; |
| + rc = ngx_quic_send_udp_packet(c, |
| + out + send_size, last_segment_size, 0, true); |
| + |
| + if (rc == NGX_AGAIN) { |
| + /* Update where to resume sending once unblocked. */ |
| + qc->send_buf_offset = send_size; |
| + qc->blocked = 1; |
| + |
| + return NGX_OK; |
| + } |
| + |
| + if (rc == NGX_ERROR) { |
| + ngx_quic_finalize_connection(c, NGX_QUIC_INTERNAL_ERROR); |
| + return rc; |
| + } |
| + } |
| + |
| + /* Packets sent, clear sending state for next cycle. */ |
| + ngx_quic_clear_sending_state(c->quic); |
| + |
| + return NGX_OK; |
| +} |
| + |
| +static void |
| +ngx_quic_set_timer(ngx_connection_t *c) |
| +{ |
| + uint64_t expiry; |
| + ngx_event_t *wev; |
| + |
| + wev = c->write; |
| + |
| + expiry = quiche_conn_timeout_as_millis(c->quic->conn); |
| + expiry = ngx_max(expiry, 1); |
| + |
| + if (wev->timer_set) { |
| + ngx_del_timer(wev); |
| + } |
| + |
| + /* quiche_conn_timeout_as_millis() will return UINT64_MAX when the timer |
| + * should be unset (this would be equvalent to returning Option::None in |
| + * Rust). To avoid overflow we need to explicitly check for this value. */ |
| + if (expiry != UINT64_MAX) { |
| + ngx_add_timer(wev, (ngx_msec_t)expiry); |
| + } |
| +} |
| + |
| + |
| +static void |
| +ngx_quic_handshake_completed(ngx_connection_t *c) |
| +{ |
| +#if (NGX_DEBUG) |
| + { |
| + char buf[129], *s, *d; |
| +#if OPENSSL_VERSION_NUMBER >= 0x10000000L |
| + const |
| +#endif |
| + SSL_CIPHER *cipher; |
| + |
| + cipher = SSL_get_current_cipher(c->ssl->connection); |
| + |
| + if (cipher) { |
| + SSL_CIPHER_description(cipher, &buf[1], 128); |
| + |
| + for (s = &buf[1], d = buf; *s; s++) { |
| + if (*s == ' ' && *d == ' ') { |
| + continue; |
| + } |
| + |
| + if (*s == LF || *s == CR) { |
| + continue; |
| + } |
| + |
| + *++d = *s; |
| + } |
| + |
| + if (*d != ' ') { |
| + d++; |
| + } |
| + |
| + *d = '\0'; |
| + |
| + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "QUIC: %s, cipher: \"%s\"", |
| + SSL_get_version(c->ssl->connection), &buf[1]); |
| + |
| + if (SSL_session_reused(c->ssl->connection)) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "quic reused session"); |
| + } |
| + |
| + } else { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "quic no shared ciphers"); |
| + } |
| + } |
| +#endif |
| + |
| + ngx_del_timer(c->read); |
| + |
| + c->ssl->handshaked = 1; |
| + |
| + /* Notify application layer that the handshake is complete. */ |
| + c->ssl->handler(c); |
| +} |
| + |
| + |
| +ngx_int_t |
| +ngx_quic_shutdown(ngx_connection_t *c) |
| +{ |
| + ssize_t written; |
| + static uint8_t out[MAX_DATAGRAM_SIZE]; |
| + |
| + /* Connection is closed, free memory. */ |
| + if (quiche_conn_is_closed(c->quic->conn)) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "free quic connection"); |
| + |
| + quiche_conn_free(c->quic->conn); |
| + |
| + c->quic = NULL; |
| + c->ssl = NULL; |
| + |
| + return NGX_OK; |
| + } |
| + |
| + /* We can't free the connection state yet, as we need to wait for the |
| + * draining timeout to expire. |
| + * |
| + * Setup event handlers such that we will try again when that happens (or |
| + * when another event is triggered). */ |
| + c->read->handler = ngx_quic_shutdown_handler; |
| + c->write->handler = ngx_quic_shutdown_handler; |
| + |
| + /* Try sending a packet in order to flush pending frames (CONNECTION_CLOSE |
| + * for example), but ignore errors as we are already closing the connection |
| + * anyway. */ |
| + written = quiche_conn_send(c->quic->conn, out, sizeof(out), &c->quic->send_info); |
| + |
| + if (written > 0) { |
| + ngx_quic_send_udp_packet(c, out, written, 0, false); |
| + } |
| + |
| + ngx_quic_set_timer(c); |
| + |
| + return NGX_AGAIN; |
| +} |
| + |
| + |
| +static void |
| +ngx_quic_shutdown_handler(ngx_event_t *ev) |
| +{ |
| + ngx_connection_t *c; |
| + ngx_connection_handler_pt handler; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, 0, "quic shutdown handler"); |
| + |
| + c = ev->data; |
| + handler = c->quic->handler; |
| + |
| + if (ev->timedout) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "quic alarm fired"); |
| + |
| + quiche_conn_on_timeout(c->quic->conn); |
| + } |
| + |
| + if (ngx_quic_shutdown(c) == NGX_AGAIN) { |
| + return; |
| + } |
| + |
| + handler(c); |
| +} |
| + |
| + |
| +static void |
| +ngx_quic_finalize_connection(ngx_connection_t *c, ngx_uint_t status) |
| +{ |
| + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "finalize quic connection: %d", c->fd); |
| + |
| + c->error = 1; |
| + |
| + if (quiche_conn_is_closed(c->quic->conn)) { |
| + c->close = 1; |
| + } |
| + |
| + ngx_quic_clear_sending_state(c->quic); |
| + |
| + quiche_conn_close(c->quic->conn, false, status, NULL, 0); |
| + |
| + /* Notify the application layer that the connection is in an error |
| + * state and will be closed. */ |
| + if (c->quic->handler != NULL) { |
| + c->quic->handler(c); |
| + return; |
| + } |
| + |
| + ngx_quic_close_connection(c); |
| +} |
| + |
| + |
| +static void |
| +ngx_quic_close_connection(ngx_connection_t *c) |
| +{ |
| + ngx_pool_t *pool; |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "close quic connection: %d", c->fd); |
| + |
| + if (c->quic) { |
| + if (ngx_quic_shutdown(c) == NGX_AGAIN) { |
| + c->quic->handler = ngx_quic_close_connection; |
| + return; |
| + } |
| + } |
| + |
| +#if (NGX_STAT_STUB) |
| + (void) ngx_atomic_fetch_add(ngx_stat_active, -1); |
| +#endif |
| + |
| + c->destroyed = 1; |
| + |
| + pool = c->pool; |
| + |
| + ngx_close_connection(c); |
| + |
| + ngx_destroy_pool(pool); |
| +} |
| + |
| + |
| +void |
| +ngx_quic_cleanup_ctx(void *data) |
| +{ |
| + ngx_quic_t *quic = data; |
| + |
| + quiche_config_free(quic->config); |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_quic_send_udp_packet(ngx_connection_t *c, uint8_t *buf, size_t len, |
| + uint16_t segment_size, bool retry) |
| +{ |
| + ngx_event_t *ls_wev; |
| + ngx_event_t *wev; |
| + ngx_connection_t *ls_c; |
| + ngx_listening_t *ls; |
| + ngx_buf_t out_buf = {0}; |
| + ngx_chain_t out_chain = {0}; |
| + ngx_chain_t *cl; |
| + |
| + wev = c->write; |
| + ls = c->listening; |
| + ls_c = ls->connection; |
| + ls_wev = ls_c->write; |
| + |
| + /* The send_chain() API takes an ngx_chain_t parameter instead of a simple |
| + * buffer, so we need to initialize the chain such that it contains only a |
| + * single buffer. |
| + * |
| + * The c->send_chain() call is required (instead of just c->send()) because |
| + * it uses the sendmsg(2) syscall (instead of sendto(2)), which allows us to |
| + * specify the correct source IP address for the connection. */ |
| + |
| + out_buf.start = out_buf.pos = buf; |
| + out_buf.end = out_buf.last = buf + len; |
| + out_buf.memory = 1; |
| + out_buf.flush = 1; |
| + |
| + out_chain.buf = &out_buf; |
| + out_chain.next = NULL; |
| + |
| + c->write->ready = 1; |
| + |
| + cl = c->send_chain(c, &out_chain, 0); |
| + |
| + if (cl != NULL) { |
| + if (retry) { |
| + ls_wev->ready = 0; |
| + |
| + if (!ls_wev->active) { |
| + |
| + ls_wev->handler = ngx_quic_listener_write_event_handler; |
| + |
| + if (ngx_handle_write_event(ls_wev, 0) != NGX_OK) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "failed to add quic write event"); |
| + |
| + return NGX_ERROR; |
| + } |
| + } |
| + |
| + ngx_post_event(wev, &ls->quic_blocked_events); |
| + } |
| + |
| + return NGX_AGAIN; |
| + } |
| + |
| + if (cl == NGX_CHAIN_ERROR) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "failed to send quic packet"); |
| + |
| + return NGX_ERROR; |
| + } |
| + |
| + return NGX_OK; |
| +} |
| diff --git a/src/event/ngx_event_quic.h b/src/event/ngx_event_quic.h |
| new file mode 100644 |
| index 000000000..1459f3b79 |
| --- /dev/null |
| +++ b/src/event/ngx_event_quic.h |
| @@ -0,0 +1,63 @@ |
| + |
| +/* |
| + * Copyright (C) Cloudflare, Inc. |
| + */ |
| + |
| + |
| +#ifndef _NGX_EVENT_QUIC_H_INCLUDED_ |
| +#define _NGX_EVENT_QUIC_H_INCLUDED_ |
| + |
| + |
| +#include <stdbool.h> |
| + |
| +#include <ngx_config.h> |
| +#include <ngx_core.h> |
| + |
| +#include <quiche.h> |
| + |
| +/* Limit outgoing packets to 1200 bytes. This is the minimum value allowed. */ |
| +#define MAX_DATAGRAM_SIZE 1200 |
| + |
| +typedef struct ngx_quic_s ngx_quic_t; |
| +typedef struct ngx_quic_connection_s ngx_quic_connection_t; |
| + |
| +struct ngx_quic_s { |
| + quiche_config *config; |
| + ngx_log_t *log; |
| + bool pacing; |
| +}; |
| + |
| +struct ngx_quic_connection_s { |
| + quiche_conn *conn; |
| + |
| + ngx_connection_handler_pt handler; |
| + |
| + uint8_t *send_buf; |
| + size_t send_buf_offset; |
| + uint16_t send_buf_size; |
| + uint16_t segment_size; |
| + uint16_t last_segment_size; |
| + |
| + bool blocked; |
| + bool pacing; |
| + quiche_send_info send_info; |
| +}; |
| + |
| + |
| +ngx_int_t ngx_quic_create_conf(ngx_quic_t *quic); |
| + |
| +ngx_int_t ngx_quic_validate_initial(ngx_event_t *ev, u_char *buf, |
| + ssize_t buf_len); |
| + |
| +ngx_int_t ngx_quic_create_connection(ngx_quic_t *quic, ngx_connection_t *c); |
| + |
| +ngx_int_t ngx_quic_create_ssl_connection(ngx_ssl_t *ssl, ngx_connection_t *c, |
| + ngx_uint_t flags); |
| + |
| +ngx_int_t ngx_quic_handshake(ngx_connection_t *c); |
| + |
| +ngx_int_t ngx_quic_shutdown(ngx_connection_t *c); |
| + |
| +void ngx_quic_cleanup_ctx(void *data); |
| + |
| +#endif /* _NGX_EVENT_QUIC_H_INCLUDED_ */ |
| diff --git a/src/event/ngx_event_udp.c b/src/event/ngx_event_udp.c |
| index 557283050..daa504bb0 100644 |
| --- a/src/event/ngx_event_udp.c |
| +++ b/src/event/ngx_event_udp.c |
| @@ -9,6 +9,10 @@ |
| #include <ngx_core.h> |
| #include <ngx_event.h> |
| |
| +/* |
| + * Max number of recvmsg() tried in ngx_event_recvmsg() |
| + */ |
| +#define NGX_RECVMSG_MAX 64 |
| |
| #if !(NGX_WIN32) |
| |
| @@ -70,8 +74,9 @@ ngx_event_recvmsg(ngx_event_t *ev) |
| |
| ecf = ngx_event_get_conf(ngx_cycle->conf_ctx, ngx_event_core_module); |
| |
| + /* For non-kqueue, ev->available is a packet counter */ |
| if (!(ngx_event_flags & NGX_USE_KQUEUE_EVENT)) { |
| - ev->available = ecf->multi_accept; |
| + ev->available = NGX_RECVMSG_MAX; |
| } |
| |
| lc = ev->data; |
| @@ -276,6 +281,14 @@ ngx_event_recvmsg(ngx_event_t *ev) |
| (void) ngx_atomic_fetch_add(ngx_stat_accepted, 1); |
| #endif |
| |
| +#if (NGX_QUIC) |
| + if (ls->quic) { |
| + if (ngx_quic_validate_initial(ev, buffer, n) != NGX_OK) { |
| + goto next; |
| + } |
| + } |
| +#endif |
| + |
| ngx_accept_disabled = ngx_cycle->connection_n / 8 |
| - ngx_cycle->free_connection_n; |
| |
| @@ -413,13 +426,37 @@ ngx_event_recvmsg(ngx_event_t *ev) |
| |
| ls->handler(c); |
| |
| + /* When multi_accept is off, exit the loop now */ |
| + if (!ecf->multi_accept) { |
| + break; |
| + } |
| + |
| next: |
| |
| if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) { |
| ev->available -= n; |
| + } else { |
| + ev->available --; |
| } |
| |
| } while (ev->available); |
| + |
| + /* Reschedule recvmsg if there is more packets to read */ |
| + n = recvmsg(lc->fd, &msg, MSG_PEEK); |
| + |
| + if (n == -1) { |
| + err = ngx_socket_errno; |
| + |
| + if (err != EAGAIN) { |
| + ngx_log_error(NGX_LOG_ALERT, ev->log, err, "recvmsg() peek error"); |
| + } |
| + } else { |
| + ev->delayed = 1; |
| + ngx_post_event(ev, &ngx_posted_delayed_events); |
| + |
| + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0, |
| + "recvmsg on %V, reschedule: %d", &ls->addr_text, ev->available); |
| + } |
| } |
| |
| |
| diff --git a/src/http/modules/ngx_http_ssl_module.c b/src/http/modules/ngx_http_ssl_module.c |
| index b3f8f4795..00dd9c61a 100644 |
| --- a/src/http/modules/ngx_http_ssl_module.c |
| +++ b/src/http/modules/ngx_http_ssl_module.c |
| @@ -371,7 +371,7 @@ ngx_http_ssl_alpn_select(ngx_ssl_conn_t *ssl_conn, const unsigned char **out, |
| #if (NGX_DEBUG) |
| unsigned int i; |
| #endif |
| -#if (NGX_HTTP_V2) |
| +#if (NGX_HTTP_V2 || NGX_HTTP_V3) |
| ngx_http_connection_t *hc; |
| #endif |
| #if (NGX_HTTP_V2 || NGX_DEBUG) |
| @@ -388,9 +388,11 @@ ngx_http_ssl_alpn_select(ngx_ssl_conn_t *ssl_conn, const unsigned char **out, |
| } |
| #endif |
| |
| -#if (NGX_HTTP_V2) |
| +#if (NGX_HTTP_V2 || NGX_HTTP_V3) |
| hc = c->data; |
| +#endif |
| |
| +#if (NGX_HTTP_V2) |
| if (hc->addr_conf->http2) { |
| srv = |
| (unsigned char *) NGX_HTTP_V2_ALPN_ADVERTISE NGX_HTTP_NPN_ADVERTISE; |
| @@ -398,6 +400,13 @@ ngx_http_ssl_alpn_select(ngx_ssl_conn_t *ssl_conn, const unsigned char **out, |
| |
| } else |
| #endif |
| +#if (NGX_HTTP_V3) |
| + if (hc->addr_conf->quic) { |
| + srv = (unsigned char *) QUICHE_H3_APPLICATION_PROTOCOL; |
| + srvlen = sizeof(QUICHE_H3_APPLICATION_PROTOCOL) - 1; |
| + |
| + } else |
| +#endif |
| { |
| srv = (unsigned char *) NGX_HTTP_NPN_ADVERTISE; |
| srvlen = sizeof(NGX_HTTP_NPN_ADVERTISE) - 1; |
| diff --git a/src/http/ngx_http.c b/src/http/ngx_http.c |
| index 79ef9c644..a29bff836 100644 |
| --- a/src/http/ngx_http.c |
| +++ b/src/http/ngx_http.c |
| @@ -1141,6 +1141,7 @@ ngx_int_t |
| ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf, |
| ngx_http_listen_opt_t *lsopt) |
| { |
| + int t; |
| in_port_t p; |
| ngx_uint_t i; |
| struct sockaddr *sa; |
| @@ -1159,11 +1160,13 @@ ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf, |
| |
| sa = lsopt->sockaddr; |
| p = ngx_inet_get_port(sa); |
| + t = lsopt->quic ? SOCK_DGRAM : SOCK_STREAM; |
| |
| port = cmcf->ports->elts; |
| for (i = 0; i < cmcf->ports->nelts; i++) { |
| |
| - if (p != port[i].port || sa->sa_family != port[i].family) { |
| + if (p != port[i].port || sa->sa_family != port[i].family |
| + || t != port[i].type) { |
| continue; |
| } |
| |
| @@ -1182,6 +1185,7 @@ ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf, |
| port->family = sa->sa_family; |
| port->port = p; |
| port->addrs.elts = NULL; |
| + port->type = t; |
| |
| return ngx_http_add_address(cf, cscf, port, lsopt); |
| } |
| @@ -1199,6 +1203,9 @@ ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf, |
| #if (NGX_HTTP_V2) |
| ngx_uint_t http2; |
| #endif |
| +#if (NGX_HTTP_V3) |
| + ngx_uint_t quic; |
| +#endif |
| |
| /* |
| * we cannot compare whole sockaddr struct's as kernel |
| @@ -1234,6 +1241,9 @@ ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf, |
| #if (NGX_HTTP_V2) |
| http2 = lsopt->http2 || addr[i].opt.http2; |
| #endif |
| +#if (NGX_HTTP_V3) |
| + quic = lsopt->quic || addr[i].opt.quic; |
| +#endif |
| |
| if (lsopt->set) { |
| |
| @@ -1270,6 +1280,9 @@ ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf, |
| #if (NGX_HTTP_V2) |
| addr[i].opt.http2 = http2; |
| #endif |
| +#if (NGX_HTTP_V3) |
| + addr[i].opt.quic = quic; |
| +#endif |
| |
| return NGX_OK; |
| } |
| @@ -1688,6 +1701,12 @@ ngx_http_init_listening(ngx_conf_t *cf, ngx_http_conf_port_t *port) |
| break; |
| } |
| |
| +#if (NGX_HTTP_V3) |
| + if (addr[i].opt.quic) { |
| + ls->type = SOCK_DGRAM; |
| + } |
| +#endif |
| + |
| addr++; |
| last--; |
| } |
| @@ -1770,6 +1789,12 @@ ngx_http_add_listening(ngx_conf_t *cf, ngx_http_conf_addr_t *addr) |
| ls->reuseport = addr->opt.reuseport; |
| #endif |
| |
| +#if (NGX_HTTP_V3) |
| + ls->quic = addr->opt.quic; |
| + |
| + ls->wildcard = addr->opt.wildcard; |
| +#endif |
| + |
| return ls; |
| } |
| |
| @@ -1803,6 +1828,9 @@ ngx_http_add_addrs(ngx_conf_t *cf, ngx_http_port_t *hport, |
| addrs[i].conf.http2 = addr[i].opt.http2; |
| #endif |
| addrs[i].conf.proxy_protocol = addr[i].opt.proxy_protocol; |
| +#if (NGX_HTTP_V3) |
| + addrs[i].conf.quic = addr[i].opt.quic; |
| +#endif |
| |
| if (addr[i].hash.buckets == NULL |
| && (addr[i].wc_head == NULL |
| @@ -1868,6 +1896,9 @@ ngx_http_add_addrs6(ngx_conf_t *cf, ngx_http_port_t *hport, |
| addrs6[i].conf.http2 = addr[i].opt.http2; |
| #endif |
| addrs6[i].conf.proxy_protocol = addr[i].opt.proxy_protocol; |
| +#if (NGX_HTTP_V3) |
| + addrs6[i].conf.quic = addr[i].opt.quic; |
| +#endif |
| |
| if (addr[i].hash.buckets == NULL |
| && (addr[i].wc_head == NULL |
| diff --git a/src/http/ngx_http.h b/src/http/ngx_http.h |
| index 8b43857ee..444f93536 100644 |
| --- a/src/http/ngx_http.h |
| +++ b/src/http/ngx_http.h |
| @@ -20,6 +20,7 @@ typedef struct ngx_http_file_cache_s ngx_http_file_cache_t; |
| typedef struct ngx_http_log_ctx_s ngx_http_log_ctx_t; |
| typedef struct ngx_http_chunked_s ngx_http_chunked_t; |
| typedef struct ngx_http_v2_stream_s ngx_http_v2_stream_t; |
| +typedef struct ngx_http_v3_stream_s ngx_http_v3_stream_t; |
| |
| typedef ngx_int_t (*ngx_http_header_handler_pt)(ngx_http_request_t *r, |
| ngx_table_elt_t *h, ngx_uint_t offset); |
| @@ -38,6 +39,9 @@ typedef u_char *(*ngx_http_log_handler_pt)(ngx_http_request_t *r, |
| #if (NGX_HTTP_V2) |
| #include <ngx_http_v2.h> |
| #endif |
| +#if (NGX_HTTP_V3) |
| +#include <ngx_http_v3.h> |
| +#endif |
| #if (NGX_HTTP_CACHE) |
| #include <ngx_http_cache.h> |
| #endif |
| diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c |
| index cb49ef74a..1e991c0b3 100644 |
| --- a/src/http/ngx_http_core_module.c |
| +++ b/src/http/ngx_http_core_module.c |
| @@ -4099,6 +4099,13 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) |
| continue; |
| } |
| |
| +#if (NGX_HTTP_V3) |
| + if (ngx_strcmp(value[n].data, "quic") == 0) { |
| + lsopt.quic = 1; |
| + continue; |
| + } |
| +#endif |
| + |
| ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, |
| "invalid parameter \"%V\"", &value[n]); |
| return NGX_CONF_ERROR; |
| diff --git a/src/http/ngx_http_core_module.h b/src/http/ngx_http_core_module.h |
| index 85f6d66dc..fa08e3631 100644 |
| --- a/src/http/ngx_http_core_module.h |
| +++ b/src/http/ngx_http_core_module.h |
| @@ -82,6 +82,7 @@ typedef struct { |
| unsigned reuseport:1; |
| unsigned so_keepalive:2; |
| unsigned proxy_protocol:1; |
| + unsigned quic:1; |
| |
| int backlog; |
| int rcvbuf; |
| @@ -238,6 +239,7 @@ struct ngx_http_addr_conf_s { |
| unsigned ssl:1; |
| unsigned http2:1; |
| unsigned proxy_protocol:1; |
| + unsigned quic:1; |
| }; |
| |
| |
| @@ -268,6 +270,7 @@ typedef struct { |
| ngx_int_t family; |
| in_port_t port; |
| ngx_array_t addrs; /* array of ngx_http_conf_addr_t */ |
| + ngx_int_t type; |
| } ngx_http_conf_port_t; |
| |
| |
| diff --git a/src/http/ngx_http_request.c b/src/http/ngx_http_request.c |
| index 80c19656f..a4f396753 100644 |
| --- a/src/http/ngx_http_request.c |
| +++ b/src/http/ngx_http_request.c |
| @@ -64,6 +64,10 @@ static void ngx_http_ssl_handshake(ngx_event_t *rev); |
| static void ngx_http_ssl_handshake_handler(ngx_connection_t *c); |
| #endif |
| |
| +#if (NGX_HTTP_V3) |
| +static void ngx_http_quic_handshake(ngx_event_t *rev); |
| +#endif |
| + |
| |
| static char *ngx_http_client_errors[] = { |
| |
| @@ -349,6 +353,19 @@ ngx_http_init_connection(ngx_connection_t *c) |
| c->log->action = "reading PROXY protocol"; |
| } |
| |
| +#if (NGX_HTTP_V3) |
| + if (hc->addr_conf->quic) { |
| + hc->quic = 1; |
| + c->log->action = "QUIC handshaking"; |
| + |
| + /* We already have a UDP packet in the connection buffer, so we don't |
| + * need to wait for another read event to kick-off the handshake. */ |
| + ngx_add_timer(rev, c->listening->post_accept_timeout); |
| + ngx_http_quic_handshake(rev); |
| + return; |
| + } |
| +#endif |
| + |
| if (rev->ready) { |
| /* the deferred accept(), iocp */ |
| |
| @@ -797,7 +814,7 @@ ngx_http_ssl_handshake_handler(ngx_connection_t *c) |
| |
| c->ssl->no_wait_shutdown = 1; |
| |
| -#if (NGX_HTTP_V2 \ |
| +#if ((NGX_HTTP_V2 || NGX_HTTP_V3) \ |
| && (defined TLSEXT_TYPE_application_layer_protocol_negotiation \ |
| || defined TLSEXT_TYPE_next_proto_neg)) |
| { |
| @@ -807,7 +824,7 @@ ngx_http_ssl_handshake_handler(ngx_connection_t *c) |
| |
| hc = c->data; |
| |
| - if (hc->addr_conf->http2) { |
| + if (hc->addr_conf->http2 || hc->addr_conf->quic) { |
| |
| #ifdef TLSEXT_TYPE_application_layer_protocol_negotiation |
| SSL_get0_alpn_selected(c->ssl->connection, &data, &len); |
| @@ -822,11 +839,29 @@ ngx_http_ssl_handshake_handler(ngx_connection_t *c) |
| SSL_get0_next_proto_negotiated(c->ssl->connection, &data, &len); |
| #endif |
| |
| + } |
| + |
| +#if (NGX_HTTP_V2) |
| + if (hc->addr_conf->http2) { |
| if (len == 2 && data[0] == 'h' && data[1] == '2') { |
| ngx_http_v2_init(c->read); |
| return; |
| } |
| } |
| +#endif |
| + |
| +#if (NGX_HTTP_V3) |
| + if (hc->addr_conf->quic) { |
| + if (len >= 2 && data[0] == 'h' && data[1] == '3') { |
| + ngx_http_v3_init(c->read); |
| + return; |
| + } |
| + |
| + ngx_http_close_connection(c); |
| + return; |
| + } |
| +#endif |
| + |
| } |
| #endif |
| |
| @@ -1033,6 +1068,68 @@ failed: |
| |
| #endif |
| |
| +#if (NGX_HTTP_V3) |
| + |
| +static void |
| +ngx_http_quic_handshake(ngx_event_t *rev) |
| +{ |
| + ngx_int_t rc; |
| + ngx_connection_t *c; |
| + ngx_http_connection_t *hc; |
| + ngx_http_v3_srv_conf_t *qscf; |
| + ngx_http_ssl_srv_conf_t *sscf; |
| + |
| + c = rev->data; |
| + hc = c->data; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, rev->log, 0, |
| + "http check quic handshake"); |
| + |
| + if (rev->timedout) { |
| + ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out"); |
| + ngx_http_close_connection(c); |
| + return; |
| + } |
| + |
| + if (c->close) { |
| + ngx_http_close_connection(c); |
| + return; |
| + } |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, rev->log, 0, "https quic handshake"); |
| + |
| + sscf = ngx_http_get_module_srv_conf(hc->conf_ctx, |
| + ngx_http_ssl_module); |
| + |
| + if (ngx_ssl_create_connection(&sscf->ssl, c, 0) != NGX_OK) { |
| + ngx_http_close_connection(c); |
| + return; |
| + } |
| + |
| + qscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v3_module); |
| + |
| + if (ngx_quic_create_connection(&qscf->quic, c) != NGX_OK) { |
| + ngx_http_close_connection(c); |
| + return; |
| + } |
| + |
| + rc = ngx_quic_handshake(c); |
| + |
| + if (rc == NGX_AGAIN) { |
| + |
| + if (!rev->timer_set) { |
| + ngx_add_timer(rev, c->listening->post_accept_timeout); |
| + } |
| + |
| + c->ssl->handler = ngx_http_ssl_handshake_handler; |
| + return; |
| + } |
| + |
| + ngx_http_ssl_handshake_handler(c); |
| +} |
| + |
| +#endif |
| + |
| |
| static void |
| ngx_http_process_request_line(ngx_event_t *rev) |
| @@ -2687,6 +2784,13 @@ ngx_http_finalize_connection(ngx_http_request_t *r) |
| } |
| #endif |
| |
| +#if (NGX_HTTP_V3) |
| + if (r->qstream) { |
| + ngx_http_close_request(r, 0); |
| + return; |
| + } |
| +#endif |
| + |
| clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); |
| |
| if (r->main->count != 1) { |
| @@ -2896,6 +3000,19 @@ ngx_http_test_reading(ngx_http_request_t *r) |
| |
| #endif |
| |
| +#if (NGX_HTTP_V3) |
| + |
| + if (r->qstream) { |
| + if (c->error) { |
| + err = 0; |
| + goto closed; |
| + } |
| + |
| + return; |
| + } |
| + |
| +#endif |
| + |
| #if (NGX_HAVE_KQUEUE) |
| |
| if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) { |
| @@ -3563,7 +3680,15 @@ ngx_http_close_request(ngx_http_request_t *r, ngx_int_t rc) |
| } |
| #endif |
| |
| +#if (NGX_HTTP_V3) |
| + if (r->qstream) { |
| + ngx_http_v3_close_stream(r->qstream, rc); |
| + return; |
| + } |
| +#endif |
| + |
| ngx_http_free_request(r, rc); |
| + |
| ngx_http_close_connection(c); |
| } |
| |
| @@ -3684,6 +3809,17 @@ ngx_http_close_connection(ngx_connection_t *c) |
| ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, |
| "close http connection: %d", c->fd); |
| |
| +#if (NGX_HTTP_V3) |
| + |
| + if (c->quic) { |
| + if (ngx_quic_shutdown(c) == NGX_AGAIN) { |
| + c->quic->handler = ngx_http_close_connection; |
| + return; |
| + } |
| + } |
| + |
| +#endif |
| + |
| #if (NGX_HTTP_SSL) |
| |
| if (c->ssl) { |
| diff --git a/src/http/ngx_http_request.h b/src/http/ngx_http_request.h |
| index fce70efe6..8ac19658c 100644 |
| --- a/src/http/ngx_http_request.h |
| +++ b/src/http/ngx_http_request.h |
| @@ -24,6 +24,7 @@ |
| #define NGX_HTTP_VERSION_10 1000 |
| #define NGX_HTTP_VERSION_11 1001 |
| #define NGX_HTTP_VERSION_20 2000 |
| +#define NGX_HTTP_VERSION_3 3000 |
| |
| #define NGX_HTTP_UNKNOWN 0x0001 |
| #define NGX_HTTP_GET 0x0002 |
| @@ -323,6 +324,7 @@ typedef struct { |
| ngx_chain_t *free; |
| |
| unsigned ssl:1; |
| + unsigned quic:1; |
| unsigned proxy_protocol:1; |
| } ngx_http_connection_t; |
| |
| @@ -445,6 +447,7 @@ struct ngx_http_request_s { |
| |
| ngx_http_connection_t *http_connection; |
| ngx_http_v2_stream_t *stream; |
| + ngx_http_v3_stream_t *qstream; |
| |
| ngx_http_log_handler_pt log_handler; |
| |
| diff --git a/src/http/ngx_http_request_body.c b/src/http/ngx_http_request_body.c |
| index c4f092e59..220cd142f 100644 |
| --- a/src/http/ngx_http_request_body.c |
| +++ b/src/http/ngx_http_request_body.c |
| @@ -312,6 +312,12 @@ ngx_http_do_read_client_request_body(ngx_http_request_t *r) |
| ngx_del_timer(c->read); |
| } |
| |
| +#if (NGX_HTTP_V3) |
| + if (r->qstream) { |
| + return NGX_AGAIN; |
| + } |
| +#endif |
| + |
| if (ngx_handle_read_event(c->read, 0) != NGX_OK) { |
| return NGX_HTTP_INTERNAL_SERVER_ERROR; |
| } |
| @@ -404,6 +410,12 @@ ngx_http_do_read_client_request_body(ngx_http_request_t *r) |
| clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); |
| ngx_add_timer(c->read, clcf->client_body_timeout); |
| |
| +#if (NGX_HTTP_V3) |
| + if (r->qstream) { |
| + return NGX_AGAIN; |
| + } |
| +#endif |
| + |
| if (ngx_handle_read_event(c->read, 0) != NGX_OK) { |
| return NGX_HTTP_INTERNAL_SERVER_ERROR; |
| } |
| @@ -525,6 +537,17 @@ ngx_http_discard_request_body(ngx_http_request_t *r) |
| } |
| #endif |
| |
| +#if (NGX_HTTP_V3) |
| + if (r->qstream) { |
| + r->qstream->skip_data = 1; |
| + |
| + /* disable stream read to avoid pointless data events */ |
| + ngx_http_v3_stop_stream_read(r->qstream, 0); |
| + |
| + return NGX_OK; |
| + } |
| +#endif |
| + |
| if (ngx_http_test_expect(r) != NGX_OK) { |
| return NGX_HTTP_INTERNAL_SERVER_ERROR; |
| } |
| @@ -808,6 +831,9 @@ ngx_http_test_expect(ngx_http_request_t *r) |
| || r->http_version < NGX_HTTP_VERSION_11 |
| #if (NGX_HTTP_V2) |
| || r->stream != NULL |
| +#endif |
| +#if (NGX_HTTP_V3) |
| + || r->qstream != NULL |
| #endif |
| ) |
| { |
| @@ -848,6 +874,13 @@ ngx_http_test_expect(ngx_http_request_t *r) |
| static ngx_int_t |
| ngx_http_request_body_filter(ngx_http_request_t *r, ngx_chain_t *in) |
| { |
| + |
| +#if (NGX_HTTP_V3) |
| + if (r->qstream) { |
| + return ngx_http_v3_request_body_filter(r, in); |
| + } |
| +#endif |
| + |
| if (r->headers_in.chunked) { |
| return ngx_http_request_body_chunked_filter(r, in); |
| |
| diff --git a/src/http/ngx_http_upstream.c b/src/http/ngx_http_upstream.c |
| index a7391d09a..398af2797 100644 |
| --- a/src/http/ngx_http_upstream.c |
| +++ b/src/http/ngx_http_upstream.c |
| @@ -526,6 +526,13 @@ ngx_http_upstream_init(ngx_http_request_t *r) |
| } |
| #endif |
| |
| +#if (NGX_HTTP_V3) |
| + if (r->qstream) { |
| + ngx_http_upstream_init_request(r); |
| + return; |
| + } |
| +#endif |
| + |
| if (c->read->timer_set) { |
| ngx_del_timer(c->read); |
| } |
| @@ -1351,6 +1358,12 @@ ngx_http_upstream_check_broken_connection(ngx_http_request_t *r, |
| } |
| #endif |
| |
| +#if (NGX_HTTP_V3) |
| + if (r->qstream) { |
| + return; |
| + } |
| +#endif |
| + |
| #if (NGX_HAVE_KQUEUE) |
| |
| if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) { |
| diff --git a/src/http/v3/ngx_http_v3.c b/src/http/v3/ngx_http_v3.c |
| new file mode 100644 |
| index 000000000..d7c4fbf1d |
| --- /dev/null |
| +++ b/src/http/v3/ngx_http_v3.c |
| @@ -0,0 +1,2270 @@ |
| + |
| +/* |
| + * Copyright (C) Cloudflare, Inc. |
| + */ |
| + |
| + |
| +#include <ngx_config.h> |
| +#include <ngx_core.h> |
| +#include <ngx_http.h> |
| +#include <ngx_http_v3_module.h> |
| + |
| + |
| +typedef struct { |
| + ngx_str_t name; |
| + ngx_uint_t offset; |
| + ngx_uint_t hash; |
| + ngx_http_header_t *hh; |
| +} ngx_http_v3_parse_header_t; |
| + |
| + |
| +/* errors */ |
| +#define NGX_HTTP_V3_NO_ERROR 0x0100 |
| +#define NGX_HTTP_V3_PROTOCOL_ERROR 0x0101 |
| +#define NGX_HTTP_V3_INTERNAL_ERROR 0x0102 |
| +#define NGX_HTTP_V3_TRANSPORT_STREAM_INVALID -1007 |
| +#define NGX_HTTP_V3_TRANSPORT_STREAM_STOPPED -1015 |
| + |
| + |
| +static void ngx_http_v3_handler(ngx_connection_t *c); |
| + |
| +static void ngx_http_v3_idle_handler(ngx_connection_t *c); |
| + |
| +static void ngx_http_v3_handle_connection(ngx_http_v3_connection_t *h3c); |
| + |
| +static ngx_http_v3_stream_t *ngx_http_v3_stream_lookup( |
| + ngx_http_v3_connection_t *h3c, ngx_uint_t stream_id); |
| +static ngx_http_v3_stream_t *ngx_http_v3_create_stream( |
| + ngx_http_v3_connection_t *h3c); |
| +static void ngx_http_v3_close_stream_handler(ngx_event_t *ev); |
| + |
| +static ngx_int_t ngx_http_v3_validate_header(ngx_http_request_t *r, |
| + ngx_http_v3_header_t *header); |
| +static ngx_int_t ngx_http_v3_pseudo_header(ngx_http_request_t *r, |
| + ngx_http_v3_header_t *header); |
| +static ngx_int_t ngx_http_v3_parse_path(ngx_http_request_t *r, |
| + ngx_str_t *value); |
| +static ngx_int_t ngx_http_v3_parse_method(ngx_http_request_t *r, |
| + ngx_str_t *value); |
| +static ngx_int_t ngx_http_v3_parse_scheme(ngx_http_request_t *r, |
| + ngx_str_t *value); |
| +static ngx_int_t ngx_http_v3_parse_authority(ngx_http_request_t *r, |
| + ngx_str_t *value); |
| +static ngx_int_t ngx_http_v3_parse_header(ngx_http_request_t *r, |
| + ngx_http_v3_parse_header_t *header, ngx_str_t *value); |
| +static ngx_int_t ngx_http_v3_cookie(ngx_http_request_t *r, |
| + ngx_http_v3_header_t *header); |
| +static ngx_int_t ngx_http_v3_construct_cookie_header(ngx_http_request_t *r); |
| +static ngx_int_t ngx_http_v3_construct_request_line(ngx_http_request_t *r); |
| + |
| +static void ngx_http_v3_run_request(ngx_http_request_t *r); |
| + |
| +static ssize_t ngx_http_v3_recv_body(ngx_connection_t *c, u_char *buf, |
| + size_t size); |
| +static ngx_chain_t *ngx_http_v3_send_chain(ngx_connection_t *fc, |
| + ngx_chain_t *in, off_t limit); |
| + |
| +static void ngx_http_v3_finalize_connection(ngx_http_v3_connection_t *h3c, |
| + ngx_uint_t status); |
| + |
| +static void ngx_http_v3_pool_cleanup(void *data); |
| + |
| + |
| +static ngx_http_v3_parse_header_t ngx_http_v3_parse_headers[] = { |
| + { ngx_string("host"), |
| + offsetof(ngx_http_headers_in_t, host), 0, NULL }, |
| + |
| + { ngx_string("accept-encoding"), |
| + offsetof(ngx_http_headers_in_t, accept_encoding), 0, NULL }, |
| + |
| + { ngx_string("accept-language"), |
| + offsetof(ngx_http_headers_in_t, accept_language), 0, NULL }, |
| + |
| + { ngx_string("user-agent"), |
| + offsetof(ngx_http_headers_in_t, user_agent), 0, NULL }, |
| + |
| + { ngx_null_string, 0, 0, NULL } |
| +}; |
| + |
| + |
| +void |
| +ngx_http_v3_init(ngx_event_t *rev) |
| +{ |
| + ngx_connection_t *c; |
| + ngx_pool_cleanup_t *cln; |
| + ngx_http_connection_t *hc; |
| + ngx_http_v3_srv_conf_t *h3scf; |
| + ngx_http_v3_connection_t *h3c; |
| + |
| + c = rev->data; |
| + hc = c->data; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "init http3 connection"); |
| + |
| + c->log->action = "processing HTTP/3 connection"; |
| + |
| + h3c = ngx_pcalloc(c->pool, sizeof(ngx_http_v3_connection_t)); |
| + if (h3c == NULL) { |
| + ngx_http_close_connection(c); |
| + return; |
| + } |
| + |
| + h3scf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_v3_module); |
| + |
| + h3c->h3 = quiche_h3_conn_new_with_transport(c->quic->conn, h3scf->http3); |
| + if (h3c->h3 == NULL) { |
| + ngx_http_close_connection(c); |
| + return; |
| + } |
| + |
| + h3c->http_connection = hc; |
| + |
| + h3c->connection = c; |
| + |
| + h3c->pool = c->pool; |
| + |
| + c->data = h3c; |
| + |
| + c->quic->handler = ngx_http_v3_handler; |
| + |
| + cln = ngx_pool_cleanup_add(c->pool, 0); |
| + if (cln == NULL) { |
| + ngx_http_close_connection(c); |
| + return; |
| + } |
| + |
| + cln->handler = ngx_http_v3_pool_cleanup; |
| + cln->data = h3c; |
| + |
| + ngx_rbtree_init(&h3c->streams, &h3c->streams_sentinel, |
| + ngx_rbtree_insert_value); |
| +} |
| + |
| + |
| +static int |
| +ngx_http_v3_for_each_header(uint8_t *name, size_t name_len, |
| + uint8_t *value, size_t value_len, void *argp) |
| +{ |
| + ngx_int_t rc; |
| + ngx_table_elt_t *h; |
| + ngx_http_header_t *hh; |
| + ngx_http_request_t *r; |
| + ngx_http_v3_header_t header; |
| + ngx_http_core_srv_conf_t *cscf; |
| + ngx_http_core_main_conf_t *cmcf; |
| + |
| + static ngx_str_t cookie = ngx_string("cookie"); |
| + |
| + r = argp; |
| + |
| + /* Duplicate the header name because we don't own it. */ |
| + header.name.data = ngx_pnalloc(r->pool, name_len); |
| + if (header.name.data == NULL) { |
| + return NGX_ERROR; |
| + } |
| + header.name.len = name_len; |
| + |
| + ngx_memcpy(header.name.data, name, name_len); |
| + |
| + /* Duplicate the header value because we don't own it. Some of the |
| + * functions that process headers require a NULL-terminated string, |
| + * so allocate enough memory for that. */ |
| + header.value.data = ngx_pcalloc(r->pool, value_len + 1); |
| + if (header.value.data == NULL) { |
| + return NGX_ERROR; |
| + } |
| + header.value.len = value_len; |
| + |
| + ngx_memcpy(header.value.data, value, value_len); |
| + |
| + if (ngx_http_v3_validate_header(r, &header) != NGX_OK) { |
| + return NGX_ERROR; |
| + } |
| + |
| + /* Check for pseudo-header. */ |
| + if (header.name.data[0] == ':') { |
| + rc = ngx_http_v3_pseudo_header(r, &header); |
| + |
| + if (rc == NGX_OK) { |
| + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| + "http3 header: \":%V: %V\"", |
| + &header.name, &header.value); |
| + |
| + return NGX_OK; |
| + } |
| + |
| + return NGX_ERROR; |
| + } |
| + |
| + if (r->invalid_header) { |
| + cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module); |
| + |
| + if (cscf->ignore_invalid_headers) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent invalid header: \"%V\"", &header.name); |
| + |
| + return NGX_ERROR; |
| + } |
| + } |
| + |
| + /* Handle Cookie header separately. Not sure why, but the HTTP/2 code does |
| + * the same. */ |
| + if (header.name.len == cookie.len |
| + && ngx_memcmp(header.name.data, cookie.data, cookie.len) == 0) |
| + { |
| + if (ngx_http_v3_cookie(r, &header) != NGX_OK) { |
| + return NGX_ERROR; |
| + } |
| + |
| + } else { |
| + h = ngx_list_push(&r->headers_in.headers); |
| + if (h == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h->key.len = header.name.len; |
| + h->key.data = header.name.data; |
| + |
| + /* |
| + * TODO Optimization: precalculate hash |
| + * and handler for indexed headers. |
| + */ |
| + h->hash = ngx_hash_key(h->key.data, h->key.len); |
| + |
| + h->value.len = header.value.len; |
| + h->value.data = header.value.data; |
| + |
| + h->lowcase_key = h->key.data; |
| + |
| + cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); |
| + |
| + hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash, |
| + h->lowcase_key, h->key.len); |
| + |
| + if (hh && hh->handler(r, h, hh->offset) != NGX_OK) { |
| + return NGX_ERROR; |
| + } |
| + } |
| + |
| + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| + "http3 header: \"%V: %V\"", |
| + &header.name, &header.value); |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static void |
| +ngx_http_v3_process_headers(ngx_connection_t *c, quiche_h3_event *ev, |
| + int64_t stream_id) |
| +{ |
| + int rc; |
| + ngx_http_v3_stream_t *stream; |
| + ngx_http_v3_srv_conf_t *h3scf; |
| + ngx_http_v3_connection_t *h3c; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 process headers"); |
| + |
| + h3c = c->data; |
| + |
| + h3scf = ngx_http_get_module_srv_conf(h3c->http_connection->conf_ctx, |
| + ngx_http_v3_module); |
| + |
| + if (h3c->connection->requests >= h3scf->max_requests) { |
| + ngx_http_v3_finalize_connection(h3c, NGX_HTTP_V3_NO_ERROR); |
| + return; |
| + } |
| + |
| + /* Create a new stream to handle the incoming request. */ |
| + stream = ngx_http_v3_create_stream(h3c); |
| + if (stream == NULL) { |
| + ngx_log_error(NGX_LOG_ERR, c->log, 0, "failed to create HTTP/3 stream"); |
| + |
| + ngx_http_v3_finalize_connection(h3c, NGX_HTTP_V3_INTERNAL_ERROR); |
| + return; |
| + } |
| + |
| + stream->id = stream_id; |
| + |
| + stream->node.key = stream_id; |
| + |
| + ngx_rbtree_insert(&h3c->streams, &stream->node); |
| + |
| + /* Populate ngx_http_request_t from raw HTTP/3 headers. */ |
| + rc = quiche_h3_event_for_each_header(ev, |
| + ngx_http_v3_for_each_header, stream->request); |
| + |
| + if (rc != NGX_OK) { |
| + ngx_log_error(NGX_LOG_ERR, c->log, 0, |
| + "received invalid HTTP/3 headers"); |
| + |
| + ngx_http_v3_finalize_connection(h3c, NGX_HTTP_V3_INTERNAL_ERROR); |
| + return; |
| + } |
| + |
| + stream->in_closed = !quiche_h3_event_headers_has_body(ev); |
| + |
| + ngx_http_v3_run_request(stream->request); |
| +} |
| + |
| + |
| +static void |
| +ngx_http_v3_process_blocked_streams(ngx_http_v3_connection_t *h3c) |
| +{ |
| + ngx_event_t *wev; |
| + quiche_stream_iter *writable; |
| + ngx_http_v3_stream_t *stream; |
| + uint64_t stream_id; |
| + |
| + writable = quiche_conn_writable(h3c->connection->quic->conn); |
| + |
| + while (quiche_stream_iter_next(writable, &stream_id)) { |
| + stream = ngx_http_v3_stream_lookup(h3c, stream_id); |
| + |
| + if (stream == NULL) { |
| + continue; |
| + } |
| + |
| + if (!stream->blocked) { |
| + continue; |
| + } |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, h3c->connection->log, 0, |
| + "http3 stream unblocked %ui", stream->id); |
| + |
| + stream->blocked = 0; |
| + |
| + wev = stream->request->connection->write; |
| + |
| + wev->active = 0; |
| + wev->ready = 1; |
| + |
| + if (!stream->headers_sent) { |
| + ngx_http_v3_send_response(stream->request); |
| + } |
| + |
| + if (!wev->delayed) { |
| + wev->handler(wev); |
| + } |
| + } |
| + |
| + quiche_stream_iter_free(writable); |
| +} |
| + |
| + |
| +static void |
| +ngx_http_v3_handler(ngx_connection_t *c) |
| +{ |
| + ngx_chain_t out; |
| + ngx_connection_t *fc; |
| + ngx_http_request_t *r; |
| + ngx_http_v3_connection_t *h3c; |
| + ngx_http_v3_stream_t *stream; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 handler"); |
| + |
| + h3c = c->data; |
| + |
| + if (c->read->timedout) { |
| + ngx_http_v3_finalize_connection(h3c, NGX_HTTP_V3_PROTOCOL_ERROR); |
| + return; |
| + } |
| + |
| + if (c->error) { |
| + ngx_http_v3_finalize_connection(h3c, NGX_HTTP_V3_INTERNAL_ERROR); |
| + return; |
| + } |
| + |
| + ngx_http_v3_process_blocked_streams(h3c); |
| + |
| + while (!c->error) { |
| + quiche_h3_event *ev; |
| + |
| + int64_t stream_id = quiche_h3_conn_poll(h3c->h3, c->quic->conn, &ev); |
| + if (stream_id == QUICHE_H3_ERR_DONE) { |
| + break; |
| + } |
| + |
| + if (stream_id < 0) { |
| + ngx_http_v3_finalize_connection(h3c, NGX_HTTP_V3_PROTOCOL_ERROR); |
| + return; |
| + } |
| + |
| + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, h3c->connection->log, 0, |
| + "http3 event stream:%ui ev:%ui", stream_id, |
| + quiche_h3_event_type(ev)); |
| + |
| + switch (quiche_h3_event_type(ev)) { |
| + case QUICHE_H3_EVENT_HEADERS: { |
| + /* If there is no stream, these are headers. If there is a |
| + * stream, these are trailers and they are ignored.*/ |
| + stream = ngx_http_v3_stream_lookup(h3c, stream_id); |
| + |
| + if (stream == NULL) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, |
| + "http3 headers"); |
| + |
| + ngx_http_v3_process_headers(c, ev, stream_id); |
| + } else { |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, |
| + "http3 trailers"); |
| + } |
| + |
| + break; |
| + } |
| + |
| + case QUICHE_H3_EVENT_DATA: { |
| + /* Lookup stream. If there isn't one, it means it has already |
| + * been closed, so ignore the event. */ |
| + stream = ngx_http_v3_stream_lookup(h3c, stream_id); |
| + |
| + if (stream != NULL && !stream->in_closed) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, |
| + "http3 data"); |
| + |
| + ngx_post_event(stream->request->connection->read, |
| + &ngx_posted_events); |
| + } |
| + |
| + break; |
| + } |
| + |
| + case QUICHE_H3_EVENT_FINISHED: { |
| + /* Lookup stream. If there isn't one, it means it has already |
| + * been closed, so ignore the event. */ |
| + stream = ngx_http_v3_stream_lookup(h3c, stream_id); |
| + |
| + if (stream != NULL && !stream->in_closed) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, |
| + "http3 finished"); |
| + |
| + /* Flush request body that was buffered. */ |
| + if (stream->request->request_body) { |
| + out.buf = stream->request->request_body->buf; |
| + out.next = NULL; |
| + |
| + ngx_http_v3_request_body_filter(stream->request, &out); |
| + |
| + ngx_post_event(stream->request->connection->read, |
| + &ngx_posted_events); |
| + } |
| + |
| + stream->in_closed = 1; |
| + } |
| + |
| + break; |
| + } |
| + |
| + case QUICHE_H3_EVENT_RESET: { |
| + /* Lookup stream. If there isn't one, it means it has already |
| + * been closed, so ignore the event. */ |
| + stream = ngx_http_v3_stream_lookup(h3c, stream_id); |
| + |
| + if (stream != NULL && !stream->in_closed) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, |
| + "http3 reset"); |
| + |
| + r = stream->request; |
| + fc = r->connection; |
| + |
| + fc->error = 1; |
| + |
| + ngx_post_event(stream->request->connection->read, |
| + &ngx_posted_events); |
| + |
| + stream->in_closed = 1; |
| + } |
| + |
| + break; |
| + } |
| + |
| + case QUICHE_H3_EVENT_PRIORITY_UPDATE: |
| + break; |
| + |
| + case QUICHE_H3_EVENT_GOAWAY: |
| + break; |
| + } |
| + |
| + quiche_h3_event_free(ev); |
| + } |
| + |
| + ngx_http_v3_handle_connection(h3c); |
| +} |
| + |
| + |
| +static void |
| +ngx_http_v3_idle_handler(ngx_connection_t *c) |
| +{ |
| + ngx_http_v3_connection_t *h3c; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 idle handler"); |
| + |
| + h3c = c->data; |
| + |
| + if (c->read->timedout) { |
| + ngx_http_v3_finalize_connection(h3c, NGX_HTTP_V3_NO_ERROR); |
| + return; |
| + } |
| + |
| + if (c->error) { |
| + ngx_http_v3_finalize_connection(h3c, NGX_HTTP_V3_INTERNAL_ERROR); |
| + return; |
| + } |
| + |
| + if (!quiche_conn_is_readable(c->quic->conn)) { |
| + return; |
| + } |
| + |
| + if (c->read->timer_set) { |
| + ngx_del_timer(c->read); |
| + } |
| + |
| + c->quic->handler = ngx_http_v3_handler; |
| + |
| + ngx_http_v3_handler(c); |
| +} |
| + |
| + |
| +static void |
| +ngx_http_v3_handle_connection(ngx_http_v3_connection_t *h3c) |
| +{ |
| + ngx_connection_t *c; |
| + ngx_http_v3_srv_conf_t *h3scf; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, h3c->connection->log, 0, |
| + "http3 handle connection"); |
| + |
| + c = h3c->connection; |
| + |
| + if (h3c->processing || c->error) { |
| + return; |
| + } |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, h3c->connection->log, 0, |
| + "http3 connection is idle"); |
| + |
| + h3scf = ngx_http_get_module_srv_conf(h3c->http_connection->conf_ctx, |
| + ngx_http_v3_module); |
| + |
| + c->quic->handler = ngx_http_v3_idle_handler; |
| + |
| + ngx_add_timer(c->read, h3scf->idle_timeout); |
| +} |
| + |
| + |
| +static ngx_http_v3_stream_t * |
| +ngx_http_v3_create_stream(ngx_http_v3_connection_t *h3c) |
| +{ |
| + ngx_log_t *log; |
| + ngx_event_t *rev, *wev; |
| + ngx_connection_t *fc; |
| + ngx_http_log_ctx_t *ctx; |
| + ngx_http_request_t *r; |
| + ngx_http_v3_stream_t *stream; |
| + ngx_http_core_srv_conf_t *cscf; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, h3c->connection->log, 0, |
| + "http3 create stream"); |
| + |
| + fc = h3c->free_fake_connections; |
| + |
| + if (fc) { |
| + h3c->free_fake_connections = fc->data; |
| + |
| + rev = fc->read; |
| + wev = fc->write; |
| + log = fc->log; |
| + ctx = log->data; |
| + |
| + } else { |
| + fc = ngx_palloc(h3c->pool, sizeof(ngx_connection_t)); |
| + if (fc == NULL) { |
| + return NULL; |
| + } |
| + |
| + rev = ngx_palloc(h3c->pool, sizeof(ngx_event_t)); |
| + if (rev == NULL) { |
| + return NULL; |
| + } |
| + |
| + wev = ngx_palloc(h3c->pool, sizeof(ngx_event_t)); |
| + if (wev == NULL) { |
| + return NULL; |
| + } |
| + |
| + log = ngx_palloc(h3c->pool, sizeof(ngx_log_t)); |
| + if (log == NULL) { |
| + return NULL; |
| + } |
| + |
| + ctx = ngx_palloc(h3c->pool, sizeof(ngx_http_log_ctx_t)); |
| + if (ctx == NULL) { |
| + return NULL; |
| + } |
| + |
| + ctx->connection = fc; |
| + ctx->request = NULL; |
| + ctx->current_request = NULL; |
| + } |
| + |
| + ngx_memcpy(log, h3c->connection->log, sizeof(ngx_log_t)); |
| + |
| + log->data = ctx; |
| + |
| + ngx_memzero(rev, sizeof(ngx_event_t)); |
| + |
| + rev->data = fc; |
| + rev->ready = 1; |
| + rev->handler = ngx_http_v3_close_stream_handler; |
| + rev->log = log; |
| + |
| + ngx_memcpy(wev, rev, sizeof(ngx_event_t)); |
| + |
| + wev->write = 1; |
| + |
| + ngx_memcpy(fc, h3c->connection, sizeof(ngx_connection_t)); |
| + |
| + fc->data = h3c->http_connection; |
| + fc->quic = h3c->connection->quic; |
| + fc->read = rev; |
| + fc->write = wev; |
| + fc->sent = 0; |
| + fc->buffer = NULL; |
| + fc->log = log; |
| + fc->buffered = 0; |
| + fc->sndlowat = 1; |
| + fc->tcp_nodelay = NGX_TCP_NODELAY_DISABLED; |
| + |
| + fc->recv = ngx_http_v3_recv_body; |
| + |
| + fc->send_chain = ngx_http_v3_send_chain; |
| + fc->need_last_buf = 1; |
| + |
| + r = ngx_http_create_request(fc); |
| + if (r == NULL) { |
| + return NULL; |
| + } |
| + |
| + ngx_str_set(&r->http_protocol, "HTTP/3"); |
| + |
| + r->http_version = NGX_HTTP_VERSION_3; |
| + r->valid_location = 1; |
| + |
| + fc->data = r; |
| + h3c->connection->requests++; |
| + |
| + cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module); |
| + |
| + r->header_in = ngx_create_temp_buf(r->pool, |
| + cscf->client_header_buffer_size); |
| + if (r->header_in == NULL) { |
| + ngx_http_free_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); |
| + return NULL; |
| + } |
| + |
| + if (ngx_list_init(&r->headers_in.headers, r->pool, 20, |
| + sizeof(ngx_table_elt_t)) |
| + != NGX_OK) |
| + { |
| + ngx_http_free_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); |
| + return NULL; |
| + } |
| + |
| + r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE; |
| + |
| + stream = ngx_pcalloc(h3c->pool, sizeof(ngx_http_v3_stream_t)); |
| + if (stream == NULL) { |
| + ngx_http_free_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); |
| + return NULL; |
| + } |
| + |
| + /* Default value */ |
| + stream->priority.urgency = 3; |
| + stream->priority.incremental = false; |
| + |
| + r->qstream = stream; |
| + |
| + stream->request = r; |
| + stream->connection = h3c; |
| + |
| + h3c->processing++; |
| + |
| + return stream; |
| +} |
| + |
| + |
| +static ngx_http_v3_stream_t * |
| +ngx_http_v3_stream_lookup(ngx_http_v3_connection_t *h3c, ngx_uint_t stream_id) |
| +{ |
| + ngx_rbtree_node_t *node, *sentinel; |
| + |
| + node = h3c->streams.root; |
| + sentinel = h3c->streams.sentinel; |
| + |
| + while (node != sentinel) { |
| + |
| + if (stream_id < node->key) { |
| + node = node->left; |
| + continue; |
| + } |
| + |
| + if (stream_id > node->key) { |
| + node = node->right; |
| + continue; |
| + } |
| + |
| + /* stream_id == node->key */ |
| + |
| + return (ngx_http_v3_stream_t *) node; |
| + } |
| + |
| + /* not found */ |
| + |
| + return NULL; |
| +} |
| + |
| + |
| +/* The following functions are copied from the HTTP/2 module, and adapted to |
| + * work independently. In theory we could refactor the HTTP/2 module to expose |
| + * these functions, but that would be fairly invasive and likely cause more |
| + * merge conflicts in the future. */ |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_validate_header(ngx_http_request_t *r, ngx_http_v3_header_t *header) |
| +{ |
| + u_char ch; |
| + ngx_uint_t i; |
| + ngx_http_core_srv_conf_t *cscf; |
| + |
| + if (header->name.len == 0) { |
| + return NGX_ERROR; |
| + } |
| + |
| + r->invalid_header = 0; |
| + |
| + cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module); |
| + |
| + for (i = (header->name.data[0] == ':'); i != header->name.len; i++) { |
| + ch = header->name.data[i]; |
| + |
| + if ((ch >= 'a' && ch <= 'z') |
| + || (ch == '-') |
| + || (ch >= '0' && ch <= '9') |
| + || (ch == '_' && cscf->underscores_in_headers)) |
| + { |
| + continue; |
| + } |
| + |
| + if (ch == '\0' || ch == LF || ch == CR || ch == ':' |
| + || (ch >= 'A' && ch <= 'Z')) |
| + { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent invalid header name: \"%V\"", |
| + &header->name); |
| + |
| + return NGX_ERROR; |
| + } |
| + |
| + r->invalid_header = 1; |
| + } |
| + |
| + for (i = 0; i != header->value.len; i++) { |
| + ch = header->value.data[i]; |
| + |
| + if (ch == '\0' || ch == LF || ch == CR) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent header \"%V\" with " |
| + "invalid value: \"%V\"", |
| + &header->name, &header->value); |
| + |
| + return NGX_ERROR; |
| + } |
| + } |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_pseudo_header(ngx_http_request_t *r, ngx_http_v3_header_t *header) |
| +{ |
| + header->name.len--; |
| + header->name.data++; |
| + |
| + switch (header->name.len) { |
| + case 4: |
| + if (ngx_memcmp(header->name.data, "path", sizeof("path") - 1) |
| + == 0) |
| + { |
| + return ngx_http_v3_parse_path(r, &header->value); |
| + } |
| + |
| + break; |
| + |
| + case 6: |
| + if (ngx_memcmp(header->name.data, "method", sizeof("method") - 1) |
| + == 0) |
| + { |
| + return ngx_http_v3_parse_method(r, &header->value); |
| + } |
| + |
| + if (ngx_memcmp(header->name.data, "scheme", sizeof("scheme") - 1) |
| + == 0) |
| + { |
| + return ngx_http_v3_parse_scheme(r, &header->value); |
| + } |
| + |
| + break; |
| + |
| + case 9: |
| + if (ngx_memcmp(header->name.data, "authority", sizeof("authority") - 1) |
| + == 0) |
| + { |
| + return ngx_http_v3_parse_authority(r, &header->value); |
| + } |
| + |
| + break; |
| + } |
| + |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent unknown pseudo-header \":%V\"", |
| + &header->name); |
| + |
| + return NGX_DECLINED; |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_parse_path(ngx_http_request_t *r, ngx_str_t *value) |
| +{ |
| + if (r->unparsed_uri.len) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent duplicate :path header"); |
| + |
| + return NGX_DECLINED; |
| + } |
| + |
| + if (value->len == 0) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent empty :path header"); |
| + |
| + return NGX_DECLINED; |
| + } |
| + |
| + r->uri_start = value->data; |
| + r->uri_end = value->data + value->len; |
| + |
| + if (ngx_http_parse_uri(r) != NGX_OK) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent invalid :path header: \"%V\"", value); |
| + |
| + return NGX_DECLINED; |
| + } |
| + |
| + if (ngx_http_process_request_uri(r) != NGX_OK) { |
| + /* |
| + * request has been finalized already |
| + * in ngx_http_process_request_uri() |
| + */ |
| + return NGX_ABORT; |
| + } |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_parse_method(ngx_http_request_t *r, ngx_str_t *value) |
| +{ |
| + size_t k, len; |
| + ngx_uint_t n; |
| + const u_char *p, *m; |
| + |
| + /* |
| + * This array takes less than 256 sequential bytes, |
| + * and if typical CPU cache line size is 64 bytes, |
| + * it is prefetched for 4 load operations. |
| + */ |
| + static const struct { |
| + u_char len; |
| + const u_char method[11]; |
| + uint32_t value; |
| + } tests[] = { |
| + { 3, "GET", NGX_HTTP_GET }, |
| + { 4, "POST", NGX_HTTP_POST }, |
| + { 4, "HEAD", NGX_HTTP_HEAD }, |
| + { 7, "OPTIONS", NGX_HTTP_OPTIONS }, |
| + { 8, "PROPFIND", NGX_HTTP_PROPFIND }, |
| + { 3, "PUT", NGX_HTTP_PUT }, |
| + { 5, "MKCOL", NGX_HTTP_MKCOL }, |
| + { 6, "DELETE", NGX_HTTP_DELETE }, |
| + { 4, "COPY", NGX_HTTP_COPY }, |
| + { 4, "MOVE", NGX_HTTP_MOVE }, |
| + { 9, "PROPPATCH", NGX_HTTP_PROPPATCH }, |
| + { 4, "LOCK", NGX_HTTP_LOCK }, |
| + { 6, "UNLOCK", NGX_HTTP_UNLOCK }, |
| + { 5, "PATCH", NGX_HTTP_PATCH }, |
| + { 5, "TRACE", NGX_HTTP_TRACE } |
| + }, *test; |
| + |
| + if (r->method_name.len) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent duplicate :method header"); |
| + |
| + return NGX_DECLINED; |
| + } |
| + |
| + if (value->len == 0) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent empty :method header"); |
| + |
| + return NGX_DECLINED; |
| + } |
| + |
| + r->method_name.len = value->len; |
| + r->method_name.data = value->data; |
| + |
| + len = r->method_name.len; |
| + n = sizeof(tests) / sizeof(tests[0]); |
| + test = tests; |
| + |
| + do { |
| + if (len == test->len) { |
| + p = r->method_name.data; |
| + m = test->method; |
| + k = len; |
| + |
| + do { |
| + if (*p++ != *m++) { |
| + goto next; |
| + } |
| + } while (--k); |
| + |
| + r->method = test->value; |
| + return NGX_OK; |
| + } |
| + |
| + next: |
| + test++; |
| + |
| + } while (--n); |
| + |
| + p = r->method_name.data; |
| + |
| + do { |
| + if ((*p < 'A' || *p > 'Z') && *p != '_' && *p != '-') { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent invalid method: \"%V\"", |
| + &r->method_name); |
| + |
| + return NGX_DECLINED; |
| + } |
| + |
| + p++; |
| + |
| + } while (--len); |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_parse_scheme(ngx_http_request_t *r, ngx_str_t *value) |
| +{ |
| + u_char c, ch; |
| + ngx_uint_t i; |
| + |
| + if (r->schema.len) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent duplicate :scheme header"); |
| + |
| + return NGX_DECLINED; |
| + } |
| + |
| + if (value->len == 0) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent empty :scheme header"); |
| + |
| + return NGX_DECLINED; |
| + } |
| + |
| + for (i = 0; i < value->len; i++) { |
| + ch = value->data[i]; |
| + |
| + c = (u_char) (ch | 0x20); |
| + if (c >= 'a' && c <= 'z') { |
| + continue; |
| + } |
| + |
| + if (((ch >= '0' && ch <= '9') || ch == '+' || ch == '-' || ch == '.') |
| + && i > 0) |
| + { |
| + continue; |
| + } |
| + |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent invalid :scheme header: \"%V\"", value); |
| + |
| + return NGX_DECLINED; |
| + } |
| + |
| + r->schema = *value; |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_parse_authority(ngx_http_request_t *r, ngx_str_t *value) |
| +{ |
| + return ngx_http_v3_parse_header(r, &ngx_http_v3_parse_headers[0], value); |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_parse_header(ngx_http_request_t *r, |
| + ngx_http_v3_parse_header_t *header, ngx_str_t *value) |
| +{ |
| + ngx_table_elt_t *h; |
| + ngx_http_core_main_conf_t *cmcf; |
| + |
| + h = ngx_list_push(&r->headers_in.headers); |
| + if (h == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h->key.len = header->name.len; |
| + h->key.data = header->name.data; |
| + h->lowcase_key = header->name.data; |
| + |
| + if (header->hh == NULL) { |
| + header->hash = ngx_hash_key(header->name.data, header->name.len); |
| + |
| + cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); |
| + |
| + header->hh = ngx_hash_find(&cmcf->headers_in_hash, header->hash, |
| + h->lowcase_key, h->key.len); |
| + if (header->hh == NULL) { |
| + return NGX_ERROR; |
| + } |
| + } |
| + |
| + h->hash = header->hash; |
| + |
| + h->value.len = value->len; |
| + h->value.data = value->data; |
| + |
| + if (header->hh->handler(r, h, header->hh->offset) != NGX_OK) { |
| + /* header handler has already finalized request */ |
| + return NGX_ABORT; |
| + } |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_construct_request_line(ngx_http_request_t *r) |
| +{ |
| + u_char *p; |
| + |
| + static const u_char ending[] = " HTTP/3"; |
| + |
| + if (r->method_name.len == 0 |
| + || r->schema.len == 0 |
| + || r->unparsed_uri.len == 0) |
| + { |
| + if (r->method_name.len == 0) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent no :method header"); |
| + |
| + } else if (r->schema.len == 0) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent no :scheme header"); |
| + |
| + } else { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client sent no :path header"); |
| + } |
| + |
| + ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST); |
| + return NGX_ERROR; |
| + } |
| + |
| + r->request_line.len = r->method_name.len + 1 |
| + + r->unparsed_uri.len |
| + + sizeof(ending) - 1; |
| + |
| + p = ngx_pnalloc(r->pool, r->request_line.len + 1); |
| + if (p == NULL) { |
| + ngx_http_v3_close_stream(r->qstream, NGX_HTTP_INTERNAL_SERVER_ERROR); |
| + return NGX_ERROR; |
| + } |
| + |
| + r->request_line.data = p; |
| + |
| + p = ngx_cpymem(p, r->method_name.data, r->method_name.len); |
| + |
| + *p++ = ' '; |
| + |
| + p = ngx_cpymem(p, r->unparsed_uri.data, r->unparsed_uri.len); |
| + |
| + ngx_memcpy(p, ending, sizeof(ending)); |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| + "http3 request line: \"%V\"", &r->request_line); |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_cookie(ngx_http_request_t *r, ngx_http_v3_header_t *header) |
| +{ |
| + ngx_str_t *val; |
| + ngx_array_t *cookies; |
| + |
| + cookies = r->qstream->cookies; |
| + |
| + if (cookies == NULL) { |
| + cookies = ngx_array_create(r->pool, 2, sizeof(ngx_str_t)); |
| + if (cookies == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + r->qstream->cookies = cookies; |
| + } |
| + |
| + val = ngx_array_push(cookies); |
| + if (val == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + val->len = header->value.len; |
| + val->data = header->value.data; |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_construct_cookie_header(ngx_http_request_t *r) |
| +{ |
| + u_char *buf, *p, *end; |
| + size_t len; |
| + ngx_str_t *vals; |
| + ngx_uint_t i; |
| + ngx_array_t *cookies; |
| + ngx_table_elt_t *h; |
| + ngx_http_header_t *hh; |
| + ngx_http_core_main_conf_t *cmcf; |
| + |
| + static ngx_str_t cookie = ngx_string("cookie"); |
| + |
| + cookies = r->qstream->cookies; |
| + |
| + if (cookies == NULL) { |
| + return NGX_OK; |
| + } |
| + |
| + vals = cookies->elts; |
| + |
| + i = 0; |
| + len = 0; |
| + |
| + do { |
| + len += vals[i].len + 2; |
| + } while (++i != cookies->nelts); |
| + |
| + len -= 2; |
| + |
| + buf = ngx_pnalloc(r->pool, len + 1); |
| + if (buf == NULL) { |
| + ngx_http_v3_close_stream(r->qstream, NGX_HTTP_INTERNAL_SERVER_ERROR); |
| + return NGX_ERROR; |
| + } |
| + |
| + p = buf; |
| + end = buf + len; |
| + |
| + for (i = 0; /* void */ ; i++) { |
| + |
| + p = ngx_cpymem(p, vals[i].data, vals[i].len); |
| + |
| + if (p == end) { |
| + *p = '\0'; |
| + break; |
| + } |
| + |
| + *p++ = ';'; *p++ = ' '; |
| + } |
| + |
| + h = ngx_list_push(&r->headers_in.headers); |
| + if (h == NULL) { |
| + ngx_http_v3_close_stream(r->qstream, NGX_HTTP_INTERNAL_SERVER_ERROR); |
| + return NGX_ERROR; |
| + } |
| + |
| + h->hash = ngx_hash(ngx_hash(ngx_hash(ngx_hash( |
| + ngx_hash('c', 'o'), 'o'), 'k'), 'i'), 'e'); |
| + |
| + h->key.len = cookie.len; |
| + h->key.data = cookie.data; |
| + |
| + h->value.len = len; |
| + h->value.data = buf; |
| + |
| + h->lowcase_key = cookie.data; |
| + |
| + cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); |
| + |
| + hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash, |
| + h->lowcase_key, h->key.len); |
| + |
| + if (hh == NULL) { |
| + ngx_http_v3_close_stream(r->qstream, NGX_HTTP_INTERNAL_SERVER_ERROR); |
| + return NGX_ERROR; |
| + } |
| + |
| + if (hh->handler(r, h, hh->offset) != NGX_OK) { |
| + /* |
| + * request has been finalized already |
| + * in ngx_http_process_multi_header_lines() |
| + */ |
| + return NGX_ERROR; |
| + } |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static void |
| +ngx_http_v3_run_request(ngx_http_request_t *r) |
| +{ |
| + if (ngx_http_v3_construct_request_line(r) != NGX_OK) { |
| + return; |
| + } |
| + |
| + if (ngx_http_v3_construct_cookie_header(r) != NGX_OK) { |
| + return; |
| + } |
| + |
| + r->http_state = NGX_HTTP_PROCESS_REQUEST_STATE; |
| + |
| + if (ngx_http_process_request_header(r) != NGX_OK) { |
| + return; |
| + } |
| + |
| + if (r->headers_in.content_length_n > 0 && r->qstream->in_closed) { |
| + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, |
| + "client prematurely closed stream"); |
| + |
| + r->qstream->skip_data = 1; |
| + |
| + ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST); |
| + return; |
| + } |
| + |
| + if (r->headers_in.content_length_n == -1 && !r->qstream->in_closed) { |
| + r->headers_in.chunked = 1; |
| + } |
| + |
| + ngx_http_process_request(r); |
| +} |
| + |
| + |
| +/* End of functions copied from HTTP/2 module. */ |
| + |
| + |
| +ngx_int_t |
| +ngx_http_v3_request_body_filter(ngx_http_request_t *r, ngx_chain_t *in) |
| +{ |
| + size_t size; |
| + ngx_int_t rc; |
| + ngx_buf_t *b; |
| + ngx_chain_t *cl, *tl, *out, **ll; |
| + ngx_connection_t *c; |
| + ngx_http_request_body_t *rb; |
| + ngx_http_core_loc_conf_t *clcf; |
| + bool stream_fin; |
| + |
| + c = r->qstream->connection->connection; |
| + |
| + rb = r->request_body; |
| + |
| + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); |
| + |
| + if (rb->rest == -1) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| + "http3 request body filter"); |
| + |
| + if (r->headers_in.chunked) { |
| + rb->rest = clcf->client_body_buffer_size; |
| + r->headers_in.content_length_n = 0; |
| + } else { |
| + rb->rest = r->headers_in.content_length_n; |
| + } |
| + } |
| + |
| + out = NULL; |
| + ll = &out; |
| + |
| + for (cl = in; cl; cl = cl->next) { |
| + |
| + if (rb->rest == 0) { |
| + break; |
| + } |
| + |
| + stream_fin = quiche_conn_stream_finished(c->quic->conn, r->qstream->id); |
| + |
| + if ((ngx_buf_size(cl->buf) == 0) && !stream_fin) { |
| + continue; |
| + } |
| + |
| + tl = ngx_chain_get_free_buf(r->pool, &rb->free); |
| + if (tl == NULL) { |
| + return NGX_HTTP_INTERNAL_SERVER_ERROR; |
| + } |
| + |
| + b = tl->buf; |
| + |
| + ngx_memzero(b, sizeof(ngx_buf_t)); |
| + |
| + b->temporary = 1; |
| + b->tag = (ngx_buf_tag_t) &ngx_http_read_client_request_body; |
| + b->start = cl->buf->pos; |
| + b->pos = cl->buf->pos; |
| + b->last = cl->buf->last; |
| + b->end = cl->buf->end; |
| + b->flush = r->request_body_no_buffering; |
| + |
| + size = cl->buf->last - cl->buf->pos; |
| + |
| + cl->buf->pos = cl->buf->last; |
| + |
| + if (r->headers_in.chunked) { |
| + r->headers_in.content_length_n += size; |
| + } |
| + |
| + if (stream_fin) { |
| + rb->rest = 0; |
| + b->last = cl->buf->pos; |
| + b->last_buf = 1; |
| + } |
| + |
| + *ll = tl; |
| + ll = &tl->next; |
| + } |
| + |
| + rc = ngx_http_top_request_body_filter(r, out); |
| + |
| + ngx_chain_update_chains(r->pool, &rb->free, &rb->busy, &out, |
| + (ngx_buf_tag_t) &ngx_http_read_client_request_body); |
| + |
| + return rc; |
| +} |
| + |
| + |
| +size_t |
| +ngx_http_v3_get_headers_out_count(ngx_http_request_t *r) |
| +{ |
| + size_t headers_count; |
| + ngx_uint_t i; |
| + ngx_list_part_t *part; |
| + ngx_table_elt_t *header; |
| + |
| + headers_count = 1; /* :status */ |
| + |
| + if (r->headers_out.server == NULL) { |
| + headers_count += 1; |
| + } |
| + |
| + if (r->headers_out.date == NULL) { |
| + headers_count += 1; |
| + } |
| + |
| + if (r->headers_out.content_type.len) { |
| + headers_count += 1; |
| + } |
| + |
| + if (r->headers_out.content_length == NULL |
| + && r->headers_out.content_length_n >= 0) |
| + { |
| + headers_count += 1; |
| + } |
| + |
| + if (r->headers_out.last_modified == NULL |
| + && r->headers_out.last_modified_time != -1) |
| + { |
| + headers_count += 1; |
| + } |
| + |
| + if (r->headers_out.location && r->headers_out.location->value.len) { |
| + headers_count += 1; |
| + } |
| + |
| +#if (NGX_HTTP_GZIP) |
| + if (r->gzip_vary) { |
| + headers_count += 1; |
| + } |
| +#endif |
| + |
| + part = &r->headers_out.headers.part; |
| + header = part->elts; |
| + |
| + for (i = 0; /* void */; i++) { |
| + |
| + if (i >= part->nelts) { |
| + if (part->next == NULL) { |
| + break; |
| + } |
| + |
| + part = part->next; |
| + header = part->elts; |
| + i = 0; |
| + } |
| + |
| + if (header[i].hash == 0) { |
| + continue; |
| + } |
| + |
| + headers_count += 1; |
| + } |
| + |
| + return headers_count; |
| +} |
| + |
| + |
| +ngx_int_t |
| +ngx_http_v3_push_response_headers(ngx_http_request_t *r) |
| +{ |
| + u_char *tmp; |
| + size_t len, headers_count; |
| + ngx_str_t host, location; |
| + ngx_uint_t i, port; |
| + ngx_list_part_t *part; |
| + ngx_table_elt_t *header; |
| + ngx_connection_t *fc; |
| + quiche_h3_header *h; |
| + ngx_http_core_loc_conf_t *clcf; |
| + ngx_http_core_srv_conf_t *cscf; |
| + u_char addr[NGX_SOCKADDR_STRLEN]; |
| + |
| + /* The list of response headers was already generated, so there's nothing |
| + * more to do here. */ |
| + if (r->qstream->headers != NULL) { |
| + return NGX_OK; |
| + } |
| + |
| + fc = r->connection; |
| + |
| + if (r->method == NGX_HTTP_HEAD) { |
| + r->header_only = 1; |
| + } |
| + |
| + switch (r->headers_out.status) { |
| + |
| + case NGX_HTTP_OK: |
| + break; |
| + |
| + case NGX_HTTP_NO_CONTENT: |
| + r->header_only = 1; |
| + |
| + if (!r->headers_out.status_line.len) { |
| + ngx_str_null(&r->headers_out.content_type); |
| + |
| + r->headers_out.content_length = NULL; |
| + r->headers_out.content_length_n = -1; |
| + |
| + r->headers_out.last_modified_time = -1; |
| + r->headers_out.last_modified = NULL; |
| + } |
| + break; |
| + |
| + case NGX_HTTP_PARTIAL_CONTENT: |
| + break; |
| + |
| + case NGX_HTTP_NOT_MODIFIED: |
| + r->header_only = 1; |
| + break; |
| + |
| + default: |
| + r->headers_out.last_modified_time = -1; |
| + r->headers_out.last_modified = NULL; |
| + } |
| + |
| + headers_count = ngx_http_v3_get_headers_out_count(r); |
| + |
| + r->qstream->headers = |
| + ngx_array_create(r->pool, headers_count, sizeof(quiche_h3_header)); |
| + |
| + if (r->qstream->headers == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + /* Generate :status pseudo-header. */ |
| + { |
| + h = ngx_array_push(r->qstream->headers); |
| + if (h == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h->name = (u_char *) ":status"; |
| + h->name_len = sizeof(":status") - 1; |
| + |
| + tmp = ngx_pnalloc(r->pool, sizeof("418") - 1); |
| + if (tmp == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h->value = tmp; |
| + h->value_len = ngx_sprintf(tmp, "%03ui", r->headers_out.status) - tmp; |
| + } |
| + |
| + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); |
| + |
| + /* Generate Server header.*/ |
| + if (r->headers_out.server == NULL) { |
| + h = ngx_array_push(r->qstream->headers); |
| + if (h == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h->name = (u_char *) "server"; |
| + h->name_len = sizeof("server") - 1; |
| + |
| + if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) { |
| + h->value = (u_char *) NGINX_VER; |
| + h->value_len = sizeof(NGINX_VER) - 1; |
| + |
| + } else if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_BUILD) { |
| + h->value = (u_char *) NGINX_VER_BUILD; |
| + h->value_len = sizeof(NGINX_VER_BUILD) - 1; |
| + |
| + } else { |
| + h->value = (u_char *) "nginx"; |
| + h->value_len = sizeof("nginx") - 1; |
| + } |
| + } |
| + |
| + /* Generate Date header. */ |
| + if (r->headers_out.date == NULL) { |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0, |
| + "http3 output header: \"date: %V\"", |
| + &ngx_cached_http_time); |
| + |
| + h = ngx_array_push(r->qstream->headers); |
| + if (h == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h->name = (u_char *) "date"; |
| + h->name_len = sizeof("date") - 1; |
| + |
| + h->value = ngx_cached_http_time.data; |
| + h->value_len = ngx_cached_http_time.len; |
| + } |
| + |
| + /* Generate Content-Type header. */ |
| + if (r->headers_out.content_type.len) { |
| + h = ngx_array_push(r->qstream->headers); |
| + if (h == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + if (r->headers_out.content_type_len == r->headers_out.content_type.len |
| + && r->headers_out.charset.len) |
| + { |
| + len = r->headers_out.content_type.len + sizeof("; charset=") - 1 |
| + + r->headers_out.charset.len; |
| + |
| + tmp = ngx_pnalloc(r->pool, len); |
| + if (tmp == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + tmp = ngx_cpymem(tmp, r->headers_out.content_type.data, |
| + r->headers_out.content_type.len); |
| + |
| + tmp = ngx_cpymem(tmp, "; charset=", sizeof("; charset=") - 1); |
| + |
| + tmp = ngx_cpymem(tmp, r->headers_out.charset.data, |
| + r->headers_out.charset.len); |
| + |
| + /* updated r->headers_out.content_type is also needed for logging */ |
| + |
| + r->headers_out.content_type.len = len; |
| + r->headers_out.content_type.data = tmp - len; |
| + } |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0, |
| + "http3 output header: \"content-type: %V\"", |
| + &r->headers_out.content_type); |
| + |
| + h->name = (u_char *) "content-type"; |
| + h->name_len = sizeof("content-type") - 1; |
| + |
| + h->value = r->headers_out.content_type.data; |
| + h->value_len = r->headers_out.content_type.len; |
| + } |
| + |
| + /* Generate Content-Length header. */ |
| + if (r->headers_out.content_length == NULL |
| + && r->headers_out.content_length_n >= 0) |
| + { |
| + h = ngx_array_push(r->qstream->headers); |
| + if (h == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h->name = (u_char *) "content-length"; |
| + h->name_len = sizeof("content-length") - 1; |
| + |
| + tmp = ngx_pnalloc(r->pool, NGX_OFF_T_LEN); |
| + if (tmp == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h->value = tmp; |
| + h->value_len = |
| + ngx_sprintf(tmp, "%O", r->headers_out.content_length_n) - tmp; |
| + } |
| + |
| + /* Generate Last-Modified header. */ |
| + if (r->headers_out.last_modified == NULL |
| + && r->headers_out.last_modified_time != -1) |
| + { |
| + h = ngx_array_push(r->qstream->headers); |
| + if (h == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h->name = (u_char *) "last-modified"; |
| + h->name_len = sizeof("last-modified") - 1; |
| + |
| + tmp = ngx_pnalloc(r->pool, sizeof("Wed, 31 Dec 1986 18:00:00 GMT") - 1); |
| + if (tmp == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h->value = tmp; |
| + h->value_len = |
| + ngx_http_time(tmp, r->headers_out.last_modified_time) - tmp; |
| + |
| + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, fc->log, 0, |
| + "http3 output header: \"last-modified: %*.s\"", |
| + h->value_len, h->value); |
| + } |
| + |
| + /* Generate Location header. */ |
| + if (r->headers_out.location && r->headers_out.location->value.len) { |
| + |
| + if (r->headers_out.location->value.data[0] == '/' |
| + && clcf->absolute_redirect) |
| + { |
| + if (clcf->server_name_in_redirect) { |
| + cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module); |
| + host = cscf->server_name; |
| + |
| + } else if (r->headers_in.server.len) { |
| + host = r->headers_in.server; |
| + |
| + } else { |
| + host.data = addr; |
| + host.len = NGX_SOCKADDR_STRLEN; |
| + |
| + if (ngx_connection_local_sockaddr(fc, &host, 0) != NGX_OK) { |
| + return NGX_ERROR; |
| + } |
| + } |
| + |
| + port = ngx_inet_get_port(fc->local_sockaddr); |
| + |
| + location.len = sizeof("https://") - 1 + host.len |
| + + r->headers_out.location->value.len; |
| + |
| + if (clcf->port_in_redirect) { |
| + |
| +#if (NGX_HTTP_SSL) |
| + if (fc->ssl) |
| + port = (port == 443) ? 0 : port; |
| + else |
| +#endif |
| + port = (port == 80) ? 0 : port; |
| + |
| + } else { |
| + port = 0; |
| + } |
| + |
| + if (port) { |
| + location.len += sizeof(":65535") - 1; |
| + } |
| + |
| + location.data = ngx_pnalloc(r->pool, location.len); |
| + if (location.data == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + tmp = ngx_cpymem(location.data, "http", sizeof("http") - 1); |
| + |
| +#if (NGX_HTTP_SSL) |
| + if (fc->ssl) { |
| + *tmp++ = 's'; |
| + } |
| +#endif |
| + |
| + *tmp++ = ':'; *tmp++ = '/'; *tmp++ = '/'; |
| + tmp = ngx_cpymem(tmp, host.data, host.len); |
| + |
| + if (port) { |
| + tmp = ngx_sprintf(tmp, ":%ui", port); |
| + } |
| + |
| + tmp = ngx_cpymem(tmp, r->headers_out.location->value.data, |
| + r->headers_out.location->value.len); |
| + |
| + /* update r->headers_out.location->value for possible logging */ |
| + |
| + r->headers_out.location->value.len = tmp - location.data; |
| + r->headers_out.location->value.data = location.data; |
| + ngx_str_set(&r->headers_out.location->key, "Location"); |
| + } |
| + |
| + r->headers_out.location->hash = 0; |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0, |
| + "http3 output header: \"location: %V\"", |
| + &r->headers_out.location->value); |
| + |
| + h = ngx_array_push(r->qstream->headers); |
| + if (h == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h->name = (u_char *) "location"; |
| + h->name_len = sizeof("location") - 1; |
| + |
| + h->value = r->headers_out.location->value.data; |
| + h->value_len = r->headers_out.location->value.len; |
| + } |
| + |
| +#if (NGX_HTTP_GZIP) |
| + /* Generate Vary header. */ |
| + if (r->gzip_vary) { |
| + h = ngx_array_push(r->qstream->headers); |
| + if (h == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0, |
| + "http3 output header: \"vary: Accept-Encoding\""); |
| + |
| + h->name = (u_char *) "vary"; |
| + h->name_len = sizeof("vary") - 1; |
| + |
| + h->value = (u_char *) "Accept-Encoding"; |
| + h->value_len = sizeof("Accept-Encoding") - 1; |
| + } |
| +#endif |
| + |
| + part = &r->headers_out.headers.part; |
| + header = part->elts; |
| + |
| + /* Generate all other headers. */ |
| + for (i = 0; /* void */; i++) { |
| + |
| + if (i >= part->nelts) { |
| + if (part->next == NULL) { |
| + break; |
| + } |
| + |
| + part = part->next; |
| + header = part->elts; |
| + i = 0; |
| + } |
| + |
| + if (header[i].hash == 0) { |
| + continue; |
| + } |
| + |
| + h = ngx_array_push(r->qstream->headers); |
| + if (h == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| +#if (NGX_DEBUG) |
| + if (fc->log->log_level & NGX_LOG_DEBUG_HTTP) { |
| + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, fc->log, 0, |
| + "http3 output header: \"%V: %V\"", |
| + &header[i].key, &header[i].value); |
| + } |
| +#endif |
| + |
| + h->name = header[i].key.data; |
| + h->name_len = header[i].key.len; |
| + |
| + h->value = header[i].value.data; |
| + h->value_len = header[i].value.len; |
| + } |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +ngx_int_t |
| +ngx_http_v3_send_response(ngx_http_request_t *r) |
| +{ |
| + int rc; |
| + ngx_uint_t fin; |
| + ngx_connection_t *c, *fc; |
| + ngx_http_v3_connection_t *h3c; |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, |
| + "http3 send response stream %ui", r->qstream->id); |
| + |
| + fc = r->connection; |
| + |
| + if (fc->error) { |
| + return NGX_ERROR; |
| + } |
| + |
| + h3c = r->qstream->connection; |
| + c = h3c->connection; |
| + |
| + if (ngx_http_v3_push_response_headers(r) != NGX_OK) { |
| + return NGX_ERROR; |
| + } |
| + |
| + fin = r->header_only |
| + || (r->headers_out.content_length_n == 0 && !r->expect_trailers); |
| + |
| + rc = quiche_h3_send_response_with_priority(h3c->h3, c->quic->conn, r->qstream->id, |
| + r->qstream->headers->elts, |
| + r->qstream->headers->nelts, |
| + &r->qstream->priority, |
| + fin); |
| + |
| + if (rc == QUICHE_H3_ERR_STREAM_BLOCKED) { |
| + r->qstream->blocked = 1; |
| + |
| + fc->write->active = 1; |
| + fc->write->ready = 0; |
| + |
| + return NGX_AGAIN; |
| + } |
| + |
| + if (rc != NGX_OK) { |
| + return NGX_ERROR; |
| + } |
| + |
| + if (fin) { |
| + r->qstream->out_closed = 1; |
| + } |
| + |
| + r->qstream->headers_sent = 1; |
| + |
| + if (r->done && r->main->count == 0) { |
| + fc->write->handler = ngx_http_v3_close_stream_handler; |
| + fc->read->handler = ngx_http_empty_handler; |
| + } |
| + |
| + ngx_post_event(c->write, &ngx_posted_events); |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static ssize_t |
| +ngx_http_v3_stream_do_send(ngx_connection_t *fc, ngx_buf_t *b, ngx_int_t fin) |
| +{ |
| + ssize_t n; |
| + ngx_connection_t *c; |
| + ngx_http_request_t *r; |
| + ngx_http_v3_connection_t *h3c; |
| + ngx_http_v3_stream_t *stream; |
| + |
| + uint8_t *buf = b ? b->pos : NULL; |
| + size_t buf_len = b ? ngx_buf_size(b) : 0; |
| + |
| + r = fc->data; |
| + stream = r->qstream; |
| + h3c = stream->connection; |
| + c = h3c->connection; |
| + |
| + ngx_log_debug3(NGX_LOG_DEBUG_EVENT, fc->log, 0, |
| + "http3 stream %uz to write %uz bytes, fin=%d", |
| + stream->id, buf_len, fin); |
| + |
| + if (!stream->headers_sent) { |
| + return NGX_AGAIN; |
| + } |
| + |
| + n = quiche_h3_send_body(h3c->h3, c->quic->conn, r->qstream->id, |
| + buf, buf_len, fin); |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, stream->connection->connection->log, 0, |
| + "http3 stream written %z bytes", n); |
| + |
| + if (n == QUICHE_H3_ERR_DONE) { |
| + return NGX_AGAIN; |
| + } |
| + |
| + if (n < 0) { |
| + if (n == NGX_HTTP_V3_TRANSPORT_STREAM_INVALID || |
| + n == NGX_HTTP_V3_TRANSPORT_STREAM_STOPPED ) { |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, fc->log, 0, "stream write failed: %d", n); |
| + } else { |
| + ngx_log_error(NGX_LOG_ERR, fc->log, 0, "stream write failed: %d", n); |
| + } |
| + |
| + return NGX_ERROR; |
| + } |
| + |
| + return n; |
| +} |
| + |
| + |
| +static ssize_t |
| +ngx_http_v3_recv_body(ngx_connection_t *c, u_char *buf, size_t size) |
| +{ |
| + ssize_t n; |
| + ngx_event_t *rev; |
| + ngx_http_request_t *r; |
| + ngx_http_v3_connection_t *h3c; |
| + |
| + rev = c->read; |
| + |
| + r = c->data; |
| + h3c = r->qstream->connection; |
| + |
| + if (c->error) { |
| + rev->ready = 0; |
| + |
| + return NGX_ERROR; |
| + } |
| + |
| + n = quiche_h3_recv_body(h3c->h3, c->quic->conn, r->qstream->id, buf, size); |
| + |
| + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "http3 body recv: %z of %uz", n, size); |
| + |
| + if (quiche_conn_stream_finished(c->quic->conn, r->qstream->id)) { |
| + rev->ready = 0; |
| + |
| + /* Re-schedule connection read event to poll for Finished event. */ |
| + ngx_post_event(h3c->connection->read, &ngx_posted_events); |
| + } |
| + |
| + if (n == 0) { |
| + rev->ready = 0; |
| + |
| + return 0; |
| + } |
| + |
| + if (n > 0) { |
| + |
| + if ((size_t) n < size) { |
| + rev->ready = 0; |
| + } |
| + |
| + return n; |
| + } |
| + |
| + if (n == QUICHE_H3_ERR_DONE) { |
| + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, |
| + "quiche_h3_recv_body() not ready"); |
| + |
| + n = NGX_AGAIN; |
| + |
| + } else { |
| + rev->error = 1; |
| + |
| + n = NGX_ERROR; |
| + } |
| + |
| + rev->ready = 0; |
| + |
| + return n; |
| +} |
| + |
| + |
| +static ngx_chain_t * |
| +ngx_http_v3_send_chain(ngx_connection_t *fc, ngx_chain_t *in, off_t limit) |
| +{ |
| + ssize_t n, sent; |
| + off_t send, prev_send; |
| + ngx_uint_t blocked, fin; |
| + |
| + ngx_http_request_t *r; |
| + ngx_http_v3_stream_t *stream; |
| + |
| + r = fc->data; |
| + stream = r->qstream; |
| + |
| + send = 0; |
| + |
| + blocked = 0; |
| + |
| + while (in) { |
| + off_t size = ngx_buf_size(in->buf); |
| + |
| + if (size || in->buf->last_buf) { |
| + break; |
| + } |
| + |
| + in = in->next; |
| + } |
| + |
| + if (in == NULL || stream->out_closed) { |
| + return NULL; |
| + } |
| + |
| + while (in) { |
| + prev_send = send; |
| + |
| + fin = in->buf->last_buf; |
| + |
| + send += ngx_buf_size(in->buf); |
| + |
| + n = ngx_http_v3_stream_do_send(fc, in->buf, fin); |
| + |
| + if (n == NGX_ERROR) { |
| + return NGX_CHAIN_ERROR; |
| + } |
| + |
| + sent = (n == NGX_AGAIN) ? 0 : n; |
| + |
| + fc->sent += sent; |
| + |
| + in->buf->pos += sent; |
| + |
| + /* Partial (or no) write, end now. */ |
| + if ((n == NGX_AGAIN) || (send - prev_send != sent)) { |
| + blocked = 1; |
| + break; |
| + } |
| + |
| + /* Buffer is fully written, switch to the next. */ |
| + if (in->buf->pos == in->buf->last) { |
| + in = in->next; |
| + } |
| + |
| + if (fin) { |
| + stream->out_closed = 1; |
| + } |
| + } |
| + |
| + if (blocked) { |
| + if (!stream->blocked) { |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, stream->connection->connection->log, 0, |
| + "http3 stream blocked %ui", stream->id); |
| + |
| + stream->blocked = 1; |
| + |
| + fc->write->active = 1; |
| + fc->write->ready = 0; |
| + } |
| + } |
| + |
| + ngx_post_event(stream->connection->connection->write, &ngx_posted_events); |
| + |
| + return in; |
| +} |
| + |
| + |
| +void |
| +ngx_http_v3_close_stream(ngx_http_v3_stream_t *stream, ngx_int_t rc) |
| +{ |
| + ngx_event_t *ev; |
| + ngx_connection_t *c, *fc; |
| + ngx_http_v3_connection_t *h3c; |
| + |
| + h3c = stream->connection; |
| + c = h3c->connection; |
| + |
| + fc = stream->request->connection; |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, h3c->connection->log, 0, |
| + "http3 close stream %ui", stream->id); |
| + |
| + if (stream->blocked) { |
| + fc->write->handler = ngx_http_v3_close_stream_handler; |
| + fc->read->handler = ngx_http_empty_handler; |
| + return; |
| + } |
| + |
| + quiche_conn_stream_shutdown(h3c->connection->quic->conn, stream->id, |
| + QUICHE_SHUTDOWN_READ, NGX_HTTP_V3_NO_ERROR); |
| + |
| + /* If the stream has closed, a QUIC stream FIN was sent. In that case don't |
| + send RESET_STREAM or else we risk data loss on the client side.*/ |
| + if (!stream->out_closed) { |
| + quiche_conn_stream_shutdown(h3c->connection->quic->conn, stream->id, |
| + QUICHE_SHUTDOWN_WRITE, NGX_HTTP_V3_NO_ERROR); |
| + } |
| + |
| + /* Post a connection write event to flush QUIC frames */ |
| + ngx_post_event(c->write, &ngx_posted_events); |
| + |
| + ngx_rbtree_delete(&h3c->streams, &stream->node); |
| + |
| + ngx_http_free_request(stream->request, rc); |
| + |
| + ev = fc->read; |
| + |
| + if (ev->timer_set) { |
| + ngx_del_timer(ev); |
| + } |
| + |
| + if (ev->posted) { |
| + ngx_delete_posted_event(ev); |
| + } |
| + |
| + ev = fc->write; |
| + |
| + if (ev->timer_set) { |
| + ngx_del_timer(ev); |
| + } |
| + |
| + if (ev->posted) { |
| + ngx_delete_posted_event(ev); |
| + } |
| + |
| + fc->data = h3c->free_fake_connections; |
| + h3c->free_fake_connections = fc; |
| + |
| + h3c->processing--; |
| + |
| + ngx_http_v3_handle_connection(h3c); |
| +} |
| + |
| + |
| +static void |
| +ngx_http_v3_close_stream_handler(ngx_event_t *ev) |
| +{ |
| + ngx_connection_t *fc; |
| + ngx_http_request_t *r; |
| + |
| + fc = ev->data; |
| + r = fc->data; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, fc->log, 0, |
| + "http3 close stream handler"); |
| + |
| + if (ev->timedout) { |
| + ngx_log_error(NGX_LOG_INFO, fc->log, NGX_ETIMEDOUT, "client timed out"); |
| + |
| + fc->timedout = 1; |
| + |
| + ngx_http_v3_close_stream(r->qstream, NGX_HTTP_REQUEST_TIME_OUT); |
| + return; |
| + } |
| + |
| + ngx_http_v3_close_stream(r->qstream, 0); |
| +} |
| + |
| +void |
| +ngx_http_v3_stop_stream_read(ngx_http_v3_stream_t *stream, ngx_int_t rc) |
| +{ |
| + ngx_http_v3_connection_t *h3c; |
| + |
| + if (!stream) { |
| + return; |
| + } |
| + |
| + h3c = stream->connection; |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, h3c->connection->log, 0, |
| + "http3 stream shutdown read %ui", stream->id); |
| + |
| + quiche_conn_stream_shutdown(h3c->connection->quic->conn, |
| + stream->id, |
| + QUICHE_SHUTDOWN_READ, NGX_HTTP_V3_NO_ERROR); |
| +} |
| + |
| + |
| +static void |
| +ngx_http_v3_finalize_connection(ngx_http_v3_connection_t *h3c, |
| + ngx_uint_t status) |
| +{ |
| + ngx_event_t *ev; |
| + ngx_connection_t *c, *fc; |
| + ngx_rbtree_node_t *node, *root, *sentinel; |
| + ngx_http_request_t *r; |
| + ngx_http_v3_stream_t *stream; |
| + |
| + c = h3c->connection; |
| + |
| + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http3 finalize connection"); |
| + |
| + quiche_conn_close(c->quic->conn, true, status, NULL, 0); |
| + |
| + c->error = 1; |
| + |
| + if (!h3c->processing) { |
| + ngx_http_close_connection(c); |
| + return; |
| + } |
| + |
| + c->read->handler = ngx_http_empty_handler; |
| + c->write->handler = ngx_http_empty_handler; |
| + |
| + root = h3c->streams.root; |
| + sentinel = h3c->streams.sentinel; |
| + |
| + if (root != sentinel) { |
| + node = ngx_rbtree_min(h3c->streams.root, sentinel); |
| + } else { |
| + node = NULL; |
| + } |
| + |
| + /* Close all pending streams / requests. */ |
| + while (node != NULL) { |
| + stream = (ngx_http_v3_stream_t *) node; |
| + |
| + r = stream->request; |
| + fc = r->connection; |
| + |
| + fc->error = 1; |
| + |
| + if (c->close) { |
| + fc->close = 1; |
| + } |
| + |
| + if (stream->blocked) { |
| + stream->blocked = 0; |
| + |
| + ev = fc->write; |
| + ev->active = 0; |
| + ev->ready = 1; |
| + |
| + } else { |
| + ev = fc->read; |
| + } |
| + |
| + node = ngx_rbtree_next(&h3c->streams, node); |
| + |
| + ev->eof = 1; |
| + ev->handler(ev); |
| + } |
| + |
| + if (h3c->processing) { |
| + return; |
| + } |
| + |
| + ngx_http_close_connection(c); |
| +} |
| + |
| + |
| +static void |
| +ngx_http_v3_pool_cleanup(void *data) |
| +{ |
| + ngx_http_v3_connection_t *h3c = data; |
| + |
| + if (h3c->h3) { |
| + quiche_h3_conn_free(h3c->h3); |
| + |
| + h3c->h3 = NULL; |
| + } |
| +} |
| diff --git a/src/http/v3/ngx_http_v3.h b/src/http/v3/ngx_http_v3.h |
| new file mode 100644 |
| index 000000000..2ca965fd8 |
| --- /dev/null |
| +++ b/src/http/v3/ngx_http_v3.h |
| @@ -0,0 +1,80 @@ |
| + |
| +/* |
| + * Copyright (C) Cloudflare, Inc. |
| + */ |
| + |
| + |
| +#ifndef _NGX_HTTP_V3_H_INCLUDED_ |
| +#define _NGX_HTTP_V3_H_INCLUDED_ |
| + |
| + |
| +#include <ngx_config.h> |
| +#include <ngx_core.h> |
| +#include <ngx_http.h> |
| +#include <ngx_http_v3_module.h> |
| + |
| + |
| +#define NGX_HTTP_V3_ALPN_ADVERTISE "\x05h3-18" |
| + |
| + |
| +typedef struct ngx_http_v3_connection_s ngx_http_v3_connection_t; |
| + |
| + |
| +struct ngx_http_v3_connection_s { |
| + quiche_h3_conn *h3; |
| + |
| + ngx_connection_t *connection; |
| + ngx_http_connection_t *http_connection; |
| + |
| + ngx_pool_t *pool; |
| + |
| + ngx_uint_t processing; |
| + |
| + ngx_rbtree_t streams; |
| + ngx_rbtree_node_t streams_sentinel; |
| + |
| + ngx_connection_t *free_fake_connections; |
| +}; |
| + |
| +struct ngx_http_v3_stream_s { |
| + ngx_rbtree_node_t node; |
| + |
| + uint64_t id; |
| + |
| + quiche_h3_priority priority; |
| + |
| + ngx_http_request_t *request; |
| + |
| + ngx_http_v3_connection_t *connection; |
| + |
| + ngx_array_t *headers; |
| + ngx_array_t *cookies; |
| + |
| + ngx_http_v3_stream_t *next; |
| + |
| + ngx_uint_t headers_sent:1; |
| + ngx_uint_t in_closed:1; |
| + ngx_uint_t out_closed:1; |
| + ngx_uint_t skip_data:1; |
| + ngx_uint_t blocked:1; |
| +}; |
| + |
| + |
| +typedef struct { |
| + ngx_str_t name; |
| + ngx_str_t value; |
| +} ngx_http_v3_header_t; |
| + |
| + |
| +void ngx_http_v3_init(ngx_event_t *rev); |
| + |
| +ngx_int_t ngx_http_v3_send_response(ngx_http_request_t *r); |
| + |
| +void ngx_http_v3_close_stream(ngx_http_v3_stream_t *stream, ngx_int_t rc); |
| +void ngx_http_v3_stop_stream_read(ngx_http_v3_stream_t *stream, ngx_int_t rc); |
| + |
| +ngx_int_t ngx_http_v3_request_body_filter(ngx_http_request_t *r, |
| + ngx_chain_t *in); |
| + |
| + |
| +#endif /* _NGX_HTTP_V3_H_INCLUDED_ */ |
| diff --git a/src/http/v3/ngx_http_v3_filter_module.c b/src/http/v3/ngx_http_v3_filter_module.c |
| new file mode 100644 |
| index 000000000..7cba70535 |
| --- /dev/null |
| +++ b/src/http/v3/ngx_http_v3_filter_module.c |
| @@ -0,0 +1,74 @@ |
| + |
| +/* |
| + * Copyright (C) Cloudflare, Inc. |
| + */ |
| + |
| + |
| +#include <ngx_config.h> |
| +#include <ngx_core.h> |
| +#include <ngx_http.h> |
| +#include <ngx_http_v3_module.h> |
| + |
| + |
| +static ngx_int_t ngx_http_v3_filter_init(ngx_conf_t *cf); |
| + |
| + |
| +static ngx_http_module_t ngx_http_v3_filter_module_ctx = { |
| + NULL, /* preconfiguration */ |
| + ngx_http_v3_filter_init, /* postconfiguration */ |
| + |
| + NULL, /* create main configuration */ |
| + NULL, /* init main configuration */ |
| + |
| + NULL, /* create server configuration */ |
| + NULL, /* merge server configuration */ |
| + |
| + NULL, /* create location configuration */ |
| + NULL /* merge location configuration */ |
| +}; |
| + |
| + |
| +ngx_module_t ngx_http_v3_filter_module = { |
| + NGX_MODULE_V1, |
| + &ngx_http_v3_filter_module_ctx, /* module context */ |
| + NULL, /* module directives */ |
| + NGX_HTTP_MODULE, /* module type */ |
| + NULL, /* init master */ |
| + NULL, /* init module */ |
| + NULL, /* init process */ |
| + NULL, /* init thread */ |
| + NULL, /* exit thread */ |
| + NULL, /* exit process */ |
| + NULL, /* exit master */ |
| + NGX_MODULE_V1_PADDING |
| +}; |
| + |
| + |
| +static ngx_http_output_header_filter_pt ngx_http_next_header_filter; |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_header_filter(ngx_http_request_t *r) |
| +{ |
| + if (!r->qstream) { |
| + return ngx_http_next_header_filter(r); |
| + } |
| + |
| + r->header_sent = 1; |
| + |
| + if (r != r->main) { |
| + return NGX_OK; |
| + } |
| + |
| + return ngx_http_v3_send_response(r); |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_filter_init(ngx_conf_t *cf) |
| +{ |
| + ngx_http_next_header_filter = ngx_http_top_header_filter; |
| + ngx_http_top_header_filter = ngx_http_v3_header_filter; |
| + |
| + return NGX_OK; |
| +} |
| diff --git a/src/http/v3/ngx_http_v3_module.c b/src/http/v3/ngx_http_v3_module.c |
| new file mode 100644 |
| index 000000000..d413c887b |
| --- /dev/null |
| +++ b/src/http/v3/ngx_http_v3_module.c |
| @@ -0,0 +1,321 @@ |
| + |
| +/* |
| + * Copyright (C) Cloudflare, Inc. |
| + */ |
| + |
| + |
| +#include <ngx_config.h> |
| +#include <ngx_core.h> |
| +#include <ngx_http.h> |
| +#include <ngx_http_v3_module.h> |
| + |
| +#include <quiche.h> |
| + |
| + |
| +static ngx_int_t ngx_http_v3_add_variables(ngx_conf_t *cf); |
| + |
| +static void *ngx_http_v3_create_srv_conf(ngx_conf_t *cf); |
| +static char *ngx_http_v3_merge_srv_conf(ngx_conf_t *cf, |
| + void *parent, void *child); |
| + |
| +static ngx_int_t ngx_http_v3_variable(ngx_http_request_t *r, |
| + ngx_http_variable_value_t *v, uintptr_t data); |
| + |
| +static void ngx_http_v3_cleanup_ctx(void *data); |
| + |
| + |
| +static ngx_command_t ngx_http_v3_commands[] = { |
| + |
| + { ngx_string("http3_max_concurrent_streams"), |
| + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, |
| + ngx_conf_set_num_slot, |
| + NGX_HTTP_SRV_CONF_OFFSET, |
| + offsetof(ngx_http_v3_srv_conf_t, concurrent_streams), |
| + NULL }, |
| + |
| + { ngx_string("http3_max_requests"), |
| + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, |
| + ngx_conf_set_num_slot, |
| + NGX_HTTP_SRV_CONF_OFFSET, |
| + offsetof(ngx_http_v3_srv_conf_t, max_requests), |
| + NULL }, |
| + |
| + { ngx_string("http3_max_header_size"), |
| + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, |
| + ngx_conf_set_size_slot, |
| + NGX_HTTP_SRV_CONF_OFFSET, |
| + offsetof(ngx_http_v3_srv_conf_t, max_header_size), |
| + NULL }, |
| + |
| + { ngx_string("http3_initial_max_data"), |
| + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, |
| + ngx_conf_set_size_slot, |
| + NGX_HTTP_SRV_CONF_OFFSET, |
| + offsetof(ngx_http_v3_srv_conf_t, max_data), |
| + NULL }, |
| + |
| + { ngx_string("http3_initial_max_stream_data"), |
| + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, |
| + ngx_conf_set_size_slot, |
| + NGX_HTTP_SRV_CONF_OFFSET, |
| + offsetof(ngx_http_v3_srv_conf_t, max_stream_data), |
| + NULL }, |
| + |
| + { ngx_string("http3_idle_timeout"), |
| + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, |
| + ngx_conf_set_msec_slot, |
| + NGX_HTTP_SRV_CONF_OFFSET, |
| + offsetof(ngx_http_v3_srv_conf_t, idle_timeout), |
| + NULL }, |
| + |
| + { ngx_string("http3_congestion_control"), |
| + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, |
| + ngx_conf_set_str_slot, |
| + NGX_HTTP_SRV_CONF_OFFSET, |
| + offsetof(ngx_http_v3_srv_conf_t, congestion_control), |
| + NULL }, |
| + |
| + { ngx_string("http3_pacing"), |
| + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, |
| + ngx_conf_set_flag_slot, |
| + NGX_HTTP_SRV_CONF_OFFSET, |
| + offsetof(ngx_http_v3_srv_conf_t, pacing), |
| + NULL }, |
| + |
| + ngx_null_command |
| +}; |
| + |
| + |
| +static ngx_http_module_t ngx_http_v3_module_ctx = { |
| + ngx_http_v3_add_variables, /* preconfiguration */ |
| + NULL, /* postconfiguration */ |
| + |
| + NULL, /* create main configuration */ |
| + NULL, /* init main configuration */ |
| + |
| + ngx_http_v3_create_srv_conf, /* create server configuration */ |
| + ngx_http_v3_merge_srv_conf, /* merge server configuration */ |
| + |
| + NULL, /* create location configuration */ |
| + NULL /* merge location configuration */ |
| +}; |
| + |
| + |
| +ngx_module_t ngx_http_v3_module = { |
| + NGX_MODULE_V1, |
| + &ngx_http_v3_module_ctx, /* module context */ |
| + ngx_http_v3_commands, /* module directives */ |
| + NGX_HTTP_MODULE, /* module type */ |
| + NULL, /* init master */ |
| + NULL, /* init module */ |
| + NULL, /* init process */ |
| + NULL, /* init thread */ |
| + NULL, /* exit thread */ |
| + NULL, /* exit process */ |
| + NULL, /* exit master */ |
| + NGX_MODULE_V1_PADDING |
| +}; |
| + |
| + |
| +static ngx_http_variable_t ngx_http_v3_variables[] = { |
| + |
| + { ngx_string("http3"), NULL, |
| + ngx_http_v3_variable, 0, |
| + NGX_HTTP_VAR_CHANGEABLE, 0 }, |
| + |
| + ngx_http_null_variable |
| +}; |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_add_variables(ngx_conf_t *cf) |
| +{ |
| + ngx_http_variable_t *var, *v; |
| + |
| + for (v = ngx_http_v3_variables; v->name.len; v++) { |
| + var = ngx_http_add_variable(cf, &v->name, v->flags); |
| + if (var == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + var->get_handler = v->get_handler; |
| + var->data = v->data; |
| + } |
| + |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static void * |
| +ngx_http_v3_create_srv_conf(ngx_conf_t *cf) |
| +{ |
| + ngx_http_v3_srv_conf_t *h3scf; |
| + |
| + h3scf = ngx_pcalloc(cf->pool, sizeof(ngx_http_v3_srv_conf_t)); |
| + if (h3scf == NULL) { |
| + return NULL; |
| + } |
| + |
| + h3scf->idle_timeout = NGX_CONF_UNSET_MSEC; |
| + h3scf->max_data = NGX_CONF_UNSET_SIZE; |
| + h3scf->max_stream_data = NGX_CONF_UNSET_SIZE; |
| + h3scf->max_requests = NGX_CONF_UNSET_UINT; |
| + h3scf->max_header_size = NGX_CONF_UNSET_SIZE; |
| + h3scf->concurrent_streams = NGX_CONF_UNSET_UINT; |
| + /* h3scf->congestion_control = { 0, NULL }; */ |
| + h3scf->pacing = NGX_CONF_UNSET; |
| + |
| + return h3scf; |
| +} |
| + |
| + |
| +#if (NGX_DEBUG) |
| +static void |
| +quiche_log(const char *line, void *argp) |
| +{ |
| + ngx_log_t *log = ngx_cycle->log; |
| + |
| + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0, "%s", line); |
| +} |
| +#endif |
| + |
| + |
| +static char * |
| +ngx_http_v3_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) |
| +{ |
| + ngx_http_v3_srv_conf_t *prev = parent; |
| + ngx_http_v3_srv_conf_t *conf = child; |
| + |
| + ngx_pool_cleanup_t *cln; |
| + |
| + ngx_conf_merge_msec_value(conf->idle_timeout, |
| + prev->idle_timeout, 180000); |
| + |
| + ngx_conf_merge_size_value(conf->max_data, |
| + prev->max_data, 10485760); |
| + |
| + ngx_conf_merge_size_value(conf->max_stream_data, |
| + prev->max_stream_data, 1048576); |
| + |
| + ngx_conf_merge_uint_value(conf->max_requests, |
| + prev->max_requests, 1000); |
| + |
| + ngx_conf_merge_size_value(conf->max_header_size, |
| + prev->max_header_size, 16384); |
| + |
| + ngx_conf_merge_uint_value(conf->concurrent_streams, |
| + prev->concurrent_streams, 128); |
| + |
| + ngx_conf_merge_str_value(conf->congestion_control, |
| + prev->congestion_control, "cubic"); |
| + |
| + ngx_conf_merge_value(conf->pacing, prev->pacing, 0); |
| + |
| + conf->quic.log = cf->log; |
| + |
| +#if (NGX_DEBUG) |
| + /* Enable quiche debug logging. quiche commit ceade4 or later is required */ |
| + quiche_enable_debug_logging(quiche_log, NULL); |
| +#endif |
| + |
| + if (ngx_quic_create_conf(&conf->quic) != NGX_OK) { |
| + return NGX_CONF_ERROR; |
| + } |
| + |
| + quiche_config_set_max_send_udp_payload_size(conf->quic.config, |
| + MAX_DATAGRAM_SIZE); |
| + |
| + quiche_config_set_max_idle_timeout(conf->quic.config, conf->idle_timeout); |
| + |
| + quiche_config_set_initial_max_data(conf->quic.config, conf->max_data); |
| + |
| + quiche_config_set_initial_max_stream_data_bidi_remote(conf->quic.config, |
| + conf->max_stream_data); |
| + |
| + quiche_config_set_initial_max_stream_data_uni(conf->quic.config, |
| + conf->max_stream_data); |
| + |
| + quiche_config_set_initial_max_streams_bidi(conf->quic.config, |
| + conf->concurrent_streams); |
| + |
| + /* For HTTP/3 we only need 3 unidirectional streams. */ |
| + quiche_config_set_initial_max_streams_uni(conf->quic.config, 3); |
| + |
| + if (quiche_config_set_cc_algorithm_name(conf->quic.config, |
| + (char *) conf->congestion_control.data) != 0) |
| + { |
| + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, |
| + "failed to configure congestion control algorithm"); |
| + return NGX_CONF_ERROR; |
| + } |
| + |
| + quiche_config_enable_pacing(conf->quic.config, conf->pacing); |
| + conf->quic.pacing = conf->pacing; |
| + |
| + conf->http3 = quiche_h3_config_new(); |
| + if (conf->http3 == NULL) { |
| + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, |
| + "failed to create HTTP/3 config"); |
| + return NGX_CONF_ERROR; |
| + } |
| + |
| + quiche_h3_config_set_max_field_section_size(conf->http3, |
| + conf->max_header_size); |
| + |
| + cln = ngx_pool_cleanup_add(cf->pool, 0); |
| + if (cln == NULL) { |
| + return NGX_CONF_ERROR; |
| + } |
| + |
| + cln->handler = ngx_quic_cleanup_ctx; |
| + cln->data = &conf->quic; |
| + |
| + cln = ngx_pool_cleanup_add(cf->pool, 0); |
| + if (cln == NULL) { |
| + return NGX_CONF_ERROR; |
| + } |
| + |
| + cln->handler = ngx_http_v3_cleanup_ctx; |
| + cln->data = conf->http3; |
| + |
| + return NGX_CONF_OK; |
| +} |
| + |
| + |
| +static ngx_int_t |
| +ngx_http_v3_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v, |
| + uintptr_t data) |
| +{ |
| + ngx_connection_t *c; |
| + |
| + v->valid = 1; |
| + v->no_cacheable = 1; |
| + v->not_found = 0; |
| + |
| + c = r->connection; |
| + if (c == NULL) { |
| + return NGX_ERROR; |
| + } |
| + |
| + if (c->quic != NULL) { |
| + v->len = sizeof("h3") - 1; |
| + v->valid = 1; |
| + v->no_cacheable = 0; |
| + v->not_found = 0; |
| + v->data = (u_char *) "h3"; |
| + |
| + return NGX_OK; |
| + } |
| + |
| + *v = ngx_http_variable_null_value; |
| + return NGX_OK; |
| +} |
| + |
| + |
| +static void |
| +ngx_http_v3_cleanup_ctx(void *data) |
| +{ |
| + quiche_h3_config *config = data; |
| + |
| + quiche_h3_config_free(config); |
| +} |
| diff --git a/src/http/v3/ngx_http_v3_module.h b/src/http/v3/ngx_http_v3_module.h |
| new file mode 100644 |
| index 000000000..88f7497e6 |
| --- /dev/null |
| +++ b/src/http/v3/ngx_http_v3_module.h |
| @@ -0,0 +1,36 @@ |
| + |
| +/* |
| + * Copyright (C) Cloudflare, Inc. |
| + */ |
| + |
| + |
| +#ifndef _NGX_HTTP_V3_MODULE_H_INCLUDED_ |
| +#define _NGX_HTTP_V3_MODULE_H_INCLUDED_ |
| + |
| + |
| +#include <ngx_config.h> |
| +#include <ngx_core.h> |
| + |
| +#include <quiche.h> |
| + |
| + |
| +typedef struct { |
| + ngx_quic_t quic; |
| + |
| + quiche_h3_config *http3; |
| + |
| + ngx_msec_t idle_timeout; |
| + size_t max_data; |
| + size_t max_stream_data; |
| + ngx_uint_t max_requests; |
| + ngx_uint_t max_header_size; |
| + ngx_uint_t concurrent_streams; |
| + ngx_str_t congestion_control; |
| + ngx_flag_t pacing; |
| +} ngx_http_v3_srv_conf_t; |
| + |
| + |
| +extern ngx_module_t ngx_http_v3_module; |
| + |
| + |
| +#endif /* _NGX_HTTP_V3_MODULE_H_INCLUDED_ */ |
| diff --git a/src/os/unix/ngx_linux_config.h b/src/os/unix/ngx_linux_config.h |
| index 3036caebf..c3a207005 100644 |
| --- a/src/os/unix/ngx_linux_config.h |
| +++ b/src/os/unix/ngx_linux_config.h |
| @@ -104,6 +104,16 @@ typedef struct iocb ngx_aiocb_t; |
| #endif |
| |
| |
| +#if (NGX_HAVE_UDP_SEGMENT) |
| +#include <netinet/udp.h> |
| +#endif |
| + |
| + |
| +#if (NGX_HAVE_SO_TXTIME) |
| +#include <linux/net_tstamp.h> |
| +#endif |
| + |
| + |
| #define NGX_LISTEN_BACKLOG 511 |
| |
| |
| diff --git a/src/os/unix/ngx_udp_sendmsg_chain.c b/src/os/unix/ngx_udp_sendmsg_chain.c |
| index 5399c7916..f82d088b3 100644 |
| --- a/src/os/unix/ngx_udp_sendmsg_chain.c |
| +++ b/src/os/unix/ngx_udp_sendmsg_chain.c |
| @@ -10,6 +10,11 @@ |
| #include <ngx_event.h> |
| |
| |
| +#ifndef UDP_SEGMENT |
| +#define UDP_SEGMENT 103 |
| +#endif |
| + |
| + |
| static ngx_chain_t *ngx_udp_output_chain_to_iovec(ngx_iovec_t *vec, |
| ngx_chain_t *in, ngx_log_t *log); |
| static ssize_t ngx_sendmsg(ngx_connection_t *c, ngx_iovec_t *vec); |
| @@ -199,20 +204,25 @@ ngx_udp_output_chain_to_iovec(ngx_iovec_t *vec, ngx_chain_t *in, ngx_log_t *log) |
| static ssize_t |
| ngx_sendmsg(ngx_connection_t *c, ngx_iovec_t *vec) |
| { |
| - ssize_t n; |
| - ngx_err_t err; |
| - struct msghdr msg; |
| + ssize_t n; |
| + ngx_err_t err; |
| + struct msghdr msg; |
| + struct cmsghdr *cmsg = NULL; |
| |
| #if (NGX_HAVE_MSGHDR_MSG_CONTROL) |
| |
| #if (NGX_HAVE_IP_SENDSRCADDR) |
| u_char msg_control[CMSG_SPACE(sizeof(struct in_addr))]; |
| #elif (NGX_HAVE_IP_PKTINFO) |
| - u_char msg_control[CMSG_SPACE(sizeof(struct in_pktinfo))]; |
| + u_char msg_control[CMSG_SPACE(sizeof(struct in_pktinfo) + |
| + CMSG_SPACE(sizeof(uint16_t)) + |
| + CMSG_SPACE(sizeof(uint64_t)))]; |
| #endif |
| |
| #if (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO) |
| - u_char msg_control6[CMSG_SPACE(sizeof(struct in6_pktinfo))]; |
| + u_char msg_control6[CMSG_SPACE(sizeof(struct in6_pktinfo)) + |
| + CMSG_SPACE(sizeof(uint16_t)) + |
| + CMSG_SPACE(sizeof(uint64_t))]; |
| #endif |
| |
| #endif |
| @@ -234,7 +244,6 @@ ngx_sendmsg(ngx_connection_t *c, ngx_iovec_t *vec) |
| #if (NGX_HAVE_IP_SENDSRCADDR) |
| |
| if (c->local_sockaddr->sa_family == AF_INET) { |
| - struct cmsghdr *cmsg; |
| struct in_addr *addr; |
| struct sockaddr_in *sin; |
| |
| @@ -255,12 +264,13 @@ ngx_sendmsg(ngx_connection_t *c, ngx_iovec_t *vec) |
| #elif (NGX_HAVE_IP_PKTINFO) |
| |
| if (c->local_sockaddr->sa_family == AF_INET) { |
| - struct cmsghdr *cmsg; |
| struct in_pktinfo *pkt; |
| struct sockaddr_in *sin; |
| |
| + ngx_memzero(&msg_control, sizeof(msg_control)); |
| + |
| msg.msg_control = &msg_control; |
| - msg.msg_controllen = sizeof(msg_control); |
| + msg.msg_controllen = CMSG_SPACE(sizeof(struct in_pktinfo)); |
| |
| cmsg = CMSG_FIRSTHDR(&msg); |
| cmsg->cmsg_level = IPPROTO_IP; |
| @@ -279,12 +289,13 @@ ngx_sendmsg(ngx_connection_t *c, ngx_iovec_t *vec) |
| #if (NGX_HAVE_INET6 && NGX_HAVE_IPV6_RECVPKTINFO) |
| |
| if (c->local_sockaddr->sa_family == AF_INET6) { |
| - struct cmsghdr *cmsg; |
| struct in6_pktinfo *pkt6; |
| struct sockaddr_in6 *sin6; |
| |
| + ngx_memzero(&msg_control6, sizeof(msg_control6)); |
| + |
| msg.msg_control = &msg_control6; |
| - msg.msg_controllen = sizeof(msg_control6); |
| + msg.msg_controllen = CMSG_SPACE(sizeof(struct in6_pktinfo)); |
| |
| cmsg = CMSG_FIRSTHDR(&msg); |
| cmsg->cmsg_level = IPPROTO_IPV6; |
| @@ -301,6 +312,91 @@ ngx_sendmsg(ngx_connection_t *c, ngx_iovec_t *vec) |
| #endif |
| } |
| |
| +#if (NGX_QUIC && NGX_HAVE_UDP_SEGMENT) |
| + |
| + if (c->quic && c->quic->segment_size && |
| + c->local_sockaddr && |
| + (c->local_sockaddr->sa_family == AF_INET || |
| + c->local_sockaddr->sa_family == AF_INET6)) |
| + { |
| + uint16_t *segment_size; |
| + |
| + if (cmsg == NULL) { |
| + if (c->local_sockaddr->sa_family == AF_INET) { |
| + ngx_memzero(&msg_control, sizeof(msg_control)); |
| + msg.msg_control = &msg_control; |
| + } else { |
| + ngx_memzero(&msg_control6, sizeof(msg_control6)); |
| + msg.msg_control = &msg_control6; |
| + } |
| + |
| + msg.msg_controllen = CMSG_SPACE(sizeof(uint16_t)); |
| + |
| + cmsg = CMSG_FIRSTHDR(&msg); |
| + |
| + } else { |
| + msg.msg_controllen += CMSG_SPACE(sizeof(uint16_t)); |
| + |
| + cmsg = CMSG_NXTHDR(&msg, cmsg); |
| + } |
| + |
| + cmsg->cmsg_level = SOL_UDP; |
| + cmsg->cmsg_type = UDP_SEGMENT; |
| + cmsg->cmsg_len = CMSG_LEN(sizeof(uint16_t)); |
| + |
| + segment_size = (uint16_t *) CMSG_DATA(cmsg); |
| + ngx_memzero(segment_size, sizeof(uint16_t)); |
| + *segment_size = c->quic->segment_size; |
| + } |
| + |
| +#endif |
| + |
| +#if (NGX_QUIC && NGX_HAVE_SO_TXTIME) |
| + |
| + ngx_listening_t *ls = c->listening; |
| + |
| + if (c->quic && c->quic->pacing && |
| + ls && ls->quic_so_txtime && |
| + c->local_sockaddr && |
| + (c->local_sockaddr->sa_family == AF_INET || |
| + c->local_sockaddr->sa_family == AF_INET6)) { |
| + uint64_t timestamp_ns, *tx_time; |
| + |
| + if (cmsg == NULL) { |
| + if (c->local_sockaddr->sa_family == AF_INET) { |
| + ngx_memzero(&msg_control, sizeof(msg_control)); |
| + msg.msg_control = &msg_control; |
| + } else { |
| + ngx_memzero(&msg_control6, sizeof(msg_control6)); |
| + msg.msg_control = &msg_control6; |
| + } |
| + |
| + msg.msg_controllen = CMSG_SPACE(sizeof(uint64_t)); |
| + |
| + cmsg = CMSG_FIRSTHDR(&msg); |
| + |
| + } else { |
| + msg.msg_controllen += CMSG_SPACE(sizeof(uint64_t)); |
| + |
| + cmsg = CMSG_NXTHDR(&msg, cmsg); |
| + } |
| + |
| + cmsg->cmsg_level = SOL_SOCKET; |
| + cmsg->cmsg_type = SCM_TXTIME; |
| + cmsg->cmsg_len = CMSG_LEN(sizeof(uint64_t)); |
| + |
| + /* Convert struct timespec to nanoseconds. */ |
| + timestamp_ns = c->quic->send_info.at.tv_sec * |
| + (1000ULL * 1000 * 1000) + |
| + c->quic->send_info.at.tv_nsec; |
| + |
| + tx_time = (uint64_t *) CMSG_DATA(cmsg); |
| + ngx_memzero(tx_time, sizeof(uint64_t)); |
| + *tx_time = timestamp_ns; |
| + } |
| + |
| +#endif |
| + |
| #endif |
| |
| eintr: |
| @@ -315,6 +411,7 @@ eintr: |
| |
| switch (err) { |
| case NGX_EAGAIN: |
| + case ENOBUFS: |
| ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, err, |
| "sendmsg() not ready"); |
| return NGX_AGAIN; |
| -- |
| 2.40.1 |
| |