From 672721a1f487a4dc9e767606bfad9951ee84519b Mon Sep 17 00:00:00 2001 From: Xiaochen Wang Date: Sat, 8 Jul 2023 12:01:29 +0800 Subject: [PATCH 01/11] supported http/2: return 400 while failing to parse connect host --- ngx_http_proxy_connect_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngx_http_proxy_connect_module.c b/ngx_http_proxy_connect_module.c index cd7e517..f09206b 100644 --- a/ngx_http_proxy_connect_module.c +++ b/ngx_http_proxy_connect_module.c @@ -1515,7 +1515,7 @@ ngx_http_proxy_connect_handler(ngx_http_request_t *r) ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "proxy_connect: %s in connect host \"%V\"", url.err, &url.url); - return NGX_HTTP_FORBIDDEN; + return NGX_HTTP_BAD_REQUEST; } return NGX_HTTP_INTERNAL_SERVER_ERROR; From 7be3739b3afdcda1c002cbd9716984bdb97e9734 Mon Sep 17 00:00:00 2001 From: Xiaochen Wang Date: Sat, 8 Jul 2023 13:51:13 +0800 Subject: [PATCH 02/11] supported http/2 (part2): Indicate the location of the code that needs to be changed RFC: https://httpwg.org/specs/rfc7540.html#CONNECT The location where the code needs to be changed is as follows: 1. client read before data proxying: check and process HTTP/2 header ":authority" 2. client write before data proxying: Once backend connection is successfully established, the proxy sends a HEADERS frame containing a 2xx series status code to the client, as defined in 3. client read/write during data proxying: read/write DATA frames from/to client --- ngx_http_proxy_connect_module.c | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/ngx_http_proxy_connect_module.c b/ngx_http_proxy_connect_module.c index f09206b..e8a60f9 100644 --- a/ngx_http_proxy_connect_module.c +++ b/ngx_http_proxy_connect_module.c @@ -146,6 +146,9 @@ static ngx_int_t ngx_http_proxy_connect_sock_ntop(ngx_http_request_t *r, static ngx_int_t ngx_http_proxy_connect_create_peer(ngx_http_request_t *r, ngx_http_upstream_resolved_t *ur); +#if (NGX_HTTP_V2) +static void ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r); +#endif static ngx_command_t ngx_http_proxy_connect_commands[] = { @@ -577,6 +580,13 @@ ngx_http_proxy_connect_send_connection_established(ngx_http_request_t *r) ctx->send_established = 1; +#if (NGX_HTTP_V2) + if (r->stream) { + ngx_http_v2_proxy_connect_send_connection_established(r); + return; + } +#endif + for (;;) { n = c->send(c, b->pos, b->last - b->pos); @@ -645,6 +655,16 @@ ngx_http_proxy_connect_send_connection_established(ngx_http_request_t *r) } +#if (NGX_HTTP_V2) +static void +ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r) +{ + // A proxy that supports CONNECT establishes a TCP connection [TCP] to the server identified in the :authority pseudo-header field. Once this connection is successfully established, the proxy sends a HEADERS frame containing a 2xx series status code to the client, as defined in [RFC7231], Section 4.3.6. + // TODO: send HEADERS frame with 2xx status +} +#endif + + static void ngx_http_proxy_connect_tunnel(ngx_http_request_t *r, ngx_uint_t from_upstream, ngx_uint_t do_write) @@ -669,6 +689,10 @@ ngx_http_proxy_connect_tunnel(ngx_http_request_t *r, pc = u->peer.connection; +#if (NGX_HTTP_V2) + // TODO: rewrite c->send, c->recv to read/write HTTP/2 DATA frame +#endif + if (from_upstream) { src = pc; dst = c; @@ -1471,6 +1495,17 @@ ngx_http_proxy_connect_handler(ngx_http_request_t *r) return NGX_DECLINED; } +#if (NGX_HTTP_V2) + /* check and preprocess v2 header */ + /* The :scheme and :path pseudo-header fields MUST be omitted. */ + // TODO: if has :scheme and :path, return 400 + + + /* The :authority pseudo-header field contains the host and port to connect to */ + // TODO: + // r->connect_host, r->connect_port_n <- :authority +#endif + rc = ngx_http_proxy_connect_allow_handler(r, plcf); if (rc != NGX_OK) { @@ -1487,6 +1522,8 @@ ngx_http_proxy_connect_handler(ngx_http_request_t *r) u->conf = plcf; + /* get or resolve upstream peer address */ + ngx_memzero(&url, sizeof(ngx_url_t)); if (plcf->address) { @@ -2310,6 +2347,7 @@ ngx_http_proxy_connect_variable_set_response(ngx_http_request_t *r, ctx->buf.last = ctx->buf.pos + v->len; } + static ngx_int_t ngx_http_proxy_connect_add_variables(ngx_conf_t *cf) { From 9a7255d67e4598f9f12a6bdf8062a623bef44b47 Mon Sep 17 00:00:00 2001 From: Xiaochen Wang Date: Fri, 14 Jul 2023 20:55:48 +0800 Subject: [PATCH 03/11] supported http/2 (part3): process connect host and port from HTTP/2 stream --- ngx_http_proxy_connect_module.c | 78 +++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/ngx_http_proxy_connect_module.c b/ngx_http_proxy_connect_module.c index e8a60f9..6611170 100644 --- a/ngx_http_proxy_connect_module.c +++ b/ngx_http_proxy_connect_module.c @@ -148,6 +148,7 @@ static ngx_int_t ngx_http_proxy_connect_create_peer(ngx_http_request_t *r, #if (NGX_HTTP_V2) static void ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r); +static ngx_int_t ngx_http_v2_proxy_connect_process_header(ngx_http_request_t *r); #endif @@ -1496,14 +1497,12 @@ ngx_http_proxy_connect_handler(ngx_http_request_t *r) } #if (NGX_HTTP_V2) - /* check and preprocess v2 header */ - /* The :scheme and :path pseudo-header fields MUST be omitted. */ - // TODO: if has :scheme and :path, return 400 - - - /* The :authority pseudo-header field contains the host and port to connect to */ - // TODO: - // r->connect_host, r->connect_port_n <- :authority + if (r->stream) { + rc = ngx_http_v2_proxy_connect_process_header(r); + if (rc != NGX_OK) { + return rc; + } + } #endif rc = ngx_http_proxy_connect_allow_handler(r, plcf); @@ -1651,6 +1650,69 @@ ngx_http_proxy_connect_handler(ngx_http_request_t *r) } +#if (NGX_HTTP_V2) +static ngx_int_t +ngx_http_v2_proxy_connect_process_header(ngx_http_request_t *r) +{ + ngx_str_t host; + ngx_int_t port; + + /* check and preprocess v2 header */ + /* The :scheme and :path pseudo-header fields MUST be omitted. */ + if (r->unparsed_uri.len) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "proxy_connect: client sent :path header."); + + return NGX_HTTP_BAD_REQUEST; + } + + if (r->schema.len) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "proxy_connect: client sent :schema header."); + + return NGX_HTTP_BAD_REQUEST; + } + + /* The :authority pseudo-header field contains the host and port to connect to */ + /* :authority field is handled by ngx_http_process_host() */ + if (r->headers_in.host == NULL) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "proxy_connect: client does not send :authority field."); + + + return NGX_HTTP_BAD_REQUEST; + } + + /* initialized but not used */ + host = r->headers_in.host->value; + r->connect_host_start = host.data; + r->connect_host_end = host.data + r->headers_in.server.len; + + r->connect_port_end = host.data + host.len; + + /* get connect_host: fields that has been processed by http/2 logic */ + r->connect_host = r->headers_in.server; + + /* get connect_port */ + + r->connect_port.data = r->connect_host_end + 1; + r->connect_port.len = r->connect_port_end - r->connect_host_end - 1; + + port = ngx_atoi(r->connect_port.data, r->connect_port.len); + if (port == NGX_ERROR || port < 1 || port > 65535) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "proxy_connect: client sent invalid port in :authority"); + + return NGX_HTTP_BAD_REQUEST; + } + + r->connect_port_n = port; + + return NGX_OK; +} +#endif + + static ngx_int_t ngx_http_proxy_connect_sock_ntop(ngx_http_request_t *r, ngx_http_proxy_connect_upstream_t *u) From 4fc823e145013db5af30336090b559d558947cfb Mon Sep 17 00:00:00 2001 From: Xiaochen Wang Date: Fri, 14 Jul 2023 21:15:24 +0800 Subject: [PATCH 04/11] supported http/2 (part3.1): added debug log and testing for port --- ngx_http_proxy_connect_module.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ngx_http_proxy_connect_module.c b/ngx_http_proxy_connect_module.c index 6611170..0d55bad 100644 --- a/ngx_http_proxy_connect_module.c +++ b/ngx_http_proxy_connect_module.c @@ -1677,20 +1677,29 @@ ngx_http_v2_proxy_connect_process_header(ngx_http_request_t *r) /* :authority field is handled by ngx_http_process_host() */ if (r->headers_in.host == NULL) { ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, - "proxy_connect: client does not send :authority field."); + "proxy_connect: client sent no :authority header."); return NGX_HTTP_BAD_REQUEST; } /* initialized but not used */ + host = r->headers_in.host->value; r->connect_host_start = host.data; r->connect_host_end = host.data + r->headers_in.server.len; r->connect_port_end = host.data + host.len; + if (r->connect_port_end == r->connect_host_end) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "proxy_connect: client sent no port in :authority header"); + + return NGX_HTTP_BAD_REQUEST; + } + /* get connect_host: fields that has been processed by http/2 logic */ + r->connect_host = r->headers_in.server; /* get connect_port */ @@ -1708,6 +1717,10 @@ ngx_http_v2_proxy_connect_process_header(ngx_http_request_t *r) r->connect_port_n = port; + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "proxy_connect: process h2 host: %V, port: %d", + &r->connect_host, r->connect_port_n); + return NGX_OK; } #endif From d6bf313c0d329e4b2baa461a7c6ad32c9339772a Mon Sep 17 00:00:00 2001 From: Xiaochen Wang Date: Fri, 14 Jul 2023 22:22:37 +0800 Subject: [PATCH 05/11] supported http/2 (part4): added template for test case --- t/http_proxy_connect_h2.t | 180 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 t/http_proxy_connect_h2.t diff --git a/t/http_proxy_connect_h2.t b/t/http_proxy_connect_h2.t new file mode 100644 index 0000000..09616d5 --- /dev/null +++ b/t/http_proxy_connect_h2.t @@ -0,0 +1,180 @@ +#!/usr/bin/perl + +# (C) Xiaochen Wang + +# Tests for HTTP/2 protocol with ssl. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ SOL_SOCKET SO_RCVBUF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP2; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_ssl http_v2 socket_ssl_alpn/) + ->has_daemon('openssl'); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080 http2 ssl; + server_name localhost; + + ssl_certificate_key localhost.key; + ssl_certificate localhost.crt; + + lingering_close off; + + location / { } + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('localhost') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +$t->write_file('index.html', ''); +$t->write_file('tbig.html', + join('', map { sprintf "XX%06dXX", $_ } (1 .. 500000))); + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +plan(skip_all => 'no ALPN negotiation') unless defined getconn(); +$t->plan(5); + +############################################################################### + +SKIP: { +skip 'LibreSSL too old', 1 + if $t->has_module('LibreSSL') + and not $t->has_feature('libressl:3.4.0'); +skip 'OpenSSL too old', 1 + if $t->has_module('OpenSSL') + and not $t->has_feature('openssl:1.1.0'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.21.4'); + +ok(!get_ssl_socket(['unknown']), 'alpn rejected'); + +} + +} + +like(http_get('/', socket => get_ssl_socket(['http/1.1'])), + qr/200 OK/, 'alpn to HTTP/1.1 fallback'); + +my $s = getconn(['http/1.1', 'h2']); +my $sid = $s->new_stream(); +my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 200, 'alpn to HTTP/2'); +# h2c preface on ssl-enabled socket is rejected as invalid HTTP/1.x request, +# ensure that HTTP/2 auto-detection doesn't kick in + +like(http("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"), qr/Bad Request/, + 'no h2c on ssl socket'); + +# client cancels last stream after HEADERS has been created, +# while some unsent data was left in the SSL buffer +# HEADERS frame may stuck in SSL buffer and won't be sent producing alert + +$s = getconn(['http/1.1', 'h2']); +$s->{socket}->setsockopt(SOL_SOCKET, SO_RCVBUF, 1024*1024) or die $!; + +# for proxy_connect +$sid = $s->new_stream({ headers => [ + { name => ':method', value => 'CONNECT', mode => 1 }, + { name => ':authority', value => 'localhost:8888', mode => 1 } ]}); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + +TODO: { + local $TODO = 'not yet'; + is($frame->{headers}->{':status'}, 400, 'CONNECT - Bad Request'); +} + +$t->stop(); + +############################################################################### + +sub getconn { + my ($alpn) = @_; + $alpn = ['h2'] if !defined $alpn; + + my $sock = get_ssl_socket($alpn); + my $s = Test::Nginx::HTTP2->new(undef, socket => $sock) + if $sock->alpn_selected(); +} + +sub get_ssl_socket { + my ($alpn) = @_; + my $s; + + eval { + local $SIG{ALRM} = sub { die "timeout\n" }; + local $SIG{PIPE} = sub { die "sigpipe\n" }; + alarm(8); + $s = IO::Socket::SSL->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1', + PeerPort => port(8080), + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_alpn_protocols => $alpn, + SSL_error_trap => sub { die $_[1] } + ); + alarm(0); + }; + alarm(0); + + if ($@) { + log_in("died: $@"); + return undef; + } + + return $s; +} + +############################################################################### From b71ebfad2d01aab1906cc21d365984601383bfc9 Mon Sep 17 00:00:00 2001 From: Xiaochen Wang Date: Fri, 14 Jul 2023 22:24:45 +0800 Subject: [PATCH 06/11] supported http/2 (part5): sent "200" status for tunnel establishment --- ngx_http_proxy_connect_module.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ngx_http_proxy_connect_module.c b/ngx_http_proxy_connect_module.c index 0d55bad..241a42b 100644 --- a/ngx_http_proxy_connect_module.c +++ b/ngx_http_proxy_connect_module.c @@ -661,7 +661,15 @@ static void ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r) { // A proxy that supports CONNECT establishes a TCP connection [TCP] to the server identified in the :authority pseudo-header field. Once this connection is successfully established, the proxy sends a HEADERS frame containing a 2xx series status code to the client, as defined in [RFC7231], Section 4.3.6. - // TODO: send HEADERS frame with 2xx status + ngx_int_t rc; + + rc = ngx_http_send_response(r, 200, NULL, NULL); + + if (rc == NGX_AGAIN) { + + } + + // TODO: error handling } #endif @@ -1367,6 +1375,12 @@ ngx_http_proxy_connect_check_broken_connection(ngx_http_request_t *r, return; } +#if (NGX_HTTP_V2) + if (r->stream) { + return; + } +#endif + #if (NGX_HAVE_KQUEUE) if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) { From 2ed77da9a9f42077086ad0253dbafb136548dd0f Mon Sep 17 00:00:00 2001 From: Xiaochen Wang Date: Sun, 16 Jul 2023 18:36:50 +0800 Subject: [PATCH 07/11] supported http/2 (part5.1): completed logic of sending status 200 OK --- ngx_http_proxy_connect_module.c | 91 +++++++++++++++-- t/http_proxy_connect_h2.t | 172 +++++++++++++++++++++++++++++++- 2 files changed, 250 insertions(+), 13 deletions(-) diff --git a/ngx_http_proxy_connect_module.c b/ngx_http_proxy_connect_module.c index 241a42b..138f8ed 100644 --- a/ngx_http_proxy_connect_module.c +++ b/ngx_http_proxy_connect_module.c @@ -558,6 +558,13 @@ ngx_http_proxy_connect_send_connection_established(ngx_http_request_t *r) u->state.connect_time = ngx_current_msec - u->start_time; } +#if (NGX_HTTP_V2) + if (r->stream) { + ngx_http_v2_proxy_connect_send_connection_established(r); + return; + } +#endif + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); b = &ctx->buf; @@ -581,13 +588,6 @@ ngx_http_proxy_connect_send_connection_established(ngx_http_request_t *r) ctx->send_established = 1; -#if (NGX_HTTP_V2) - if (r->stream) { - ngx_http_v2_proxy_connect_send_connection_established(r); - return; - } -#endif - for (;;) { n = c->send(c, b->pos, b->last - b->pos); @@ -661,15 +661,86 @@ static void ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r) { // A proxy that supports CONNECT establishes a TCP connection [TCP] to the server identified in the :authority pseudo-header field. Once this connection is successfully established, the proxy sends a HEADERS frame containing a 2xx series status code to the client, as defined in [RFC7231], Section 4.3.6. - ngx_int_t rc; + ngx_int_t rc; + ngx_connection_t *c; + ngx_http_core_loc_conf_t *clcf; + ngx_http_proxy_connect_ctx_t *ctx; + ngx_http_proxy_connect_upstream_t *u; + + ctx = ngx_http_get_module_ctx(r, ngx_http_proxy_connect_module); + + u = ctx->u; + + c = r->connection; + + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); + + + if (ctx->send_established) { + rc = ngx_http_output_filter(r, NULL); + + } else { + ctx->send_established = 1; + + r->headers_out.status = 200; - rc = ngx_http_send_response(r, 200, NULL, NULL); + /* ensure HERADERS frame: fin=0 */ + r->headers_out.content_length_n = -1; + + rc = ngx_http_send_header(r); + } if (rc == NGX_AGAIN) { + r->write_event_handler = ngx_http_proxy_connect_send_handler; + + ngx_add_timer(c->write, ctx->data_timeout); + + if (ngx_handle_write_event(c->write, clcf->send_lowat) != NGX_OK) { + ngx_http_proxy_connect_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + + return; + } + + if (rc == NGX_ERROR) { + ngx_http_proxy_connect_finalize_request(r, u, rc); + return; + } + + if (rc > NGX_OK && rc != NGX_HTTP_OK) { + ngx_http_proxy_connect_finalize_request(r, u, rc); + return; + } + + /* NGX_HTTP_OK or NGX_OK */ + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, + "proxy_connect: sent 200 connection established"); + + if (c->write->timer_set) { + ngx_del_timer(c->write); + } + + ctx->send_established_done = 1; + + r->write_event_handler = ngx_http_proxy_connect_write_downstream; + r->read_event_handler = ngx_http_proxy_connect_read_downstream; + + if (ngx_handle_write_event(c->write, clcf->send_lowat) != NGX_OK) { + ngx_http_proxy_connect_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; } - // TODO: error handling + if (c->read->ready) { + r->read_event_handler(r); + return; + } + + return; } #endif diff --git a/t/http_proxy_connect_h2.t b/t/http_proxy_connect_h2.t index 09616d5..e31107d 100644 --- a/t/http_proxy_connect_h2.t +++ b/t/http_proxy_connect_h2.t @@ -2,7 +2,7 @@ # (C) Xiaochen Wang -# Tests for HTTP/2 protocol with ssl. +# Tests for HTTP/2 protocol with proxy_connect module. ############################################################################### @@ -24,6 +24,14 @@ use Test::Nginx::HTTP2; select STDERR; $| = 1; select STDOUT; $| = 1; +# SRV record, not used +my %route_map; + +# A record +my %aroute_map = ( + 'localhost' => [[300, "127.0.0.1"]], +); + my $t = Test::Nginx->new()->has(qw/http http_ssl http_v2 socket_ssl_alpn/) ->has_daemon('openssl'); @@ -39,6 +47,8 @@ events { http { %%TEST_GLOBALS_HTTP%% + resolver 127.0.0.1:%%PORT_8981_UDP%% ipv6=off; # NOTE: cannot connect ipv6 address ::1 in mac os x. + server { listen 127.0.0.1:8080 http2 ssl; server_name localhost; @@ -48,8 +58,18 @@ http { lingering_close off; + proxy_connect; + proxy_connect_allow all; + proxy_connect_connect_timeout 10s; + proxy_connect_data_timeout 10s; + location / { } } + + server { + listen 127.0.0.1:8081; + return 200 "OK\n"; + } } EOF @@ -76,6 +96,9 @@ $t->write_file('index.html', ''); $t->write_file('tbig.html', join('', map { sprintf "XX%06dXX", $_ } (1 .. 500000))); +$t->run_daemon(\&dns_daemon, port(8981), $t); +$t->waitforfile($t->testdir . '/' . port(8981)); + open OLDERR, ">&", \*STDERR; close STDERR; $t->run(); open STDERR, ">&", \*OLDERR; @@ -126,14 +149,14 @@ $s->{socket}->setsockopt(SOL_SOCKET, SO_RCVBUF, 1024*1024) or die $!; # for proxy_connect $sid = $s->new_stream({ headers => [ { name => ':method', value => 'CONNECT', mode => 1 }, - { name => ':authority', value => 'localhost:8888', mode => 1 } ]}); + { name => ':authority', value => 'localhost:'.port(8081), mode => 1 } ]}); $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; TODO: { local $TODO = 'not yet'; - is($frame->{headers}->{':status'}, 400, 'CONNECT - Bad Request'); + is($frame->{headers}->{':status'}, 200, 'CONNECT - connection established'); } $t->stop(); @@ -178,3 +201,146 @@ sub get_ssl_socket { } ############################################################################### + +sub reply_handler { + my ($recv_data, $port, $state, %extra) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant FORMERR => 1; + use constant SERVFAIL => 2; + use constant NXDOMAIN => 3; + + use constant A => 1; + use constant CNAME => 5; + use constant DNAME => 39; + + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600); + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + + my $name = join('.', @name); + + if (($type == A) && exists($aroute_map{$name})) { + + my @records = @{$aroute_map{$name}}; + + for (my $i = 0; $i < scalar(@records); $i++) { + my ($ttl, $origin_addr) = @{$records[$i]}; + push @rdata, rd_addr($ttl, $origin_addr); + + #print("dns reply: $name $ttl $class $type $origin_addr\n"); + } + } + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq ''; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($port, $t, %extra) = @_; + + print("+ dns daemon: try to listen on 127.0.0.1:$port\n"); + + my ($data, $recv_data); + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $sel = IO::Select->new($socket); + my $tcp = 0; + + if ($extra{tcp}) { + $tcp = port(8983, socket => 1); + $sel->add($tcp); + } + + local $SIG{PIPE} = 'IGNORE'; + + # track number of relevant queries + + my %state = ( + cnamecnt => 0, + twocnt => 0, + ttlcnt => 0, + ttl0cnt => 0, + cttlcnt => 0, + cttl2cnt => 0, + manycnt => 0, + casecnt => 0, + idcnt => 0, + fecnt => 0, + ); + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . $port; + close $fh; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($tcp == $fh) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $port, + \%state); + $fh->send($data); + + } else { + $fh->recv($recv_data, 65536); + unless (length $recv_data) { + $sel->remove($fh); + $fh->close; + next; + } + +again: + my $len = unpack("n", $recv_data); + $data = substr $recv_data, 2, $len; + $data = reply_handler($data, $port, \%state, + tcp => 1); + $data = pack("n", length $data) . $data; + $fh->send($data); + $recv_data = substr $recv_data, 2 + $len; + goto again if length $recv_data; + } + } + } +} + +############################################################################### From bdae33bd932aef32c46f27f5f17138bbe9fd1369 Mon Sep 17 00:00:00 2001 From: Xiaochen Wang Date: Fri, 21 Jul 2023 19:48:44 +0800 Subject: [PATCH 08/11] supported http/2 (part5.2): completed logic of sending data to downstream via http/2 protocol --- ngx_http_proxy_connect_module.c | 59 ++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/ngx_http_proxy_connect_module.c b/ngx_http_proxy_connect_module.c index 138f8ed..d79d40e 100644 --- a/ngx_http_proxy_connect_module.c +++ b/ngx_http_proxy_connect_module.c @@ -149,6 +149,8 @@ static ngx_int_t ngx_http_proxy_connect_create_peer(ngx_http_request_t *r, #if (NGX_HTTP_V2) static void ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r); static ngx_int_t ngx_http_v2_proxy_connect_process_header(ngx_http_request_t *r); +ssize_t ngx_http_v2_proxy_connect_recv(ngx_connection_t *c, u_char *buf, size_t size); +ssize_t ngx_http_v2_proxy_connect_send(ngx_connection_t *c, u_char *buf, size_t size); #endif @@ -770,7 +772,12 @@ ngx_http_proxy_connect_tunnel(ngx_http_request_t *r, pc = u->peer.connection; #if (NGX_HTTP_V2) - // TODO: rewrite c->send, c->recv to read/write HTTP/2 DATA frame + /* rewrite c->send, c->recv to read/write HTTP/2 DATA frame */ + + if (r->stream) { + r->connection->send = ngx_http_v2_proxy_connect_send; + r->connection->recv = ngx_http_v2_proxy_connect_recv; + } #endif if (from_upstream) { @@ -909,6 +916,56 @@ ngx_http_proxy_connect_tunnel(ngx_http_request_t *r, } +#if (NGX_HTTP_V2) +ssize_t +ngx_http_v2_proxy_connect_recv(ngx_connection_t *c, u_char *buf, size_t size) +{ + ngx_http_request_t *r = c->data; + // r->reading_body = ; // TODO + r->request_body_no_buffering = 1; + + r->headers_in.content_length_n = -1; + r->headers_in.chunked = 1; + + return 0; +} + + +ssize_t +ngx_http_v2_proxy_connect_send(ngx_connection_t *c, u_char *buf, size_t size) +{ + ngx_http_request_t *r; + ngx_buf_t *b; + ngx_chain_t out; + ngx_http_proxy_connect_ctx_t *ctx; + ngx_http_proxy_connect_upstream_t *u; + + r = c->data; + ctx = ngx_http_get_module_ctx(r, ngx_http_proxy_connect_module); + + c = r->connection; + u = ctx->u; + + /* disable the function of http core module: limit rate */ + r->limit_rate = 0; + r->limit_rate_set = 1; + + b = ngx_calloc_buf(r->pool); //TODO: free b + if (b == NULL) { + return NGX_ERROR; + } + + *b = u->buffer; + + out.buf = b; + out.next = NULL; + + return ngx_http_write_filter(r, &out); +} +#endif + + + static void ngx_http_proxy_connect_read_downstream(ngx_http_request_t *r) { From 0ae1f3f4630730a479485022cda1da38f6c20fc5 Mon Sep 17 00:00:00 2001 From: Xiaochen Wang Date: Tue, 25 Jul 2023 13:09:56 +0800 Subject: [PATCH 09/11] supported http/2 (part5.3): removed logic of handling event after sending 200 OK ngx_http_v2_send_output_queue() will do this for us. --- ngx_http_proxy_connect_module.c | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/ngx_http_proxy_connect_module.c b/ngx_http_proxy_connect_module.c index d79d40e..6e8c508 100644 --- a/ngx_http_proxy_connect_module.c +++ b/ngx_http_proxy_connect_module.c @@ -665,7 +665,6 @@ ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r) // A proxy that supports CONNECT establishes a TCP connection [TCP] to the server identified in the :authority pseudo-header field. Once this connection is successfully established, the proxy sends a HEADERS frame containing a 2xx series status code to the client, as defined in [RFC7231], Section 4.3.6. ngx_int_t rc; ngx_connection_t *c; - ngx_http_core_loc_conf_t *clcf; ngx_http_proxy_connect_ctx_t *ctx; ngx_http_proxy_connect_upstream_t *u; @@ -675,9 +674,6 @@ ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r) c = r->connection; - clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); - - if (ctx->send_established) { rc = ngx_http_output_filter(r, NULL); @@ -698,12 +694,6 @@ ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r) ngx_add_timer(c->write, ctx->data_timeout); - if (ngx_handle_write_event(c->write, clcf->send_lowat) != NGX_OK) { - ngx_http_proxy_connect_finalize_request(r, u, - NGX_HTTP_INTERNAL_SERVER_ERROR); - return; - } - return; } @@ -731,18 +721,14 @@ ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r) r->write_event_handler = ngx_http_proxy_connect_write_downstream; r->read_event_handler = ngx_http_proxy_connect_read_downstream; - if (ngx_handle_write_event(c->write, clcf->send_lowat) != NGX_OK) { - ngx_http_proxy_connect_finalize_request(r, u, - NGX_HTTP_INTERNAL_SERVER_ERROR); - return; - } - - if (c->read->ready) { - r->read_event_handler(r); - return; - } + /* + * If there is an upstream read event, we dont need to handle + * it actively here. + * + * We can read the client body data (DATA frame) actively here. + */ - return; + r->read_event_handler(r); } #endif From 357a2d4513f00dff34946d452d66bf8b2ad577ff Mon Sep 17 00:00:00 2001 From: Xiaochen Wang Date: Tue, 25 Jul 2023 20:40:18 +0800 Subject: [PATCH 10/11] supported http/2 (part5.4): added request body reading logic --- ngx_http_proxy_connect_module.c | 44 +++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/ngx_http_proxy_connect_module.c b/ngx_http_proxy_connect_module.c index 6e8c508..1dd5421 100644 --- a/ngx_http_proxy_connect_module.c +++ b/ngx_http_proxy_connect_module.c @@ -721,14 +721,44 @@ ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r) r->write_event_handler = ngx_http_proxy_connect_write_downstream; r->read_event_handler = ngx_http_proxy_connect_read_downstream; - /* - * If there is an upstream read event, we dont need to handle - * it actively here. - * - * We can read the client body data (DATA frame) actively here. - */ + /* init request_body for reading DATA frame from http2 */ + +#if 0 + r->main->count--; /* ++ in ngx_http_read_client_request_body */ + rc = ngx_http_read_client_request_body(r, + ngx_http_proxy_connect_read_downstream); + + if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { + ngx_http_proxy_connect_finalize_request(r, u, rc); + return; + } +#else + { + ngx_http_request_body_t *rb; - r->read_event_handler(r); + rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t)); + + if (rb == NULL) { + ngx_http_proxy_connect_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + + rb->rest = -1; + rb->post_handler = ngx_http_proxy_connect_read_downstream; + + r->request_body = rb; + + rc = ngx_http_v2_read_request_body(r); + + if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { + ngx_http_proxy_connect_finalize_request(r, u, rc); + return; + } + + return; + } +#endif } #endif From 37c42ba792ae10673c9e010268ace5c1f69499f9 Mon Sep 17 00:00:00 2001 From: Xiaochen Wang Date: Mon, 31 Jul 2023 22:17:01 +0800 Subject: [PATCH 11/11] supported http/2 (part5.5): refactor IO operations to buf/chain API --- ngx_http_proxy_connect_module.c | 324 ++++++++++++++++++++++++++++++-- 1 file changed, 312 insertions(+), 12 deletions(-) diff --git a/ngx_http_proxy_connect_module.c b/ngx_http_proxy_connect_module.c index 1dd5421..c480c52 100644 --- a/ngx_http_proxy_connect_module.c +++ b/ngx_http_proxy_connect_module.c @@ -69,6 +69,7 @@ struct ngx_http_proxy_connect_upstream_s { ngx_buf_t from_client; ngx_output_chain_ctx_t output; + ngx_chain_writer_ctx_t writer; ngx_buf_t buffer; @@ -81,6 +82,8 @@ struct ngx_http_proxy_connect_upstream_s { ngx_msec_t start_time; ngx_http_proxy_connect_upstream_state_t state; + + unsigned request_body_blocked:1; }; struct ngx_http_proxy_connect_address_s { @@ -151,6 +154,8 @@ static void ngx_http_v2_proxy_connect_send_connection_established(ngx_http_reque static ngx_int_t ngx_http_v2_proxy_connect_process_header(ngx_http_request_t *r); ssize_t ngx_http_v2_proxy_connect_recv(ngx_connection_t *c, u_char *buf, size_t size); ssize_t ngx_http_v2_proxy_connect_send(ngx_connection_t *c, u_char *buf, size_t size); +void ngx_http_v2_proxy_connect_process_upstream(ngx_http_request_t *r, ngx_uint_t do_write); +void ngx_http_v2_proxy_connect_process_downstream(ngx_http_request_t *r, ngx_uint_t do_write); #endif @@ -734,20 +739,26 @@ ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r) } #else { - ngx_http_request_body_t *rb; + /* init upstream output writer */ - rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t)); + u->output.pool = r->pool; + u->output.bufs.num = 1; + u->output.bufs.size = u->conf->buffer_size; - if (rb == NULL) { - ngx_http_proxy_connect_finalize_request(r, u, - NGX_HTTP_INTERNAL_SERVER_ERROR); - return; + if (u->output.output_filter == NULL) { + u->output.output_filter = ngx_chain_writer; + u->output.filter_ctx = &u->writer; + } + + u->writer.pool = r->pool; + u->writer.out = NULL; + u->writer.last = &u->writer.out; + u->writer.connection = u->peer.connection; + u->writer.limit = 0; } - rb->rest = -1; - rb->post_handler = ngx_http_proxy_connect_read_downstream; - r->request_body = rb; + { rc = ngx_http_v2_read_request_body(r); @@ -788,12 +799,35 @@ ngx_http_proxy_connect_tunnel(ngx_http_request_t *r, pc = u->peer.connection; #if (NGX_HTTP_V2) - /* rewrite c->send, c->recv to read/write HTTP/2 DATA frame */ + +#if 1 + /* + * There is no low-level recv/send IO operations of http/v2 in nginx core, + * so we just need use high-level buf/chain IO operations. + */ + + if (r->stream) { + if (from_upstream) { + ngx_http_v2_proxy_connect_process_downstream(r, do_write); + } else { + ngx_http_v2_proxy_connect_process_upstream(r, do_write); + } + return ; + } +#else + + /* + * Alough there is no low-level IO operations, we can monitor them + * trickly. We need to rewrite c->send, c->recv interface to read/write + * HTTP/2 DATA frame + */ if (r->stream) { r->connection->send = ngx_http_v2_proxy_connect_send; r->connection->recv = ngx_http_v2_proxy_connect_recv; } +#endif + #endif if (from_upstream) { @@ -933,11 +967,252 @@ ngx_http_proxy_connect_tunnel(ngx_http_request_t *r, #if (NGX_HTTP_V2) +/* + * data flow: downstream -> upstream + * + * referer to + * ngx_http_upstream_send_request() + * -> ngx_http_upstream_send_request_body() + */ +void +ngx_http_v2_proxy_connect_process_upstream(ngx_http_request_t *r, + ngx_uint_t do_write) +{ + ngx_int_t rc; + ngx_chain_t *out, /**cl,*/ *ln; + ngx_connection_t *c; + ngx_http_proxy_connect_ctx_t *ctx; + ngx_http_proxy_connect_upstream_t *u; + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "proxy_connect: h2 downstream -> upstream"); + + ctx = ngx_http_get_module_ctx(r, ngx_http_proxy_connect_module); + u = ctx->u; + +#if 0 + if (!r->request_body_no_buffering) { + + /* buffered request body */ + + if (!u->request_sent) { + u->request_sent = 1; + out = u->request_bufs; + + } else { + out = NULL; + } + + rc = ngx_output_chain(&u->output, out); + + if (rc == NGX_AGAIN) { + u->request_body_blocked = 1; + + } else { + u->request_body_blocked = 0; + } + + goto done; + } + + if (!u->request_sent) { + u->request_sent = 1; + out = u->request_bufs; + + if (r->request_body->bufs) { + for (cl = out; cl->next; cl = cl->next) { /* void */ } + cl->next = r->request_body->bufs; + r->request_body->bufs = NULL; + } + + c = u->peer.connection; + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); + + if (clcf->tcp_nodelay && ngx_tcp_nodelay(c) != NGX_OK) { + return NGX_ERROR; + } + + r->read_event_handler = ngx_http_upstream_read_request_handler; + + } else { + out = NULL; + } +#else + out = NULL; + r->request_body_no_buffering = 1; +#endif + + for ( ;; ) { + + if (do_write) { + rc = ngx_output_chain(&u->output, out); + + if (rc == NGX_ERROR) { + ngx_http_proxy_connect_finalize_request(r, u, NGX_ERROR); + return; + } + + while (out) { + ln = out; + out = out->next; + ngx_free_chain(r->pool, ln); + } + + if (rc == NGX_AGAIN) { + u->request_body_blocked = 1; + + } else { + u->request_body_blocked = 0; + } + + if (rc == NGX_OK && !r->reading_body) { + break; + } + } + + if (r->reading_body) { + /* read client request body */ + + rc = ngx_http_read_unbuffered_request_body(r); + + if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { + ngx_http_proxy_connect_finalize_request(r, u, rc); + return; + } + + out = r->request_body->bufs; + r->request_body->bufs = NULL; + } + + /* stop if there is nothing to send */ + + if (out == NULL) { + rc = NGX_AGAIN; + break; + } + + do_write = 1; + } + +//done: + +#if 0 + if (!r->reading_body) { + if (!u->store && !r->post_action && !u->conf->ignore_client_abort) { + r->read_event_handler = + ngx_http_upstream_rd_check_broken_connection; + } + } +#endif + + c = u->peer.connection; + + if (rc == NGX_AGAIN) { + if (!c->write->ready || u->request_body_blocked) { + ngx_add_timer(c->write, ctx->data_timeout); + + } else if (c->write->timer_set) { + ngx_del_timer(c->write); + } + + if (ngx_handle_write_event(c->write, 0) != NGX_OK) { + ngx_http_proxy_connect_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + +#if 0 + if (c->write->ready && c->tcp_nopush == NGX_TCP_NOPUSH_SET) { + if (ngx_tcp_push(c->fd) == -1) { + ngx_log_error(NGX_LOG_CRIT, c->log, ngx_socket_errno, + ngx_tcp_push_n " failed"); + ngx_http_proxy_connect_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + + c->tcp_nopush = NGX_TCP_NOPUSH_UNSET; + } +#endif + + if (c->read->ready) { + ngx_post_event(c->read, &ngx_posted_events); + } + + return; + } + + /* rc == NGX_OK */ + + // TODO: how to handle timer + + if (c->write->timer_set) { + ngx_del_timer(c->write); + } + +#if 0 + if (c->tcp_nopush == NGX_TCP_NOPUSH_SET) { + if (ngx_tcp_push(c->fd) == -1) { + ngx_log_error(NGX_LOG_CRIT, c->log, ngx_socket_errno, + ngx_tcp_push_n " failed"); + ngx_http_upstream_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + + c->tcp_nopush = NGX_TCP_NOPUSH_UNSET; + } + + if (!u->conf->preserve_output) { + u->write_event_handler = ngx_http_upstream_dummy_handler; + } +#endif + + if (ngx_handle_write_event(c->write, 0) != NGX_OK) { + ngx_http_proxy_connect_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + +#if 0 + if (!u->request_body_sent) { + u->request_body_sent = 1; + + if (u->header_sent) { + return; + } + + ngx_add_timer(c->read, u->conf->read_timeout); + + if (c->read->ready) { + ngx_http_upstream_process_header(r, u); + return; + } + } +#endif + + return; +} + + +void +ngx_http_v2_proxy_connect_process_downstream(ngx_http_request_t *r, + ngx_uint_t do_write) +{ + /* downstream -> upstream */ + +} + + ssize_t ngx_http_v2_proxy_connect_recv(ngx_connection_t *c, u_char *buf, size_t size) { ngx_http_request_t *r = c->data; - // r->reading_body = ; // TODO + // TODO: read body data + // ngx_http_upstream_send_request_body + // -> ngx_http_read_unbuffered_request_body + // ... get bufs from DATA frame into r->request_body->bufs + // -> then we need return r->request_body->bufs to caller r->request_body_no_buffering = 1; r->headers_in.content_length_n = -1; @@ -981,7 +1256,6 @@ ngx_http_v2_proxy_connect_send(ngx_connection_t *c, u_char *buf, size_t size) #endif - static void ngx_http_proxy_connect_read_downstream(ngx_http_request_t *r) { @@ -1660,6 +1934,32 @@ ngx_http_proxy_connect_handler(ngx_http_request_t *r) if (rc != NGX_OK) { return rc; } + + { + /* + * init request body before handling DATA frames and init_upstream, + * otherwise the DATA frame may not be received in + * ngx_http_v2_state_read_data()? + */ + ngx_http_request_body_t *rb; + + rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t)); + + if (rb == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; +#if 0 + ngx_http_proxy_connect_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; +#endif + } + + rb->rest = -1; + // TODO: changed to init_upstream () + rb->post_handler = ngx_http_proxy_connect_read_downstream; + + r->request_body = rb; + } } #endif